DP基础(状态设计,贪心,背包,区间,前缀和优化等)

一、状态设计优化:

乌龟棋 (nowcoder.com)

暴力状态设计:dp[i][j][k][l][x]表示前四种卡片分别用了i,j,k,l张,目前走到第x个格子,以用了第三张为例,状态转移方程为

dp[i][j][k][l][x]=max(dp[i][j][k][l][x],dp[i][j][k-1][l][x-3]+w[x]);

优化:容易发现x=i+2j+3k+4l+1,x那一维可以被优化掉,状态转移方程为 

dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k-1][l]+w[x]);

失衡天平 (nowcoder.com)

暴力状态设计:dp[i][j][k]表示在前i个里选,左端重量为j,右端重量为k,需要开dp[100][10000][10000]的数组,明显不可行。

优化:注意到只需要考虑差值即可,用dp[i][j]表示考虑前i个物品,天平重量差为j,枚举两种选择:在重端放物品 和 在轻端放物品,状态转移方程为

dp[i][j]=max({dp[i-1][j],dp[i-1][abs(j-a[i])]+a[i],dp[i-1][j+a[i]]+a[i]});

美丽序列 (nowcoder.com)

二、贪心:

美味菜肴 (nowcoder.com)

如果直接用背包做会导致答案错误,因为背包选择的物品是无序的,但是这题的价值会随着时间而减小,需要先贪心地进行排序,再进行01背包。

struct node{
    ll b,a,c;
}a[1000010];
bool cmp(node x,node y)
{
	return x.c*y.b<y.c*x.b;
}

[NOIP2007]守望者的逃离 (nowcoder.com)

很容易想到用dp[i][j]表示经过时间i时剩余的魔法值为j,但是总魔法值有1e8之大。所以用贪心的思路来想,只要魔法值足够就开始瞬移来尽可能快地逃离。

注:贪心需慎重

三、滚动数组优化:

第一次遇见的就是在01背包一维优化的时候。如果dp[i][j]都是由dp[i-1][j]转移而来时,同时i的范围高达1e5,那么可以用滚动数组反复覆盖i-1的状态,只需要开dp[2][j]的数组即可,用dp[i&1][j]表示i的状态,dp[i&1^1][j]表示i-1的状态,初始化的时候需要写在i的内部。

for(int i=1;i<=n;i++)  dp[i&1][0]=1;

网格图 (nowcoder.com)

本题转移方程简单,但是需要对时间这一维做滚动数组。

dp[0][1][1][4]=1;
for(int T=1;T<=t;T++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			for(int d=0;d<5;d++)
			{
				int k=i+dx[d],l=j+dy[d];
				if(k<1||k>n||l<1||l>m)  continue;
				ll sum=0;
				if(max(abs(x1-k),abs(y1-l))<=D)
				{
					if(d==4)
						for(int idx=0;idx<=4;idx++)  sum=(sum+dp[T&1^1][k][l][idx])%mod;
					else  sum=(sum+dp[T&1^1][k][l][d]+dp[T&1^1][k][l][4])%mod;
				}
				else
					for(int idx=0;idx<=4;idx++)  sum=(sum+dp[T&1^1][k][l][idx])%mod;
				dp[T&1][i][j][d]=sum%mod;
			}
ll ans=0;
for(int i=0;i<=4;i++)  ans=(ans+dp[t&1][x2][y2][i])%mod;

四、区间分割:

乘积最大 (nowcoder.com)

经典的将区间分割成几部分,然后求总体价值。枚举分割次数、当前分割点和上一个分割点即可。

for(int i=1;i<=n;i++)  dp[0][i]=calc(1,i);
for(ll i=1;i<=k;i++)
	for(ll j=i+1;j<=n;j++)
		for(ll l=i;l<j;l++)  dp[i][j]=max(dp[i][j],dp[i-1][l]*calc(l+1,j));

[NOIP2015]子串 (nowcoder.com)

将区间分割成几部分,但是有一些部分可以不选,设dp[i][j][k][1/0]表示A匹配到第i位,B匹配到第j位,选了k个子串,A的第i位选或不选的状态。那么状态的可能情况为:

第i位不选,i-1位选或不选都可以,转移方程为

dp[i&1][j][k][0]=(dp[i&1^1][j][k][0]+dp[i&1^1][j][k][1])%mod;

只有A[i]==B[j]第i位才能选。上一位如果不选,这一位就相当于新开了一个子串;如果上一位选了,可以将上一位与这一位看成是一个子串,也可以看成是相邻的两个子串,转移方程为

dp[i&1][j][k][1]=(dp[i&1^1][j-1][k][1]+dp[i&1^1][j-1][k-1][0]+dp[i&1^1][j-1][k-1][1])%mod;

找爸爸 (nowcoder.com)

本题重点是对g(k)=-A-B(k-1)这个表达式进行分析,设dp[i][j][0/1/2]表示A遍历到第i位,B遍历到第j位,没选空格/在A上选了空格/在B上选了空格的最大相似度。如果上一次没选空格,这次在某个串上选了空格,那么答案应该是上一状态-A;如果两次都在同一个串上选了空格,那么答案应该是上一状态-B。

dp[0][0][0]=0,dp[1][0][2]=-A,dp[0][1][1]=-A;
for(int i=2;i<=n;i++)  dp[i][0][2]=dp[i-1][0][2]-B;
for(int i=2;i<=m;i++)  dp[0][i][1]=dp[0][i-1][1]-B;
for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++)
		for(int k=0;k<=2;k++)//0没选空格,1在A选空格,2在B选空格 
			for(int l=0;l<=2;l++)
				if(l==0)  dp[i][j][l]=max(dp[i][j][l],dp[i-1][j-1][k]+w[a[i]-'A'][b[j]-'A']);
				else if(l==1)
					if(k==0||k==2)  dp[i][j][l]=max(dp[i][j][l],dp[i][j-1][k]-A);
					else  dp[i][j][l]=max(dp[i][j][l],dp[i][j-1][k]-B);
				else
					if(k==0||k==1)  dp[i][j][l]=max(dp[i][j][l],dp[i-1][j][k]-A);
					else  dp[i][j][l]=max(dp[i][j][l],dp[i-1][j][k]-B);

五、背包dp:

[NOIP2014]飞扬的小鸟 (nowcoder.com)

01背包与完全背包的综合,重点在完全背包部分。设dp[i][j]为小鸟飞到横坐标i,纵坐标j的位置最小点击次数。由于上升可以无限点击,因此是个完全背包,二维的完全背包比01背包多了一种状态,就是需要自己更新自己。同时,由于限制最高坐标为m,所以完全背包的容量其实是m加上一次点击的上升高度up[i],最后将dp[i][m]~dp[i][m+up[i]]的结果遍历更新dp[i][m]即可。

for(int j=1;j<=m+up[i];j++)  dp[i][j]=min({dp[i][j],dp[i-1][j-up[i]]+1,dp[i][j-up[i]]+1});
for(int j=m+1;j<=m+up[i];j++)  dp[i][m]=min(dp[i][j],dp[i][m]);

六、数据结构优化状态计算:

[NOIP2003]数字游戏 (nowcoder.com)

类似于乘积最大这道题,每次转移都需要求出[l+1,j]这段区间的和,因此可以用前缀和预处理。

[NOIP2001]统计单词个数 (nowcoder.com)

转移方程依然类似于乘积最大,但是要计算区间内出现单词个数,需要用字典树优化。

int query(int l,int r)
{
	int num=0;
	for(int i=l;i<=r;i++)
	{
		int p=0;
		for(int j=i;j<=r;j++)
		{
			int u=s[j]-'a';
			if(!trie[p][u])  break;
			p=trie[p][u];
			if(cnt[p])
            {
                num++;
                break;
            }
		}
	}
	return num;
}

Cashback - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

得出转移方程后需要求固定区间长度的最值,需要用单调队列或者线段树优化。

七、前缀和优化:

相当重要的一种优化。对于dp[i][j]=\sum dp[i-1][k]这种转移方程,看似是O(n^3)的枚举,但是注意到每次都要用到上一轮状态第二维遍历的总和,因此每次遍历第二维后可以将总和先记录下来,以便遍历下一个i时使用,时间复杂度可以降到O(n^2)。

Problem - 7304 (hdu.edu.cn)

注意到xi的范围很大,同时数组元素之间只有大小关系是有用的,因此可以离散化。用b[i]表示小于等于i的数的个数,f[i][j]表示合法的长度为i且第i个数为j的序列个数,状态转移方程为

f[i][j]=\sum_{k=1}^{j}f[i-1][k]

发现可以用前缀和进行优化,用dp[i][j]辅助记录前缀和,代码如下:

for(int i=1;i<=n;i++)
{
	for(int j=1;j<=m;j++)
	{
		if(i==1)  f[i][j]=1;
		else if(i<=b[j])  f[i][j]=dp[i-1][j];
		else  f[i][j]=0;
		ans[i]=(ans[i]+f[i][j])%mod;
		dp[i][j]=(dp[i][j-1]+f[i][j])%mod;
	}
}

八、状态压缩dp:

1、最常见的一种是在二维表格上涂色。

Corn Fields (nowcoder.com)  玉米田,经典的简单状压dp(第一道应该是蒙德里安的梦想)

[HNOI2012]集合选数 (nowcoder.com)

经过数学分析之后,发现对于每个数来说就是在构建一个二维表格A,A[i][j]=A[i][j-1]*3,A[i][j]=A[i-1][j]*2。初始状态A所有格子都是白色,现在对一些部分涂黑,问有多少种涂色方案保证没有两个涂黑的格子相邻。这样的话,这题就相当于不规则图形版的玉米田。

从小到大遍历集合中的数,每次遍历线性筛去它的倍数,并构建表格进行状压dp。

ll solve(int x)
{
	n=1;
	for(int i=1;i<=17;i++)  len[i]=0;
	while((x<<(n-1))<=num)
	{
		p=x<<(n-1);
		st[p]=1;//筛去倍数
		while(p<=num)  st[p]=1,len[n]++,p*=3;
		n++;
	}
	n--;
	for(int i=1;i<=n+1;i++)
		for(int j=0;j<1<<len[i];j++)  dp[i][j]=0;
	dp[0][0]=1;
	for(int i=1;i<=n+1;i++)
		for(int k=0;k<1<<len[i-1];k++)
			if(dp[i-1][k])
				for(int j=0;j<1<<len[i];j++)
					if((!(j&(j<<1)))&&!(j&k))
						dp[i][j]=(dp[i][j]+dp[i-1][k])%mod;
	return dp[n+1][0];
}

方格填色 (nowcoder.com)

发现列数高达1e18,但是行数范围很小。设f[i][j]表示枚举到第i行状态为j时的方案数,则转移方程为f[i][j]=\sum f[i-1][k](当然这个和前缀和没啥关系)。线性递推是行不通的,一种常用的优化线性递推的方法为矩阵快速幂:

用转移矩阵进行状态转移,

\begin{Bmatrix}\ f[i][0] \\ ... \\ f[i][j] \\ ... \\f[i][1<<m-1] \end{Bmatrix}=\begin{Bmatrix} & ... & \\ ...& tran[j][k] &... \\ & ... & \end{Bmatrix}\ast \begin{Bmatrix} f[i-1][0]\\ ...\\ f[i-1][k]\\ ...\\ f[i-1][1<<m-1] \end{Bmatrix}=tran^{i-1}\ast \begin{Bmatrix} f[1][0]\\ ...\\ f[1][1<<m-1] \end{Bmatrix}

最终答案为\sum_{i=0}^{2^{m}-1}f[n][i]

for(ll i=0;i<(1<<m);i++)
	for(ll j=0;j<(1<<m);j++)
		if((i&j)==0&&(i|j))  tran[i][j]=1;//状态转移矩阵,白色为1,黑色为0
for(ll i=0;i<(1<<m);i++)  f[i][i]=1;
ll k=n-1;
while(k)//矩阵乘法
{
	if(k&1)  mul(f,f,tran);
	k>>=1;
	mul(tran,tran,tran);
}
for(ll i=0;i<(1<<m);i++)
	for(ll j=0;j<(1<<m);j++)  ans=(ans+f[i][j])%mod;

2、还有一种常见的图论上的状压dp

郊区春游 (nowcoder.com)

用二进制串表示各个目标点是否已经到达,则用dp[i][j]表示当前状态为i,最后一个到达的点为j时的最小花费,则状态转移方程为

dp[i+(1<<k)][k]=min(dp[i+(1<<k)][k],dp[i][j]+g[r[j]][r[k]]);

其中g[r[j]][r[k]]需要用Floyd算法预处理,dp部分代码如下:

for(int i=0;i<(1<<R);i++)
	for(int j=0;j<R;j++)
		if(i&(1<<j))
			for(int k=0;k<R;k++)
                dp[i+(1<<k)][k]=min(dp[i+(1<<k)][k],dp[i][j]+g[r[j]][r[k]]);

九、概率dp:

饱和式救援 (nowcoder.com)

先求出每台发动机启动成功的概率b[i],用dp[i][j]表示在前i个发动机中有j台被启动成功,状态转移方程显然为

dp[i][j]+=dp[i-1][j]*(1-b[i])+dp[i-1][j-1]*b[i]

收益 (nowcoder.com)

(以后还会回来更新的)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值