树形dp的时间复杂度一般为O(n),因为每个节点只被遍历一次
目录
1.普通树形dp
一般的树形dp就是通过遍历子树,然后根据子树的情况求的结果,大概思路就是从根节点出发向子树进 行搜索,再将子树的信息合并到该节点上
2.背包类树形dp
1.树上01背包类
题目:二叉苹果树P2015 二叉苹果树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题解:树形DP入门详解+题目推荐 - 子谦。 - 博客园 (cnblogs.com)
通过这道题可以了解到树形dp的基本思路就是从根节点出发不断向子树进行搜索,再将子树的信息合并 到该节点上
题目:选课[P2014P2014 [CTSC1997] 选课 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题解:这道主要让我们学会一种思想---建虚点
因为每门的先修课最多只有一门,所以这N门课程可构成一个森林结构(若干棵树,因为可能有不只一 门课没有先修课)。为了方便起见,可以新建一个”虚拟课程“---0号节点,作为实际上没有先修课的课程 的先修课,把包含N个节点的森林转化为包含N+1个节点的树,其中0节点为根节点。在处理每一个节点 时可以将其转化为分组背包模型。
3.树的直径
一般有两种方式求得树的直径,分别为dfs和树形dp,但dfs不能处理边权为负数的情况。
方法1:dfs
总共有两步,(1)任取一点作为起点,找到距离起点最远的点u(可以通过反证法证明u一定为某条直径 的端点);(2)再找到距离u最远的一点v,那么u和v之间的路径就是一条直径
模板如下:
void dfs(int u,int pa)
{
//dp[u]记录从u出发能到达的最远距离,rem[u]记录离u最远的结点
rem[u]=u; dp[u]=0;
for(int i=head[u];i;i=E[i].nxt)
{
int v=E[i].v;
if(v==pa) continue;
dfs(v,u);
if(dp[u]<=dp[v]+E[i].dis)
dp[u]=dp[v]+E[i].dis,rem[u]=rem[v];
}
}
dfs1(1,0); p=rem[1];
dfs1(p,0); q=rem[p];
方法2:树形dp
假设树的根为root,那么树的直径有两种情况:(1)直径的两个端点分别在root的两个子树内,(2) 直径在root的某个子树内
所以我们先以root为根处理情况1,在递归root的子树将情况2转为情况1。
dp[u]表示从节点u出发向它的子树走能走到的最远距离,则dp[u]=max(dp[v]+dis(u,v))
对于以u为根的子树内,经过u的最长链长度为maxlen[u],则 maxlen[u]=max(dp[v1]+dp[v2]+dis(v1,u)+dis(v2,u));
由于dfs的特性,当我们搜到u的子节点v时,dp[u]已经被之前的节点中最大的一个更新过了,所以可以 直接用已有的dp[u]代替另一个子节点的枚举。
最后整个树的最长链就是max(maxlen[u])(1<=u<=n)
模板:
void DP(int u,int pa)
{
dp[u]=0;
for(int i=head[u];i;i=E[i].nxt)
{
int v=E[i].v;
if(v==pa) continue;
DP(v,u);
mxlen=max(mxlen,dp[u]+dp[v]+E[i].dis);
//这里直接用一个全部变量更新也可以
dp[u]=max(dp[u],dp[v]+E[i].dis);
}
}
例题:F-树上子链_牛客竞赛动态规划专题班树型dp例题 (nowcoder.com)其实是树的直径的变形
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
using namespace std;
typedef long double ld;
typedef pair<int, int> pll;
typedef unsigned long long ULL;
const ll mod = 998244353;
const int N = 1e5 + 5;
int n;
ll a[N];
int head[N],e[N<<1],ne[N<<1],cnt=0;
int st,en;
ll dp[N],ans=-1e5-5;
void add(int x,int y)
{
e[cnt]=y,ne[cnt]=head[x],head[x]=cnt++;
}
void dfs(int x,int fa)
{
ans=max(ans,a[x]);
for(int i=head[x];i!=-1;i=ne[i])
{
int go=e[i];
if(go==fa) continue;
dfs(go,x);
ans=max(ans,dp[x]+dp[go]);
dp[x]=max(dp[x],dp[go]+a[x]);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin>>n;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++) cin>>a[i],dp[i]=a[i];
for(int i=1;i<n;i++)
{
int u,v;cin>>u>>v;
add(u,v);
add(v,u);
}
dfs(1,-1);
cout<<ans;
return 0;
}
4.树的重心
题目:小G有一个大树A-小G有一个大树_牛客竞赛动态规划专题班树型dp例题 (nowcoder.com)
代码:
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
using namespace std;
typedef long double ld;
typedef pair<int, int> pll;
typedef unsigned long long ULL;
const ll mod = 998244353;
const int N = 1e5 + 5;
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int cmp(int a,int b)
{
return a>b;
}
int n;
int head[N],e[N<<1],ne[N<<1],cnt=0;
int dp[N],ans=1005;
int root=1005;
void add(int x,int y)
{
e[cnt]=y,ne[cnt]=head[x],head[x]=cnt++;
}
void dfs(int x,int fa)
{
int sum=0;
for(int i=head[x];i!=-1;i=ne[i])
{
int to=e[i];
if(to==fa) continue;
dfs(to,x);
dp[x]+=dp[to];
sum=max(sum,dp[to]);
}
sum=max(sum,n-dp[x]);
if(sum<ans)
{
ans=sum,root=x;
}
else if(sum==ans)
{
root=min(root,x);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin>>n;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
for(int i=1;i<n;i++)
{
int x,y;cin>>x>>y;
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++) dp[i]=1;
dfs(1,0);
cout<<root<<" "<<ans<<endl;
return 0;
}
5.换根树形dp(二次扫描与换根法)
一般处理不定根问题
例题:Accumulation Degree
题目描述:有一个树形的水系,由N-1条河道和N个交叉点组成。我们可以把交叉点看作树中的节点,编 号为1-N,河道看作无向边。每条河道都有一个容量,链接x与y的河道的容量记为c(x,y)。河道中单位时间 内流过的水量不能超过河道的容量。
有一个节点是整个水系的发源地,可以不断流出水,称之为源点,树中所有度数为1的节点是入海口,可 以吸收无限多的水,称之为汇点。水系中的水从源点出发,沿着河道,流向各个汇点。除源点和汇点 外,各点不存储水。在流量不超过河道容量的前提下,求哪个点作为源点时,整个水系的流量最大,输 出最大值。N<=200000。
分析:
本题是一个”不定根“的树形dp,这类题目特点是,给定一个树形结构,需要以每个节点为根进行统计。 一般通过两次扫描来解:
1.第一次扫描时,任选一点为根,执行一次树形dp.
2.第二次扫描时,从刚才选的根出发,再对整棵树进行一次深度优先遍历,计算出”换根“后的解。
首先,任选一个源点作为根节点,记为root,然后采用以下代码进行一次树形dp,求出D数组。
void dp(int x)
{
v[x]=1;//访问标记
d[x]=0;
for(int i=head[x];i;i=ne[i])
{
int y=e[i];
if(v[y]) continue;
dp[y];
if(edg[y]==1) d[x]+=edge[i];//edge[i]保存c(x,y);
else d[x]+=min(d[y],edge[i]);
}
}
接着,设F[x]表示把x作为源点,流向整个水系的最大流量,对于根节点root,可知F[root]=D[root]。假 设F[x]已被正确求出,考虑其子节点y,F[y]尚未被计算。F[y]则包含两部分:
(1)从y流向以y为根的子树,已经计算并保存到D[y]中。
(2)从y沿父节点x的河道,流向水系的其他部分。
因为把x作为源点的总流量为F[x],从x流向y的流量为min(D[y],c(x,y)),所以从x流向除y以外的部分的流 量就是二者之差(因为F[x]等于流向y部分和流向除y以外的部分的流量之和),于是,把y作为源点,先 流到x,再流向其他部分的流量就是把这个差再与c(x,y)取最小值后的结果。对于度数为1的节点,要特殊 判断。
F[y]就是把源点从x换成y后的流量计算结果。这是一个自顶向下的过程。
void dfs(int x)
{
v[x]=1;
for(int i=head[x];i;i=ne[i])
{
int y=e[i];
if(v[y]) continue;
if(deg[x]==1) f[y]=d[y]+edge[i];
else f[y]=d[y]+min(f[x]-min(d[y],edge[i]),edge[i]);
dfs(y);
}
}
主函数中的调用
int root=1;
dp[root];
memset(v,0,sizeof(v));
f[root]=d[root];
dfs(root);
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,f[i]);
cout<<ans<<endl;