DP

动态规划

  • 动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

三个条件:最优子结构,无后效性和子问题重叠。

最优子结构

  • 证明问题最优解的第一个组成部分是做出一个选择;
  • 对于一个给定问题,在其可能的第一步选择中,假定你已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择;
  • 是否可以选择到当前最优

无后效性

  • 当前的最优子问题,不会再受到后续决策的影响。

子问题重叠

  • 可以通过储存答案的方式来避免重复求解相同的子问题,从而提升效率。;

基本方案

  • 将原问题划分为若干阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为状态);
  • 寻找每一个状态的可能决策,或者说是各状态间的相互转移方式(用数学的语言描述就是状态转移方程)。
  • 按顺序求解每一个阶段的问题。

背包DP

01背包

通常问题为给你可以使用的最大容积m,所有物品n的价值w[i]和体积v[i],求最大价值

二维
  • 定义状态 dp[i][j]表示前i个物品使用了j的体积可以获得的最大价值
  • 状态转移 dp[i][j]=max(dp[iz][j],dp[i-1][j-v[i]]+w[i]);
  • 输出结果 dp[n][m]
#include<bits/stdc++.h>
using namespace std;
int w[1010],dp[1010][1010],sum[1010];
int m,t;
int main(){
	cin>>t>>m;
	for(int i=1;i<=m;i++){
		cin>>sum[i]>>w[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=0;j<=t;j++){
			dp[i][j]=max(dp[i-1][j],dp[i][j]);
			if(j>=sum[i]){
				dp[i][j]=max(dp[i][j],dp[i-1][j-sum[i]]+w[i]);
			}
		}
	}
	cout<<dp[m][t];
	return 0;
}
一维
  • 定义状态 dp[j]表示使用体积为j的背包获得的最大价值
  • 状态转移 dp[j]=dp[j-v[i]]+w[i]
  • 输出结果 dp[m]
#include<bits/stdc++.h>
using namespace std;
int t,m;
int v[210],w[210];
int dp[2100];
signed main(){
	ios::sync_with_stdio(false);
	cin>>t>>m;
	for(int i=1;i<=m;i++)	cin>>v[i]>>w[i];
	for(int i=1;i<=m;i++){
		for(int j=t;j>=v[i];j--){
			dp[j]=max(dp[j-v[i]]+w[i],dp[j]); 
		}
	}
	cout<<dp[t];
	return 0;
}

注意:一维循环容积需要倒过来,这样才能保证只用了一次,其没有破坏上一次的更新

完全背包

与01背包类似,但没有每种物品只能使用一次的要求

二维
  • 定义状态 dp[i][j]表示前i个物品使用了j的体积可以获得的最大价值
  • 状态转移 dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
  • 输出结果 dp[n][m]
#include<bits/stdc++.h>
using namespace std;
int w[1010],dp[1010][1010],sum[1010];
int m,t;
int main(){
	cin>>t>>m;
	for(int i=1;i<=m;i++){
		cin>>sum[i]>>w[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=1;j<=t;j++){
			dp[i][j]=max(dp[i-1][j],dp[i][j]);
			if(j>=sum[i]){
				dp[i][j]=max(dp[i][j],dp[i][j-sum[i]]+w[i]);
			}
		}
	}
	cout<<dp[m][t];
	return 0;
}
一维
  • 定义状态 dp[j]表示使用了j的体积可以获得的最大价值
  • 状态转移 dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
  • 输出结果 dp[m]
#include<bits/stdc++.h>
using namespace std;
int w[1010],dp[1010],sum[1010];
int m,t;
int main(){
	cin>>t>>m;
	for(int i=1;i<=m;i++){
		cin>>sum[i]>>w[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=sum[i];j<=t;j++){
			dp[j]=max(dp[j],dp[j-sum[i]]+w[i]);
		}
	}
	cout<<dp[t];
	return 0;
}

注意:枚举时要正着,想想为什么

多重背包

与01背包类似,但加上了最多一个可以装k[i]次

二维
  • 定义状态 dp[i][j]表示前i个物品使用了j的体积可以获得的最大价值
  • 状态转移 dp[i][j]=max(dp[i][j],dp[i-1][j-cv[i]]+cw[i]);
  • 输出结果 dp[n][m]
#include<bits/stdc++.h>
using namespace std;
int k[2010];
int w[1010],dp[1010][1010],sum[1010];
int m,t;
int main(){
	cin>>t>>m;
	for(int i=1;i<=m;i++){
		cin>>sum[i]>>w[i]>>k[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=1;j<=t;j++){
			for(int c=0;c<=k[i]&&c*v[i]<=j;c++){
				dp[i][j]=max(dp[i][j],dp[i-1][j-c*sum[i]]+c*w[i]);	
			}
		}
	}
	cout<<dp[m][t];
	return 0;
}
一维
  • 定义状态 dp[j]表示使用了j的体积可以获得的最大价值
  • 状态转移 dp[j]=max(dp[j],dp[j-v[i]c]+w[i]c);
  • 输出结果 dp[m]
#include<bits/stdc++.h>
using namespace std;
int t,m;
int v[210],w[210];
int dp[2100];
int k[1010];
signed main(){
	ios::sync_with_stdio(false);
	cin>>t>>m;
	for(int i=1;i<=m;i++)	cin>>v[i]>>w[i]>>k[i];
	for(int i=1;i<=m;i++){
		for(int j=t;j>=v[i];j--){
			for(int c=0;c<=k[i]&&c*v[i]<=j;c++)
				dp[j]=max(dp[j-v[i]*c]+w[i]*c,dp[j]); 
		}
	}
	cout<<dp[t];
	return 0;
}

分组背包

与01背包类似

只需要加上k组中每组选一个最大的即可

#include<bits/stdc++.h>
using namespace std;
int n,m,maxn;
int a[10010],b[10001],dp[1100],c,t[1010],ans[1010][1100];
int main(){
	cin>>m>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i]>>c;
		maxn=max(maxn,c);
		ans[c][++t[c]]=i;
	}
	for(int i=1;i<=maxn;i++){
		for(int j=m;j>=0;j--){
			for(int k=1;k<=t[i];k++){
				if(j>=a[ans[i][k]]){
					dp[j]=max(dp[j],dp[j-a[ans[i][k]]]+b[ans[i][k]]);
				}
			}
		}
	}
	cout<<dp[m];
	return 0;
} 

LIS与LCS

最长上升子序列(LIS)

  • 给出一个序列,求最长上升子序列
    -设dp[i]表示前i个数的最长上升子序列是多长
    -dp[i]=max((dp[j]+1)*(a[j]<a[i]),dp[i])(j<i)
#include<bits/stdc++.h>
using namespace std;
long long n,a[5010],dp[5010];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>a[i];
	dp[1]=1;
	for(int i=2;i<=n;i++){
		dp[i]=1;
		for(int j=1;j<=i-1;j++){
			dp[i]=max(dp[i],(dp[j]+1)*(a[j]<a[i]));
		}
	}
	long long maxn=-1e9;
	for(int i=1;i<=n;i++)
		maxn=max(maxn,dp[i]); 
	cout<<maxn;
	return 0;
}

但此方法只能通过O(N*N)的数据,我们还可以进行优化。

  • 通过维护一个单调递增的队列,来求最长上升子序列。
  • 期间通过二分的方法,来找到当前的值可以放在队列中的最优位置。
  • 最后答案就是队列的大小
#include<bits/stdc++.h>
using namespace std;
int n,a[20010];
int dp[20010];
int main(){
	cin>>n;
	int len=0;
	for(int i=1;i<=n;i++)	cin>>a[i];
	for(int i=1;i<=n;i++){
		if(a[i]>=dp[len]){
			dp[++len]=a[i];continue;
		}
		int tot=lower_bound(dp+1,dp+1+len,a[i])-dp;
		dp[tot]=a[i];
	}
	cout<<len; 
	return 0;
}

最长公共子序列(LCS)

  • 给出两个序列,求最长公共子序列
    -设dp[i][j]表示第一个序列前i个数与第二个序列的前j个匹配的最长公共子序列是多长
  • if(s1[i]==s2[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
    -如果不满足那就继承上一次更新的。
  • if(s1[i]!=s2[j]) dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
#include<bits/stdc++.h>
using namespace std;
int n;
int dp[5010][5010];
string s1,s2;
int main(){
	cin>>n;
	s1=" "+s1;
	int len1=s1.size()-1;
	s2=" "+s2;
	int len2=s2.size()-1;
	for(int i=1;i<=len1;i++){
		for(int j=1;j<=len2;j++){
			if(s1[i]==s2[j]){
				dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
			}
			else{
				dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
			}
		}
	}	
	cout<<dp[len1][len2];
	return 0;
}

区间DP

  • 顾名思义,区间DP,是把小区间转移到大区间,从而解决全局性问题。

一般有以下特点

  • 合并:将两个或多个部分进行整合,当然也可以反过来;
    -特征:能将问题分解为能两两合并的形式;
    -求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。

例题:

石子合并

  • 给定一排石子,合并代价为两堆的代价和,求一堆时最小代价
  • 设dp[l][r]表示l~r区间的最小代价;
  • dp[l][r]=dp[l][k]+dp[k+1][r]+pre[r]-pre[l-1]
  • 一个区间可以由两个区间的各自价值加上两堆本身的价值
#include<bits/stdc++.h>
using namespace std;
int n;
int dp[110][110];
int pre[110];
int a[110];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)	dp[i][j]=INT_MAX;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		pre[i]=pre[i-1]+a[i];
		dp[i][i]=0;
	}
	for(int len=2;len<=n;len++){
		for(int l=1;l<=n-len+1;l++){
			int r=l+len-1; 	
			for(int k=l;k<r;k++){
				dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+pre[r]-pre[l-1]);
			}
		}
	} 
	cout<<dp[1][n];
	return 0;
}

关路灯

  • 初始站在一个位置,给定每一个路灯的位置与每一秒的耗能,求怎样关路灯使耗能最少。
  • 由题意得每一个区间要最后到一个地方最优,这个位置一定是最左或最右边。
  • 所以设dp[l][r][0/1]为把区间l~r的区间全部关完站在(左/右)端点的最小值
  • 转移为 当前节点从一旁区间的(左/右)端点走到当前端点所有没有关的路灯的总耗能,可以用前缀和维护。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,c;
int a[210],dp[110][110][2];
int pre[210];
int d;
signed main(){
	cin>>n>>c;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>d;
		pre[i]=pre[i-1]+d;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			dp[i][j][0]=dp[i][j][1]=INT_MAX;
		}
	}
	dp[c][c][0]=dp[c][c][1]=0;
	for(int i=1;i<=n;i++){
		if(i!=c){
			dp[i][i][0]=dp[i][i][1]=abs(a[c]-a[i])*pre[n];
		}		
	}
	for(int len=2;len<=n;len++){
		for(int l=1;l<=n-len+1;l++){
			int r=l+len-1;
			dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][0]+(a[l+1]-a[l])*(pre[n]-(pre[r]-pre[l])));
			dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][1]+(a[r]-a[l])*(pre[n]-(pre[r]-pre[l])));
			dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][0]+(a[r]-a[l])*(pre[n]-(pre[r-1]-pre[l-1])));
			dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][1]+(a[r]-a[r-1])*(pre[n]-(pre[r-1]-pre[l-1])));
		}
	}
	cout<<min(dp[1][n][1],dp[1][n][0]);
	return 0;
}

树形DP与树上背包问题

树是什么?

  • n个点n-1条边且没有重边的图,两点之间的简单路径只有一条。

树的重心

  • 定义:以一个点作为根,使其子树的大小最小。

  • 设dp[i]为以i为根的最大字数大小,siz[i]表示子树大小

  • 由定义得:dp[u]=max(dp[u],siz[son[u]]);

  • 最后不要忘了dp[u]=max(dp[u],n-siz[u]);

void dfs(int now,int fa){
    siz[now]=1;
    dp[now]=0;
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==fa)	continue;
		dfs(to,now);
        siz[now]+=siz[to];
		dp[now]=max(dp[now],siz[to]);	
		
	}	
	dp[now]=max(dp[now],n-siz[now]);
}

树的直径

  • 定义:树的最长两点的距离
  • 设dp[i][0/1]表示以i为父亲节点的[最大/次大](一点到父亲节点的长度)。
  • 为什么要这样设计?
  • 因为直径就是子树中的最远两点的距离,所以记录两个不同子树的最大值再+1,及从儿子节点跳到父亲节点。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,u,v;
struct node{
	int to,nxt;
}a[200010];
int head[200010],tot;
void add(int x,int y){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	head[x]=tot;
}
int dp[100010][2];
int maxn;
void dfs(int now,int fa){
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==fa)	continue;
		dfs(to,now);
		if(dp[now][0]<=dp[to][0]+1){
			dp[now][1]=dp[now][0];
			dp[now][0]=dp[to][0]+1;	
		}
		else{
			if(dp[now][1]<=dp[to][0]+1){
				dp[now][1]=dp[to][0]+1;
			}
		}
	}
	maxn=max(maxn,dp[now][0]+dp[now][1]);
}
signed main(){
	cin>>n;
	for(int i=1;i<n;i++){
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	dfs(1,0);
	cout<<maxn;
	return 0;
}

树形DP

  • 从儿子到父亲(子信息合并)
  • 从父亲到儿子(深度累加,答案累加)

例题:

没有上司的舞会

  • 一个树形结构,一个点与父节点只能选择一个获得权值,问最大权值
  • 考虑DP,设DP[i][0/1]表示第i个节点不选以他为父节点的子树的最大权值。
  • DP[u][1]=DP[v][0]+w[u];
  • DP[u][0]=max(DP[v][1],DP[v][0]);
#include<bits/stdc++.h>
using namespace std;
int n,r[6010],l,k;
int fa[6010];
int dp[6010][2];
struct node{
	int to,nxt;
}a[12010];
int head[12010],tot;
void add(int x,int y){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	head[x]=tot;
}
void dfs(int now,int f){
	fa[now]=f;
	dp[now][1]=r[now];
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==f)	continue;
		dfs(to,now);
		dp[now][0]+=max(dp[to][0],dp[to][1]);
		dp[now][1]+=dp[to][0];
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>r[i];
	for(int i=1;i<n;i++){
		int l,k;
		cin>>l>>k;
		add(l,k);
		add(k,l);
	} 
	dfs(1,0);
	cout<<max(dp[1][1],dp[1][0]);
	return 0;
}

树上背包

二叉苹果树

  • 给出一棵树,求断边后剩下的边的权值最大和
  • 考虑dp,设dp[i][k]表示i节点剩下k条边的最大权值
  • 转移,当前节点最大值是子节点用k条边最大值加上父节点用q-k-1条边,-1是因为父节点与子节点连边;
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,q;
int head[1010],tot;
struct node{
	int to,nxt,w;
}a[200];
void add(int x,int y,int w){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	a[tot].w=w;
	head[x]=tot;
}
int dp[200][200];
int siz[200];
void dfs(int now,int fa){
	siz[now]=1;
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==fa)	continue;
		dfs(to,now);
		siz[now]+=siz[to];
		for(int j=siz[now]-1;j>=1;j--){
			for(int k=0;k<j;k++){
				dp[now][j]=max(dp[now][j],dp[now][j-k-1]+dp[to][k]+a[i].w);
			}
		}
	}
}
signed main(){
	cin>>n>>q;
	for(int i=1;i<n;i++){
		int u,v,c;
		cin>>u>>v>>c;
		add(u,v,c),add(v,u,c);
	}
	dfs(1,-1);
	cout<<dp[1][q];
	return 0;
}

换根DP

例题:

STA-Station

  • 找到一个点作为根使深度总和最大
  • 朴素做法,做n遍dfs找到最大值
  • 正解:随便找一个点先做一遍dfs,找到以它为根节点的深度和。
  • 在第一次dfs时可以获得每个节点的的子树大小
  • 在第二遍dfs时可以得知父节点向子节点转移后,子节点最大应为父节点最大+父节点向子节点移的距离(即为1)乘以除子节点以外的节点数之和再减去向子节点移的距离*子节点的树的大小
#include<bits/stdc++.h>
#define int long long
int n,u,v;
using namespace std;
struct node{
	int to,nxt;
}a[2000010];
int f[1000100],g[1000010],sz[1000100],dep[1000010];
int tot,head[1000100];
void add(int x,int y){
	a[++tot].to=y;
	a[tot].nxt=head[x];
	head[x]=tot;
}
void dfs1(int now,int fa){
	sz[now]=1,dep[now]=dep[fa]+1;
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==fa)	continue;
		dfs1(to,now);
		sz[now]+=sz[to];
	}
}
void dfs2(int now,int fa){	
	for(int i=head[now];i;i=a[i].nxt){
		int to=a[i].to;
		if(to==fa)	continue;	
		f[to]=f[now]+sz[1]-2*sz[to];
		dfs2(to,now);
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<n;i++){
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	dfs1(1,0);
	for(int i=1;i<=n;i++)
		f[1]+=dep[i];
	dfs2(1,0);
	int maxn=-1e9,id=0;
	for(int i=1;i<=n;i++){
		if(f[i]>maxn)
			maxn=f[i],id=i;
	}
	cout<<id;
	return 0;
} 

状压dp

  • 通过状态压缩的方式将每个情况枚举用2/3进制数来表示状态。
  • 一般适用于N<=20。
  • 一般把状态用二进制数相加。

例题

售货员的难题

  • 看到数据范围N<=20,发现可以状态压缩每个村庄去或不去。
  • 设dp[i][k]表示第i个村庄的状态
  • 可以知道一个村庄去某个村庄的状态可以由不去这个村庄再加上去这个村庄的路程
#include<bits/stdc++.h>
using namespace std;
int n,a[30][30],dp[21][1<<20|1];
int main(){
	cin>>n;
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)	cin>>a[i][j];
	}	
	dp[1][1]=0;
	for(int i=0;i<(1<<n);i++){ 
		for(int j=1;j<=n;j++){
			if(!(i&(1<<(j-1)))){
				for(int k=1;k<=n;k++){
					if(j==k)	continue;
					if((i&(1<<(k-1))))
						dp[j][i|(1<<(j-1))]=min(dp[j][i|(1<<(j-1))],dp[k][i]+a[k][j]);
				}
			}
		}
	}
	int ans=2e9;
	for(int i=2;i<=n;i++){
		ans=min(ans,dp[i][((1<<n)-1)]+a[i][1]);
	}
	cout<<ans;
	return 0; 
}

互不侵犯

  • 发现N很小,可以状压每一行的状态,发现可以由之前的状态到当前状态。
  • 考虑dp,dp[i][k][sum]表示当前到第i行状态为k,放了sum个的方案数的方案数
  • 当当前位置的上方,左上,右上或者左侧有棋子时无法放置;
  • 然后可以知道当前状态放了j个,可以由上一个状态放了sum[i]个转移
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=9;
int n,m;
int sum[1<<MAXN|1];
int dp[MAXN+1][1<<MAXN|1][MAXN*10]; 
bool check(int x,int y){
	if((y&(y<<1))||(x&y)||(x&(y<<1))||(y&(x<<1)))	return 1;
	return 0;                                                                                                                                                                      
}
signed main(){
	cin>>n>>m;
	for(int i=0;i<(1<<n);i++){
		if(i&(i<<1))	continue;
		sum[i]=__builtin_popcount(i);
		dp[1][i][sum[i]]=1;
	} 
	for(int j=2;j<=n;j++){
		for(int i=0;i<(1<<n);i++){	
			if(i&(i<<1))	continue;
			for(int k=0;k<(1<<n);k++){
				if(check(i,k))	continue;
				for(int l=sum[i];l<=m;l++){
					dp[j][i][l]+=dp[j-1][k][l-sum[i]];
				}
			}
		}
	}
	int ans=0;
	for(int i=0;i<(1<<n);i++){
		ans+=dp[n][i][m];
	}
	cout<<ans;
	return 0;
}
posted @ 2024-09-11 09:32  lmy333  阅读(42)  评论(0)    收藏  举报