【总结】树形dp

树形dp的时间复杂度一般为O(n),因为每个节点只被遍历一次

目录

1.普通树形dp

2.背包类树形dp

1.树上01背包类

3.树的直径

4.树的重心

5.换根树形dp(二次扫描与换根法)


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;

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值