DP合集

写在前面


在本篇开始前,想先说明写这篇博客的初衷和希望达到的效果。
本文开始写于2023.8.23,博主第一年高中开学前,\(dp\)是从初中开始就一直很薄弱的版块,在考试中稍微难一点就做不出来,只能打暴力。但其实正解很好实现,很好得分。
初中时,教练和学长都说\(dp\)就是要多做题才能体悟它,初中来不及写,高中就必须直面自己的问题,才能提升自己的能力。所以决定写这样一篇文章,记录自己的\(dp\)学习之旅,我承诺每天自己至少独立思考出一道\(dp\)
(不知道你们什么时候能看到,等我多写一些再公开,不然有点尴尬)
本博客可能的用法:
1.看一些题的题解和代码
2.每天和博主一起写几道题
3.和博主交流\(dp\)的思路

正文


P1944 最长括号匹配

一发就过了,爽!
第一眼是区间dp,再一看范围\(1e6\),很明显\(dp\)的复杂度为\(O(n)\)\(O(nlogn)\).
所以\(dp\)先只设一位\(f[i]\)表示以\(i\)结尾的最长括号匹配。
如果i与i-1的最长括号匹配的前一位可以匹配的,就有\(f[i]=f[i-1]+2+f[i-f[i-1]-2]\)(然后我就不会不匹配的情况)
看一眼题解发现题解就是这么转的,它说不匹配则没有结果。
思考一下,发现很对,i-1的最长匹配一应是左右括号数相等,如果i与i-1的最长匹配最长匹配中一个点匹配,则这个点后面的右括号一定多于左括号,不会是一个匹配

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char s[N];
int f[N],ans,cur;
int main()
{
	scanf("%s",s+1);
	int n=strlen(s+1);
	f[1]=0;
	for(int i=2;i<=n;i++)
	{
		if(s[i]=='('||s[i]=='[')continue;
		if((s[i]==')'&&s[i-f[i-1]-1]=='(')||(s[i]==']'&&s[i-f[i-1]-1]=='['))f[i]=f[i-1]+2+f[i-f[i-1]-2];
		else f[i]=0;
		if(f[i]>ans)ans=f[i],cur=i;
	}
	if(ans==0)return 0;
	for(int i=cur-ans+1;i<=cur;i++)printf("%c",s[i]); 
	return 0;
 } 

P4310 绝世好题

没看题解!
很显然的dp,最朴素的做法是\(f[i]=max_{j<i,i\& j\ne 0}(f[j])+1\)
复杂度为\(O(n^2)\),虽然过不去,但给了我们提示。枚举\(j\) 是复杂度的瓶颈,考虑快速找\(j\),就要思考什么时候\(i\& j\ne 0\)
可以想到\(i,j\) 二进制上有一位相同则不为0.
所以改为枚举二进制,复杂度就可行了。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int dp[N],ans,n,a[N];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
	{
		int x=a[i],l=0,mx=0;
		while(x)
		{
			if(x&1)mx=max(dp[l],mx);
			x>>=1;
			l+=1;
		}
		if(mx+1>ans)ans=mx+1;
		x=a[i],l=0;
		while(x)
		{
			if(x&1)dp[l]=mx+1;
			x>>=1;
			l+=1;
		}
	}
	printf("%d",ans); 
	return 0;
 } 

P4059 [Code+#1] 找爸爸

破防了,绿题都不会,要退役了。
很显然,答案的统计与当前匹配的位置和0的位置有关,所以有\(dp[i][j][0/1/2]\)表示第一行统计到i,第二行统计到j,结尾没有0/第一行结尾有0/第二行结尾有0。(两位都为0,一定不优)
转移方程

\[dp[i][j][0]=max(dp[i-1][j-1][0],dp[i-1][j-1][1],dp[i-1][j-1][2])+d(a[i],b[j]) \]

\[dp[i][j][1]=max(dp[i][j-1][0]-A,dp[i][j-1][1]-B,dp[i][j-1][2]-A) \]

\[dp[i][j][2]=max(dp[i][j-1][0]-A,dp[i][j-1][2]-B,dp[i][j-1][1]-A \]

#include <bits/stdc++.h>
using namespace std;
int a[3010],b[3010],d[5][5],A,B;
char s[3010],t[3010]; 
long long dp[3010][3010][3];
int main()
{
	scanf("%s%s",s,t);
	int len1=strlen(s),len2=strlen(t);
	for(int i=0;i<len1;i++)
	{
		if(s[i]=='A')a[i+1]=1;
		if(s[i]=='T')a[i+1]=2;
		if(s[i]=='G')a[i+1]=3;
		if(s[i]=='C')a[i+1]=4;
	}
	for(int i=0;i<len2;i++)
	{
		if(t[i]=='A')b[i+1]=1;
		if(t[i]=='T')b[i+1]=2;
		if(t[i]=='G')b[i+1]=3;
		if(t[i]=='C')b[i+1]=4;
	}
	for(int i=1;i<=4;i++)
		for(int j=1;j<=4;j++) scanf("%d",&d[i][j]);
	scanf("%d%d",&A,&B); 
	for(int i = max(len1,len2);i; i--) 
	{
    	dp[0][i][0] = dp[i][0][0] = dp[0][i][2] = dp[i][0][1] = -(1LL << 60);
    	dp[0][i][1] = dp[i][0][2] = - A - B * (i - 1);
  	}
  	dp[0][0][1] = dp[0][0][2] = -(1LL << 60);
	for(int i=1;i<=len1;i++)
		for(int j=1;j<=len2;j++)
		{
			dp[i][j][0]=max(max(dp[i-1][j-1][0],dp[i-1][j-1][1]),dp[i-1][j-1][2])+d[a[i]][b[j]];
			dp[i][j][1]=max(dp[i][j-1][0]-A,max(dp[i][j-1][1]-B,dp[i][j-1][2]-A));
			dp[i][j][2]=max(max(dp[i-1][j][0]-A,dp[i-1][j][1]-A),dp[i-1][j][2]-B); 
		}
	printf("%lld\n",max(max(dp[len1][len2][0],dp[len1][len2][1]),dp[len1][len2][2]));
	return 0;
} 

P1868 饥饿的奶牛

又活过来了(果然数据结构)
简单思想
\(f[区间右边]=max_{x<区间左边}(f[x])+(右边-左边+1)\)
复杂度是\(O(n^2)\)
考虑从一个区间的最大值转移,上数据结构,只求1-n的最大值,用树状数组快速算即可。

#include <bits/stdc++.h>
using namespace std;
const int N=3e6+10;
int n,sh[N],dp[N],mx;
struct node
{
    int x,y;
}a[N];
bool cmp(node x,node y)
{
    return x.y<y.y;
}
int lowbit(int x)
{
    return x&(-x);
}
int query(int x)
{
	if(x<0)return 0;//防负数
    int ans=0;
    while(x)
    {
        ans=max(ans,sh[x]);
        x-=lowbit(x);
    }
    return ans;
}
void add(int x,int val)
{
	if(x<0)return ;
    while(x<=mx)
    {
        if(sh[x]<val)
        {
            sh[x]=val;
            x+=lowbit(x);
        }
        else break;
    }
    return ;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y),mx=max(mx,max(a[i].x,a[i].y));
    sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++)
    {
        dp[a[i].y]=max(dp[a[i].y],query(a[i].x-1)+(a[i].y-a[i].x+1));
        add(a[i].y,dp[a[i].y]);
    }
    printf("%d\n",query(a[n].y)); 
    return 0;
}

P1282 多米诺骨牌

又没想清楚。
首先可以想到,答案一定与第一第二行的和有关,因为范围最大为6000,所以只能记一维,这里因为我们知道一二行的和有关,所以一行就可以知道另一行。
所以,理所当然设出\(dp[i][j]\)处理了前i个数,第一行和为j需要的最小旋转次数、
转移就像背包。

\[dp[i][j]=min(dp[i][j],min(dp[i-1][j-a[i]],dp[i-1][j-b[i]]+1)); \]

#include <bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,dp[N][2*N],sum,ans=0x3f3f3f3f,ans1=0x3f3f3f3f,a[N],b[N];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]),sum+=a[i]+b[i];
	memset(dp,0x3f,sizeof(dp));
	dp[1][a[1]]=0,dp[1][b[1]]=1;
	for(int i=2;i<=n;i++)
	{
		for(int j=sum;j>=min(a[i],b[i]);j--)
		{
			if(j>a[i])dp[i][j]=min(dp[i][j],dp[i-1][j-a[i]]);
			if(j>b[i])dp[i][j]=min(dp[i][j],dp[i-1][j-b[i]]+1);
			if(i==n&&dp[i][j]!=1061109567)
			{
				int c=abs(2*j-sum);
				if(c<ans)
				{
					ans=c;
					ans1=dp[i][j];
				}
				else if(c==ans)ans1=min(dp[i][j],ans1);
			}
		}
	}
	printf("%d\n",ans1); 
	return 0;
}

P1284 三角形牧场
做出来了,还推出了一些无用的性质。
先还在想周长一定是,什么三角形面积最大。
经过无用的证明用海伦公式证明了,等边三角形最大(没有任何用处,但题解区有贪心加模拟退火用这个性质)
发现可以暴算,800*800直接枚举。
\(dp[i][j]\)能否拼成一条边为i,一条边为j。
转移方程为\(dp[i][j]|=dp[i][j-a[k]],dp[i][j]|=dp[i-a[k]][j]\)

#include <bits/stdc++.h>
using namespace std;
int n,a[1100],ans=-1,sum;
bool dp[8100][8100];
bool cheak(int x,int y)
{
	int z=sum-x-y;
	if(x+y>z&&x+z>y&&y+z>x)return 1;
	else return 0;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),sum+=a[i];
	dp[0][0]=1;
	for(int k=1;k<=n;k++)
	{
		for(int i=sum;i>=0;i--)
			for(int j=sum;j>=0;j--)
			{
				if(j>=a[k])dp[i][j]|=dp[i][j-a[k]];
				if(i>=a[k])dp[i][j]|=dp[i-a[k]][j];
			}
	} 
	double p=sum/2.0;
	for(int i=1;i<=sum;i++)
		for(int j=1;j<=sum;j++)
		{
			if(!dp[i][j]||!cheak(i,j))continue;
			double s=sqrt(p*(p-i)*(p-j)*(p-(sum-i-j)))*100;
			ans=max((int)s,ans);
		}
	printf("%d\n",ans);
	return 0;
 } 

P4084 [USACO17DEC] Barn Painting G

今天考试在树型\(dp\)想出方法后唐了,写一道树型dp奖励自己,结果更唐,虽然对了,单写法一点都不优秀。
首先题目很显然树型\(dp\),\(dp[u][1/2/3]\)表示\(u\)涂颜色1,颜色2,颜色3的方案数。
然后对于有特定的颜色的点,就除了特定颜色外初始化为0,其他点每个颜色都设为1。
然后转移即可。有一个小问题,如果儿子节点有特定颜色则父节点该颜色为0,我自己是再写一遍循坏判断(不简洁)

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,mod=1e9+7;
#define int long long
int n,k;
int tot,hd[N],go[N*2],nxt[N*2],col[N],dp[N][4];
void add(int x,int y)
{
	nxt[++tot]=hd[x];go[tot]=y;hd[x]=tot;
	nxt[++tot]=hd[y];go[tot]=x;hd[y]=tot;
	return ;	
}
void dfs(int u,int f)
{
	if(col[u]==-1)dp[u][1]=dp[u][2]=dp[u][3]=1;
	else dp[u][col[u]]=1;
	for(int i=hd[u];i;i=nxt[i])
	{
		int v=go[i];
		if(v==f)continue;
		dfs(v,u);
		if(col[u]==-1) 
		for(int j=1;j<=3;j++)
		{
			int cnt=0;
			for(int k=1;k<=3;k++)
			{
				if(j==k)continue;
				cnt+=dp[v][k];	
			}
			dp[u][j]=(dp[u][j]*cnt)%mod;		
		}
		else
		{
			int cnt=0;
			for(int k=1;k<=3;k++)
				if(k!=col[u])cnt+=dp[v][k];
			dp[u][col[u]]=(dp[u][col[u]]*cnt)%mod;			
		}
	}
	for(int i=hd[u];i;i=nxt[i])
	{
		int v=go[i];
		if(v==f)continue; 
		if(~col[v]) dp[u][col[v]]=0;
	} 
}
signed main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<n;i++)
	{
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);
		col[i]=-1;
	}
	col[n]=-1;
	for(int i=1;i<=k;i++)
	{
		int x,y;scanf("%d%d",&x,&y);
		col[x]=y;
	}
	dfs(1,0);
	if(col[1]==-1) printf("%d\n",(dp[1][1]+dp[1][2]+dp[1][3])%mod);
	else printf("%d\n",dp[1][col[1]]%mod); 
	return 0;
} 

更优秀的写法

#include<bits/stdc++.h>
#define maxn 200005
#define ll long long
using namespace std;
const int TT=1e9+7;
int n,x,y,lnk[maxn],nxt[maxn],son[maxn],tot,m;
ll f[maxn][3];
inline int read(){
	int ret=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-f;ch=getchar();}
	while (ch<='9'&&ch>='0') ret=ret*10+ch-'0',ch=getchar();
	return ret*f;
}
inline void add(int x,int y){nxt[++tot]=lnk[x];lnk[x]=tot;son[tot]=y;}
inline void Dfs(int x,int fa){
	for (int i=0;i<3;i++){
		if (f[x][i]){for (int j=0;j<i;j++) f[x][j]=0;break;}
		f[x][i]=1;
	}
	for (int i=lnk[x];i;i=nxt[i])
	  if (son[i]!=fa){
	  	Dfs(son[i],x);
	  	f[x][0]=f[x][0]*((f[son[i]][1]+f[son[i]][2])%TT)%TT;
                f[x][1]=f[x][1]*((f[son[i]][0]+f[son[i]][2])%TT)%TT;
                f[x][2]=f[x][2]*((f[son[i]][1]+f[son[i]][0])%TT)%TT;
	  }
}
int main(){
	n=read(),m=read();
	for (int i=1;i<n;i++) x=read(),y=read(),add(x,y),add(y,x);
	for (int i=1;i<=m;i++) x=read(),y=read()-1,f[x][y]=1;
	Dfs(1,0);
	printf("%lld",(f[1][0]+f[1][1]+f[1][2])%TT);
	return 0;
}

“破锣摇滚”乐队 Raucous Rockers

感觉没什么难度。读完题后就有了一个暴力的想法。设\(dp[i][j][k]\)存前\(i\)首歌用\(j\)\(CD\)最后一张用了k分钟的最多乐曲数,显然一首歌要么不用,要么新建一张CD,要么接在当前CD上。直接转移即可。

#include <bits/stdc++.h>
using namespace std;
const int N=22;
int n,t,m,a[N],dp[N][N][N],ans;
int main()
{
	scanf("%d%d%d",&n,&t,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			dp[i][j][a[i]]=dp[i-1][j-1][t]+1;//新建
			for(int tt=1;tt<=t;tt++)
			{
				dp[i][j][tt]=max(max(dp[i][j][tt-1],dp[i-1][j][tt]),dp[i][j][tt]);//不要
				if(tt>a[i])dp[i][j][tt]=max(dp[i][j][tt],dp[i-1][j][tt-a[i]]+1);//接在后面
			}	
			if(i==n)ans=max(ans,dp[i][j][t]);
		}
	}
	printf("%d\n",ans);
	return 0;
 } 

P5322 [BJOI2019] 排兵布阵

\(dp[i][j]\)表示前\(i\)个城堡用\(j\)个士兵最大总分,转移不用说简单背包,一开始感觉时间复杂度不对,是\(O(nm^2)\).
看题解发现,这有一个贪心与dp的结合。显然只比对手的两倍多一个是最优的,不然会浪费,所以确定赢几局来转移,复杂度是\(O(n^2m)\)

#include <bits/stdc++.h>
using namespace std;
const int N=210,M=20010;
#define int long long 
int s,n,m,ans,gs[N][N],dp[N][M];
signed main()
{
	scanf("%lld%lld%lld",&s,&n,&m);
	for(int i=1;i<=s;i++)
		for(int j=1;j<=n;j++)
			scanf("%lld",&gs[j][i]);
	for(int i=1;i<=n;i++)
		sort(gs[i]+1,gs[i]+s+1);
	for(int i=1;i<=n;i++)
		for(int j=m;j>=1;j--)
		{
			dp[i][j]=dp[i-1][j];
			for(int k=1;k<=s;k++) 
				if(j-gs[i][k]*2-1>=0)dp[i][j]=max(dp[i][j],dp[i-1][j-gs[i][k]*2-1]+i*k);			
		}
	for(int i=1;i<=m;i++)ans=max(dp[n][i],ans);
	printf("%lld\n",ans);
	return 0;
}

P2339 [USACO04OPEN] Turning in Homework G

开学后很久没写了,一放国庆全是蓝紫黑,感觉越来越菜了,啥也不会做,都是听老师讲看题解才会的。
回归正题,贪心与dp的结合。
先有一个简单的贪心对于一个区间,交作业的最优的一种解是先交区间的一个端点。如果先交中间的点,一定要绕回来交前面的作业,不如直接等到时间走一遍。
所以常规定义\(dp[i][j][0/1]\)表示交了区间[i][j]最后落在\(i\)\(j\)的最小时间,发现不好合并(考虑两个点),所以转化一下方式,
定义\(dp[i][j][0/1]\)表示除了区间[i][j]都交了最后交在\(i\)\(j\)的最小时间(应为贪心然我们知道一个大区间如何变成小区间)
然后转移方程就好写了(反正我理解了很久)

#include <bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int c,h,b,dp[N][N][2];
struct node
{
	int x,t;
}zy[N];
bool cmp(node x,node y)
{
	if(x.x!=y.x)return x.x<y.x;
	else x.t<y.t; 
}
int main()
{
	scanf("%d%d%d",&c,&h,&b);
	for(int i=1;i<=c;i++)scanf("%d%d",&zy[i].x,&zy[i].t);
	sort(zy+1,zy+c+1,cmp);
	memset(dp,0x3f,sizeof(dp));
	dp[1][c][0]=max(zy[1].x,zy[1].t);
	dp[1][c][1]=max(zy[c].x,zy[c].t);
	for(int k=c-1;k>=1;k--)
	{
		for(int i=1;i+k-1<=c;i++)
		{
			int j=i+k-1;
			dp[i][j][1]=min(dp[i][j][1],max(dp[i][j+1][1]+zy[j+1].x-zy[j].x,zy[j].t));
			dp[i][j][1]=min(dp[i][j][1],max(dp[i-1][j][0]+zy[j].x-zy[i-1].x,zy[j].t));
			dp[i][j][0]=min(dp[i][j][0],max(dp[i][j+1][1]+zy[j+1].x-zy[i].x,zy[i].t));
			dp[i][j][0]=min(dp[i][j][0],max(dp[i-1][j][0]+zy[i].x-zy[i-1].x,zy[i].t));
//dp[i][j+1]只能落在j+1上,不然i就已经交了,同理。其他的要深刻理解贪心
		}
	}
	int ans=1e9+7;
	for(int i=1;i<=c;i++)
	{
		ans=min(ans,min(dp[i][i][0],dp[i][i][1])+(int)abs(zy[i].x-b));
	}
	printf("%d\n",ans);
	return 0;
}

P1912 [NOI2009] 诗人小G

因为是第一篇决策单调性,就单独写了题解戳这里

P4042 [AHOI2014/JSOI2014] 骑士游戏

以为dp不了,因为有环不好处理有后效性,但我们先不管,直接写出方程。

\[dp[i]=max(k[i],s[i]+\sum_{j}dp[j]) \]

显然\(dp[i]\)能从第二项转移当且仅当所有dp[j]<dp[i],且要先算出dp[j],所以反向连边。不然直接魔法攻击。
所以按dp[i],大小排序来更新,有点像最短路,最小的dp[i]一定是固定下来了,用它去更新别的区间。
以魔法攻击为初值,只有正环不影响。

#include <bits/stdc++.h>
using namespace std;
//#define int long long
const int N=2e5+10,M=1e6+10;
#define ll long long
int n,tot,nxt[M],go[M],hd[N],p[N];
ll dp[N],ans[N],s[N],k[N];
bool vis[N];
void add(int x,int y)
{
	nxt[++tot]=hd[x];go[tot]=y;hd[x]=tot;
	return ;
}
struct node
{
	ll id,w;
	bool operator<(const node& x)const{
		return x.w<w; 
	}
};
priority_queue<node> q;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int x,y;cin>>s[i]>>k[i]>>p[i];
		for(int j=1;j<=p[i];j++)cin>>y,add(y,i);
		q.push((node){i,k[i]}); 
		dp[i]=s[i];
	}
	while(!q.empty())
	{
		ll u=q.top().id,w=q.top().w;q.pop();
		if(vis[u])continue;
		vis[u]=1,ans[u]=w;
		for(int i=hd[u];i;i=nxt[i])
		{
			int v=go[i];
			if(vis[v]||dp[v]>k[v])continue;
			dp[v]+=w;p[v]--;
			if(!p[v])q.push((node){v,dp[v]});
		}
	}
	printf("%lld\n",ans[1]);
	return 0;
}

P4749 [CERC2017] Kitchen Knobs

想水一个题解,不知道过没过。
https://www.luogu.com.cn/article/lqk1zw0e

[ABC221G] Jumping sequence

先有一个套路,直接做不了dp的原因是x,y相互影响,所以考虑见它们分开,将(x,y)变成(x-y,x+y),那么如果我们转化后可以凑出x,y,那么对于每一步如论x,y加或减都有对应的操作。
只转化不够,减\(di\)不好做,所将两边同时加\(\sum di\),系数变为0,2,就可以做了

#include <bits/stdc++.h>
using namespace std;
const int N=2e3+10;
int n,A,B,d[N],mx,ans[N];
bitset<3600010> dp[N];//没开够
int aabs(int x){
	return x>0?x:-x; 
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>A>>B;
	for(int i=1;i<=n;i++)cin>>d[i],mx+=d[i];
	int t=A;
	A=A-B,B=t+B;
	if (aabs(A)>mx||aabs(B)>mx) return cout<<"No",0;//特判
    if ((A+mx)&1||(B+mx)&1) return cout<<"No",0;
	A=(A+mx)/2,B=(B+mx)/2;
	dp[1][0]=1;
	for(int i=1;i<=n;i++)dp[i+1]=dp[i]|(dp[i]<<d[i]);
	if(dp[n+1][A]==0||dp[n+1][B]==0)
	{
		cout<<"No"<<'\n';
		return 0;
	}
	for(int i=n;i>=1;i--)
	{
		int tmp=0;
		if(!dp[i][A]){A-=d[i],tmp++;}
		if(!dp[i][B]){B-=d[i],tmp+=2;}
		ans[i]=tmp;
	}
	cout<<"Yes"<<'\n';
	for(int i=1;i<=n;i++)
	{
		if(ans[i]==0)cout<<"L";
        if(ans[i]==1)cout<<"D";
        if(ans[i]==2)cout<<"U";
        if(ans[i]==3)cout<<"R";
	}
	return 0;
}
posted @ 2024-08-24 08:56  storms11  阅读(6)  评论(0编辑  收藏  举报