首页 > 其他 > 详细

树形DP

时间:2020-04-01 17:05:17      阅读:49      评论:0      收藏:0      [点我收藏+]

题目:https://blog.csdn.net/weixin_44584560/article/details/86599565

https://blog.csdn.net/dcx2001/article/details/78269908@##@@@

1.子树和计数。

这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质。

例如:

 

1.codeforces 767C Garland

 

 这道题是让你把树分成3部分,使每部分点权和相等,这就是通过算子树的size[]实现的。

 

2.洛谷1122最大子树和

 

这道题让你剪去一些子树,让剩下的子树点权和最大。这题就要维护子树的点权和,f[i]表示i这颗子树的点权和最大值。

1、codeforce garland

分割一棵树
分成三部分,每部分的和一样

//codeforce 的题,超时了???? 
//分割一棵树
//分成三部分,每部分的和一样
int summ=0,tot=0;
int w[maxn];  //暖度 
int to[maxn*2],fi[maxn],ne[maxn*2];
void link(int x,int y){
	to[++tot]=y;
	ne[tot]=fi[x];
	fi[x]=tot;
}
int n,root,sz[maxn],ans[5],cnt;
void dfs(int x,int fa){ //开始分割,要有fa是因为这是一棵无根树 
	sz[x]=w[x];
	for(int i=fi[x];i;i=ne[i]){
		int v=to[i];
		if(v!=fa){
			dfs(v,x);
			sz[x]+=sz[v]; //加上子树的值 
		}
	}
	if(sz[x]==summ) {
		ans[++cnt]=x;
		sz[x]=0;
	}
}


int main(){
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d %d",&x,&y);
		if(x!=0){
			link(x,i);
			link(i,x);
		}
		else root=i;
		w[i]=y;
		summ+=y;
	}
	if(summ%3!=0){
		printf("-1\n");
		return 0;
	}
	summ/=3;
	dfs(root,0);
	if(cnt<=2) {
		printf("-1\n");
	}
	else printf("%d %d\n",ans[2],ans[1]);
return 0;
}

2、P1122 洛谷 最大子树和

技术分享图片

是一个很标准的树形DP,上面的第二类:求在树上选一些点满足价值最大的问题

这题是一个比较常见的树型dp的模型(稍有变形):子树型,给一棵 n 个点的树,以 1 号点为根,求以每个点为根的子树大小

先设立状态: f[u] 表示以u为根且包含u的最大权联通块

状态转移方程:f[u]+=max(0,f[v]); (v为u的孩子) 有些儿子比较丑,美丽指数小于0,可能不选,所以与0作个比较

状态转移比较容易,主要基于dfs实现,还是要多做题才能熟练

int fi[maxn],to[maxn*2],nex[maxn*2];
int n,tot;
void link(int x,int y){
	to[++tot]=y;
	nex[tot]=fi[x];
	fi[x]=tot;
}
int dp[maxn],w[maxn];
int cut[maxn]; //记录要不要剪掉,如果这支树为负的话,就剪掉 
int maxx=-1;
void dfs(int x,int fa){
	dp[x]=w[x]; //先赋值 
	for(int i=fi[x];i;i=nex[i]){
		int t=to[i];
		if(t!=fa) dfs(t,x);
	}
	for(int i=fi[x];i;i=nex[i]){
		int t=to[i];
		if(t!=fa&&!cut[t]) dp[x]+=dp[t]; //因为后面递归的时候会自己判断 
	}
	if(dp[x]<0) cut[x]=1;
	maxx=max(maxx,dp[x]);
}


int main(){
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	for(int i=1;i<=n-1;i++){
		scanf("%d %d",&x,&y);
		link(x,y);
		link(y,x);
	}
	dfs(1,1);
	printf("%d\n",maxx);
return 0;
}

 

2.树上背包问题

这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设f[i][j]表示i这颗子树选j个点的最优解。

1.洛谷1272重建道路

这道题是让你剪去最少的边实现最后剩P个点。所以P个点就是背包,剪去的边数就是价值。这题需要转化一下把剪边变成加边。

2.洛谷1273有线电视网

这道题是让你在保证不亏损的情况下满足更多的客户看到转播。此时用户的个数就是容量,f[i][j]表示i这颗子树选j个用户能赚的最多的钱。

1、P1272 洛谷 道路重建

技术分享图片

 讲解:https://www.luogu.com.cn/problemnew/solution/P1272

int n,num,p;
/*
每个状态代表一棵子树,这个子树与父节点相连。 那么初始化的话dp[i][1]=son[i],一开始都是不连儿子只连父亲

那么转移的那个就变成了dp[u][j]=max(dp[u][j-k]+dp[v][k]-1)

为什么减1呢,因为注意到我们一开始点都是不与儿子相连的,我们通过dp的过程一步一步把他们连起来。

那么对于u->v这个边,一开始在初始化u的时候已经被减掉了,因为他是连接儿子的边,而v->u这个边并没有,因为这个连向父亲的边,
我们要通过v转移,就得用u->v这个边,所以就得补回来。

而且最终算的时候,除了根节点无父亲外,其他的都要和父节点断开,也就是边数+1

第二种就是dp[i][j]表示以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数。

那么我们的初始化就变成了dp[i][1]=deg[i]

也就是把与i相连的所有边全部拆掉。
*/
struct node{
	int to;
	int next;
}edge[maxn*2];
int head[maxn];
void link(int x,int y){
	edge[++num].next=head[x];
	edge[num].to=y;
	head[x]=num;
}
int dp[maxn][maxn]; //这个的含义是以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数。
int in[maxn]; //给个结点的度
void dfs(int x,int fa){
	dp[x][1]=in[x];
	for(int i=head[x];i;i=edge[i].next){
		if(edge[i].to!=fa){
			dfs(edge[i].to,x);
		for(int j=p;j>0;j--){
			for(int k=1;k<j;k++)
			dp[x][j]=min(dp[x][j],dp[x][k]+dp[edge[i].to][j-k]-2); //为甚么减2
			//因为再两个in[]里面都加了 
		}
		}
	}
} 
 
int main(){
	scanf("%d %d",&n,&p);
	int x,y;
	for(int i=1;i<n;i++){
		scanf("%d %d",&x,&y);
		in[x]++;
		in[y]++;
		link(x,y);
		link(y,x);
	}
	memset(dp,0x3f,sizeof(dp));
	dfs(1,0);
	int ans=INF;
	for(int i=1;i<=n;i++) ans=min(ans,dp[i][p]);
	printf("%d\n",ans);
return 0;
}

2、P1273 有线电视网

技术分享图片

1.明确dp[i][j]含义,表示i节点,选j个用户,能得到的钱的最大值,然后对每个节点做分组背包。

2.怎么看出是分组背包?首先,背包的总容量相当于该点为根节点的子树中所有的用户数量(dp[i][j]的 j 不可能超过它连接的所有用户数)。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。

3.转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。

4.最后输出dp[1][i]>=0的i的最大值,所以反向枚举。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m;//n为整个有线电视网的结点总数,m为用户终端的数量
//第一个转播站即树的根结点编号为1,其他的转播站编号为2到n-m,用户终端编号为n-m+1到n
int head[3010],k;
int val[3010];//用户为观看比赛而准备支付的钱数
int dp[3010][3010];//dp[i][j]表示i节点,选j个用户,能得到的钱的最大值 
struct node
{
    int to,next,w;
}edge[10000010];
void adde(int u,int v,int w)
{
    edge[++k].to=v; edge[k].next=head[u];
    edge[k].w=w; head[u]=k;
}
int dfs(int u)
{
    if(u>n-m)//u为用户终端
    {
        dp[u][1]=val[u];
        return 1;
    } 
    int sum=0,t;
    for(int k=head[u];k;k=edge[k].next)//该点连接了几个节点意为有几组,遍历每一组 
    {
        int v=edge[k].to;
        t=dfs(v); sum+=t; //t为该组的元素个数,或是说这个儿子为根的子树大小(这里的大小指子树中用户的个数),sum为该节点最多可以选多少个用户,即背包容量 
        for(int j=sum;j>0;j--)
        {
            for(int i=1;i<=t;i++)//遍历组中的元素,选多少个用户相当于一个元素 
            {
                if(j-i>=0) dp[u][j]=max(dp[u][j],dp[u][j-i]+dp[v][i]-edge[k].w);
            }
        }
    }
    return sum;
}
int main()
{
    memset(dp,~0x3f,sizeof(dp));//初始化一个极大负值,因为dp可能为负
    scanf("%d%d",&n,&m);
    for(int u=1;u<=n-m;u++)
    {
        int size,v,w;
        scanf("%d",&size);
        for(int j=1;j<=size;j++)
        {
            scanf("%d%d",&v,&w);
            adde(u,v,w);
        }
    }
    for(int i=n-m+1;i<=n;i++) scanf("%d",&val[i]);
    for (int i=1;i<=n;i++) dp[i][0]=0;//选0个用户的花费肯定是0啦
    dfs(1);
    for (int i=m;i>=1;i--)
        if (dp[1][i]>=0)
        {
            printf("%d",i);
            break;
        }
    return 0;
}

3.花费最少的费用覆盖所有点

这类问题是父亲与孩子有联系的题。基本有两种出法:1.选父亲必须不能选孩子(强制)2.选父亲可以不用选孩子(不强制)。

1.洛谷2458 [SDOI2006]保安站岗
这题就属于类型2.这就需要预估,要不要选这个节点的父亲。f[i][0]表示选自己,f[i][1]表示选孩子,f[i][2]表示选父亲。
2.UVA 1220 Party at Hali-Bula(这是最大独立集问题,用做和上面题区分)  
这题就是强制要求父亲和孩子不能同时选,但这题没有要求必须把所有点完全覆盖,只是让选尽可能多的点,所以这样就可以用f[i][0]表示选自己,f[i][1]表示选孩子,省去f[i][2]表示选父亲了,因为没有都覆盖的强制要求,他的父亲选不选可以到他父亲再判断。

1、hdu Anniversary party 1520

选父不选子 不选父则都可以

using namespace std;
const int maxn=1010;
//没过 
const int INF=0x3fffffff;
//树形dp
//选父不选子   不选父则都可以
int dp[6010][2]; //0位不选,为选
vector<int> tree[6010];
int w[6010]; 
int n;
int fa[6010];
void dfs(int t){
	dp[t][0]=0;
	dp[t][1]=w[t]; //先初始化
	for(int i=0;i<tree[t].size();i++){
		int son=tree[t][i];
		dfs(son); //先递归 
		dp[t][0]+=max(dp[son][0],dp[son][1]);
		dp[t][1]+=dp[son][0];
	} 
}
int main(){
	scanf("%d",&n);
		for(int i=1;i<=n;i++){
			scanf("%d",&w[i]);
			fa[i]=-1;
			tree[i].clear();
		}
		int x,y;
	//	memset(dp,0,sizeof(dp));
		while(scanf("%d %d",&x,&y)){
			if(x==0&&y==0) break;
			fa[x]=y;
			tree[y].push_back(x);
		}
		int root=1;
		while(fa[root]!=-1){
			root=fa[root];
		}
		dfs(root);
		printf("%d\n",max(dp[root][0],dp[root][1]));
return 0;
}

4.树上统计方案数问题

这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献。

1. 51nod1588幸运树。 问有多少个三元组满足幸运数字, 可以控制一个点不动,分成子树内,子树外两个部分,分别相乘就行了。

与多种算法结合&&大模拟

这种题只能根据题目分析,然后随机应变了。
1.洛谷3621 [APIO2007]风铃

把题目中的要求变成公式就行了。

2. 51nod1673树有几多愁

这道题是非常强的综合题,用到了虚树+状压dp+树形dp。

3.51nod1531树上的博弈

非常强的一道博弈题。需要分情况的讨论

 

 

 

2、hdu Computer 2196

对每个点,求距离它最远的点

	/* 
	while(scanf("%d",&n)){
		inti();
		dfs1(1);
		dp[1][2]=0; //根到上面的距离是0
		dfs2(1);
		for(int i=1;i<=n;i++) printf("%d\n",max(dp[i][0],dp[i][2])); //两个方向,要么是 
	}
	
	*/ 
	//下面的会超时。。。虽然好理解 
/*
struct node{
	int id;
	int cost; //到爸爸的距离 
};
vector<node> tree[maxn];
int dp[maxn][3];
//0代表到子树的最长距离,1代表到子树的次长距离,2代表从i往上走的最短距离
int n;
void inti(){
	for(int i=1;i<=n;i++) tree[i].clear();
	int x,co;
	memset(dp,0,sizeof(dp));
	for(int i=2;i<=n;i++){
		scanf("%d %d",&x,&co);
		node temp;
		temp.id=i;
		temp.cost=co;
		tree[x].push_back(temp);
	}
}
void dfs1(int fa){
	//最长距离,次长距离
	int one=0,two=0;
	for(int i=0;i<tree[fa].size();i++){
		node child=tree[fa][i];
		dfs1(child.id); //先递归
		int cost=child.cost+dp[child.id][0];
		if(cost>=one){
			two=one;
			one=+cost;
		} 
		else if(cost>two) two=cost;
	} 
	dp[fa][0]=one;
	dp[fa][1]=two;
}

void dfs2(int fa){  //先处理父节点,在处理子节点,所以处理顺序是从上到下,所以最后递归
	for(int i=0;i<tree[fa].size();i++){
		node temp=tree[fa][i];
		if(dp[temp.id][0]+temp.cost==dp[fa][0]){
			dp[temp.id][2]=max(dp[fa][2],dp[fa][1])+temp.cost; //如果再最长的路上,那么就加上次长的距离 
		}
		else 
		dp[temp.id][2]=max(dp[fa][2],dp[fa][0])+temp.cost;
		dfs2(temp.id);
	} 
	
}
*/



#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=10000+200;
struct edge
{
    int to;//终端点
    int next;//下一条同样起点的边号
    int w;//权值
} edges[MAXN*2];
int tot;//总边数
int head[MAXN];//head[u]=i表示以u为起点的所有边中的第一条边是 i号边
void add_edge(int u,int v,int w)//添加从u->v,权值为w的边
{
    edges[tot].to=v;
    edges[tot].w=w;
    edges[tot].next = head[u];
    head[u] = tot++;
}
int dist[MAXN][3];//dist[i][0,1,2]分别为正向最大距离,正向次大距离,反向最大距离
int longest[MAXN];
int dfs1(int u,int fa)//返回u的正向最大距离
{
    if(dist[u][0]>=0)return dist[u][0];  //记忆化
    dist[u][0]=dist[u][1]=dist[u][2]=longest[u]=0;  //递归边界
 
    for(int e=head[u]; e!=-1; e=edges[e].next)
    {
        int v= edges[e].to;
        if(v==fa)continue;
 
        if(dist[u][0]<dfs1(v,u)+edges[e].w) //更新最长
        {
            longest[u]=v;
            dist[u][1] = max(dist[u][1] , dist[u][0]);
            dist[u][0]=dfs1(v,u)+edges[e].w;
        }
//更新次长 else if( dist[u][1]< dfs1(v,u)+edges[e].w )//这里一定要记得,要不然无情WA dist[u][1] = max(dist[u][1] , dfs1(v,u)+edges[e].w); } return dist[u][0]; } void dfs2(int u,int fa) { for(int e=head[u];e!=-1;e=edges[e].next) { int v = edges[e].to; if(v==fa)continue; if(v==longest[u]) dist[v][2] = max(dist[u][2],dist[u][1])+edges[e].w; //如果在最长的一条上 else dist[v][2] = max(dist[u][2],dist[u][0])+edges[e].w; //不在最长的 dfs2(v,u); //后递归 } } int main() { int n; while(scanf("%d",&n)==1&&n) { tot=0; memset(dist,-1,sizeof(dist)); memset(head,-1,sizeof(head)); memset(longest,-1,sizeof(longest)); for(int i=2; i<=n; i++) { int v,w; scanf("%d%d",&v,&w); add_edge(i,v,w);//加边 add_edge(v,i,w);//加边 } dfs1(1,-1); dfs2(1,-1); for(int i=1;i<=n;i++) printf("%d\n",max(dist[i][0],dist[i][2])); } return 0; }

  

树形DP

原文:https://www.cnblogs.com/shirlybaby/p/12319687.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!