1.通常的动态规划都是线性的,或者说是在有向无环图上进行的。而树形dp就是在树的基础上进行的dp
2.树形dp通常有两种方向,一种是自下而上,另一种是自上而下。具体利用方法根据实际要求来
叶——根,在回溯的时候从叶子节点往上更新信息
根——叶,往往是在叶——根dfs一遍以后(相当于预处理),再重新往下获取,得到的最终答案。
3.和普通dp一样,树形dp的重点在于找状态转移方程。
一.入门题
上司的舞会上司的舞会
题目大意:
如题目所言,就是自己的上司参加的话,自己就不能参加。每个节点定义一个价值,子节点和父亲节点不能同时选取。求能够选取的最大价值
题目分析:令f【u】【0】表示不选取u节点,u子树所能得到的最大价值;f【u】【1】表示选取了u节点,u子树能得到的最大价值。
在回溯的过程中通过子节点向上更新信息
设i号节点的价值为a【i】,<u,v>是一条有向边
f【u】【0】=max(f【v】【0】,f【v】【1】)
f【u】【1】=a【u】+f【v】【0】u取了,v肯定不取
void dfs(int x)
{
int l=tu[x].size();
for(int i=0;i<l;i++)
{
int t=tu[x][i];
dfs(t);
dp[x][0]+=max(dp[t][1],dp[t][0]);
dp[x][1]+=dp[t][0];
}
}
dfs(root);
cout<<max(dp[root][0],dp[root][1])<<endl;
具体代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
#include<algorithm>
#include<math.h>
#include<queue>
using namespace std;
typedef long long ll;
const int N=6010;
int m,n;
int dp[N][2],rd[N];
vector <int>tu[N];
void dfs(int x)
{
int l=tu[x].size();
for(int i=0;i<l;i++)
{
int t=tu[x][i];
dfs(t);
dp[x][0]+=max(dp[t][1],dp[t][0]);
dp[x][1]+=dp[t][0];
}
}
int main()
{
while(~scanf("%d",&n))
{
for(int i=1;i<=n;i++)
tu[i].clear();
memset(rd,0,sizeof(rd));
for(int i=1;i<=n;i++)
{
scanf("%d",&dp[i][1]);
dp[i][0]=0;
}
int x,y;
while(~scanf("%d%d",&x,&y)&&x+y)
tu[y].push_back(x),rd[x]++;
int ans=-1;
for(int i=1;i<=n;i++)
if(rd[i]==0)///防止不止一个根节点
{dfs(i);ans=max(ans,max(dp[i][0],dp[i][1]));}
cout<<ans<<endl;
}
}
入门题二:
树的重心
题目:点击打开链接
题目大意:
若树上一个节点满足其所有子树中最大子树节点数最少,则这个点即为重心
题目分析:
一个节点有多支是儿子节点,而父亲节点只有一支,题目的意思很清晰,就是在所有儿子子树中寻求一个最大的,然后跟父亲节点的一支进行比较,实际上难在理解,此题将父亲节点的那支也定义成了节点u的子树之一了。而父亲节点的那一支就用总数n减去u的子树所有的节点个数。
任选一点为根,只要统计出每个点的子树大小,就能很快求出每个点子树节点的数量最大值
求每个点子树的大小同样只需要自底向上的更新信息。记size【u】为子树节点u节点的数量(包括u本身)
size【u】=1+(size【v】的总和)
void dfs(int father,int u,int n)
{
int v,m,temp;
for(int i=next[u];i!=-1;i=edge[i].next)
{
v=edge[i].v;
if(v==father)
continue;
dfs(u,v,n);//先dfs再加
sum[u]+=sum[v];
}
dp[u]=n-1-sum[u];
for(int i=next[u];i!=-1;i=edge[i].next)
{
v=edge[i].v;
if(v==father)
continue;
dp[u]=max(dp[u],sum[v]);
}
sum[u]++;
}
代码:
#include<iostream>
#include<string.h>
#include<algorithm>
#define MAX 22000
using namespace std;
struct edge
{
int v;
int next;
}edge[MAX*2];
int next[MAX];
int sum[MAX];
int dp[MAX];
int ans,n,num;
void add_edge(int u,int v)
{
edge[num].next=next[u];
next[u]=num;
edge[num++].v=v;
}
void dfs(int father,int u,int n)
{
int v,m,temp;
for(int i=next[u];i!=-1;i=edge[i].next)
{
v=edge[i].v;
if(v==father)
continue;
dfs(u,v,n);
sum[u]+=sum[v];
}
// cout<<u<<": "<<sum[u]<<endl;
dp[u]=n-1-sum[u];
for(int i=next[u];i!=-1;i=edge[i].next)
{
v=edge[i].v;
if(v==father)
continue;
dp[u]=max(dp[u],sum[v]);
}
sum[u]++;
}
int main()
{
int t;
int p,q;
cin>>t;
while(t--)
{
cin>>n;
num=0;
memset(sum,0,sizeof(sum));
memset(next,-1,sizeof(next));
for(int i=0;i<n-1;i++)
{
cin>>p>>q;
add_edge(p,q);
add_edge(q,p);
}
ans=1<<29;
p=0;
dfs(0,1,n);
for(int i=1;i<=n;i++)
{
if(dp[i]<ans)
{
ans=dp[i];
p=i;
}
}
cout<<p<<" "<<ans<<endl;
}
return 0;
}
入门题3:树上最远距离
这道题最好需要画图理解。
题目大意:如题目所述,一棵无向树,辺有权值,求树上每个点能到达的最远距离
题目分析:
一个点能够走最远距离,第一步要么往儿子方向走,要么找父亲节点。
如果往儿子结点走,就会一直往儿子结点走,不存在返回的现象(以后会遇到又返回的那种题)
如果第一步往父亲节点走,那么第二步要么继续往他的父亲节点走,要么走u的兄弟子树(注意是兄弟,因为不可能回来)兄弟中的最大距离就是次最大距离的含义
%%%往儿子方向走的最远距离是很容易求出来的,而往父亲节点走的就需要自上往下推。这道题与之前两道题是不一样的,就牵扯到了从根向下传递信息。
&&&记f【u】为节点u第一步想儿子方向走的最远距离
&&&记g【u】为节点u第一步向父亲方向走的最远距离
&&&记p【u】为节点u的父亲节点的编号
&&&f【u】=max{f【v】+w(u,v)}
&&&g【u】=w(u,p【u】,max{g【p【u】】,f【v】+w(p【u】,u)})v是u的兄弟
&&&所以要两边dfs,先求出f再求出g
&&&最后节点u的最远距离为 max{f【u】,g【u】}
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define MAXN 10200
struct edge
{
int to;//终端点
int next;//下一条同样起点的边号
int w;//权值
}bians[MAXN*2];
int tot;//总边数
int head[MAXN];//head[u]=i表示以u为起点的所有边中的第一条边是 i号边
void add_bian(int u,int v,int w)//添加从u->v,权值为w的边
{
bians[tot].to=v;
bians[tot].w=w;
bians[tot].next = head[u];
head[u] = tot++;
}
int dist[MAXN][3];//dist[i][0,1,2]分别为正向最大距离,正向次大距离,反向最大距离
int jilu[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]=jilu[u]=0;
for(int e=head[u]; e!=-1; e=bians[e].next)
{
int v= bians[e].to;
if(v==fa)
continue;
if(dist[u][0]<dfs1(v,u)+bians[e].w)
{
jilu[u]=v;
dist[u][1] = max(dist[u][1] , dist[u][0]);
dist[u][0]=dfs1(v,u)+bians[e].w;
}
else if( dist[u][1]< dfs1(v,u)+bians[e].w )
dist[u][1] = max(dist[u][1] , dfs1(v,u)+bians[e].w);
}
return dist[u][0];
}
void dfs2(int u,int fa)
{
for(int e=head[u];e!=-1;e=bians[e].next)
{
int v = bians[e].to;
if(v==fa)
continue;
if(v==jilu[u])//以u为根节点的经过下一叶子的标号为v
dist[v][2] = max(dist[u][2],dist[u][1])+bians[e].w;//如果是这样的话,那么就找down次最大值与up最大值的最大值
else
dist[v][2] = max(dist[u][2],dist[u][0])+bians[e].w;//如果没有前边的限制,则说明还没到最后,就找down与up哪种最大
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(jilu,-1,sizeof(jilu));
for(int i=2; i<=n; i++)
{
int v,w;
scanf("%d%d",&v,&w);
add_bian(i,v,w);//加边
add_bian(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的状态转移方程和解题思路,帮助读者理解和掌握这一技巧。
761

被折叠的 条评论
为什么被折叠?



