ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

[总结] 树形背包的细节问题

2021-08-12 17:32:19  阅读:212  来源: 互联网

标签:背包 int ch fa 细节 树形 maxn -- include


树形背包的细节问题

本博客用于说明树形背包的细节问题(非常重要)

一个下午就想了个这个

选课

很经典的题目,题解见[总结] 树形 dp

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=305,maxm=305;
int head[maxn],w[maxn],f[maxn][maxn],cnt = 0,n,m;
int sz[maxn]; 
struct Tree{
	int to,nxt;
}e[maxm];
inline void link(int u,int v){
	e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;
}
void dp(int x){
	sz[x]=1;
	f[x][0]=0;
	for(int i=head[x];i;i=e[i].nxt){
		int y=e[i].to;
		dp(y);
		sz[x]+=sz[y];
		for(int t=min(sz[x],m);t>=0;t--){//选课门数(容量),倒序循环来正确处理体积为 0 的物品 
			for(int j=0;j<=min(t,sz[y]);j++){
			    f[x][j]+=f[y][0];
				if(t-j>=0 && f[x][t-j]!=-1)
					f[x][t]=max(f[x][t],f[y][j]+f[x][t-j]); 
			}
		}
	}
	if(x!=0){
		for(int t=m;t>0;t--)f[x][t]=f[x][t-1]+w[x];//最后必须覆盖掉 ,自己占一份体积 
	}
}
int main(){
	scanf("%d%d",&n,&m);
	memset(f,-1,sizeof f);
	for(int i=1;i<=n;i++){
		int pre;
		scanf("%d%d",&pre,w+i);
		if(pre)link(pre,i);
		else link(0,i);
	}
	dp(0);
	printf("%d",f[0][m]);
	return 0;
}

但是注意看这行代码:

f[x][j]+=f[y][0];

显然,\(f[y][0]=0\),这样做是没有意义的。

但是我之前在做树形背包题的时候忽略了下面这个细节:

  • 假设当前子树根节点为 \(u\) ,它的子节点分别为 \(v_1,v_2,v_3\cdots v_k\)。
  • 当前处理完了 \(v_1,v_2\cdots v_{k-1}\) ,现在在处理 \(v_k\) 这个子树。
  • 无论做哪种题的时候,都要对当前处理的 \(f[u][j]\) 状态进行合并

合并操作即将 \(u\) 与 \(v_1,v_2\cdots v_{k-1}\) 和 \(f[v_k][0]\) 进行信息合并再进行转移

当然对于选课这道题来说,这是多次一举的,我们看下一道。


树上染色

边权互换,在这里仅仅给出状态转移方程。

\(f[u][j]=min(f[u][j-k]+f[v][k]+value)\)

其中 \(value\) 表示 \((u,v)\) 这条边权值 \(w\) 对整体的贡献。

题解

其中有一篇题解中说:

这道题 \(k\) 前几篇题解必须正序枚举的原因并不是什么要用 \(j−k\) 更新答案,而是因为正序枚举 \(k\) 是从 \(0\) 开始的,而这道题的状态转移必须要先将 \(k=0\) 的状态转移过来才能成立。也就是说,这只是个巧合,\(j\) 的枚举要倒序没错,但 \(k\) 的枚举必须正序简直就是无稽之谈。要想避免这一情况,只需提前转移一下\(k=0\) 的情况即可。

所以 \(j\) 是倒序这是不可否定的,对于 \(k\) 的顺序应该没有限制才对。

分析

这道题属于不同于选课的“二类题”。

什么意思?就是必须要考虑 \(f[v][0]\) 的代价才可(即 \(v\) 这棵子树全是白点的情况,这是有代价的)

下面分析两种情况。

  1. 先进行合并(信息的加和),再按照状态转移方程进行转移取 \(max\)
  2. 先进行转移,再进行合并。

这两种情况的结果显然是不同的。第二种情况会让数组被刷的很大(先取 \(max\) 再加和)。

所以这就是为什么倒序 \(k\) 不可做的原因。

所以要么处理一下 \(k=0\) 的情况,要么正序就好。

#include <iostream>
#include <cstdio>
#include <cstring> 
#include <cctype>
#include <algorithm>
using namespace std;
#define LL long long
inline int read(){
	char ch=getchar();int x=0;bool fl=false;
	while(!isdigit(ch)){
		if(ch=='-')fl=true;
		ch=getchar();
	}
	while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
	return fl?-x:x;
}
const int maxn = 2000 + 100;
int head[maxn],cnt=0;
struct edge{
	int to,nxt,w;
}e[maxn<<1];
inline void link(int u,int v,int w){
	e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;e[cnt].w=w;
}
inline LL Mx(LL x,LL y){
	return x<y?y:x;
}
int n,m,sz[maxn];
LL f[maxn][maxn];
void dfs(int u,int fa){
	sz[u]=1;
	f[u][0]=f[u][1]=0;
	for(int i=head[u],v;i;i=e[i].nxt){
		v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		sz[u]+=sz[v];
		for(int j=min(sz[u],m);j>=0;j--)
			for(int k=0;k<=min(sz[v],j);k++){
				if(f[u][j-k]!=-1){
					LL val=(LL)k*(m-k)*e[i].w+(LL)(sz[v]-k)*(n-m-sz[v]+k)*e[i].w;
					f[u][j]=Mx(f[u][j],f[v][k]+f[u][j-k]+val);
				}
			}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	if(n<2*m)m=n-m;
	memset(f,-1,sizeof f); 
	for(int i=1;i<n;i++){
		int u,v,w;
		u=read();v=read();w=read();
		link(u,v,w);link(v,u,w);
	}
	dfs(1,0);
	printf("%lld\n",f[1][m]);
	return 0; 
}

[IOI2005]Riv 河流

很自然的想到 \(f[u][j]\) 的状态表示,但是发现无法转移。

因此加一维 \(f[u][j][fa]\) ,表示离 \(u\) 最近的父亲。

由于 \(n\leq 100\),直接记录即可,同时记下每个点是否建设伐木场。

看下面这一行代码:

f[u][fa][k][0]+=f[v][fa][0][0];
f[u][fa][k][1]+=f[v][u][0][0];

这就是上面所说的初始化(信息合并)。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
template <typename T>
inline T read(){
    T x=0;char ch=getchar();bool fl=false;
    while(!isdigit(ch)){if(ch=='-')fl=true;ch=getchar();}
    while(isdigit(ch)){
        x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
    }
    return fl?-x:x;
}
const int maxn = 110 + 10;
int head[maxn],cnt=0;
struct edge{
    int to,nxt,w;
}e[maxn<<1];
inline void link(int u,int v,int w){
    e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;e[cnt].w=w;
}
int n,K,val[maxn];
int f[maxn][maxn][52][2],de[maxn];//当前节点,祖先,背包容量
int stk[maxn],top=0;
void dfs(int u,int fa){
    stk[++top]=u;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa)continue;
        de[v]=de[u]+e[i].w;
        dfs(v,u);
        for(int j=1;j<=top;j++){
            int fa=stk[j];
            for(int k=K;k>=0;k--){//前面都是状态,顺序无影响
				f[u][fa][k][0]+=f[v][fa][0][0];
				f[u][fa][k][1]+=f[v][u][0][0];
				for(int o=0;o<=k;o++){
					f[u][fa][k][0]=min(f[u][fa][k][0],f[u][fa][k-o][0]+f[v][fa][o][0]);
					f[u][fa][k][1]=min(f[u][fa][k][1],f[u][fa][k-o][1]+f[v][u][o][0]);
				}
            }
        }
    }
    for(int i=1;i<=top;i++){
    	int fa=stk[i];
    	for(int j=K;j>=0;j--){
    		if(j>=1)f[u][fa][j][0]=min(f[u][fa][j][0]+val[u]*(de[u]-de[fa]),f[u][fa][j-1][1]);
			else f[u][fa][j][0]+=val[u]*(de[u]-de[fa]);
			//cerr<<f[u][fa][j][0]<<endl;//
		}
	}
	--top;
}
#define read() read<int>()
int main(){
    n=read();K=read();
    for(int i=1,fa,w;i<=n;i++){
        val[i]=read();fa=read();w=read();
	    link(fa,i,w);link(i,fa,w);
    }
	dfs(0,-1);
	//for(int i=1;i<=n;i++)cerr<<de[i]<<endl;//
	printf("%d\n",f[0][0][K][0]);
	return 0;
}

标签:背包,int,ch,fa,细节,树形,maxn,--,include
来源: https://www.cnblogs.com/Liang-sheng/p/15133821.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有