地址:https://www.cnblogs.com/FReQuenter5156/p/shuxingyilaidp.html/
简介
这类背包本质上是分组背包问题。
将一个节点的每一棵子树看作一组,进行分组背包。所谓分组背包,即在选择物品的时候,一开始将物品分为好几组,在选择时,可以从每一组中至多选择一件物品,问如何获得最大的价值,所以我们每次可以枚举这个组数,用 \(i\) 表示第几组,用 \(j\) 表示体积,用 \(k\) 来表示选择的物品,伪代码如下:
for (int i=0到组数)
for (int j=max_v到0)
for (int k=此组所有物品)
f[j]=max(f[j],f[j-v[k]]+w[k])
注意循环顺序。这个和 01 背包的道理类似。
例题
[CTSC1997] 选课
题面
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 \(N\) 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 \(M\) 门课程学习,问他能获得的最大学分是多少?
题解
定义
\(f_{i,j}\) 表示当前第 \(i\) 个点的子树内选 \(j\) 个点(不包括自己)的最大学分。
转移
直接套板子。初值在输入时已经设置好了,DP 中只要比大小。具体参考代码。
代码
#include<bits/stdc++.h>
#define MAXN 305
using namespace std;
int n,m,k[MAXN],s[MAXN],sz[MAXN],f[MAXN][MAXN];
vector<int> gv[MAXN];
void dfs(int now,int father){
sz[now]=1;
for(auto nx:gv[now]){
if(nx!=father) dfs(nx,now),sz[now]+=sz[nx];
}
}
void dp(int now,int father){
int sum=0;
for(auto nx:gv[now]){
if(nx==father) continue;
sum+=sz[nx];
dp(nx,now);
for(int j=sum;j>=0;j--){
for(int k=0;k<sz[nx];k++){
if(j-k>=1){
f[now][j]=max(f[now][j],f[now][j-k-1]+f[nx][k]);
}
}
}
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>k[i]>>s[i];
gv[k[i]].push_back(i);
f[i][0]=s[i];
}
dfs(0,0);
dp(0,0);
cout<<f[0][m];
}
有线电视网
题面
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
题解
定义
\(f_{i,j}\) 表示到第 \(i\) 个点供应 \(j\) 个用户的最大收益。最后求答案的时候枚举下 \(f_{1,i}\geq 0\) 的最大 \(i\)。注意初值赋 -INF。
转移
也是套板子。直接看代码应该就能理解。
代码
#include<bits/stdc++.h>
#define fi first
#define se second
#define MAXN 3005
using namespace std;
vector<pair<int,int>> gv[MAXN];
int n,m,f[MAXN][MAXN],v[MAXN];
int dp(int now){
if(now>=n-m+1){
f[now][1]=v[now];
return 1;
}
int sum=0;
for(auto nx:gv[now]){
int tmp=dp(nx.fi);
sum+=tmp;
for(int j=sum;j>=1;j--){
for(int k=1;k<=tmp;k++){
if(j-k>=0) f[now][j]=max(f[now][j],f[now][j-k]+f[nx.fi][k]-nx.se);
}
}
}
return sum;
}
signed main(){
memset(f,-0x3f,sizeof(f));
cin>>n>>m;
for(int i=1;i<=n-m;i++){
int k;
cin>>k;
for(int j=1;j<=k;j++){
int a,c;
cin>>a>>c;
gv[i].push_back({a,c});
}
}
for(int i=n-m+1;i<=n;i++) cin>>v[i];
for(int i=1;i<=n;i++) f[i][0]=0;
int maxn=0;
dp(1);
for(int i=0;i<=m;i++) if(f[1][i]>=0) maxn=i;
cout<<maxn;
}
重建道路
题面
一场可怕的地震后,人们用 \(N\) 个牲口棚(编号 \(1\sim N\))重建了农夫 John 的牧场。由于人们没有时间建设多余的道路,所以现在从一个牲口棚到另一个牲口棚的道路是惟一的。因此,牧场运输系统可以被构建成一棵树。
John 想要知道另一次地震会造成多严重的破坏。有些道路一旦被毁坏,就会使一棵含有 \(P\) 个牲口棚的子树和剩余的牲口棚分离,John 想知道这些道路的最小数目。
题解
定义
\(f_{i,j}\) 表示当前第 \(i\) 个点为根的子树保留 \(j\) 个点(包括自己)删去的最小的边数。答案需要枚举所有的子树。
转移
其实也差不多。可以直接看代码。注意初始值 \(f_{i,1}\) 为每个点的出度。这题相对来说比较细节。如方程中的 -1
,和自己的“父亲”的连边不能拆。
代码
#include<bits/stdc++.h>
using namespace std;
vector<int> gv[155];
int n,p,ans=0x3f3f3f3f,f[155][155],sz[155],od[155];
bool ir[155];
void dp(int now){
sz[now]=1;
if(!od[now]) return f[now][1]=0,void();
for(auto nx:gv[now]){
dp(nx);
sz[now]+=sz[nx];
for(int j=sz[now];j>=0;j--) for(int k=1;k<j;k++) f[now][j]=min(f[now][j],f[now][j-k]+f[nx][k]-1);
}
}
signed main(){
cin>>n>>p;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
gv[x].push_back(y),od[x]++,ir[y]=true;
}
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++) f[i][1]=od[i];
int root=0;
for(int i=1;i<=n;i++) if(!ir[i]) root=i;
dp(root);
for(int i=1;i<=n;i++){
if(i==root) ans=min(ans,f[i][p]);
else ans=min(ans,f[i][p]+1);
}
cout<<ans;
return 0;
}
拓展
[JSOI2016]最佳团体
题面
JSOI 信息学代表队一共有 \(N\) 名候选人,这些候选人从 \(1\) 到 \(N\) 编号。方便起见,JYY 的编号是 \(0\) 号。每个候选人都由一位编号比他小的候选人\(R_i\) 推荐。如果 \(R_i = 0\)?,则说明这个候选人是 JYY 自己看上的。
为了保证团队的和谐,JYY 需要保证,如果招募了候选人 \(i\),那么候选人 \(R_i\) 也一定需要在团队中。当然了,JYY 自己总是在团队里的。每一个候选人都有一个战斗值 \(P_i\) ,也有一个招募费用 \(S_i\) 。JYY 希望招募 \(K\) 个候选人(JYY 自己不算),组成一个性价比最高的团队。也就是,这 \(K\) 个被 JYY 选择的候选人的总战斗值与总招募费用的比值最大。
题解
简单说一下。比值类似于 最优比例生成树,可以使用分数规划,然后就可以去套板子了。
没来得及实现,可以参考洛谷题解。
标签:gv,选做,sz,int,题解,nx,MAXN,now,DP From: https://www.cnblogs.com/FReQuenter5156/p/shuxingyilaidp.html