preparing

树形DP

树形DP

算法思路

总而言之,我认为树形\(DP\)和线性\(DP\)没什么大区别,唯一不同的是它要用树储存,且要从叶向根进行更新。其它没什么技巧性的东西,只要推出式子往往就能过,没什么好说的。

题目

P1352/qzez1697 [模板]没有上司的舞会

这是一道树形DP模板题.
思路:设 \(dp_{i,0}\) 表示以 \(i\) 为根的子树在 \(i\) 不去时的最大值, \(dp_{i,1}\) 表示以 \(i\) 为根的子树在 \(i\) 要去时的最大值。
显然 \(dp_{i,0} = \sum\limits_{j \in i}{\max (f_ {j,0},f_ {j,1}) }\)\(dp_{i,1} = \sum\limits_{j \in i}(f_{j,0})\)
不难理解,如果 \(i\) 不去,那么就等于它的子树的去或不去的最大值之和;如果要去,那么它的子树的根一定不能去,故等于它的子树不去的和。

#include<iostream>
#include<cstdio>
#define maxn 6005 
using namespace std;
int n,x,y;
int a[maxn],f[maxn][2],inde[maxn];
struct node{
	int to,nex;
}g[maxn];
int head[maxn],w=0;
void add(int from,int to){
	g[++w].to=to;
	g[w].nex=head[from];
	head[from]=w;
}
void dp(int x){
	if(head[x]==0){
		f[x][1]=a[x];
		return;
	}
	f[x][1]=a[x];
	for(int i=head[x];i!=0;i=g[i].nex){
		dp(g[i].to);
		f[x][0]+=max(f[g[i].to][0],f[g[i].to][1]);
		f[x][1]+=f[g[i].to][0];
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		add(y,x);
		inde[x]++;
	}
	for(int i=1;i<=n;i++){
		if(inde[i]==0){
			dp(i);
			printf("%d",max(f[i][1],f[i][0]));
			break;
		}
	}
	return 0;
}

poj1985 Cow Marathon

树的直径
先从任意一个点\(A\)开始,\(DFS\)找到离他最远的点\(B\),再从点\(B\)开始,再\(DFS\)找到离他最远的点\(C\)\(BC\)即为树的直径。

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 200005
using namespace std;
int n,m,x,y,dis;
char xx;
int head[maxn],w=0;
int fp,fd=-1;
struct node{
	int to,nex,dis;
}a[maxn*2];
void add(int from,int to,int dis){
	a[++w].to=to;
	a[w].dis=dis;
	a[w].nex=head[from];
	head[from]=w;
}
void dfs(int p,int fa,int tot){
	bool flag=0;
	for(int i=head[p];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		flag=1;
		dfs(a[i].to,p,tot+a[i].dis);
	}
	if(!flag){
		if(tot>fd){
			fd=tot;
			fp=p;
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d %c",&x,&y,&dis,&xx);
		add(x,y,dis);
		add(y,x,dis);
	}
	fp=0;
	fd=-1;
	dfs(1,-1,0);
	dfs(fp,-1,0);
	printf("%d",fd);
	return 0;
}

重心

poj1655 Balancing Act

树的重心
先计算每棵子树的大小,再计算把某节点去掉后剩下的树的大小的最大值\(bal_i\)

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 20005
using namespace std;
int t;
int n,x,y;
int head[maxn],w=0;
struct node{
	int to,nex;
}a[maxn*2];
void add(int from,int to){
	a[++w].to=to;
	a[w].nex=head[from];
	head[from]=w;
}
int size[maxn],bal[maxn];
void search(int x,int fa){
	size[x]=1;
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		search(a[i].to,x);
		size[x]+=size[a[i].to];
	}
}
void searchb(int x,int fa){
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		searchb(a[i].to,x);
	}
	bal[x]=n-size[x];
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		bal[x]=max(bal[x],size[a[i].to]);
	}
}
void set(){
	memset(a,0,sizeof(a));
	memset(head,0,sizeof(head));
	w=0;
	memset(size,0,sizeof(size));
	memset(bal,0,sizeof(bal));
}
int main(){
	scanf("%d",&t);
	while(t--){
		set();
		scanf("%d",&n);
		for(int i=1;i<n;i++){
			scanf("%d%d",&x,&y);
			add(x,y);
			add(y,x);
		}
		search(1,-1);
		searchb(1,-1);
		int mini=1;
		for(int i=2;i<=n;i++){
			mini=bal[i]<bal[mini]?i:mini;
		}
		printf("%d %d\n",mini,bal[mini]);
	}
	return 0;
}
/*
1
6
1 2
1 3
2 4
2 5
5 6
*/

poj3107 Godfather

还是找树的重心
思路同上,不过这题要输出所有重心。

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 50005
using namespace std;
int t;
int n,x,y;
int head[maxn],w=0;
struct node{
	int to,nex;
}a[maxn*2];
void add(int from,int to){
	a[++w].to=to;
	a[w].nex=head[from];
	head[from]=w;
}
int size[maxn],bal[maxn];
void search(int x,int fa){
	size[x]=1;
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		search(a[i].to,x);
		size[x]+=size[a[i].to];
	}
}
void searchb(int x,int fa){
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		searchb(a[i].to,x);
	}
	bal[x]=n-size[x];
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		bal[x]=max(bal[x],size[a[i].to]);
	}
}
void set(){
	memset(a,0,sizeof(a));
	memset(head,0,sizeof(head));
	w=0;
	memset(size,0,sizeof(size));
	memset(bal,0,sizeof(bal));
}
int main(){
	set();
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	search(1,-1);
	searchb(1,-1);
	int minn=bal[1];
	for(int i=2;i<=n;i++){
		minn=bal[i]<minn?bal[i]:minn;
	}
    for(int i=1;i<=n;i++){
        if(bal[i]==minn){
            printf("%d ",i);
        }
    }
	return 0;
}

poj2378 Tree Cutting

去除一个点,使剩下的每个连通子图中点的数量都不超过\(n/2\)
本质上,还是用找树的重心一样的方法。因为我们已经计算出了去掉每个点后剩下的图中的最大节点数,存在\(bal_i\)中。所以我们只要找所有的\(bal_i\)中,如果\(bal_i < (n/2)\),那么输出,注意可能有多个解。

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 50005
using namespace std;
int t;
int n,x,y;
int head[maxn],w=0;
struct node{
	int to,nex;
}a[maxn*2];
void add(int from,int to){
	a[++w].to=to;
	a[w].nex=head[from];
	head[from]=w;
}
int size[maxn],bal[maxn];
void search(int x,int fa){
	size[x]=1;
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		search(a[i].to,x);
		size[x]+=size[a[i].to];
	}
}
void searchb(int x,int fa){
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		searchb(a[i].to,x);
	}
	bal[x]=n-size[x];
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		bal[x]=max(bal[x],size[a[i].to]);
	}
}
void set(){
	memset(a,0,sizeof(a));
	memset(head,0,sizeof(head));
	w=0;
	memset(size,0,sizeof(size));
	memset(bal,0,sizeof(bal));
}
int main(){
	set();
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	search(1,-1);
	searchb(1,-1);
    for(int i=1;i<=n;i++){
        if(bal[i]<=n/2){
            printf("%d\n",i);
        }
    }
	return 0;
}

poj3140 Contestants Division

求删去某条边后,剩下两棵子树的节点权值之和的差的绝对值最小
这道题打了我一个多小时。
思路其实很简单,就是将任意一个作为根(我用的是1),之后算出每棵子树的点权和。再枚举每条边,计算删掉后的两棵子树的节点权值之和的差的绝对值,计算输出即可。
细节很多:

  1. 一定要开\(long long\)
  2. \(abs\)要手写;
  3. 数组记得初始化;
  4. 开始时\(ans\)一定要赋值成一个很\(\color{red}大\)的值 。
#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 1000005
using namespace std;
long long n,m,x,y,tot;
long long p[maxn],head[maxn],w;
long long f[maxn];
struct node{
	long long to,nex;
}a[maxn*2];
void add(long long from,long long to){
	a[++w].to=to;
	a[w].nex=head[from];
	head[from]=w;
}
void set(){
	w=0;
	tot=0;
	memset(p,0,sizeof(p));
	memset(head,0,sizeof(head));
	memset(a,0,sizeof(a));
	memset(f,0,sizeof(f));
}
void search(long long x,long long fa){
	f[x]=p[x];
	for(long long i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		search(a[i].to,x);
		f[x]+=f[a[i].to];
	}
}
long long abs(long long x){
	if(x<0){
		return -x;
	}else{
		return x;
	}
}
long long findans(){
	long long ans=1000000000000;//一定要很大!!!
	for(long long i=1;i<=n;i++){
		for(long long j=head[i];j!=0;j=a[j].nex){
			ans=min(ans,abs(f[a[j].to]-(tot-f[a[j].to])));
		}
	}
	return ans;
}
int main(){
	long long xx=0;
	while(1){
		set();
		xx++;
		scanf("%lld%lld",&n,&m);
		if(n==0&&m==0){
			return 0;
		}
		for(long long i=1;i<=n;i++){
			scanf("%lld",&p[i]);
			tot+=p[i];
		}
		for(long long i=1;i<=m;i++){
			scanf("%lld%lld",&x,&y);
			add(x,y);
			add(y,x);
		}
		search(1,-1);
		printf("Case %lld: %lld\n",xx,findans());
	}
	return 0;
}
/*
7 6
1 1 1 1 1 1 1
1 2
2 7
3 7
4 6
6 2
5 7
4 3
1 6 3 10
1 2
2 4
1 3
*/

qzez1161 【TEST 3】旅行

无描述

大意

\(1\)号节点开始遍历,不走回头路,在每一条可行的路上都选最多\(k\)个节点,和为\(s_i\),求\(max(s_i)\)。(描述较抽象,看题目即可)

思路

\(f_{i,j}\)为以\(i\)为根的子树,选取\(j\)个节点的最小值,显然最终答案为\(\max\limits_{j=1}^{k}{f_{1,j}}\)
下面推状态转移式,对于\(i\)节点来说,如果它要选,那么\(f_{i,j}=\max\limits_{k\in i}{f_{k,j}}\),因为不去它,就等于是它的子树的最大值。如果要去,显然让它的子树的第二维减\(1\)即可,就是少去一个城市而已,即\(f_{i,j}=\max\limits_{k\in i}{f_{k,j-1}+a_i}\)。稍加整理,得到\(f_{i,j}=\max (f_{i,j},\max (\max\limits_{k\in i}{f_{k,j}},\max\limits_{k\in i}{f_{k,j-1}+a_i}))\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 10005
#define maxk 1005
using namespace std;
int n,k,x,y;
long long a[maxn],f[maxn][maxk];
int w=0,head[maxn];
struct gragh{
	int to,nex;
}g[maxn*2];
void add(int x,int y){
	g[++w].to=y;
	g[w].nex=head[x];
	head[x]=w;
}
void search(int x,int fa){
	f[x][0]=0;
	bool flag=0;
	for(int i=head[x];i!=0;i=g[i].nex){
		if(g[i].to==fa){
			continue;
		}
		flag=1;
		search(g[i].to,x);
		for(int j=1;j<=k;j++){
			f[x][j]=max(f[x][j],max(f[g[i].to][j],f[g[i].to][j-1]+a[x]));
		}
	}
	if(!flag){//叶子 
		f[x][1]=a[x];
		return;
	}
}
int main(){
	memset(f,128,sizeof(f));
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	search(1,-1);
	long long ans=f[1][k];
	for(int i=1;i<=k;i++){
		ans=max(ans,f[1][i]);
	}
	printf("%lld",ans);
	return 0;
}
/*
5 2
2 4 1 3 2
1 2
1 3
3 4
1 5
*/

换根

qzez1698 Accumulation Degree

一道换根模板题
首先,初始化一遍,计算出以1(或其它任意点)为起点的最大流量。显然此时 \(f_i = \sum\limits_{j \in i}{\min (f_j,w_{i,j})}\)。特殊地,若\(j\)为叶子节点,\(f_i\)应直接加上\(w_{i,j}\)
然后进行换根。最终答案 \(g_i = f_i+\min((g_{fa}-\min(f_i,w_{fa,i})),w_{fa,i})\)\(fa\)\(i\)父亲,其中\(g_{fa}-\min(f_i,w_{fa,i})\)\(g_{fa}\)减去\(i\)子树后剩下的最大流量)。特殊地,若\(fa\)的度数为1,\(g_i\)应直接等于\(f_i+w_{fa,i}\)

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 200005
using namespace std;
int t;
int n,x,y,z;
int head[maxn],w=0,de[maxn];
struct node{
	int to,len,nex;
}a[maxn*2];
int f[maxn],g[maxn];
void set(){
	memset(a,0,sizeof(a));
	memset(head,0,sizeof(head));
	w=0;
	memset(f,0,sizeof(f));
	memset(g,0,sizeof(g));
	memset(de,0,sizeof(de));
}
void add(int from,int to,int len){
	a[++w].to=to;
	a[w].len=len;
	a[w].nex=head[from];
	head[from]=w;
}
void flow(int x,int fa){
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		if(de[a[i].to]==1){
			f[x]+=a[i].len;
		}else{
			flow(a[i].to,x);
			f[x]+=min(f[a[i].to],a[i].len);
		}
	}
}
void search(int x,int fa){
	for(int i=head[x];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		if(de[x]==1){
			g[a[i].to]=f[a[i].to]+a[i].len;
		}else{
			g[a[i].to]=f[a[i].to]+min((g[x]-min(f[a[i].to],a[i].len)),a[i].len);
		}
		search(a[i].to,x);
	}
	return;
}
int main(){
	scanf("%d",&t);
	while(t--){
		set();
		scanf("%d",&n);
		for(int i=1;i<n;i++){
			scanf("%d%d%d",&x,&y,&z);
			add(x,y,z);
			add(y,x,z);
			de[x]++;
			de[y]++;
		}
		flow(1,-1);
//		printf("%d",f[1]);
		g[1]=f[1];
		search(1,-1);
		int maxx=g[1];
		for(int i=2;i<=n;i++){
			maxx=max(maxx,g[i]);
		}
		printf("%d\n",maxx);
	}
	return 0;
}
/*
1
5
1 2 3
2 3 4
2 4 5
2 5 5
*/

P2986 Great Cow Gathering

又是换根的模板题
先算出到\(1\)的方便值,后换根即可。

#include<iostream>
#include<cstdio>
#define maxn 100005
#define ll long long
using namespace std;
int n,x,y,z;
ll num[maxn],size[maxn],f[maxn],ans[maxn];
int head[maxn],w=0;
struct node{
	int nex,to,dis;
}a[maxn*2];
void add(int from,int to,int dis){
	a[++w].to=to;
	a[w].dis=dis;
	a[w].nex=head[from];
	head[from]=w;
}
void ssize(int u,int fa){
	for(int i=head[u];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		ssize(a[i].to,u);
	}
	size[u]=num[u];
	for(int i=head[u];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		size[u]+=size[a[i].to];
	}
}
void search(int u,int fa){
	for(int i=head[u];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		search(a[i].to,u);
	}
	f[u]=0;
	for(int i=head[u];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		f[u]+=f[a[i].to]+size[a[i].to]*a[i].dis;
	}
}
void change(int u,int fa,int dis){
	ans[u]=ans[fa]-size[u]*dis+(size[1]-size[u])*dis;
	for(int i=head[u];i!=0;i=a[i].nex){
		if(a[i].to==fa){
			continue;
		}
		change(a[i].to,u,a[i].dis);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&num[i]);
	}
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
		add(y,x,z);
	}
	ssize(1,-1);
	search(1,-1);
	ans[1]=f[1];
	for(int i=head[1];i!=0;i=a[i].nex){
		change(a[i].to,1,a[i].dis);
	}
	ll anss=ans[1];
	for(int i=2;i<=n;i++){
		anss=min(anss,ans[i]);
	}
	printf("%lld",anss);
	return 0;
}
/*
5
1
3
1
5
2
1 3 1
2 3 2
3 4 3
4 5 3
*/
posted @ 2021-07-31 12:30  qzhwlzy  阅读(75)  评论(0)    收藏  举报