我们登上的并非我们所选择的舞台,演出并非我们所选择的剧本。-- 爱比克泰德 (古罗马哲学家) 。|

vanueber

园龄:2年6个月粉丝:0关注:2

dp重修

区间 dp

枚举断点型

dp 的状态表示设计为一段区间,一般为 dpl,r 为区间 [l,r] 中的答案。

状态转移时,一般按照 len 扩展答案,更新状态 dpl,r 时考虑分割成两个区间的答案。

即枚举断点 k,结合 dpl,k,dpk+1,r 的区间信息拼出 dpl,r 的信息。

P3146 [USACO16OPEN] 248 G

套路题,dpl,r 表示 [l,r] 的最大数字,显然有转移:

dpl,r=dpl,k+1(dpl,k=dpk+1,r)

イウィ

依然套路题,用 dpl,r 表示区间 [l,r] 最多删去的字符数。

有如下转移:

dpl,r=dpl,k+dpk+1,r

特别地,如果有:

iwi

中都被消除,即 dpl+1,k1=k1(l1)+1dpk+1,r1=(r1)(k+1)+1

此时答案为 rl+1

最后答案即为 dp1,n3

区间贡献/答案统计型

状态设计一般为区间的答案,可以考虑根据已知的信息逐步递推。

Coloring Brackets

考虑状态 dpl,r,x,y 表示区间 [l,r] 的左端点染色 x,右端点染色 y 的方案数。

此时根据区间长度划分递推不太好做,可以使用记忆化搜索。

考虑当前处理区间 [l,r]

  1. r=l+1,有 dpl,r,0,1=dpl,r,0,2=dpl,r,1,0=dpl,r,2,0=1
  2. matchlr,即形如 ()() 的情况,递归处理两个括号后,答案即为两者乘积。
  3. matchl=r,即形如 (()()) 的情况,有转移 dpl,r,lc1,rc1=dpl+1,r1,lc2,rc2

注意颜色判断,代码呼之欲出。

P8675 [蓝桥杯 2018 国 B] 搭积木

设计状态 dpk,l,r 表示在第 k 层的 [l,r] 填充满积木的方案数。

首先有一个 trick,判断 [l,r] 是否能合法地充满积木,可以对该层求前缀和( 1 代表 X ),那么

[l,r]合法prerprel1=0

然后,设计 dp 状态:

初始状态:关于 dpn 的初始值,即第一层的状态。

状态转移

dpi,l,r=x=1ly=rmdpi+1,x,y

最终答案

1+i=1kj=1mk=jmdpi,j,k

时间复杂度 Θ(mn4),考虑优化。

观察到状态转移的式子中是一个二维区间和,使用前缀和优化。

前缀和

时间复杂度 Θ(n2m),可以通过。

关于此题一些解释:

  1. 状态计算从何而来:考虑该层的基座如何覆盖 [l,r],如果基座满足 [l,r][l,r],那么基座的每一个方案都能对当前区间的答案做贡献。
  2. 答案为什么是上式:考虑以第 k 层为顶端的情况,所有的 dpk,l,r 都能贡献答案,最后加上一个不放的情况。

dp 做题

[ABC207E] Mod i

将序列分成若干段,使得第 i 段的所有数之和为 i 的倍数的方案数。

分析

设计出普通的 dp。

dpi,j 表示处理到第 i 个点,将序列分为 j 个序列的总方案数。

有以下转移:

dpi,j=k=0i1[jp=k+1iap]dpk,j1

这相当于枚举上一个断点 k 来转移(特别地 k=0 表示前面没有断点)。

有边界条件

dp0,0=1

答案

i=1ndpn,i

这样做时间复杂度 Θ(n3)

const int mod=1e9+7;
const int maxn=3005;
int n,a[maxn],dp[maxn][maxn],pre[maxn];
int query(int l,int r)
{
	return pre[r]-pre[l-1];
}
//到位置i分成了j段,i分到第j段 
//dp[i][j]=dp[1][j-1]+dp[2][j-1]+...+dp[i-1][j-1],(a[k-1]+...+a[i])%j=0 

signed main()
{
#ifndef ONLINE_JUDGE
#define LOCAL
//	freopen("in.txt","r",stdin);
#endif
	n=read();
	for(int i=1;i<=n;++i)
	{
		a[i]=read();
		pre[i]=pre[i-1]+a[i];
//		pre[i]%=mod;
	}
	dp[0][0]=1;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=i;++j)
		{
			for(int k=0;k<=i-1;++k)
			{
				if(query(k+1,i)%j==0)
				{
					dp[i][j]+=dp[k][j-1];
					dp[i][j]%=mod;
				}
			}
		}
	}
	int ans=0;
	for(int j=1;j<=n;++j)
	{
		ans+=dp[n][j];
		ans%=mod;
	}
	cout<<ans<<endl;
#ifdef LOCAL
	fprintf(stderr,"%f\n",1.0*clock()/CLOCKS_PER_SEC);
#endif
	return 0;
}

考虑优化,观察到

  1. j 只跟前一个状态有关,可以先枚举 j
  2. 记前缀和为 pre,转移条件 preiprek0(modj) 等价于 preiprek(modj)
  3. 每次 dp 第一位考虑的都是当前 i 的前缀。

于是开一个桶记录贡献,每次有

valprei1modj+dpi1,j1

直接转移

dpi,j=valpreimodj

时间复杂度 Θ(n2)

const int mod=1e9+7;
const int maxn=3005;
int n,a[maxn],dp[maxn][maxn],pre[maxn];
int query(int l,int r)
{
	return pre[r]-pre[l-1];
}
//到位置i分成了j段,i分到第j段 
//dp[i][j]=dp[1][j-1]+dp[2][j-1]+...+dp[i-1][j-1],(a[k]+...+a[i])%j=0 

signed main()
{
#ifndef ONLINE_JUDGE
#define LOCAL
//	freopen("in.txt","r",stdin);
#endif
	n=read();
	for(int i=1;i<=n;++i)
	{
		a[i]=read();
		pre[i]=pre[i-1]+a[i];
//		pre[i]%=mod;
	}
	dp[0][0]=1;
	for(int j=1;j<=n;++j)
	{
		map<int,int> val;
		for(int i=1;i<=n;++i)
		{
			val[pre[i-1]%j]+=dp[i-1][j-1];
			val[pre[i-1]%j]%=mod;
			dp[i][j]+=val[pre[i]%j];
		}
	}
	int ans=0;
	for(int j=1;j<=n;++j)
	{
		ans+=dp[n][j];
		ans%=mod;
	}
	cout<<ans<<endl;
#ifdef LOCAL
	fprintf(stderr,"%f\n",1.0*clock()/CLOCKS_PER_SEC);
#endif
	return 0;
}

本文作者:vanueber

本文链接:https://www.cnblogs.com/vanueber/p/18668923

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   vanueber  阅读(2)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起