7.3 KiB
教材课后习题回答
Introduction.B
举例:随问题实例规模增大,同一算法的求解时间可能波动甚至下降
比如求解hailstone(n)的整个序列,或者求它的序列长度的算法。
在哪些方面,现代电子计算机仍未达到RAM模型的要求?
首先,在RAM模型中寄存器的数量是不限的,在现代计算机中则因为指令集的设计以及成本等问题,寄存器的数量往往是有限的,并且数量很少。 其次,在RAM模型中,所有基本操作都需要相同的时间,这在现代计算机中也难以达到,即不同的指令执行需要的时钟周期不同。在实际中是采用多次存储器架构来实现对有限的寄存器的扩充的,在这些不同层次的存储器之间传递数据,所需要的时间往往具有数量级级别的差别。
在
TM,RAM等模型中衡量算法效率,为何通常只需考察运行的时间?
在渐进复杂度的意义下,任一算法的任何一次运行过程中所消耗的存储空间,都不会多于其间所执行的指令条数。这是因为,一条基本操作只能涉及常数规模的存储空间。
图灵机Increase中,以下这条指令可否省略?
(<, #, 1, R, >)
不可以。该指令是对应于原来的二进制整数,只有全1的后缀,却没有一个为零的前缀的情形;对于这种情况,应该把最前面一个'#'改写成'1',来表示递增之后的最高位。
设计一个图灵机,实现对正整数的减一(Decrease)功能
由于是正整数,该问题其实就等价于将一个带有全'0'后缀的'1'做Decrease操作,其中全'0'后缀的长度可以等于零,'1'左侧也还可以有其他数字,但是对结果没有影响。
算法是,将全'0'的后缀反转为'1',将原来高位的'1'反转为'0',有以下三条操作:
(<, 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),因此
nfib(k) = nfib(k+1) + nfib(k+2), 1 <= k <= n
并且有
nfib(n) = 1, nfib(n - 1) = 1
因此,
nfib(k) = fib(n - k + 1), 1 <= k <= n
递归算法的空间复杂度,主要取决于什么因素?
递归深度。
本节数组求和问题的两个(线性和二分)递归算法时间复杂度相同,空间呢?
每一次递归子问题的空间复杂度都是O(1)。
对于线性递归算法,递归深度为O(n),因此空间复杂度为O(n)。
而二分递归算法,递归深度为O(logn),因此空间复杂度为O(logn)。
自学递推式的一般求解性方法及规律
Master Theorem。
看这篇总结master_theorem
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)。该算法的代码如下:
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和test_lcs.cpp了。递归版的确很慢......
采用
memorization策略,改进fib()和LCS()的递归版
不想写了......