NJU 高级算法 期末复习
最坏复杂度/平均复杂度/最优性
算法的最坏复杂度/平均复杂度/最优性,概念和举例
最坏复杂度
$$ W(n) = max{t(I) | I \in D_n}$$
其中,$n$ 是规模,$t$ 是操作数量,$D_n$ 是一组输入,$I$ 是其中的一个输入。最坏复杂度就是算法处理某个输入,这个输入使算法操作数最多,所用的时间。
只需要记一下符号,用 $W$ 表示。W denotes Worst。
PPT 中举例是欧几里得算法,不过这个例子比较复杂。我建议是不如举快排最坏情况的例子。即输入为有序时,快排退化成 $O(n^2)$
平均复杂度
$$ A(n) = \sum_{I\in D_n}Pr(I)t(I)$$
$Pr$ 是某个输入出现的概率。平均复杂度是对输入集算一个操作次数的期望。
A denotes Average.
例子是一个列表中找某个 value。需要分 value 在和不在列表中两种情况讨论
最优性
如果算法 $A$ 满足:
$$ W_A(n) = F(n)$$
那么 A 就是最优的。其中 $W_A$ 是 A 的最坏复杂度。$F$ 是这个问题复杂度的下界。
这个概念有点如说的意味。
渐进复杂度
$O$, $\ohm$, $\Theta$的定义和相关函数证明
定义
不严格定义,方便记忆:
$f\in O(g)$,那 f 比 g 增长得慢;
$f\in \Theta(g)$,f 和 g 增长得一样快;
$f\in\ohm(g)$,f 比 g 增长得快。
严格定义:
$f\in O(g)$,则 $\exists c\in R^+, n_0\in N$,$\forall n >= n_0$,$f(n) <= cg(n)$。
充分条件:$f\in O(g)$ 当 $\lim_{n\to\infty}\frac{f(n)}{g(n)}=c<\infty$。注意 c 可能为 0。
另外两个定义类似。只需注意 c 的范围即可。
递归关系求解
递归关系求解:$a_n=r_1a_{n-1}+r_2a_{n-2}$ 形似斐波那契数列
递归关系特征方程
$$x^2-r_1x-r_2=0$$ 解得两根 $s_1$, $s_2$。
递推关系为:
$$a_n=us_1^n+vs_2^n$$
根据边界条件求得 u 和 v。
平滑函数与主定理
了解平滑函数性质,应⽤主定理求解⼀般式:$T(n)=bT(n/c)+f(n)$
平滑函数
平滑函数 (smooth function) 定义:
$f(n)$ 是平滑函数,当且仅当 $f(2n)\in\Theta(f(n))$
例子:$logn$, $n$, $nlogn$, $n^\alpha(\alpha>0)$ 都是平滑的。
Master Theorem 主定理
假设一个递归算法解决规模为 $n$ 的问题用时为 $T(n)$。主定理可以求出递归问题的时间复杂度。
由递归问题的性质,$T(n)$一般可以表示为递归形式:
$$T(n)=aT(n/b)+f(n)$$ 其中,a 是每次递归生成出的子问题数量,$n/b$ 是子问题规模,$f(n)$ 是处理当前问题需要消耗的代价。
设 $f(n)\in\Theta(n^d), d>=0$,可以得到这个递归算法的时间复杂度:
$$T(n)=\left{ \begin{aligned} &\Theta(n^d) & if\quad d > \log_ba \ &\Theta(n^d\log n) & if\quad d = \log_ba \ &\Theta(n^{\log_ba}) & if\quad d < \log_ba \end{aligned} \right. $$ 记忆方法,两个不等条件下可以记极端情况:
$d > \log_ba$,那就让 $d$ 趋近无穷,整个算法复杂度肯定是跟着 $\Theta(n^d)$ 即 $f(n)$,走的,所以复杂度就是 $\Theta(n^d)$。
另一个 $d < \log_ba$,那让 $f(n)$ 为 $O(1)$,这样算法的复杂度就是由递归树的节点数来决定了,因为树上每一个节点都是一个代价为 $O(1)$ 的(子)问题。而节点数为 $n^{\log_ba}$。
等于的情况就是在 $f(n)$ 复杂度的基础上乘一个 $logn$。
快排平均复杂分析
复⽤快排中平均复杂分析的⽅法来处理其它问题:$A(n)=(n-1)+\frac{2}{n}\sum_{i=1}^{n-1}A(i)$
快排每一次递归时,首先会选取一个 pivot,然后将所有元素比较一遍。比较的次数就是 n-1,即 $A(n)$ 中 n-1 的由来。
这个 pivot 等可能地分布在当前序列中的所有位置。如果 pivot 分布在第 i 个位置,那么得到了两个规模为 i 和 n - 1 - i(一个是pivot前的序列,一个是pivot后的序列)的子问题。所以这里可计算平均复杂度:
$$\sum_{i=0}^{n-1}\frac{1}{n}[A(i)+A(n-1-i)]=\frac{2}{n}\sum_{i=1}^{n-1}A(i)$$
这之中用到了 $A(1)=A(0)=0$ 的边界条件进一步化简。得到右式就是平均复杂度的第二部分。
对抗策略/对手论证 Adversary Argument
能解释对抗策略的含义、能采⽤对抗策略来分析经典问题:同时求最⼤和最⼩的问题和求取第⼆⼤元素的问题
含义
对抗策略是将算法设计者与算法分析者看作对手,同时扮演两个角色进行算法分析。算法设计者尽量多的创造更多信息,算法分析者尽量少的给予信息,拥有着随时合理改变取值的能力。
用来确定一个算法的复杂度下界。主要是基于比较的算法。
findMaxAndMin
同时求一个序列的最大值和最小值。算法能做的只有比较两个元素。
每一个元素定义如下状态:
- W,在比较中只胜出的元素
- L,在比较中只失败的元素
- W/L,既有胜出又有失败的元素
- N,没比较的元素
算法的最终状态是一个 W(最大值),一个 L(最小值),以及 n - 2 个 W/L。
初始状态是 n 个 N。
对于每一次比较,算法设计者可以得到:
- comp(N, N) -> L, W:至少 2 个信息。这是算法分析者无法改变的。
- comp(W, N) / comp(L, N): 至少 1 个信息。通过让先前 W 和 L 的元素继续保持,设计者只能得到 N 元素变成 W/L 的信息
- comp(W, L) -> W, L: 至少 0 个信息。分析者让 WL 继续保持原有赢输。
- comp(W, W) / comp(L, L) -> W, W/L 或 L, W/L 至少 1 个信息。有一个必会得到与原来比较不同的结果,这无法改变。
对于设计者而言,首先至少得让所有 N 元素变成 W 或者 L 元素。即得到 n 个 N->W 或者 N->L。这最少需要 n / 2 次 comp(N, N) -> L, W。
然后让 n - 2 个 L 或者 W 变成 L/W。设计者利用 comp(W, W) / comp(L, L) 来让每次比较至少得到一个信息。所以至少得 n - 2 次比较。
故算法比较次数下界为 $n / 2 + n - 2 = \frac{3n}{2}-2$
第二大元素
第二大元素,就是在和最大元素比较中,失败的元素里面最大的那一个。
先研究:在寻找最大元素时,最大的元素最少进行了 $\log n$ 次比较。
考虑所有元素打积分赛。元素 $x$ 的积分为 $w(x)$。一次比较胜出后,赢家通吃输者积分。
最大元素就是 $w(x)$ 增长到 n 的元素。算法分析者可以通过下面的方式来限制所有元素的 $w$ 的最快增长速度,每一次增长不超过翻倍的速度。
- 当 w(x) > w(y) 时,算法分析者让 x > y,即让 y 赢。这时 w(x) 增长为 w(y) < w(x),所以增速比翻倍小
- w(x) = w(y) 时,这增速也就将将翻倍
- 其他情况也是无法倍增的
所以最大元素至少要 $log n$ 次增长才能结束。
和最大元素比较的输家里面找最大的一个,需要 $(\log n) - 1$ 次比较。加上一开始找最大元素的 $n - 1$ 次,总共的下界就是 $\log n + n - 2$
平摊分析 Amortized Time Analysis
含义
某些数据结构支持多种操作,其中某一些操作的代价很低,某一些很高。但是这些高代价操作偶尔发生且有一定条件。分析 n 个任意操作的时间复杂度时,不能直接将所有操作的代价视作最高的代价。而是需要平摊分析。
课程中主要介绍了 account 法。不知道中文叫什么,总之有点类似银行账户存取款。
公式为 $amortized,cost = actual,cost + accounting,cost$
Multipop Stack
一个栈。支持 push、pop、multipop。最后一个 multipop 就是将 stack 清空,代价取决于当前栈中有多少元素。
可以看到,大部分情况下 push 和 pop 操作代价很低,但是偶尔的 multipop 代价就比较高,不过这个代价仍然取决于栈中有多少元素。
均摊分析要从元素角度出发:一个元素入栈,可以预支它将来出栈的代价。无论是pop还是multipop带来的代价,都提前预支好,存在 account 中。然后将来真的 pop 了,再从 account 中取出预支的代价。
所以每个操作的 amortized cost:
- push = 1 + 1 = 2
- pop = 1 + (-1) = 0
- multipop = n + (-n) = 0
加号前是 actual cost。加号后是 accounting cost,正值为预支,负值为取出。最后计算得到的结果就是 amortized cost。
做题中还需证明一下 account 始终大于等于 0。这很简单:
pop 时 stack 如果不为空,那么这些元素 (设有 n 个) push 时存了 n,pop 时只取出 1,account 不会为 0,否则 stack 为空,pop 本身就没代价。multipop 时会让 account 刚好为 0。
Delete Larger Half
一个集合,支持插入元素,以及删除当前集合中前一半大的元素。
先看 actual cost:
- Insert: 1
- DeleteLargerHalf: tn
插入集合就当它为 1 的代价吧。删除 LargerHalf 代价是 $O(n)$ 的,为了方便分析,给乘一个 t 来表示 $O(n)$。意义是删除 larger half 时均摊到每一个元素头上的代价是 t。
Accounting scheme(即计算 accounting cost):
- Insert:2t
- DeleteLargerHalf: -tn
得到均摊 cost:
- Insert = 1 + 2t = O(1)
- DeleteLargerHalf = tn - tn = 0
证明也很简单。假设调用 DeleteLargerHalf 时集合有 n 个元素,这里被删除的 n/2 个元素 account 里有 tn 余额,刚好抵掉。总的 accounting cost 永远不小于 0。
这个插入时预支 2t 的意义不太明确,我无法给出令人信服的解释。所以这个分析有拿着答案写过程的意味。
DFS & BFS
简述深度优先和⼴度优先算法的思想,采⽤该算法思想能解决经典的图上搜索问题:topo序、任务调度、关键路径和强联通区域问题
DFS 思想
一旦遇到可行的路径,就优先进入。实现上的特点是递归,方法的特点是回溯。在搜索树上的表现是沿着一条路径走到底,然后在回溯到上一步走其他分支。
BFS 思想
优先遍历相邻的顶点,保证搜索树上同层的顶点能够一起遍历。实现上一般使用队列来保存将要遍历的顶点。
拓扑排序
PPT 中讲的是 DFS 求拓扑序。思想是从没有 visit 的点出发,走到不能走为止。这个不能走的点(没有出边,出度为0)是拓扑序最靠后的。回溯的时候再依次地增加拓扑序的计数器。这个方法得到的拓扑序在数值上是反着的。
任务调度
依据任务的依赖关系建 DAG,拓扑序就是任务的执行顺序。
关键路径
和拓扑排序的骨架一毛一样。只是注意走到不能走的点的 est(earliest start time) = 0,然后反着回溯路径上的点就行了,每一个点做一个贪心来选取最晚的 eft。
但是要写成代码还是比较麻烦,因为涉及记录这个路径。
强连通分量
Tarjan 算法。网上一堆。
用一个 dfn 数组维护 dfs 的时间戳,再用 low 数组维护能回到的最早祖宗的时间戳。关键点在于这一段代码。已经是可以直接嗯背的程度了:
for (unsigned i = 0; i < g[v].size(); i++) {/
ll id = g[v][i];
//如果这个点从没访问过,则先放问它,再用它更新low[v]的值
if (!dfn[id]) {
tanjan(id);
//他的儿子如果能连到更小的祖先节点,显然他也可以
low[v] = min(low[v], low[id]);
}
else {
//不在栈中的点,要么没有访问,要么不能到达id,所以只需要判断栈中的
if (vis[id]) {
low[v] = min(low[v], low[id]);
}
}
}
贪心
贪⼼法的思想和特点,⽤贪⼼法解决经典问题并能给出证明思路:换硬币、活动安排
贪心的思想和特点
局部最优就是整体最优。
换硬币
硬币有 1,5,10,25 面额。用尽可能少的硬币凑出金额 v。
贪心策略是优先用最大的面额。需要注意的是,某些面额(如1,5,12)下,这个贪心策略是错误的。
证明:
最优解使用面额情况为 (o1, o2, o3, o4)。即 v = 1 * o1 + 5 * o2 + 10 * o3 + 25 * o4。
则有 10 * o3 < 25,否则 10 * o3 > 25,那 10 * o3 >= 30,而这 30 可以用一个 5 和一个 25 替代,得到了更优的解。
同理,5 * o2 < 10, 1 * o1 < 5。
这就说明了最优解就是贪心解,因为贪心解优先用面额大的,等价于面额小的硬币的价值不大于面额大的硬币。
活动安排
每一个活动都要使用场地,活动的时间是 [si, fi)
。尽可能地安排多的活动,活动之间时间不冲突。
贪心策略是将 fi 升序排序。从前往后地无冲突地选取活动。
证明:
假设最优解安排的活动序列为 $e_{o_1}, e_{o_2}, …, e_{o_k}$。考虑逐个替换这些活动,以构造贪心解。
对于排序后的第一个活动 $e_1$,可以替换最优解的第一个活动,因为 $f_1 <= f_{o1}$,替换后对 $e_{o_2}$ 不影响。依次替换所有的活动,可以得到一个同样最优的贪心解。
DP
动态规划的思想和特点,针对经典问题能写出⼦问题的递归式,描述算法思想和伪代码:最长公共⼦序列相关问题和最优⼆叉搜索树相关问题
思想特点
子问题有最优子结构、记忆化、无后效性。
最长公共子序列
假设输入的两个序列为 $arr1$, $arr2$。算法思想是用 $c[i][j]$ 表示 $arr1[:i]$ 和 $arr[:j]$ 的最长公共子序列的长度。
边界:
$$ c[0][0] = 0 $$
递推:
$$ c[i][j] = \left{ \begin{aligned} &c[i-1][j-1]+1\quad&x[i]=y[i]\ &max{c[i][j-1], c[i-1][j]}\quad&x[i]\neq y[i] \end{aligned} \right. $$
最优二叉搜索树
思想是将原输入数组的一个区间作为子问题,记区间上问题的解为 $c[i][j]$。问题的递推是由枚举搜索树的根来实现的。
边界:
$$ c[i][i-1] = 0 $$
递推:
$$ c[i][j] = \sum_{k=i}^j p[k] + max_{r=i}^{j}{c[i][r-1] + c[r+1][j]} $$