动态规划

树形DP

没有上司的舞会

题意

有一个有\(n+1\)个节点的树,树根为 \(0\)。编号为 $ 1到n $ 的点分别有自己的权值\(r_i\)。在\(1到n\)的节点中,如果取了一个节点的父亲节点,那么就不能取这个节点。

求总权值的最大值。

\(dp[i][0/1]\) 为以\(i\)为根的子树,取 / 不取 \(i\) 所得的权值最大值。

所以答案 $ =max(dp[0][1],dp[0][0]) $ .

\(s_i\) 表示 \(i\) 的孩子的集合,用 \(val[i]\) 表示节点 \(i\) 的权值。

那么状态转移:

如果不选\(i\):

\(dp[i][0]=\sum_{j∈s_i}^{} max(dp[j][0],dp[j][1])\)

如果选\(i\):

$dp[i][1]=( \sum_{j∈s_i} dp[j][0] ) + val[i] $

关于循环方式:\(dfs\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=6e4+105;
int dp[N][3];
int nex[N],to[N],v[N],h[N];bool f[N];
int n,x,y,z,root,cnt;
inline void add(int x,int y){to[++cnt]=y;nex[cnt]=h[x];h[x]=cnt;}
void dfs(int x){
	f[x]=1;dp[x][1]=v[x];
	for(int i=h[x];i;i=nex[i]){
		if(f[to[i]])continue;
		dfs(to[i]);
		dp[x][1]+=dp[to[i]][0];
		dp[x][0]+=max(dp[to[i]][1],dp[to[i]][0]);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>v[i];
	for(int i=1;i<n;i++){
		cin>>x>>y;
		add(x,y);add(y,x);
	}
	dfs(1);
	printf("%d",max(dp[1][0],dp[1][1]));
	return 0;
}

最大子树和

题意

找到树上点权之和最大的一个连通分量。(虽然原题说了很多,但是总结起来确实只有这句话

\(dp[i]\) 为 以\(i\)为根的子树中的最大点权和

所以有:

\(dp[i]=v[i]+( dp [j] > 0 ? dp [j] : 0 )\) ,

\(j为i的儿子\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+105;
const int INF=2147483647;
int dp[N],n,x,y,ans=-INF;
int nex[N],to[N],h[N],v[N],cnt;bool f[N];
void add(int x,int y){to[++cnt]=y;nex[cnt]=h[x];h[x]=cnt;}
void dfs(int x){
	dp[x]=v[x];f[x]=1;
	for(int i=h[x];i;i=nex[i]){
		if(f[to[i]])continue;
		dfs(to[i]);
		dp[x]+=((dp[to[i]]>0)?dp[to[i]]:0);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>v[i];
	for(int i=1;i<n;i++){
		cin>>x>>y;
		add(x,y);add(y,x);
	}
	dfs(1);
	for(int i=1;i<=n;i++)ans=max(ans,dp[i]);
	printf("%d",ans);
	return 0;	
} 

[CTSC1997]选课

题意

树形背包模板。

有一个有 \(N\) 个节点的森林,取一个节点的权值时,要求也要取其父亲节点。一共取\(M\)个点。

把所有树的根节点连接一个 \(0\) 节点,变成一棵树。

在树上跑背包。

code

#include<bits/stdc++.h>
using namespace std;
const int N=405;
int nex[N],to[N],v[N],h[N],cnt;
int dp[N][N],root,n,m,x,y;
inline void add(int x,int y){to[++cnt]=y;nex[cnt]=h[x];h[x]=cnt;}
void dfs(int x,int w){
	if(m<=0) return ;
	for(int i=h[x];i;i=nex[i]){
		for(int k=0;k<w;k++) dp[to[i]][k]=dp[x][k]+v[to[i]];
		dfs(to[i],w-1);
		for(int k=1;k<=w;k++)dp[x][k]=max(dp[x][k],dp[to[i]][k-1]);
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>x>>v[i];
		add(x,i);
	}
	dfs(0,m);
	printf("%d",dp[0][m]);
	return 0;
}

送给好友的礼物

考试题

题意

给定一棵包含 \(n\) 个结点的树 \(T\),结点从 \(1 到 n\) 顺序编号。

\(M\) 和小 \(B\) 在时刻 \(0\) 都在 \(1\) 号结点。从时刻 \(1\) 开始的每个时刻初,小 \(M\) 和小 \(B\) 都可以选择:移动到一个和自己所在结点直接相连的结点,或者停留在当前所在的结点。

树上有 \(k\) 个草莓,它们分布在 \(k\) 个不同的结点上。小 \(M\) 和小 \(B\) 想要收集到所有的草莓,任何一个时刻末,如果小 \(M\) 或者小 \(B\) 在某一个草莓所在的结点上,那么这个草莓就被收集了。

她们不想花费太多的时间,因此你需要回答:至少在第几时刻末,小 \(M\) 和小 \(B\) 可以收集到所有的草莓,并且都回到结点 \(1\)

样例

\(输入\)

\(7\) \(4\)
\(1\) \(2\)
\(2\) \(3\)
\(2\) \(4\)
\(1\) \(5\)
\(5\) \(6\)
\(6\) \(7\)
\(3\) \(4\) \(5\) \(7\)

\(输出\)

\(6\)

其实我觉得搞玄学也不是不行……

首先把一些没有草莓的子树砍掉。那么叶子节点就都是草莓了。

所以就是需要遍历整棵树。

code

#include<bits/stdc++.h>
using namespace std;
const int N=1000;
const int INF=2147483647;

int n,k;
int cnt,h[N],nex[N<<1],to[N<<1];
int f[N],siz[N],dp[N][N<<1];
bool vis[N];

inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}	
	return x*f;
}

void add(int x,int y){to[++cnt]=y;nex[cnt]=h[x];h[x]=cnt;}

bool dfs(int x){
	bool fs=0;
	for(int i=h[x];i;i=nex[i]){
		int y=to[i];
		if(y==f[x] || !y) continue;
		f[y]=x;bool now=dfs(y);
		if(!now)to[i]=0;
		fs|=now; 
	}
	return fs|vis[x];
}

void work(int x){
	siz[x]=1;dp[x][0]=0;
	for(int k=h[x];k;k=nex[k]){
		int y=to[k];
		if(y==f[x]||!y)continue;
		work(y);siz[x]+=siz[y];
		for(int i=(siz[x]-1)*2;i>=0;i--){
			dp[x][i]=dp[x][i]+dp[y][0]+2;
			if(i>=siz[y]*2)
				{dp[x][i]=min(dp[x][i],dp[x][i-siz[y]*2]+dp[y][(siz[y]-1)*2]);}
			for(int j=min((siz[y]-1)*2,i-2);j>=0;j--)
				{dp[x][i]=min(dp[x][i],dp[x][i-j-2]+dp[y][j]+2);}						
		}
	}
}
int main(){
	memset(dp,0x3f,sizeof(dp));
	n=read();k=read();
	for(int i=1;i<n;i++){
		int x=read(),y=read();
		add(x,y);add(y,x);
	}
	for(int i=1;i<=k;i++)vis[read()]=1;
	dfs(1);work(1);
	int ans=INF;
	for(int i=0;i<=(siz[1]-1)*2;i++)ans=min(ans,max(i,dp[1][i]));
	printf("%d",ans);
	return 0;
}

区间DP

[NOI1995] 石子合并

题目描述

在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆.规定每次只能选 相邻的 \(2\) 堆 合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 \(N\) 堆石子合并成 \(1\) 堆的最小得分和最大得分。

code

#include<bits/stdc++.h>
using namespace std;
const int N=305;
const int INF=2147483640;
int a[N],q[N],n,dp1[N][N],dp2[N][N];
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
	return x*f;
}
int main(){
	n=read();
	memset(dp1,0x3f,sizeof(dp1));memset(dp2,0x80,sizeof(dp2));
	int m=n*2;
	for(int i=1;i<=m;i++)dp1[i][i]=0,dp2[i][i]=0;
	for(int i=1;i<=n;i++){a[i]=read();a[i+n]=a[i];}
	for(int i=1;i<=m;i++) q[i]=a[i]+q[i-1];
	for(int len=2;len<=n;len++)
		for(int l=1;l<=m;l++){
			int r=l+len-1;
			if(r>m) break;
			for(int k=l;k<r;k++){
				dp1[l][r]=min(dp1[l][k]+dp1[k+1][r]+q[r]-q[l-1],dp1[l][r]);
				dp2[l][r]=max(dp2[l][k]+dp2[k+1][r]+q[r]-q[l-1],dp2[l][r]);
			}
		}
	int ans1=INF,ans2=(-INF);
	for(int i=1;i<=n;i++){
		ans1=min(ans1,dp1[i][i+n-1]);
		ans2=max(ans2,dp2[i][i+n-1]);
	}
	printf("%d\n%d",ans1,ans2);
	return 0;	
}

[NOIP2006 提高组] 能量项链

题目描述

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 NN 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 mm,尾标记为 rr,后一颗能量珠的头标记为 rr,尾标记为 nn,则聚合后释放的能量为 m \times r \times nm×r×n(Mars 单位),新产生的珠子的头标记为 mm,尾标记为 nn。

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N=4N=4,44 颗珠子的头标记与尾标记依次为 (2,3)(3,5)(5,10)(10,2)(2,3)(3,5)(5,10)(10,2)。我们用记号 \oplus⊕ 表示两颗珠子的聚合操作,(j \oplus k)(j⊕k) 表示第 j,kj,k 两颗珠子聚合后所释放的能量。则第 44 、 11 两颗珠子聚合后释放的能量为:

(4 \oplus 1)=10 \times 2 \times 3=60(4⊕1)=10×2×3=60。

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:

((4 \oplus 1) \oplus 2) \oplus 3)=10 \times 2 \times 3+10 \times 3 \times 5+10 \times 5 \times 10=710((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。

输入格式

第一行是一个正整数 N(4 \le N \le 100)N(4≤N≤100),表示项链上珠子的个数。第二行是 NN 个用空格隔开的正整数,所有的数均不超过 10001000。第 ii 个数为第 ii 颗珠子的头标记 (1 \le i \le N)(1≤i≤N),当 i<Ni<N 时,第 ii 颗珠子的尾标记应该等于第 i+1i+1 颗珠子的头标记。第 NN 颗珠子的尾标记应该等于第 11 颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。


普通的背包

(1) 01背包问题

P2871 [USACO07DEC]Charm Bracelet S

题目

\(N\) 件物品和一个容量是 \(M\) 的背包。每件物品只能使用一次。

\(i\) 件物品的重量是 \(C_i\),价值是 \(W_i\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

输入

第一行:物品个数 \(N\) 和背包大小 \(M\)

第二行至第 \(N+1\) 行:第 \(i\) 个物品的重量 \(C_i\) 和价值 \(W_i\)

code

注释版:

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e3+105;//注意空间大小 
int N,M,c[maxn],w[maxn],f[maxn][maxn];
int main(){
	//cout<<"Nickle's code"<<endl;
	cin>>N>>M;//N为物品个数,M为背包大小 
	for(int i=1;i<=N;i++) cin>>c[i]>>w[i];//c为重量,w为价值 
	for(int i=1;i<=N;i++)
		for(int j=M;j>=0;j--){
			if(j>=c[i]) f[i][j]=max(f[i-1][j-c[i]]+w[i],f[i-1][j]);
			else f[i][j]=f[i-1][j];
		}
	cout<<f[N][M];
	return 0;
} 

核心代码纯享版:

	for(int i=1;i<=N;i++)
		for(int j=M;j>=0;j--){
			if(j>=c[i]) f[i][j]=max(f[i-1][j-c[i]]+w[i],f[i-1][j]);
			else f[i][j]=f[i-1][j];
		}
	cout<<f[N][M];

(2) 分组(01)背包

P1757 通天之分组背包

题目描述

\(01\) 背包问世之后,小 \(A\) 对此深感兴趣。

一天,小 \(A\) 去远游,却发现他的背包不同于 \(01\) 背包,他的物品大致可分为 \(k\) 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。

输入格式

两个数 \(m,n\) ,表示一共有 \(n\) 件物品,总质量为 \(m\)

接下来 \(n\) 行,每行 \(3\) 个数 \(a_i,b_i,c_i\) 表示物品的重量,利用价值,所属组数

code

注释:

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e3+105;//注意空间大小 
int m,n,f[maxn],a[maxn],b[maxn],Kind,c_cnt[1005],c_num[1005][1005],x;
//kind记录组数,f是转移数组,a是重量,b是价值 
//c_cnt[i]记录第i组一共有几件东西,c_num[i][j]记录第i组第j件物品的总序号 
int main(){
	//cout<<"Nickle"'s code"<<endl;
	cin>>m>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i]>>x;
		Kind=max(x,Kind);
		c_cnt[x]++;c_num[x][c_cnt[x]]=i;
	} 
	for(int i=1;i<=Kind;i++)
	 for(int j=m;j>=0;j--)
	  for(int k=1;k<=c_cnt[i];k++)
	   if(j>=a[c_num[i][k]])f[j]=max(f[j],f[j-a[c_num[i][k]]]+b[c_num[i][k]]);
	cout<<f[m];
	return 0;
} 

核心代码:

for(int i=1;i<=Kind;i++)
	 for(int j=m;j>=0;j--)
	  for(int k=1;k<=c_cnt[i];k++)
	   if(j>=a[c_num[i][k]])f[j]=max(f[j],f[j-a[c_num[i][k]]]+b[c_num[i][k]]);

(3) 完全背包问题

完全背包例题

题目

\(N\) 种物品和一个容量是 \(V\) 的背包,每种物品都有无限件可用。第 \(i\) 种物品的体积是 \(v_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式

第一行两个整数,\(N\)\(V\),用空格隔开,分别表示物品种数和背包容积。

接下来有 \(N\) 行,每行两个整数 \(v_i,w_i\),用空格隔开,分别表示第 \(i\) 种物品的体积和价值。

code

#include<bits/stdc++.h>
#define int long long  
using namespace std;
const int N=1e7+5;
int n,V,w[N],v[N],f[N];
signed main(){
	cin>>n>>V;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++) 
                for(int j=v[i];j<=V;j++) 
                        f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[V];
	return 0;
}

核心代码:

for(int i=1;i<=n;i++) 
    for(int j=v[i];j<=V;j++) 
        f[j]=max(f[j],f[j-v[i]]+w[i]);

(4) 多重背包问题

问题一

多重背包问题 I

题目

\(N\) 种物品和一个容量是 \(V\) 的背包。

\(i\) 种物品最多有 \(s_i\) 件,每件体积是 \(v_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

输入格式

第一行两个整数,N,V,
用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,
用空格隔开,分别表示第 i 种物品的体积、价值和数量。

code

#include<bits/stdc++.h>
using namespace std;
const int m=1e4+5;
int T,M,num,val,wight,cnt,tot,w[m],v[m],f[m];
int main(){
	cin>>M>>T;
	while(M--){
		cin>>wight>>val>>num;
		while(num--){v[++cnt]=val;w[cnt]=wight;}		     					
	}
	for(int i=1;i<=cnt;i++)for(int j=T;j>=w[i];j--)f[j]=max(f[j],f[j-w[i]]+v[i]);							
	cout<<f[T];	 
	return 0;
} 

核心代码

	while(num--){v[++cnt]=val;w[cnt]=wight;}

问题二 二进制优化方法

多重背包问题 II

给定任意一个数\(s\),最少把\(s\)分为多少个数,可以使得这些数拼成小于等于\(s\)的所有数?

\(log2(s)\)

假设这个数是\(8\),可以拆成\(1,2,4\)

假设这个数是\(10\),可以拆成\(1,2,4,3\)

假设这个数是\(7\),可以拆成\(1,2,4\)

\(s-1-2-4……\)一直到不能减为止

code

#include<bits/stdc++.h>
using namespace std;
int n,m,f[2105],v,w,s;
struct qwq{int v,w;};
vector<qwq> q;
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>v>>w>>s;
		for(int k=1;k<=s;k<<=1){s-=k;q.push_back({v*k,w*k});}
		if(s>0) q.push_back({v*s,w*s});
	}
	for(auto qwq: q)for(int j=m;j>=qwq.v;j--)f[j]=max(f[j],f[j-qwq.v]+qwq.w);
	cout<<f[m]<<endl;
	return 0;
}

问题三 单调队列优化

多重背包问题 III

code

#include<bits/stdc++.h>
using namespace std;
const int N=2e4+105;
int n,m,f[N],g[N],q[N],c,w,s;
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		cin>>c>>w>>s;
		memcpy(g,f,sizeof(f));
		for(int j=0;j<c;j++){
			int hh=0,tt=-1;
			for(int k=j;k<=m;k+=c){
				f[k]=g[k];
				if(hh<=tt&&k-s*c>q[hh]) hh++;
				if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/c*w);
				while(hh<=tt&&g[q[tt]]-(q[tt]-j)/c*w<=g[k]-(k-j)/c*w) tt--;
				q[++tt]=k;
			}
		}
	}
	cout<<f[m];
	return 0;
}

没有auto的代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+105,M=2e3+105;
int n,m,v[N],w[N],f[N],cnt,a,b,s;
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a>>b>>s;
		int k=1;
		while(k<=s){v[++cnt]=a*k;w[cnt]=b*k;s-=k;k<<=1;}
		if(s>0){v[++cnt]=a*s;w[cnt]=b*s;}
	}
	for(int i=1;i<=cnt;i++)for(int j=m;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[m];
	return 0;
}

(5) 混合背包问题

混合背包问题

题目

\(N\) 种物品和一个容量是 \(V\) 的背包。

物品一共有三类:

  • 第一类物品只能用 \(1\) 次( \(01\) 背包);

  • 第二类物品可以用无限次(完全背包);

  • 第三类物品最多只能用 \(s_i\) 次(多重背包);

每种体积是 \(v_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

输入格式

第一行两个整数,\(N,V\),用空格隔开,分别表示物品种数和背包容积。

接下来有 \(N\) 行,每行三个整数 \(v_i,w_i,s_i\),用空格隔开,分别表示第 \(i\) 种物品的体积、价值和数量。

  • \(s_i=−1\) 表示第 \(i\) 种物品只能用 \(1\) 次;

  • \(s_i=0\) 表示第 \(i\) 种物品可以用无限次;

  • \(s_i>0\) 表示第 \(i\) 种物品可以使用 \(s_i\) 次;

code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+105;
int n,m,v[N],w[N],f[N],cnt,a,b,s;
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a>>b>>s;
		int k=1;
		if(s<0) s=1;
		else if(s==0) s=m/a;
		while(k<=s){v[++cnt]=a*k;w[cnt]=b*k;s-=k;k<<=1;}
		if(s>0){v[++cnt]=s*a;w[cnt]=b*s;}		
	}
	for(int i=1;i<=cnt;i++)for(int j=m;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[m];
	return 0;
}

posted @ 2021-09-11 22:09  Nickle-NI  阅读(35)  评论(0编辑  收藏  举报