多叉树的背包问题
明天开学
问题模型一般是:
给定n个物品,有一个容积为V的背包,每个物品依赖一个物品,选择一个必须选择所依赖的物品,依赖关系构成一棵树,每个物品有体积,价值,最大化价值;
这种模型称作树形有依赖背包问题;
常见方法大概有三种:
1.多叉树转化为二叉树:
将一个节点的多个儿子中选择最左的儿子保留下来,剩下的儿子放到左儿子的右子树里;
来张图片:
状态转移即为:
设dp[i][j]表示在以i为根的子树中,用大小为j的包能取得的最大价值,
dp[i][j]=max(dp[left[i]][j-w[i]]+v[i],dp[right[i]][j]);
left[i]是i在原树中的第一个儿子,right[i]是i在原树中的下一个兄弟。
第二种方法:dfs序表示法:
我们对于每一个节点求出他在原树中的dfs序dfn值,然后记录他的子数大小size;
那么对于一个节点x:x的第一个儿子y有dfn[y]=dfn[x]+1;
对于x的第二个儿子z,dfn[z]=dfn[x]+size[y]+1;
对于x的一个兄弟k,有dfn[k]=dfc[x]+size[x]+1;
一个dfs序为i的节点u,同样设dp[i][j]表示在以u为根的子树中,用大小为j的包能取得的最大价值,
dp[i][j]+w[i]->dp[i+1][j-v[i]]
dp[i][j]->dp[i+size[i]+1][j]
这也是较常用的刷表法,用当前状态更新后继的状态;
两种方法主要解决点权问题;
那么我们看一下用树形分组背包问题:
一道经典问题 选课:题目传送门;
题解(https://www.cnblogs.com/Tyouchie/p/10830072.html);
我们设dp[k][i][j]表示以i为根的子树,在前k个儿子中,选出一个大小为j的子树(必须包含i),所获得的最大学分。
那么我们每计算到第k+1个新的儿子v时(size[v]表示v的儿子个数),
dp[k+1][i][j]=min(dp[k][i][j-t]+dp[size[v]][v][t]);
k这一维显然可以无用的;
那么我们改为f[i][j]表示在以i为根的子树内选择了j门课程所获得的最大学分;
dp[i][j]=max(dp[i][j-t]+dp[v][t]);
j=m-1->1(m指容积,-1是因为当前点占据了1的体积);
t=1->j(在我那片题解中我倒序枚举了t,但是我发现两者都是正确的);
这个模型感觉要理解并且掌握,因为很多树形背包类dp的状态都是这样转移的;
#include<bits/stdc++.h> using namespace std; int lin[1000],tot,n,m,x,f[400][400],s[400]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} x*=f; } struct gg { int y,next; }a[2000]; inline void add(int x,int y) { a[++tot].y=y; a[tot].next=lin[x]; lin[x]=tot; } inline void dp(int x) { f[x][0]=0; for(int i=lin[x];i;i=a[i].next) { int y=a[i].y; dp(y); for(int t=m;t>=0;--t) { for(int j=0;j<=t;++j) { if(t-j>=0) { f[x][t]=max(f[x][t],f[x][t-j]+f[y][j]); } } } } if(x!=0) { for(int t=m;t>0;t--) f[x][t]=f[x][t-1]+s[x]; } } int main() { read(n);read(m); for(int i=1;i<=n;i++) { read(x); add(x,i); read(s[i]); } dp(0); cout<<f[0][m]<<endl; return 0; }
f[i][j]表示以i为根的子树内分离一个大小为j的子树所需要的最少操作数;
j=p->1;
t=1->j;
和上一道题目很像;
显然f[x][1]为每个点x的度;
再转移的时候-2是因为u->v的边和v->u的边都已经在初始化的时候被减掉了,所以这时候要把他们连起来就得减去他们两个造成的贡献,也就是2.,思考一下,很好理解;
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0; T f=1,ch=getchar(); while (!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar(); x*=f; } const int N=250; struct gg { int y,next,v; }a[N<<1]; int n,p,tot,x,y,lin[N],du[250],dp[250][250]; inline void add(int x,int y) { a[++tot].y=y; a[tot].next=lin[x]; lin[x]=tot; } inline void dfs(int x,int fa) { dp[x][1]=du[x]; for(int i=lin[x];i;i=a[i].next) { int y=a[i].y; if(y==fa) continue; dfs(y,x); for(int j=p;j>=1;j--) { for(int t=1;t<=j;t++) { dp[x][j]=min(dp[x][j],dp[x][j-t]+dp[y][t]-2); } } } } int main() { read(n); read(p); for(int i=1;i<n;i++) { read(x); read(y); add(x,y); add(y,x); du[x]++; du[y]++; } memset(dp,0x3f,sizeof(dp)); dfs(1,0); int ans=1<<30; for(int i=1;i<=n;i++) { ans=min(ans,dp[i][p]); } cout<<ans<<endl; return 0; }
真正理解前两到题目这道也就不难了;
我们设f[i][j]表示在以i为根的子树内,让j个人看所获得的最大收益;
那么对于每一个叶子节点x,f[x][1]就等于val[x];
那么最后我们寻找一个最大的i满足f[1][i]>=0;
所以我们倒序扫描就行了;
状态转移是j,t的枚举和前两题是一样的;
根据当前背包容积转移;
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0; T f=1,ch=getchar(); while (!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar(); x*=f; } int n,m,dp[3010][3010],val[3010],k,x,v,tot,lin[3010]; struct gg { int y,next,v; }a[1000100]; inline void add(int x,int y,int v) { a[++tot].y=y; a[tot].next=lin[x]; lin[x]=tot; a[tot].v=v; } int dfs(int u) { if(u>n-m) { dp[u][1]=val[u]; return 1; } int sum=0,s,t; for(int i=lin[u];i;i=a[i].next) { int y=a[i].y; s=dfs(y); sum+=s; for(int j=sum;j>0;j--) { for(int t=1;t<=j;t++) { dp[u][j]=max(dp[u][j],dp[u][j-t]+dp[y][t]-a[i].v); } } } return sum; } int main() { memset(dp,0xcf,sizeof(dp)); read(n); read(m); for(int i=1;i<=n-m;i++) { read(k); for(int j=1;j<=k;j++) { read(x); read(v); add(i,x,v); } } for(int i=n-m+1;i<=n;i++) read(val[i]); for(int i=1;i<=n;i++) dp[i][0]=0; dfs(1); for(int i=m;i>=1;i--) if(dp[1][i]>=0) { printf("%d",i); break; } return 0; }