递推专题 - 两种状态互推问题:经典问题 打砖块 + NOIP2015 Day2 T2
例1:打砖块
这道题的一个非常重要的细节是:只要子弹打光,就必须结束,无论是否还有可以打到的有奖励子弹的砖块。也就是说,有奖励子弹的砖块不等价于不耗费子弹就能获得分数。就是因为这个细节,我们需要双重递推。
设f[i][j]表示第i列打j下能获得的分数,g[i][j]表示第i列打j下且第j下不能接着打有奖励子弹的砖块能获得的分数。这个是可以在O(mk)时间内预处理出来的。
然后进行DP。同样设d[i][j]表示前i列打j下能获得的最大分数,d0[i][j]表示前i列打j下且最后一下(不一定在第i列打第j下)不能重复打所能获得的最大分数。设在第i列打了p下,那么:
d的转移非常显然:d[i][j]=max{ d[i-1][j-p] +f[i][p] | 0<=p<=j }
d0的转移麻烦一些。
先看p=0的情况:p=0时,不能把第j下放在第i列,所以 d0[i][j]=d0[i-1][j] + f[i][0]
再看p=j的情况:p=j时,p-j=0,不能把第j下放在前i-1列,所以d0[i][j]=d[i-1][0] + g[i][j]
最后看0<p<j的情况:d0[i][j]=max{ max(d0[i][j-p]+f[i][p] , d[i][j-p]+g[i][p] ) }
最后的答案就是d0[m][k].(数据里没出现所有砖块打完后子弹还有剩余的情况,非常良心。。)
#include <cstdio> #include <algorithm> using namespace std; typedef unsigned long long uLL; #define rep(i,a,b) for (int i=a; i<=b; i++) #define dep(i,a,b) for (int i=a; i>=b; i--) #define read(x) scanf("%d", &x) const int N=200+5, M=N, K=M; int n, m, k, a[N][M], s[N][M]; uLL f[M][K], d[M][K]; char c; int main() { read(n); read(m); read(k); rep(i,1,n) rep(j,1,m) { read(a[i][j]); scanf("%c", &c); scanf("%c", &c); if (c=='N') s[i][j]=0; else s[i][j]=1; } // 设f[i][tk]表示打第i列,用了tk发子弹所能获得的分值 rep(i,1,m) rep(j,1,k) f[i][j]=0; rep(i,1,m) { int tk=0; f[i][0]=0; dep(j,n,1) { if (s[i][j]==0) { ++tk; if (tk>0) f[i][tk]=f[i][tk-1]; } if (tk>k) break; f[i][tk]+=a[j][i]; } } // 设d[i][j]表示打到第i列,用了j发子弹所能获得的最大分值 rep(i,0,k) d[0][i]=0; rep(i,1,m) rep(j,0,k) { d[i][j]=0; rep(p,0,j) d[i][j]=max(d[i][j], f[i][p]+d[i-1][j-p]); } printf("%lld\n", d[m][k]); return 0; }
例2:NOIP2015 Day2T2字符串匹配
先来回顾一下和本题很像的两个经典问题:
① LCS:设d[i][j]表示字串A的前i位和字串B的前j位的LCS长度(不强制选a[i]或b[j]),则有:
d[i][j]=max{ d[i][j-1],d[i-1][j] | a[i]≠b[j] }, d[i][j]=d[i-1][j-1]+1(a[i]=b[j])
② LIS:设d[i]表示以a[i]结尾的LIS长度(即强制选i),则有:
d[i]=max{ d[j] | a[i]>=a[j] } +1
以上两个问题的状态设计是有其原因的。LCS中,后面的状态不会受到前面状态所依赖的原值的影响;而LIS会。
再来看本题。本题的状态如果只设计一种,比如:
d[i][j][k]表示考虑A串的前i位,匹配了B串的前j位,且已分成k段的方案数;
f[i][j][k]表示考虑A串的前i位,匹配了B串的前j位,且强制选第i位,且已分成k段的方案数;
以及我考场上傻逼出的各种奇葩的根本无法转移的状态。
然而本题的后面状态和前面状态的关系比较含糊。以上这些都无法单独转移。d[i][j][k]会造成重复,而f[i][j][k]根本不能搞:没法断开,断开的话转移的时间代价太大。
所以:两个状态互推!发现很容易转移了:
如果a[i]=b[j],则 f[i][j][k] = f[i-1][j-1][k] +g[i-1][j-1][k-1] (连着a[i-1]-b[j-1]的方案数+不连着前面的方案数)
g[i][j][k] = f[i][j][k] + g[i-1][j][k](选a[i]方案数 +不选a[i]方案数)
如果a[i]≠b[j],则 f[i][j][k] = 0
g[i][j][k]= g[i-1][j][k]
但还有一个问题:这题卡空间。如果开满100分数组的话会MLE。所以需要采用滚动数组优化。根据这个递推的特点,我们可以优化掉第一维,也就是f[j][k]表示匹配到B串的位置j且已经分成k段的方案数,g亦然。然后按照j、k降序的顺序逆推即可。详见代码(代码中d数组即为上述分析中的g)。
更新:在某神犇的博客上看到了另一种状态设计,像是把上面这两种状态揉在一起了。。也可以很神奇地转移。。非常优美。。
“令f[i][j][k]表示当前第i位,匹配到母串第j个,已经匹配了k段的方案数。仅当b[i]=a[j]时f[i][j][k]!=0。然后f[i][j][k]=f[i-1][j-1][k]+Σ(x=1,j-1)f[i-1][x][k-1]。然后滚动一下空间ok,前缀和一下时间ok。”
详见 http://blog.csdn.net/lych_cys/article/details/50094255
// NOIP2015 Day2 T2 #include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long LL; typedef unsigned long long uLL; const int N=1000+5, M=200+5, K=M, INF=0x3f3f3f3f, mod=1000000007; #define rep(i,a,b) for (int i=a; i<=b; i++) #define dep(i,a,b) for (int i=a; i>=b; i--) #define read(x) scanf("%d", &x) #define fill(a,x) memset(a, x, sizeof(a)) int n, m, k, f[M][K], d[M][K]; char a[N], b[M]; int main() { read(n); read(m); read(k); fill(d, 0); fill(f, 0); scanf("%s\n%s", a, b); d[0][0]=1; // 十分优美的初始化(雾。。 rep(i,1,n) dep(j,min(i,m),1) if (a[i-1]==b[j-1]) dep(p,min(k,j),1) { f[j][p]=(f[j-1][p]+d[j-1][p-1])%mod; d[j][p]=(f[j][p]+d[j][p])%mod; } else fill(f[j], 0); // 两个字符不相同时,f数组全置为0,s数组不变 printf("%d\n", d[m][k]); return 0; }