教材课后习题回答
==============

## 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()`的递归版

不想写了......
