一、 简介:
树形DP就是在树的数据结构上计算DP值。
树形DP有两个方向:叶->根、根->叶。
树形DP通过记忆化搜索实现,因此采用递归实现。
时间复杂度一般为O(n),若有维数m,则为O(n*m)。
二、 经典问题:
1. 树的重心:http://poj.org/problem?id=1655 叶->根
所谓重心就是各分支大小围绕该点能较均匀的分部,所以要求最大的分支大小最小。
建树完成后先以任意一点为根节点进行一次DFS,计算所有点所连子树大小。
如下图所示,以1为根,进行DFS,得到的结果为{5, 3, 1, 1, 1},那么接下来计算以任意一点如2为根的子树大小时,只需比较2直接所连4,5的大小以及(总节点数n-节点2的大小)即可。
看代码。
#include
#include
#include
using namespace std;
const int N=2e5+5;
int n;
struct node{ //链式前向星存边
int from;
int to;
int next;
}edge[2*N];
int head[N];
int cnt;
void add(int from,int to)
{
cnt++;
edge[cnt].from=from;
edge[cnt].to=to;
edge[cnt].next=head[from];
head[from]=cnt;
}
int pre[N],sz[N]; //pre用于记录某节点的父节点,sz用于记录某节点所连子树大小。
int dfs(int p,int u) //随机以某点为根DFS
{
pre[u]=p; //记录父节点
sz[u]=1;
for(int i=head[u];i!=0;i=edge[i].next)
{
int to = edge[i].to;
if(to!=p) sz[u] += dfs(u, to);
}
return sz[u];
}
int main()
{
int t;
cin>>t;
while(t--)
{
cnt=0;
memset(head,0,sizeof(head));
memset(edge,0,sizeof(edge));
memset(pre,0,sizeof(pre));
memset(sz,0,sizeof(sz));
scanf("%d",&n);
for(int i=1;i
2. 没有上司的聚会:http://poj.org/problem?id=2342 叶->根
简单来说就是给你一个公司的树形结构图,每个人有一个欢乐值,当一个人出现时他的直接上司不能出现,要求总欢乐值最大。
设dp[i][1]表示i出现时的最大欢乐值,dp[i][0]表示i不出现时的最大欢乐值, a[i]表示i自己的欢乐值。
设u为v的上司,则有dp[u][1] = a[u] + dp[v][0],dp[u][0] = max(dp[v][0], dp[v][1])。
由叶向根逐步更新即可。
#include
#include
#include
#include
using namespace std;
const int N=6e3+5;
int n;
int dp[N][2],pre[N]; //pre储存某点父节点
int head[N],cnt; //链式前向星存边
struct nodes{
int from;
int to;
int next;
}edge[N*2];
void Dfs(int node)
{
for(int i=head[node];i!=0;i=edge[i].next)
{
int to=edge[i].to;
Dfs(to);
dp[node][1]+=dp[to][0];
dp[node][0]+=max(dp[to][1],dp[to][0]);
}
}
int main()
{
while(cin>>n)
{
memset(dp,0,sizeof(dp));
memset(pre,0,sizeof(pre));
memset(head,0,sizeof(head));
memset(edge,0,sizeof(edge));
cnt=0;
for(int i=1;i<=n;i++)
{
scanf("%d",&dp[i][1]); //直接把a[i]存入dp[i][1]
}
int x,y;
while(scanf("%d%d",&x,&y)!=-1)
{
if(x==0 && y==0) break;
cnt++; //有向
edge[cnt].from=y;
edge[cnt].to=x;
edge[cnt].next=head[y];
head[y]=cnt;
pre[x]=y;
}
int root;
for(int i=1;i<=n;i++) //找根节点(根节点上级为0)
{
if(pre[i]==0)
{
root=i;
break;
}
}
Dfs(root);
printf("%d\n",max(dp[root][0],dp[root][1]));
}
}
啰嗦一下,之前有看到一种不用链式前向星存边直接暴力的方法,说实话能过是因为这题数据比较水,我试着写了一下,跑完数据用掉567ms,而链式前向星只要78ms,差距还是很大的,这题数据才6e3。
if在 && 条件下当前一个条件成立时就不会执行后面的判断语句,所以应该把能过滤掉较多答案的条件放在第一个,避免多余浪费时间的判断(像平时 || 能转成 && 的情况也要注意)。
#include
#include
#include
#include
using namespace std;
const int N=6e3+5;
int n;
int dp[N][2];
int pre[N];
bool vist[N];
void Dfs(int node)
{
vist[node]=true;
for(int i=1;i<=n;i++)
{
if(pre[i]==node && vist[i]==false) //如果将if中两个条件调换位置直接超时
{ //能过滤掉较多选项的是第一个条件
Dfs(i);
dp[node][1]+=dp[i][0];
dp[node][0]+=max(dp[i][0], dp[i][1]);
}
}
}
int main()
{
while(cin>>n)
{
memset(dp,0,sizeof(dp));
memset(pre,0,sizeof(pre));
memset(vist,false,sizeof(vist));
for(int i=1;i<=n;i++)
{
scanf("%d",&dp[i][1]);
}
int x,y;
while(scanf("%d%d",&x,&y)!=-1)
{
if(x==0 && y==0) break;
pre[x]=y;
}
int root=0;
for(int i=1;i<=n;i++)
{
if(pre[i]==0)
{
root=i;
break;
}
}
Dfs(root);
printf("%d\n",max(dp[root][1],dp[root][0]));
}
}
3. 树上最远距离:http://acm.hdu.edu.cn/showproblem.php?pid=2196 叶->根 && 根->叶
简单来说就是有一颗树,每条边有一个权值,求每一个点能到达的最远距离。
首先在一个树中,对于某一个点,它最远距离可以来自父节点方向或者子节点方向。
设dp[i][1]为i往父节点方向的最远距离,dp[i][0]为i往子节点方向的最远距离, pre[i]表示i的父节点。
对于子节点方向,设u为v父节点,有 dp[u][0] = max{dp[v][0] + w(u,v)},通过DFS自底向上求出即可。
对于父节点方向,设u,v为兄弟节点,有 dp[u][1] = w(u, pre[u]) + max{ dp[pre[u]][1], dp[v][0] + w(pre[u], v) }。
直观来说,即往父节点方向的最远距离 = 该节点到父节点的距离 + (父节点继续往父节点方向走 || 父节点往其它兄弟子节点走) 的最大值。从根开始向叶DFS即可。
#include
#include
#include
#include
using namespace std;
const int N=1e4+5;
int n;
int dp[N][2],prevs[N];
int head[N],cnt; //链式前向星存边
struct nodes{
int from;
int to;
int value;
int next;
}edge[2*N];
void Add(int x,int y,int v)
{
cnt++;
edge[cnt].from=x;
edge[cnt].to=y;
edge[cnt].value=v;
edge[cnt].next=head[x];
head[x]=cnt;
}
int Dfs(int node,int pre) //计算往子节点方向最远距离
{
for(int i=head[node];i!=0;i=edge[i].next)
{
int to=edge[i].to;
if(to==pre) continue;
int value=edge[i].value;
prevs[to]=node;
dp[node][0]=max(dp[node][0],value+Dfs(to,node));
}
return dp[node][0];
}
void Dfs2(int node,int pre) //计算往父节点方向最远距离
{
int v=0; //记录该点到父节点的距离
dp[node][1]=dp[pre][1]; //混入父节点与兄弟节点的判断
for(int i=head[pre];i!=0;i=edge[i].next) //拿出所有兄弟边比较,看看父节点下一步该往哪走
{
int to=edge[i].to;
if(to==prevs[pre]) continue;
int value=edge[i].value;
if(to==node) v=value;
else dp[node][1]=max(dp[node][1], dp[to][0]+value);
}
dp[node][1]+=v; //选完父节点下一步后再把该点到父节点的距离加上去
for(int i=head[node];i!=0;i=edge[i].next)
{
int to=edge[i].to;
if(to!=pre) Dfs2(to,node);
}
}
void Origin() //初始化函数
{
memset(prevs,0,sizeof(prevs));
memset(dp,0,sizeof(dp));
memset(head,0,sizeof(head));
cnt=0;
memset(edge,0,sizeof(edge));
}
int main()
{
while(cin>>n)
{
Origin();
int p,v;
for(int i=1;i
三、 拓展问题:
1. Godfather:http://poj.org/problem?id=3107
树的重心问题的拓展。
#include
#include
#include
using namespace std;
const int N=5e4+5;
int n;
int mxOfNode[N];
int head[N],cnt=0;
struct node{
int from;
int to;
int next;
}edge[2*N];
void Add(int x,int y)
{
cnt++;
edge[cnt].from=x;
edge[cnt].to=y;
edge[cnt].next=head[x];
head[x]=cnt;
}
int pre[N],sz[N];
int Dfs(int prev,int node)
{
pre[node]=prev;
sz[node]=1;
for(int i=head[node];i!=0;i=edge[i].next)
{
int to=edge[i].to;
if(to!=prev) sz[node] += Dfs(node,to);
}
return sz[node];
}
int main()
{
cin>>n;
for(int i=1;i
2. The more, The Better:http://acm.hdu.edu.cn/showproblem.php?pid=1561
背包类树形DP
把可以先攻克的节点作为父节点,形成一颗树,树构成森林。
由于题中有父节点为0这种情况,我们可以把所有森林中树的父节点都视为0,这样就把森林变成一颗树。
设dp[i][j]表示第i个节点中取j个节点的最大价值。
设u为v的父节点,有dp[u][j] = max( dp[u][j], dp[u][k] + dp[v][j-k] )
自底向上更新即可。
#include
#include
#include
#include
using namespace std;
const int N=205;
int n,m;
int head[N],cnt;
struct nodes{
int from;
int to;
int next;
}edge[2*N];
int dp[N][N];
bool vist[N];
void Origin()
{
memset(head,0,sizeof(head));
cnt=0;
memset(edge,0,sizeof(edge));
memset(dp,0,sizeof(dp));
memset(vist,false,sizeof(vist));
}
void Dfs(int node)
{
vist[node]=true;
for(int i=head[node];i!=0;i=edge[i].next)
{
int to=edge[i].to;
if(vist[to]==false) Dfs(to);
for(int j=m;j>=2;j--)
{
for(int k=1;k>n>>m)
{
Origin();
if(n==0 && m==0) break;
int x,y;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x,&y);
cnt++; //单向
edge[cnt].from=x;
edge[cnt].to=i;
edge[cnt].next=head[x];
head[x]=cnt;
dp[i][1]=y;
}
m++; //因为我们虚拟多处了一个0节点
Dfs(0);
printf("%d\n",dp[0][m]);
}
}