比较好的最长公共子序列的变形
2015-08-24 08:43
627 查看
题解来自:
http://hihocoder.com/discuss/question/2111
给定只包含字母的两个字符串A,B,求A,B两个字符串的最长公共子序列,要求构成子序列的子串长度都必须大于等于3。
比如
这里我们要注意
子串:连续的元素
子序列:不连续的元素
比如
首先我们来复习一道经典的题目:
给定只包含字母的两个字符串A,B,求A,B两个字符串的最长公共子序列。
比如
对于最长公共子序列,我们知道解法为
而这一道题目是在最长公共子序列上加入了一个条件:构成最长公共子序列的每一个子串长度必须大于等于3.
一个简单的想法:我们求出最长公共子序列,然后将其中长度小于3的部分去掉。
显然,这是不对的。
举个例子:
因为在
此时构成
这显然不正确,因为实际有符合题意要求的公共子序列:
其中包含有长度为3的公共子序列。
对最大公共子序列的结果进行再次处理这个方法不可行,那么我们只能从计算公共子序列的算法着手。
首先我想我们可以做一个预处理,用
a=
预处理的伪代码为:
有了这个预处理的数组,我们可以在原来最大公共子序列上做这样一个改进:
这个改进的意义为:当我们出现一个长度大于3的子串时,我们就直接将这个子串合并入我们的子序列。
加入这个改进后,我们通过了样例的数据,这样看上去似乎就应该没什么问题了。
然而事实并不是这样,在这道题目中还隐藏着陷阱:
比如
根据我们算法,上面这个例子算出的结果为4,然而其实际的结果应该为6,即
那么出错的原因在哪?就在字符串
我们计算结果出4是因为将
在DP的过程中f[6][7] = 4,我们使用了dp[6][7] = dp[2][3] + 4,而dp[2][3] = 0,所以dp[6][7] = 4。
而实际上的最有解是将f[6][7]看作3,dp[6][7] = dp[3][4] + 3,其中dp[3][4] = 3,得到了dp[6][7] = 6。
也就是说,如果我们将f[i][j]>3的子串进行分割,有可能得到更优的情况。因此我们需要进一步的改进:
但是这样的改进使得整个算法的时间复杂度变为了O(n^3),当n=2100时,有可能会超时。
让我们考虑一下如何进一步改进这个算法。以上算法复杂度高的地方在于对于每一个(i, j),我们为了计算dp[i][j]都需要枚举分割长度k:
这一步实际上我们计算了
同时
我们可以发现,dp1[i][j]的展开式中除了dp[i-3][j-3]+3这一项,是与dp1[i-1][j-1]中的每一项一一对应的,并且刚好大1。所以实际上dp[i-1][j-1]计算时枚举过分割长度,我们并不需要再次计算:
最后得到我们新的伪代码如下,其中dp[i][j][0]对应上文分析中的dp[i][j], dp[i][j][1]对应dp1[i][j]:
至此我这道题目也算是完整的解出了。
这个题目是在经典的动态规划题目《最长公共子序列》上做了一点修改。虽然只增加了一个条件,不过难度增大很多。能想出一个复杂度是O(n^2)的正确算法不是很容易,需要仔细分析清楚各种情况。一不小心就会掉进各种陷阱里。
很多选手都能够想到经典最长子序列的改进算法而获得80分。
剩下的测试点则对应了算法分析中提到的陷阱,所以能否找出这种特殊的例子也是解决这道题的关键。
不过微软的出题人似乎没有想太为难大家,数据并不是很强。在实际的比赛中,O(n^3)的算法也能拿到满分,最终该题目的通过率为9%。
很多O(N^2)的程序不能通过
http://hihocoder.com/discuss/question/2111
题意分析
给定只包含字母的两个字符串A,B,求A,B两个字符串的最长公共子序列,要求构成子序列的子串长度都必须大于等于3。比如
"abcdefghijklmn"和
"ababceghjklmn",其最长满足题意要求的子序列为
"abcjklmn",其由公共子串
"abc"和
"jklmn"组成。
这里我们要注意
子串和
子序列的区别:
子串:连续的元素
子序列:不连续的元素
比如
"abcdefghijklmn"和
"ababceghjklmn"的最长公共子串就只是
"jklmn"了。
算法分析
首先我们来复习一道经典的题目:给定只包含字母的两个字符串A,B,求A,B两个字符串的最长公共子序列。
比如
"abcde"和
"abdfg"的最长公共子序列为
"abd"
对于最长公共子序列,我们知道解法为
[code]dp[0][0..j] = 0 // 边界 dp[0..i][0] = 0 // 边界 For i = 1 .. n For j = 1 .. m If a[i] == b[j] Then dp[i][j] = dp[i - 1][j - 1] + 1 Else dp[i][j] = Max(dp[i - 1][j], dp[i][j - 1]) End If End For End For
而这一道题目是在最长公共子序列上加入了一个条件:构成最长公共子序列的每一个子串长度必须大于等于3.
一个简单的想法:我们求出最长公共子序列,然后将其中长度小于3的部分去掉。
显然,这是不对的。
举个例子:
"aaabaa"和
"acaaaca"的最长子序列为
"aaaaa"。其对应关系为:
[code]a aaba a acaa aca
因为在
"acaaaca"中第一个字母a长度为1,所以我们需要去掉它,对应的我们也去掉了
"aaabaa"中第一个字母a。
[code]. aaba a . caa aca
此时构成
"aaabaa"和
"acaaaca"公共子序列的3个子串为
"aa",
"a"和
"a",长度都小于了3,所以全部删去,则得到了新的公共子序列长度为0。
这显然不正确,因为实际有符合题意要求的公共子序列:
[code] aaa baa ac aaa ca
其中包含有长度为3的公共子序列。
对最大公共子序列的结果进行再次处理这个方法不可行,那么我们只能从计算公共子序列的算法着手。
首先我想我们可以做一个预处理,用
f[i][j]表示以a的第i个字母作为结尾的前缀和*以b的第j个字母作为结尾的前缀*的公共后缀的长度。这样看上去似乎很绕,不如举个例子:
a=
"abcd"和b=
"acbc"。
f[3][4]的就表示a[1..3]和b[1..4]的公共后缀的长度,其中a[1..3]=
"abc",b[1..4]=
"acbc",其公共后缀为
"bc",所以f[3][4]=2.
预处理的伪代码为:
[code]For i = 1 .. n For j = 1 .. m If a[i] == b[j] Then f[i][j] = f[i - 1][j - 1] + 1 Else f[i][j] = 0 End If End For End For
有了这个预处理的数组,我们可以在原来最大公共子序列上做这样一个改进:
[code]dp[0][0..j] = 0 // 边界 dp[0..i][0] = 0 // 边界 For i = 1 .. n For j = 1 .. m If f[i][j] >= 3 Then // 改进 dp[i][j] = dp[i - f[i][j]][j - f[i][j]] + f[i][j] Else dp[i][j] = Max(dp[i - 1][j], dp[i][j - 1]) End If End For End For
这个改进的意义为:当我们出现一个长度大于3的子串时,我们就直接将这个子串合并入我们的子序列。
加入这个改进后,我们通过了样例的数据,这样看上去似乎就应该没什么问题了。
然而事实并不是这样,在这道题目中还隐藏着陷阱:
比如
"abcdef"和
"abcxcdef"
根据我们算法,上面这个例子算出的结果为4,然而其实际的结果应该为6,即
"abc"和
"def"两个公共子串构成的子序列。
那么出错的原因在哪?就在字符串
"cdef"上。
我们计算结果出4是因为将
"cdef"看做了一个整体,而将
"abcdef"分割成了
"ab"和
"cdef"。
在DP的过程中f[6][7] = 4,我们使用了dp[6][7] = dp[2][3] + 4,而dp[2][3] = 0,所以dp[6][7] = 4。
[code]ab cdef abcxcdef
而实际上的最有解是将f[6][7]看作3,dp[6][7] = dp[3][4] + 3,其中dp[3][4] = 3,得到了dp[6][7] = 6。
[code]abc def abcxcdef
也就是说,如果我们将f[i][j]>3的子串进行分割,有可能得到更优的情况。因此我们需要进一步的改进:
[code]dp[0][0..j] = 0 // 边界 dp[0..i][0] = 0 // 边界 For i = 1 .. n For j = 1 .. m dp[i][j] = 0 If f[i][j] >= 3 Then // 改进 For k = 3 .. f[i][j] // 枚举分割长度 dp[i][j] = Max(dp[i][j], dp[i - k][j - k] + k) End For Else dp[i][j] = Max(dp[i - 1][j], dp[i][j - 1]) End If End For End For
但是这样的改进使得整个算法的时间复杂度变为了O(n^3),当n=2100时,有可能会超时。
让我们考虑一下如何进一步改进这个算法。以上算法复杂度高的地方在于对于每一个(i, j),我们为了计算dp[i][j]都需要枚举分割长度k:
[code]For k = 3 .. f[i][j] // 枚举分割长度 dp[i][j] = Max(dp[i][j], dp[i - k][j - k] + k) End For
这一步实际上我们计算了
max{dp[i-k][j-k]+k}, k=3..f[i][j]。我们不妨把它记作dp1[i][j],即:
[code]dp1[i][j] = max{dp[i-k][j-k]+k} = max{dp[i-3][j-3]+3, dp[i-4][j-4]+4, dp[i-5][j-5]+5, ... }
同时
[code]dp1[i-1][j-1] = max{dp[i-1-3][j-1-3]+3, dp[i-1-4][j-1-4] + 4, dp[i-1-5][j-1-5]+5 ... } = max{dp[i-4][j-4]+3, dp[i-5][j-5]+4, dp[i-6][j-6]+5, ... }
我们可以发现,dp1[i][j]的展开式中除了dp[i-3][j-3]+3这一项,是与dp1[i-1][j-1]中的每一项一一对应的,并且刚好大1。所以实际上dp[i-1][j-1]计算时枚举过分割长度,我们并不需要再次计算:
[code]dp1[i][j] = max{dp1[i-1][j-1] + 1, dp[i-3][j-3]+3}
最后得到我们新的伪代码如下,其中dp[i][j][0]对应上文分析中的dp[i][j], dp[i][j][1]对应dp1[i][j]:
[code]dp[0][0..j][0..1] = 0 // 边界 dp[0..i][0][0..1] = 0 // 边界 For i = 1 .. n For j = 1 .. m dp[i][j][1] = 0 If f[i][j] >= 3 Then // 改进 dp[i][j][1] = Max(dp[i][j][1], dp[i - 3][j - 3][0] + 3) // 以长度3为分割 If (f[i][j] > 3) Then //按照dp[i-1][j-1][1]的分割方式分割,即直接将(i,j)接在(i-1,j-1)后面 dp[i][j][1] = Max(dp[i][j][1], dp[i - 1][j - 1][1] + 1) End If End If dp[i][j][0] = Max(dp[i-1][j][0], dp[i][j-1][0], dp[i][j][1]) End For End For
至此我这道题目也算是完整的解出了。
结果分析
这个题目是在经典的动态规划题目《最长公共子序列》上做了一点修改。虽然只增加了一个条件,不过难度增大很多。能想出一个复杂度是O(n^2)的正确算法不是很容易,需要仔细分析清楚各种情况。一不小心就会掉进各种陷阱里。很多选手都能够想到经典最长子序列的改进算法而获得80分。
剩下的测试点则对应了算法分析中提到的陷阱,所以能否找出这种特殊的例子也是解决这道题的关键。
不过微软的出题人似乎没有想太为难大家,数据并不是很强。在实际的比赛中,O(n^3)的算法也能拿到满分,最终该题目的通过率为9%。
很多O(N^2)的程序不能通过
"babad"和
"babacabad"这组数据。
#include <cstring> #include <algorithm> #include <cstdio> #include <iostream> using namespace std; const int N = 2200; int d [2]; int f ,n,m; char str ,src ; int dp(){ memset(d,0,sizeof(d)); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ d[i][j][1]=0; if(f[i][j]>=3) d[i][j][1]=d[i-3][j-3][0]+3; if(f[i][j]> 3) d[i][j][1]=max(d[i][j][1],d[i-1][j-1][1]+1); d[i][j][0]=max(max(d[i-1][j][0],d[i][j-1][0]),d[i][j][1]); } return d [m][0]; } int main() { while(scanf("%s %s",str+1,src+1)!=EOF){ n = strlen(str+1),m=strlen(src+1); for(int i=1;i<=n;i++) f[i][0]=0; for(int i=1;i<=m;i++) f[0][i]=0; f[0][0]=0; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ if(str[i]==src[j]) f[i][j]=f[i-1][j-1]+1; else f[i][j]=0; } printf("%d\n",dp()); } return 0; }
相关文章推荐
- 5)Win10-UWA开发 XAML 语言等(必须掌握)
- 使用jquery点击一个实现button或连接,进行以下div显示,在点击隐藏
- unity中 拖拽任意的对象
- 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释
- 周期-14. 最大公约数与最小公倍数(15)
- 初识MVC
- 初识MVC
- POJ2778 DNA Sequence AC自动机+快速幂+DP
- 每天5道面试题(一)
- Android异步操作总结
- 3.2 声明和使用变量;3.2.1 变量的基本概念;3.2.2 声明变量;3.2.3 变量的作用域;3.2.4 变量赋值
- mco_trans_log 函数使用注意事项
- Oracle已经启动,连接sqlplus后,进行查询,出现下面错误
- hdu5419--Victor and Toys(枚举)
- 回调函数
- c#/.net 基于文件流FileStream读写的文本操作小程序
- Docker基础技术:AUFS
- mysql: Can’t start server: can’t create PID file:Permission denied 错误解决
- shutdown
- Java中的数组