题解 P2679 【子串】
这个奇怪的DP,那年我只得了20分我也是醉了
一个非常绕的dp,参考了一部分别人的解法理解了,觉得更好理解一些,分享给大家。
版权大概归属于codevs 34504和东营胜利一中的GaoTY哥哥,据说他也在洛谷~
希望管理员不要爆破,wiki的目的我想根本在于分享和促进大家更好地写代码,贡献分啥的不要也罢。
思路
初始的想法是设置f[i][j][k]表示A串前i个字符里,使用了k个子串,来匹配了B串前j个字符。
所以可列出最基本的方程f[i][j][k]=f[i-1][j-1][k-1]+f[i-1][j-1][k],翻译过来就是
A串前i个字符里,使用了k个子串,匹配了B串前j个字符的方案数,等价于 单独使用第i-1个字符作为一个子串的方案数和不单独使用的方案数(包括不使用和和前面连在一起)
但正如括号里所说,这个递归方程未能解决不使用和和前面连在一起的问题,所以我们要添加一维0/1维,来表示使用了当前字符和没使用当前的字符。
所以动态转移方程变成了两个
-
f[i][j][k][0]=f[i-1][j][k][0]+f[i-1][j][k][1],翻译是 如果第i位不使用,那么方案数就是 第i-1位未使用时,前i-1位匹配前j位使用k个子串 + 第i-1位使用时,前i-1位匹配前j位使用k个子串。
-
**(a[i]==b[j])f[i][j][k][1]=f[i-1][j-1][k-1][0]+f[i-1][j-1][k][1]+f[i-1][j-1][k-1][1] 翻译是 如果第i位使用,意味着a[i]==b[j],那么方案数就是 第i-1位未使用而第i位作为一个新子串 + 第i位和前面连在一起,不单独使用 + 第i-1位使用了,但第i位仍作为一个新子串。
这样转移方程就出来了。
我们再来思考边界条件。边界条件两种情况。
-
对于A子串前i位,匹配B串第1个字符,那么只可能使用1个字符串,这种情况如果第i位不进行匹配,那么方案数就是之前所有能与第1位匹配的字符,所以f[i][1][1][0]=sum(a[1~i-1]==b[1])。
-
同上,但如果B串第一位和A串第一位匹配了话,那么f[i][1][1][1]=1是显然的。
综上转移方程
- **f[i][j][k][0]=f[i-1][j][k][0]+f[i-1][j][k][1]**
- **(a[i]==b[j])f[i][j][k][1]=f[i-1][j-1][k-1][0]+f[i-1][j-1][k][1]+f[i-1][j-1][k-1][1]**
- **f[i][1][1][0]=sum(a[1~i-1]==b[1])**
- **(a[i]==b[1])f[i][1][1][1]=1 **
细节
空间复杂度
这个题如果开1000*200*200*2的话,掐指一算可得你需要大约300MB空间……
但不难发现,方程里只有i和i-1被来回调用,其余例如i-2,i-3在接下来的动态规划里都废了,所以那些空间是不需要的,我们可以把数组滚动起来。
我们开一个f[2][201][201][2](p党请自行设置为0~x)
使用一个now和一个pre变量,__谁为1谁为0无所谓__,只要改变i就交换就可以了。
这样空间量缩小至1/50,6MB就够了。
清空滚动数组
我开始写到这里时,怎么想都觉得应该过了,但样例都挖。
后来仔细一想,明白了,滚动数组的时候,可能存在本应该f[i][1][1][1]=0的位置,变为了1(看下面的代码你就明白了),但这个位置可能无法被接下来的动规覆盖掉,因为这个位置根据方程,仅当a[i]==b[1]时才会更新,而且更新的值始终为1,变不回0。
所以,在一遍动规完成之后,必须要把f[pre]清0。
取模
加法模运算小学生难度,但是……本傻瓜写到最后了忘了写模第一遍溢出了!只得了50分……
复杂度
空间复杂度O(mk)
时间复杂度O(nmk)
好吧,4*10^8,勉强能过,最后一个点600ms,也不算差。
代码
#include<cstdio>
#include<iostream>
#define prime 1000000007
using namespace std;
char a[1001],b[201];
int n,m,t,f[2][201][201][2],s,now,pre=1;//s是用来记录之前有多少个A串字符能和B串第一个字符匹配
int main(){
scanf("%d%d%d",&n,&m,&t);
scanf("%s%s",a+1,b+1);//吐槽:这个奇葩的读入我以前一直没用过...
for (int i=1;i<=n;i++){
swap(now,pre); //交换,只要i改变
f[now][1][1][0]=s;//边界1
if (a[i]==b[1]) f[now][1][1][1]=1,s++;//边界2,解决了s统计的问题
for (int j=2;j<=m;j++){
for (int k=1;k<=t;k++){
if (a[i]==b[j]){
f[now][j][k][1]=((f[pre][j-1][k-1][1]+f[pre][j-1][k][1])%prime+f[pre][j-1][k-1][0])%prime;
}//方程2
f[now][j][k][0]=(f[pre][j][k][0]+f[pre][j][k][1])%prime;//方程1
}
}
for(int j=1;j<=m;j++)
for(int k=1;k<=t;k++)
f[pre][j][k][1]=f[pre][j][k][0]=0;//清空
}
printf("%d",(f[now][m][t][1]+f[now][m][t][0])%prime);
return 0;
}