树形dp
Posted stungyep
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树形dp相关的知识,希望对你有一定的参考价值。
树形dp
树形dp的性质:没有环,dfs不会重复,而且具有明显而又严格的层数关系。
判断一道题是否是树形dp:首先判断数据结构是否是一棵树,然后是否符合动态规划的要求。如果都符合,那么是一道树形dp的问题。我们需要通过下面几个步骤来解题。
建树
建树过程中,我们需要通过数据量和题目的要求,选择合适的树的存储方式。
一般来说,如果节点数小于5000,我们一般可以用邻接矩阵来存储。如果节点数更多,我们一般采用邻接表来存储。(**注意,采用邻接表时,我们需将边开到2*n,因为边是双向**)。如果是二叉树或则多叉树转二叉树,我们可以用一个brother[]
、child[]
来存储。
写出树规方程
通过观察孩子与父亲之间的关系建立方程。通常我们认为,树形DP的写法有两种:
1:根到叶子:不过这种动态规划在实际问题中运用的不多。
2:叶子到根:即根的子节点传递有用的信息给根,之后根得出最优解。
注意:上面两种方法一般来说是不能相互转换的(个别情况是个例),且第二种方法适用的普遍性广泛很多。
入门例题
1:poj 2342
问题描述:对于一棵树,每个节点有自己的权值,取了一个节点其相邻的父节点和子节点就没有价值了。求最大价值。
分析可得状态转移方程:当取 i 节点时:(dp[i][1]+=dp[j][0]);当不取 i 节点时:(dp[[i][0]+=max(dp[j][1],dp[j][0]));上面的 j 都表示为节点 i 的下属。
show code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4;
int fa[maxn],dp[maxn][maxn],val[maxn];
bool v[maxn];
int n,root,res; //root为根节点
void Tree_dp(int x)
{
v[x]=true;
for(int i=1;i<=n;++i)
{
if(!v[i]&&fa[i]==x) //i是x的子节点
{
Tree_dp(i); //O(n^2)的时间复杂度
dp[x][1]+=dp[i][0];
dp[x][0]+=max(dp[i][1],dp[i][0]);
}
}
res=max(dp[x][0],dp[x][1]);
}
int main()
{
ios::sync_with_stdio(false);
cin>>n;
memset(dp,0,sizeof(dp));
memset(fa,0,sizeof(fa));
memset(v,false,sizeof(v));
for(int i=1;i<=n;++i){
cin>>val[i];
dp[i][1]=val[i];
}
root=0;
int a,b;
while(cin>>a>>b)
{
if(a==0&&b==0) break;
fa[a]=b; //a的父节点是b
root=b; //先随便更新一下根节点,下面还会更新
}
while(fa[root])
root=fa[root]; //找到这棵树真正的根节点
Tree_dp(root);
cout<<res<<endl;
system("pause");
return 0;
}
2:POJ 1947
题意:有n个点组成的树,问至少删除多少条边才能获得一颗有p个节点的子树。
我们定义状态dp[i][j]
表示以 i 为根节点生成节点数为 j 的最少删的边的个数,所以我们可以得到状态转移方程:
[
dp[i][j]=min(dp[i][j],dp[i][j-k]+dp[i.son][k]-2)
]
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=200;
const int inf=0x3f3f3f3f;
int dp[maxn][maxn]; //dp[i][j]代表以i为根节点,生成节点数为k所删掉的边
int a,b,n,p,root; //分别代表边的两个端点,边的总数,最后要得到的边的数量,根节点
int fa[maxn];
vector<int> tree[maxn];
void dfs(int x)
{
int len=tree[x].size();
for(int i=0;i<len;++i) //不能从1到Len,因为vector数组下标是从0开始的
{
dfs(tree[x][i]); //递归子节点
for(int j=p;j>1;j--) //j=1情况下面已近初始化过了
for(int k=1;k<j;++k)
dp[x][j]=min(dp[x][j],dp[x][j-k]+dp[tree[x][i]][k]-2);
}
}
int main()
{
ios::sync_with_stdio(false);
memset(fa,false,sizeof(fa));
cin>>n>>p;
for(int i=1;i<n;++i){
cin>>a>>b;
tree[a].push_back(b);
fa[b]=a;
}
root=1;
while(fa[root]) //找根节点
root=fa[root];
for(int i=1;i<=n;++i) //初始化dp
{
//以i为根,生成节点数为1的子树所需删掉的根,每个节点都有个父节点+1,根节点也有个虚拟父节点,方便处理
dp[i][1]=tree[i].size()+1; //这里也是为什么下面状态转移时要-2的原因了
for(int j=2;j<=p;++j)
dp[i][j]=inf;
}
dfs(root);
dp[root][p]--; //将根节点的虚拟节点删去
int res=inf;
for(int i=1;i<=n;++i)
res=min(res,dp[i][p]);
cout<<res<<endl;
system("pause");
return 0;
}
3.换根树形dp poj-3585
题意:树的每条边有自己的权值,以哪个结点作为源点使得总权值最大(如果一个结点有多个节点,只能取最小的)
1:O(n^2)的做法
直接上状态转移方程:
[
g[root]=sum_{son∈son(root)}left{
egin{aligned}
v(root,son) size(son)=1min(g[son],v) size(son)>1\end{aligned}
ight.
]
每一个点按照上面方法做一遍树形dp即可。
2:O(n)的做法
由于这是一颗无根树,不同的根会产生不同的答案,故我们只需思考一下如何进行换根。而换根的主要思路就是如何处理根与根的转化。
设f[i]
表示以 i 为根的子树中,答案的最大值,g[i]
表示以 i 为根节点的到子端点的最大流量。所以有状态转移方程:
[
f[i] =left{
egin{aligned}
v(i,j) size(j)=1g[j]+min(v(i,j),f[i]-min(g[j],v(i,j))) size(j)>1\end{aligned}
ight.
]
show code:
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=2e5+10;
int T,n,g[maxn],f[maxn]; //g[i]表示以i为根节点到子节点的最大流量,f[i]表示i到所有结点的流量和
struct node
{
int to; //边的邻接点
int val; //边的权值
node(){}
node(int a,int b){
to=a;
val=b;
}
};
vector<node> G[maxn];
void gdfs(int u,int fa) //自下而上求g[i]
{
for(int i=0;i<G[u].size();i++)
{
node e=G[u][i]; //e为u的一个子节点
if(e.to==fa) continue;
gdfs(e.to,u);
if(G[e.to].size()==1) //特判,子节点只有1个
g[u]+=e.val; //自下而上的原因是先dfs在求值
else
g[u]+=min(e.val,g[e.to]);
}
}
void fdfs(int u,int fa) //自上而下求f[i]
{
for(int i=0;i<G[u].size();i++)
{
node e=G[u][i];
if(e.to==fa) continue;
if(G[u].size()==1) f[e.to]=g[e.to]+e.val;
else{
f[e.to]=g[e.to]+min(e.val,f[u]-min(e.val,g[e.to]));
}
fdfs(e.to,u); //自上而下的原因是先求值在dfs
}
}
int main()
{
ios::sync_with_stdio(false);
cin>>T;
while(T--)
{
cin>>n;
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
for(int i=1;i<=n;++i) G[i].clear();
for(int i=1;i<n;++i){
int u,v,c;
cin>>u>>v>>c;
G[u].push_back(node(v,c)); //注意双向边
G[v].push_back(node(u,c));
}
gdfs(1,0);
f[1]=g[1];
fdfs(1,0);
int ans=-0x3f3f3f3f;
for(int i=1;i<=n;++i)
ans=max(ans,f[i]);
cout<<ans<<endl;
}
system("pause");
return 0;
}
以上是关于树形dp的主要内容,如果未能解决你的问题,请参考以下文章
Starship Troopers(HDU 1011 树形DP)