这一部分主要算法导论中递归式、堆排序和快速排序章节里选择的对我而言较有价值的题目。
练习4.1-1 证明 $T(n)=T(\lceil n/2\rceil)+1$ 的解为 $O(\lg n)$ 。
解答:猜测 $T(n)\leq c\cdot \lg(n-b)$ ($-b$ 的原因见《算法导论》-4.1代换法-一些细微问题),则用数学归纳法,如果其满足 $n/2$ 则对 $n$ 有:
$$T(n)=T(\lceil n/2\rceil)+1\leq c\cdot \lg(\lceil n/2\rceil-b)+1\leq c\cdot \lg(n+1-2b)-c\cdot \lg2+1\leq c\cdot \lg n$$
最后一个不等号在 $c>1$ 而且 $b>1$ 的情况下成立。
练习4.1-6 求解递归式 $T(n)=2T(\sqrt{n})+1$ 的渐进紧确解。
思路:令 $m=\lg n$ 则原递归式 $T(n)$ 可以表示为 $S(m)=T(2^{m})=2T(2^{m/2})+1=2S(m/2)+1$ 。猜测 $S(m)\leq cm-b$ ,数学归纳法证明,如果其去满足 $m/2$ 则对 $m$ 有:
$$S(m)=2S(m/2)+1\leq2(cm/2-b)+1=cm-2b+1\leq cm$$
代入到 $T(n)$ 中则 $T(n)\leq c\cdot \lg n$,即 $T(n)=\mathrm{O}(\lg n)$ 。
练习4.2-2 利用递归树证明 $T(n)=T(n/3)+T(2n/3)+cn$ 的解是 $\Omega(n\lg n)$ 。
思路:该递归树不是完全二叉树,叶子节点有深有浅,最浅的叶子节点是从根节点沿着路径 $n\rightarrow (1/3)n\rightarrow (1/3)^{2}n\rightarrow ...\rightarrow (1/3)^{k}n=1$,的层数 $k=\log_{3}n$ 。在该最浅的层数向上,每一层的代价都是 $cn$ 。因此 $T(n)\geq k\cdot cn = cn\cdot \log_{3}n=\Omega(n\lg n)$ 。
练习4.2-4 利用递归树求递归式 $T(n)=T(n-a)+T(a)+cn$ 的渐进紧确界。
思路:画出递归树,该递归树极不平衡,所有是右孩子的节点全部是叶子节点,根节点的代价是 $cn$ ,第 $i$ 层的代价是 $c(n+(1-i)a)$ 。一共有 $n/a$ 层(包括根节点的第 $0$ 层)。总代价:
$$T(n)=cn+\sum_{i=1}^{n/a-1}c(n+(1-i)a)=\Theta(n^{2})$$
练习4.2-5 利用递归树求递归式 $T(n)=T((1-a)n)+T(an)+cn$ 的渐进紧确界。
思路:做算法导论的题目,关键还是要理解题目的意图。这道题和上一道是姊妹题,意在说明递归树划分时,如果用常数划分,不管划分得多么均衡,其代价都是 $\Theta(n^{2})$ ,如果用比例划分,不管划分得多么失衡,其代价都是 $\Theta(n\lg n)$ ,其原因就是 $n$ 可以足够大,以突出常数划分的失衡和比例划分的均衡。练习4.2-2已经证明了其特殊情况的下界(一般情况也没有什么区别,$1/3$系数的影响最终进入了常数项),即 $T(n)=\Omega(n\lg n)$ 。现在只要证明 $T(n)=O(n\lg n)$ :如果 $T(n)\leq dn\lg n$ ,用代换法:
$$T(n)\leq d(1-a)n\lg(1-a)n+dan\lg an+cn=dn\lg n +dn(a\lg a+(1-a)\lg(1-a))$$
因为 $a\in (0,1)$ ,所以 $\lg a<0,\lg(1-a)<0$,$d$ 有合适的取值能够使得 $T(n)\leq dn\lg n$ 。
思考题4-2 某数组 $A[1...n]$ 含有从 $0$ 到 $n$ 的整数,但是缺了一个。另有一个数组 $B[0...n]$ 含有从 $0$ 到 $n$ 的所有整数。但是不能够单独访问数组中的某个整数,而只能访问某一位(整数是二进制的),就是说对数组 $A$ 的操作只能是“取 $A[i]$ 的第 $j$ 位”。给出一个代价为 $\Theta(n)$ 的算法,找出这个所缺的整数。
思路:二进制无符号整数一共 $\lceil \lg n\rceil$ 位。统计数组 $4$ 和 $B$ 中第 $1$ 位 为 $1$ 的整数个数(代价为 $\Theta(n)$),如果 $A$ 中第 $1$ 位为 $1$ 的整数个数小于 $B$ 中的,说明缺失的整数的第 $1$ 位是 $1$ ,否则缺失整数的第一位是 $0$ ;在数组 $A$ 和 $B$ 第 $1$ 位是 $1$ 或 $0$ 的整数里继续统计第二位为 $1$ 的整数个数(代价为 $\Theta(n/2)$)......直到确定缺失的整数的每一位。整个算法耗费的代价为:
$$\Theta(n)+\Theta(n/2)+...+\Theta(n/(2^{\lg n}))=\Theta(2n)=\Theta(n)$$
思考题4-5 斐波纳契数。定义生成函数(形式幂函数)为:
$$f(x)=\sum_{i=0}^{\infty}F_{i}z^{i}=0+z+z^{2}+2z^{3}+3z^{4}+5z^{5}+...$$
- 求证 $f(x)=z+z\cdot f(x)+z^{2}\cdot f(x)$ 。
解答:将 $f(x)$ 代入
$$f(x)=z+\sum_{i=0}^{\infty}F_{i}\cdot z^{i+1}+\sum_{i=0}^{\infty}F_{i}\cdot z^{i+2}=z+ z^{2}+\sum_{i=2}^{\infty}F_{i}\cdot z^{i+1}+\sum_{i=2}^{\infty}F_{i-1}\cdot z^{i+1}=z+z^{2}+\sum_{i=3}^{\infty}F_{i}\cdot z^{i}=\sum_{i=0}^{\infty}F_{i}\cdot z^{i}$$ - 证明:
$$f(x)=\frac{z}{1-z-z^{2}}=\frac{z}{(1-\phi z)(1+\hat{\phi}z)}=\frac{1}{\sqrt{5}}\cdot(\frac{1}{1-\phi z}-\frac{1}{1+\hat{\phi} z})$$
思路:将a中证明的式子变换就可以得到。
这一道题后面还有两道证明做不出来。
思考题4-6 VLSI(超大规模集成)芯片测试。有 $n$ 个设计完全相同的芯片,有一部分次品(坏的芯片)。这些芯片可以两两相互测试,报告另一个芯片是好的或是坏的。好的芯片总是如实报告另一个芯片的好坏,而坏的芯片的报告结果不可控制,即:
- A报告:B是好的;B报告:A是好的。则都是好的或者都是坏的。
- A报告:B是坏的;B报告:A是坏的。则至少有一个坏的。
- A报告:B是好的;B报告:A是坏的。则至少有一个坏的。
- A报告:B是坏的;B报告:A是好的。则至少有一个坏的。
已知好的芯片的数量大于 $n/2$ ,给出代价为 $\Theta(n)$ 的算法,找出所有好的芯片。
思路:只要找到一个好芯片,就可以用代价 $\Theta(n)$ 找出所有的好芯片。找一个好芯片的过程是这样:随机两两组合成 n/2 组互相测试,将互相报告是好芯片(上面的第一种)这种情况的组合挑出来,将每个组合拆下一个芯片,单独放在一起,构成原先 $n$ 芯片的一个子集(当子集的规模小于 $n/2$ 的时候,算法的代价才为 $\Theta(n)$)。对这个子集重复该方法,直到子集中只剩下一个芯片,该芯片就是好芯片。当然,这里忽略了 $n$ 为奇数的情况,那么就先假设 $n$ 是 $65536$ 之流吧。
需要证明的是这么几点:首先,需要证明该子集非空,即能选出这样的子集来:因为A中好芯片的数量大于 n/2 ,必定会出现好芯片和好芯片互相测试的组合,所以子集不可能为空;其次,需要证明子集的规模不大于 $n/2$ , 只有这样算法的代价才能够控制在 $\Theta(n)+\Theta(n/2)+...+\Theta(1)=\Theta(n)$ 以内:因为随机选取一对芯片中的一个加入子集,即使在最极端的情况下,好芯片仅仅和好芯片组合,坏芯片和坏芯片组合并共同“欺骗”测试者互相报告为好芯片,那么也仅仅有 $n/2$ 个芯片入选;最后,需要证明子集中好芯片的数量仍然大于子集元素个数的一半,使该条件传递下去直到子集中仅有一个元素:考虑没有被选入子集的芯片,要么是好芯片和坏芯片的组合,要么是坏芯片和坏芯片的组合,肯定没有好芯片和好芯片的组合,所以没有被选入子集的芯片中坏芯片的数量大于等于好芯片的数量,考虑到所有芯片中好芯片的数量大于坏芯片的数量,那么子集中好芯片的数量一定大于坏芯片的数量。
最后得到一个芯片,一定是好芯片,再用该芯片测试其他剩下的芯片,得到所有的好芯片。
思考题4-7 Monge矩阵。这道题比较长,难度不大,但结论却很有趣,因此把结论写下来,具体证明过程就略去。
一个 $m\times n$ 矩阵可以被称为Monge矩阵,当且仅当:对于所有 $i,j,k,l$ 有 $1\leq i<k\leq m$ 和 $1\leq j<l\leq m$,则:$A[i,j]+A[k,l]\leq A[i,l]+A[k,j]$ 。下面是一个Monge矩阵:
$$\begin{matrix}10 & 17 & 13\\ 17 & 22 & 16\\ 24 & 28 & 22\\11 & 13 & 6\end{matrix}$$
直观上,小的数分布在左上右下直线上,大的数分布在左下右上直线上。有这么几个有趣的结论:
- 一个矩阵为Monge矩阵的充要条件是:对所有的 $i=1,2,...,m-1$ 和 $j=1,2,...,n-1$ ,有:$A[i,j]+A[i+1,j+1]\leq A[i,j+1]+A[i+1,j]$ 。即当且仅当矩阵中的每一个 $2\times 2$的小矩阵都为Monge矩阵时,整个大矩阵才而且一定为Monge矩阵。
- 认为 $f(i)$ 表示第 $i$ 行中最小的数的列索引,如上面的矩阵,$f(1)=1,f(2)=f(3)=f(4)=3$ 。则有 $f(i)$ 是非严格递增函数,任意 $i<j$ 有 $f(i)<f(j)$ 。
- 从Monge矩阵中任意抽掉几行或几列,得到的新矩阵依然是Monger矩阵。这一个特性可以用来参加递归算法。
练习6.5-8 给出一个时间为 $O(n\lg k)$ 的算法,将 $k$ 个已排序的链表合并成一个排序链表的算法。$n$ 为输出链表中元素的个数。(最小堆做k路链接)。
思路:类似于最小堆排序,构建一个最小堆,每个节点表示一个已排序链表,节点值就是链表的第一个值,根节点就是第一个元素最小的链表。从根节点链表中取出第一个元素放入输出中,代价为 $O(1)$,这是最小的元素;然后根节点的值变大了,可能违反了最小堆性质,运行“保持最小堆性质”,代价为 $O(\lg k)$。每找出一个元素需要 $O(\lg k)+O(1)=O(\lg k)$ 代价,全部排序 $n$ 个元素需要的代价是 $O(n\lg k)$ 。
思考题6-2 对 $d$ 叉堆的分析。$d$ 叉堆和二叉堆类似,唯一的例外是每个非叶子节点有 $d$ 个子女而不是 $2$ 个。
- 如何在数组中表示 $d$ 叉堆(类似二叉堆)。我在草稿上找了半天规律,还是被卡住了……答案是:索引为 $i$ 的节点的第 $j$ 个孩子节点的索引为 $d\cdot (i-1)+j+1$,父节点的索引为 $(i-2)/d+1$ 。关键的思想就是:索引值为 $i$ 的节点的第 $j$ 个孩子节点,其前面的所有节点是:索引值为 $i$ 的节点的前面的所有节点($i-1$ 个)的子节点,加上自己的前 $j-1$ 个孩子节点,加上根节点。
- $d$ 叉堆的高度是?答案 $\lceil \log_{d}n\rceil$ 。
- 给出 $d$ 叉堆的Extract-Max,Insert,Increase-Key几个实现。都很简单,模仿二叉堆即可,只不过比较兄弟节点的时候要比较 $d$ 个节点。
思考题6-3 Young矩阵,也是一个难度不大但是有趣的问题。一个 $m\times n$ 的Young矩阵,每一行数据都是从左到右排序,每一列数据都是从上到下排序。矩阵中可以包含 $\infty$ 项。比如这样一个矩阵:
$$\begin{matrix}0 & 0 & 1 & 5\\ 2 & 5 & 5 & \infty\\ 3 & \infty & \infty & \infty\end{matrix}$$
- 如果该矩阵 $A$ 中, $A[1,1]=\infty$ ,则矩阵的所有元素都是$\infty$ ,$A[m,n]\neq \infty$ ,则矩阵的所有元素都不是$\infty$。这几乎是显然的。
- Young矩阵和二叉堆很类似,可以实现Extract-Min算法和Insert算法。
Extract-Min算法:取出一个元素 $A[i,j]$ ,然后比较 $A[i+1,j]$ 和 $A[i,j+1]$ ,选取小的一个填充到 $A[i,j]$ 中,比如 $A[i+1,j]$ 。由于 $A[i+1,j]\geq A[i,j]$,$A[i,j]\geq A[i-1,j]$,$A[i,j]\geq A[i,j-1]$ ,所以新填入的元素满足 $A_{new}[i,j]\geq A[i-1,j],A_{new}[i,j]\geq A[i,j-1]$ 。又因为 $A[i+1,j]\leq A[i,j+1]$ (之前比较过), $A[i+1,j]\leq A[i+2,j]$,后者即将成为 $A[i+1,j]$ ,所以满足 $A_{new}[i,j]\leq A[i,j+1],A_{new}[i,j]\leq A_{new}[i+1,j]$ ,四个方向都满足了循环不变式,再去对 $A[i+1,j]$ 执行这一套直到执行到遇到 $\infty$ 为止。从 $A[1,1]$ 开始执行就提取出了最小的元素。
Insert算法类似,只不过从 $A[m,n]$ 插入,遇到依次比较左边和上方的数,将较大的一个“挤”下来,同样可以证明循环不变式,跟Extract-Min基本一样。 - 利用 $n\times n$ 的Young矩阵进行排序的代价为 $O(n^{3})$ 。获取堆顶元素的代价为 $O(2n)$ ,共有 $n^{2}$ 个元素,代价为 $O(n^{3})$ 。注意这里实际上排了 $n^{2}$ 个元素,实际上排 $n$ 个元素的代价是 $O(n\sqrt{n})$ 。
- 在运行时间 $O(m+n)$ 内确定一个给定的数是否存在于矩阵中。从矩阵左下角开始,如果给定的数大就向右,否则向上。