递推专题 - 两种状态互推问题:经典问题 打砖块 + 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;
}

2NOIP2015 Day2T2字符串匹配

题目链接:https://vijos.org/p/1982

先来回顾一下和本题很像的两个经典问题:

LCSd[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亦然。然后按照jk降序的顺序逆推即可。详见代码(代码中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;
}


posted @ 2015-12-11 23:09  Armeria  阅读(225)  评论(0编辑  收藏  举报