最长公共子序列问题的动态规划解法
就我目前学习动态规划的程度,我觉得使用动态规划的方法解决一个问题,最难的是:
将问题的解决过程用递归的形式表达出来。
使用递归的形式表达出来之后,可以判断是否有重叠的子问题,或者我们在构造递归表达形式的过程中,就会进行判断。另外,可以根据递归的参数,设计数组存储计算的结果,也就是有些博客中提到的填表过程。有很多时候,觉得动态规划解法的代码中,数组运用到很是巧妙,这个主要还是来自递归形式中。 在上一篇的博客中,递归形式其实比较明显。在这一篇,我尝试分析最长公共子序列问题的动态规划解法儿,加深递归表达式产生的过程,也就是自顶向下的分析过程。 最长公共子序列问题:一个数列 S,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。 对于一般性的最长公共子序列问题(LCS)是NP-hard问题。但是,当数列数目确定的时候,可以通过动态规划的方法,在多项式时间内完成。 这里我们要注意,子序列中的元素,在圆数列中,不一定是连续的。例如ABCDEF,ABC、ABD都是子序列。如果是连续的话,就不是用动态规划的方法进行解决,而是采用后缀数树、或者后缀数组的方法进行,这个在我前边的博客有介绍,可以参考,我有时间也需要复习。 来看一个具体的例子:POJ1458这个题目就是典型的求最长公共子序列的问题,只输出最长子序列的长度即可。而且只是两个字符串的最长公共子序列。下面分析递归形式的产生过程。令第一个字符串为:a[1...i],第二个字符串为:b[1...j]。 当某一个字符串长度为0的时候,显然最长子序列长度就是0. 当某一个字符串长度为1的时候,假设a的长度为1,则a就是a[1]。两种情况:
- a[1]==b[j],则LCS=1,可以理解为max{LCS(a[0],b[j-1])} + 1。前面的值为0.
- 没有一个b[j]与a[1]相等,则LCS=0。理解为max{LCS(a[1], b[j-1]),LCS(a[0],b[j])}=0
当某一个字符串儿长度为2的时候,假设a的长度为2,则a就包含a[1],a[2]。如下分析:
- 当a[1]=b[j1]且a[2]=b[j2],则LCS=2:
- 从a[1]开始考虑,当a[1]和某个b[j]相等时,LCS=1,这个和上面1中的一样。
- 然后考虑a[2],当a[2]与某个b[j]相等时,则说明,LCS的大小,在a[1]的基础上,又增加1,即为:max{LCS(a[2],b[j])}=max{LCS(a[1]b[j-1])} + 1=2.其中j1 <= j - 1
- 当a[1]=b[j1]且a[2]!=b[j2],这时候LCS=1:
- a[1]分析同上;
- 对于分析a[2]的时候,由于找不到相等的。所以,只能依赖前面的结果。即 max{LCS(a[2],b[j2])}=max{LCS(a[1], b[j2]), LCS(a[2],b[j2-1])}
通过上面的分析,我们得到如下递归表达式:
- 当i=j=1时,
- 若a[1]=b[1],则LCS=1,否则LCS=0
- 当i!=1或者j!=1时:
- 当a[i]=b[j]时:LCS(a[i],b[j])=LCS(a[i-1], b[j-1])+1
- 当a[i]!=b[j]的时候:LCS(a[i],b[j]) = max(LCS (a[i -1, b[j]),LCS(a[i],b[j-1]))
上面的分析,并不透彻,我理解的还不够到位,继续加深理解。这道题目的核心代码如下:
int[][] lcs = new int[len1 + 1][len2 + 1]; for(int i = 0;i <= len1;i++) lcs[i][0] = 0; for(int i = 0;i <= len2;i++) lcs[0][i] = 0; for (int i = 1; i <= len1; ++i) for (int j = 1; j <= len2;++j) { if (ss[0].charAt(i - 1) == ss[1].charAt(j - 1)) lcs[i][j] = lcs[i - 1][j - 1] + 1; else lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]); } System.out.println(lcs[len1][len2]);
【引用】
http://zh.wikipedia.org/wiki/%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97