- 编程
OI Wiki
- 2023-3-20 13:00:10 @
比赛相关
比赛相关简介
本章主要介绍计算机编程比赛直接相关的知识,包括各种赛事、赛制、题型,以及赛场上常见的坑点与技巧。
学习路线,与常用的学习资源也可以在本章找到。
本章亦设出题板块,介绍出竞赛题的相关知识。
工具软件
工具软件简介
本章主要介绍与竞赛有关的工具软件,包括一些代码编辑器的介绍和 OJ 相关工具。
程序是解决 OI 问题的工具,熟练运用代码编辑器是学习 OI 的前提。
了解 OJ 相关的工具能让 OI 之旅更加舒适、便捷。
语言基础简介
语言基础简介
本章将会介绍编程相关的知识,包括 C++ 从入门到进阶教程和一些其它语言的简介。
程序是算法与数据结构的载体,是解决 OI 问题的工具。
在 OI 中,最常用的编程语言是 C++。
学习编程是学习 OI 最基础的部分。
算法基础
算法基础简介
本章主要介绍一些基础算法,为之后的进阶内容做铺垫。
一方面,这些内容可以让初学者对 OI 的一些思想有初步的认识;另一方面,本章介绍的大部分算法还会在以后的进阶内容中得到运用。
搜索
搜索部分简介
搜索,也就是对状态空间进行枚举,通过穷尽所有的可能来找到最优解,或者统计合法解的个数。
搜索有很多优化方式,如减小状态空间,更改搜索顺序,剪枝等。
搜索是一些高级算法的基础。在 OI 中,纯粹的搜索往往也是得到部分分的手段,但可以通过纯粹的搜索拿到满分的题目非常少。
习题
动态规划
动态规划部分简介
本章将介绍介绍动态规划(Dynamic Programming, DP)及其解决的问题、根据其设计的算法及优化。
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
由于动态规划并不是某种具体的算法,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。
在 OI 中,计数等非最优化问题的递推解法也常被不规范地称作 DP,因此本章将它们一并列出。事实上,动态规划与其它类型的递推的确有很多相似之处,学习时可以注意它们之间的异同。
参考资料
动态规划基础
本页面主要介绍了动态规划的基本思想,以及动态规划中状态及状态转移方程的设计思路,帮助各位初学者对动态规划有一个初步的了解。本部分的其他页面,将介绍各种类型问题中动态规划模型的建立方法,以及一些动态规划的优化技巧。
引入
[IOI1994] 数字三角形
给定一个 行的数字三角形(),需要找到一条从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到当前点左下方的点或右下方的点。
|
1
2
3
4
5
|
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
| | -------------------- | -------------------------------------------------------------------------------------- |
在上面这个例子中,最优路径是 。
最简单粗暴的思路是尝试所有的路径。因为路径条数是 级别的,这样的做法无法接受。
注意到这样一个事实,一条最优的路径,它的每一步决策都是最优的。
以例题里提到的最优路径为例,只考虑前四步 ,不存在一条从最顶端到 行第 个数的权值更大的路径。
而对于每一个点,它的下一步决策只有两种:往左下角或者往右下角(如果存在)。因此只需要记录当前点的最大权值,用这个最大权值执行下一步决策,来更新后续点的最大权值。
这样做还有一个好处:我们成功缩小了问题的规模,将一个问题分成了多个规模更小的问题。要想得到从顶端到第 行的最优方案,只需要知道从顶端到第 行的最优方案的信息就可以了。
这时候还存在一个问题:子问题间重叠的部分会有很多,同一个子问题可能会被重复访问多次,效率还是不高。解决这个问题的方法是把每个子问题的解存储下来,通过记忆化的方式限制访问顺序,确保每个子问题只被访问一次。
上面就是动态规划的一些基本思路。下面将会更系统地介绍动态规划的思想。
动态规划原理
能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠。
最优子结构
具有最优子结构也可能是适合用贪心的方法求解。
注意要确保我们考察了最优解中用到的所有子问题。
- 证明问题最优解的第一个组成部分是做出一个选择;
- 对于一个给定问题,在其可能的第一步选择中,假定你已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择;
- 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间;
- 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。
要保持子问题空间尽量简单,只在必要时扩展。
最优子结构的不同体现在两个方面:
- 原问题的最优解中涉及多少个子问题;
- 确定最优解使用哪些子问题时,需要考察多少种选择。
子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。
无后效性
已经求解的子问题,不会再受到后续决策的影响。
子问题重叠
如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。
基本思路
对于一个能用动态规划解决的问题,一般采用如下思路解决:
- 将原问题划分为若干 阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态);
- 寻找每一个状态的可能 决策,或者说是各状态间的相互转移方式(用数学的语言描述就是 状态转移方程)。
- 按顺序求解每一个阶段的问题。
如果用图论的思想理解,我们建立一个 有向无环图,每个状态对应图上一个节点,决策对应节点间的连边。这样问题就转变为了一个在 DAG 上寻找最长(短)路的问题(参见:DAG 上的 DP)。
最长公共子序列
最长公共子序列问题
给定一个长度为 的序列 和一个 长度为 的序列 (),求出一个最长的序列,使得该序列既是 的子序列,也是 的子序列。
子序列的定义可以参考 子序列。一个简要的例子:字符串 abcde
与字符串 acde
的公共子序列有 a
、c
、d
、e
、ac
、ad
、ae
、cd
、ce
、de
、ade
、ace
、cde
、acde
,最长公共子序列的长度是 4。
设 表示只考虑 的前 个元素, 的前 个元素时的最长公共子序列的长度,求这时的最长公共子序列的长度就是 子问题。 就是我们所说的 状态,则 是最终要达到的状态,即为所求结果。
对于每个 ,存在三种决策:如果 ,则可以将它接到公共子序列的末尾;另外两种决策分别是跳过 或者 。状态转移方程如下:
可参考 SourceForge 的 LCS 交互网页 来更好地理解 LCS 的实现过程。
该做法的时间复杂度为 。
另外,本题存在 的算法[1](index.html#fn:ref1)^。有兴趣的同学可以自行探索。
|
1
2
3
4
5
6
7
8
9
10
11
|
int a[MAXN], b[MAXM], f[MAXN][MAXM];
int dp() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (a[i] == b[j])
f[i][j] = f[i - 1][j - 1] + 1;
else
f[i][j] = std::max(f[i - 1][j], f[i][j - 1]);
return f[n][m];
}
| | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
最长不下降子序列
最长不下降子序列问题
给定一个长度为 的序列 (),求出一个最长的 的子序列,满足该子序列的后一个元素不小于前一个元素。
算法一
设 表示以 为结尾的最长不下降子序列的长度,则所求为 。
计算 时,尝试将 接到其他的最长不下降子序列后面,以更新答案。于是可以写出这样的状态转移方程:。
容易发现该算法的时间复杂度为 。
[X] [ ] C++Python
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int a[MAXN], d[MAXN];
int dp() {
d[1] = 1;
int ans = 1;
for (int i = 2; i <= n; i++) {
d[i] = 1;
for (int j = 1; j < i; j++)
if (a[j] <= a[i]) {
d[i] = max(d[i], d[j] + 1);
ans = max(ans, d[i]);
}
}
return ans;
}
| | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
算法二[2](index.html#fn:ref2)^
当 的范围扩大到 时,第一种做法就不够快了,下面给出了一个 的做法。
首先,定义 为原始序列, 为当前的不下降子序列, 为子序列的长度,那么 就是长度为 的不下降子序列末尾元素。
初始化:。
现在我们已知最长的不下降子序列长度为 1,那么我们让 从 2 到 循环,依次求出前 个元素的最长不下降子序列的长度,循环的时候我们只需要维护好 这个数组还有 就可以了。关键在于如何维护。
考虑进来一个元素 :
- 元素大于等于 ,直接将该元素插入到 序列的末尾。
- 元素小于 ,找到 第一个 大于它的元素,插入进去,丢弃在它之后的全部元素。
参考代码如下:
[X] [ ] C++Python
|
1
2
3
4
5
6
7
8
|
for (int i = 0; i < n; ++i) scanf("%d", a + i);
memset(dp, 0x1f, sizeof dp);
mx = dp[0];
for (int i = 0; i < n; ++i) {
*std::upper_bound(dp, dp + n, a[i]) = a[i];
}
ans = 0;
while (dp[ans] != mx) ++ans;
| | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
参考资料与注释
字符串
字符串部分简介
字符串,就是由字符连接而成的序列。
常见的字符串问题包括字符串匹配问题、子串相关问题、前缀/后缀相关问题、回文串相关问题、子序列相关问题等。
数学
数学部分简介
本章介绍 OI 中可能会用到的数学知识。计算机科学与数学紧密相关,而在算法竞赛中尤其强调以数论、排列组合、概率期望、多项式为代表离散、具体的数学:其注重程序实现和现实问题,可以出现在几乎任何类别的题目中。
实际上,算法竞赛中涉及到的算法和数据结构以及自动机等也可以被认为属于数学范畴,但是这些内容被细分到诸如字符串等的具体章节加以应用背景以更好理解。本章主要介绍数学中一些基础概念、代数、数论、博弈论及概率论等知识。运用这些知识可以帮助优化其他算法和数据结构,例如:
- 用多项式优化卷积形式的背包,来做一些字符串题。
- 很多递推类型的题背景都是排列组合或概率期望,可以更进一步用生成函数推导和解决,以及使用基于 FFT 的分治优化算法效率。
- 利用同余和环分析图上非简单路径在模意义下可能的权值和,并用带权并查集维护。
此外,高中数学是信息学竞赛数学的基础,学好课本上的基本概念和性质能更好地帮助学习本章内容。
数据结构
数据结构部分简介
数据结构是在计算机中存储、组织数据的方式。小到变量、数组,大到线段树、平衡树,都是数据结构。
程序运行离不开数据结构,不同的数据结构又各有优劣,能够处理的问题各不相同,而根据具体问题选取合适的数据结构,可以大大提升程序的效率。所以,学习各种各样的数据结构是很有必要的。
图论
图论部分简介
图论 (Graph theory) 是数学的一个分支,图是图论的主要研究对象。图 (Graph) 是由若干给定的顶点及连接两顶点的边所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系。顶点用于代表事物,连接两顶点的边则用于表示两个事物间具有这种关系。
参考资料
计算几何
计算几何部分简介
利用计算机建立数学模型解决几何问题。
杂项
杂项简介
这个板块主要介绍的是一些难以分类的算法及 OI 相关知识。