(学习笔记,无什参考价值!)
1 问题
2 算法
2.1 Brute-force LCS algorithm
- 检查每一个subsequence是否是yyy的子列时,遍历yyy的每一个元素,看是否依次可以全部覆盖subsequence所有元素,所以其复杂度为O(n)O(n)O(n);
2.2 Dynamic-programming hallmark #1
-
动态规划的第一特征–最优子结构,下面用定理的方式证明这种特征;
-
这个性质是说,一个规模稍大的最优解问题建立在一些规模较小的最优解问题基础之上,由此解决问题的思路就有两个方向:自顶向下的递归方式、自底向上的方式。
-
很容易可以推出下面的(数量上的)性质:
-
由上面的递归公式,首先想到的是使用递归实现:
-
下面用递归树分析递归实现的时间复杂度:
-
递归树的高度就是指数,所以时间复杂度是指数级别,和暴力解法没有什么大提高!
-
从上面递归树图中观察到一个现象:递归树中存在大量重复的子问题,这些是导致递归算法龟速的原因,也是动态规划诞生的另外一个必要条件。
2.3 Dynamic-programming hallmark #2
-
动态规划的第二特征–大量重复子问题;
-
有了这个特征,此时有两条改进思路,一是在递归过程中,存储算过的每一个子问题。算法描述如下:
- 另外一个改进思路是自底向上算法:
- 在上面自底向上的计算过程中,同时做了最优数值记录和最优路径记录这两件事,路径本质是:记录每一步由哪一个子问题得到父问题的解,路径的起点是(1,1),终点是(m,n),从终点到起点依照“来的时候做的箭头标记”,可以完整的回到起点,其中路径中标记’↘’路标的坐标点是最优解中的点。
- 手工演示如下:
2.4 打印最优解
- 依照上面的分析可以很容易找到一个倒序的LCS,结合递归实现原理,正好可以正序打印出LCS,算法如下:
- 看这段代码,直觉上感觉还是倒序输出LCS,但是仔细考虑一下递归底层的运行过程,递归中的“递”就是入栈,递进;“归”就是出栈,回归,以倒序压入到栈中,以正序出栈;
3 代码实现
# -*- coding: utf-8 -*-
import numpy as npdef LCS_LENGTH(X, Y):m = len(X)n = len(Y)b = np.array([0] * (m * n)).reshape((m, n))c = np.array([0] * (m * n)).reshape((m, n))for i in range(1, m):c[i][0] = 0for j in range(0, n):c[0][j] = 0for i in range(1, m):for j in range(1, n):if X[i] == Y[j]:c[i][j] = c[i - 1][j - 1] + 1b[i][j] = 0elif c[i - 1][j] >= c[i][j - 1]:c[i][j] = c[i - 1][j]b[i][j] = '1'else:c[i][j] = c[i][j - 1]b[i][j] = '-1'return c, bdef PRINT_LCS(b, X, i, j):if i == 0 or j == 0:returnif b[i][j] == 0:PRINT_LCS(b, X, i - 1, j - 1)print(X[i])elif b[i][j] == 1:PRINT_LCS(b, X, i - 1, j)else:PRINT_LCS(b, X, i, j - 1)if __name__ == '__main__':X = ['x', 'A', 'B', 'C', 'B', 'D', 'A', 'B']Y = ['x', 'B', 'D', 'C', 'A', 'B', 'A']c, b = LCS_LENGTH(X, Y)PRINT_LCS(b, X, len(X) - 1, len(Y) - 1)
运行结果:
B
C
B
A