Files
912-notes/thu_dsa/chp1/exercises.md
2019-10-15 19:08:32 +08:00

174 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
教材课后习题回答
==============
## Introduction.B
> 举例:随问题实例规模增大,同一算法的求解时间可能波动甚至下降
比如求解`hailstone(n)`的整个序列,或者求它的序列长度的算法。
> 在哪些方面现代电子计算机仍未达到RAM模型的要求
首先在RAM模型中寄存器的数量是不限的在现代计算机中则因为指令集的设计以及成本等问题寄存器的数量往往是有限的并且数量很少。
其次在RAM模型中所有基本操作都需要相同的时间这在现代计算机中也难以达到即不同的指令执行需要的时钟周期不同。在实际中是采用多次存储器架构来实现对有限的寄存器的扩充的在这些不同层次的存储器之间传递数据所需要的时间往往具有数量级级别的差别。
> 在`TM``RAM`等模型中衡量算法效率,为何通常只需考察运行的时间?
在渐进复杂度的意义下,任一算法的任何一次运行过程中所消耗的存储空间,都不会多于其间所执行的指令条数。这是因为,一条基本操作只能涉及常数规模的存储空间。
> 图灵机Increase中以下这条指令可否省略
```
(<, #, 1, R, >)
```
不可以。该指令是对应于原来的二进制整数只有全1的后缀却没有一个为零的前缀的情形对于这种情况应该把最前面一个'#'改写成'1',来表示递增之后的最高位。
> 设计一个图灵机,实现对正整数的减一(Decrease)功能
由于是正整数,该问题其实就等价于将一个带有全'0'后缀的'1'做Decrease操作其中全'0'后缀的长度可以等于零,'1'左侧也还可以有其他数字,但是对结果没有影响。
算法是,将全'0'的后缀反转为'1',将原来高位的'1'反转为'0',有以下三条操作:
```c
(<, 0; 1, L, <);
(<, 1; 0, R, >);
(>, #; #, R, h);//halt
```
## Introduction.E
> 做递归跟踪分析时,为什么递归调用语句本身可不统计?
因为递归语句本身的执行时间,是计入了对应的子实例当中,对于当前实例而言,只需要考虑函数调用的跳转执行,该执行的执行时间是`O(1)`
> 试用递归跟踪法,分析`fib()`二分递归版的复杂度。通过递归跟踪,解释该版本复杂度过高的原因
可以针对`fib(n)`画出递归跟踪图,如下:
```
fib(n)
/ \
n-1 n-2
/ \ / \
n-2 n-3 n-3 n-4
/
....
```
可以看到,该递归跟踪树的高度为`h = n - 1`,并且其中最高的满二叉树子树高度为`n / 2`,因此二分递归版本的复杂度下界为
$$
1 + 2 + 4 + \cdots + 2^{\frac{n}{2}} = \Omega(2^{\frac{n}{2}}) = \Omega(\sqrt{2}^n)
$$
而复杂度上界为
$$
1 + 2 + 4 + \cdots + 2^{n - 1} = O(2^n)
$$
从上述递归跟踪图中也可以看到,二分递归版本复杂度过高的原因是其中具有大量重复计算的值。一般地,设`fib(k)`的出现次数为`nfib(k)`,则有每一个`fib(k+1)``fib(k+2)`都会产生一个`fib(k)`,因此
```c
nfib(k) = nfib(k+1) + nfib(k+2), 1 <= k <= n
```
并且有
```c
nfib(n) = 1, nfib(n - 1) = 1
```
因此,
```c
nfib(k) = fib(n - k + 1), 1 <= k <= n
```
> 递归算法的空间复杂度,主要取决于什么因素?
递归深度。
> 本节数组求和问题的两个(线性和二分)递归算法时间复杂度相同,空间呢?
每一次递归子问题的空间复杂度都是`O(1)`
对于线性递归算法,递归深度为`O(n)`,因此空间复杂度为`O(n)`
而二分递归算法,递归深度为`O(logn)`,因此空间复杂度为`O(logn)`
> 自学递推式的一般求解性方法及规律`Master Theorem`。
看这篇总结[master_theorem](master_theorem.md)
## Introduction.F
> 本节所介绍的迭代式`LCS`算法,似乎需要记录每个子问题的局部解,从而导致空间复杂度激增。实际上,这既不现实,亦无必要。试改进该算法,使得每个子问题只需要常数空间,即可保证最终得到`LCS`的组成(而非仅仅长度)
这里说的`LCS`算法,应该是返回`LCS`的组成的,并没有在邓公的课件、教材、网课上找到,不过可以从返回`LCS`长度的迭代算法中推广得到。
`LCS`长度的迭代式算法中,需要维护一个`m*n`的向量,来保存各个子问题的`LCS`长度,仿照其思路,在上述`m*n`的向量中,保存各个子问题的`LCS`序列,即可构造出一种`LCS`组成的算法。容易看出,由于每个子问题都需要保存当前的`LCS`,子问题的空间复杂度为`O(min(m, n))`,因此整体的空间复杂度为`O(m*n(min(m, n)))`,的确增加了不少,题目就是要求对这种情况进行改进。
可以注意到,上述算法空间复杂度激增的原因,是保存了大量重复的内容。实际上,构成最终`LCS`序列的字符,只在`O(min(m, n))`个位置出现。因此可以仿照图剪枝的策略,在填充向量时动态地记录当前的移动方向,即是从对角线更新,还是从左侧元素更新,还是从上侧元素更新。遍历完成后,再沿着前面标记的方向进行一次反向的遍历,在该过程中记录`LCS`各个字符,从而可以得到整体的`LCS`的组成。该反向遍历至多只会进行`O(m + n)`次,对整体的时间复杂度`O(m*n)`没有显著影响,同时每个子问题的空间复杂度都下降到`O(1)`。该算法的代码如下:
```cpp
string lcsIt(string one, string two, int len1, int len2){
string lcs;
if (len1 == 0 || len2 == 0) return lcs;
vector<vector<State>> states(len1 + 1, vector<State>(len2 + 1));
for(int i = 0; i != len1; ++i){
for(int j = 0; j != len2; ++j){
if(one[i] == two[j]){
states[i + 1][j + 1].len = states[i][j].len + 1;
states[i + 1][j + 1].dir = DIAGON;
}
else{
if(states[i][j + 1].len < states[i + 1][j].len){
states[i + 1][j + 1].len = states[i + 1][j].len;
states[i + 1][j + 1].dir = LEFT;
}else{
states[i + 1][j + 1].len = states[i][j + 1].len;
states[i + 1][j + 1].dir = UPPER;
}
}
}
}
lcs.resize(states[len1][len2].len);
int pos = lcs.size();
for(int i = len1, j = len2; i > 0 && j > 0; ){
switch(states[i][j].dir){
case DIAGON:
lcs[--pos] = one[i - 1];
--i, --j;
break;
case UPPER:
--i;
break;
case LEFT:
--j;
break;
default:
exit(-1);
}
}
return lcs;
}
```
需要指出的是,对于多个`LCS`的情形,该算法只会返回其中的一个解,因为在算法中对于两个子问题的`LCS`长度相同的情况,是优先选择上面`UPPER`的子问题。
> 考查序列`A = "immaculate`和`B = "computer`。1它们的`LCS`是什么2这里的解是否唯一是否有歧义性3按照本节所给的算法找出的是哪一个解
+ 1`"mute"``"cute"`
+ 2所以显然不唯一有歧义性。
+ 3按照上面给的算法优先从上方进行更新即优先选择序列`A`更靠前的字符,即找出的是`"mute"`
> 实现`LCS`算法的递归版和迭代版,并通过实测比较运行时间。
代码和测试分别放在[lcs.cpp](lcs/lcs.cpp)和[test_lcs.cpp](lcs/test_lcs.cpp)了。递归版的确很慢......
> 采用`memorization`策略,改进`fib()`和`LCS()`的递归版
不想写了......