【算法竞赛入门到进阶】7.4 树形DP

Part.1【牛客树形DP例题

1:小G有一个大树

  我们令 F [ i ] F[i] F[i]表示以结点 i i i为根节点的子树有多少个结点,那么当结点 i i i被破坏时,所有子树的最大结点个数救赎 m a x ( n − n u m [ i ] , n u m [ s o n ] ) max(n-num[i],num[son]) max(nnum[i],num[son])(son是结点 i i i的所有子结点)。

2:没有上司的舞会 (最大独立集)

  子结点和父节点只能选择一个,很明显,我们可以用 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1]表示结点 i i i:0没有被选择,1被选择。那么 d p [ i ] [ 0 ] + = ∑ m a x ( d p [ s o n ] [ 0 ] , d p [ s o n ] [ 1 ] ) , d p [ i ] [ 1 ] = 1 + ∑ d p [ s o n ] [ 0 ] dp[i][0]+=\sum max(dp[son][0],dp[son][1]),dp[i][1]=1+\sum dp[son][0] dp[i][0]+=max(dp[son][0],dp[son][1]),dp[i][1]=1+dp[son][0].

3:Strategic game (树的最小点覆盖)

  一个点被选择以后,所有与他相连的边都被占据,现要求选择最少的点占据所有的边。一条边有两个端点,最少选择一个点才能把这条边给占据,因此问题依旧转换成了如何选择点的问题:用 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1]表示结点 i i i:0没有被选择,1被选择。那么 d p [ i ] [ 1 ] + = 1 + ∑ m i n ( d p [ s o n ] [ 0 ] , d p [ s o n ] [ 1 ] ) , d p [ i ] [ 0 ] = ∑ d p [ s o n ] [ 1 ] dp[i][1]+=1+\sum min(dp[son][0],dp[son][1]),dp[i][0]=\sum dp[son][1] dp[i][1]+=1+min(dp[son][0],dp[son][1]),dp[i][0]=dp[son][1].

4:Cell Phone Network (树的最小支配集)

  一个点被选择后,他连接的所有的边的另一个端点都被占据,现要求选择最少的点占据所有的点。需要考虑到一个点不选择时,会有两种情况:①被其父结点覆盖;②被其子结点覆盖。因此我们用 d p [ i ] [ 0 / 1 / 2 ] dp[i][0/1/2] dp[i][0/1/2]表示结点 i i i:0被选择,1没有被选择但被其父结点覆盖,2没有被选择但被其子结点覆盖。转移的时候我们仍然只考虑以结点 i i i为根节点的当前子树的情况
①当前结点 i i i被选择: d p [ i ] [ 0 ] = 1 + ∑ m i n ( d p [ k ] [ 0 ] , m i n ( d p [ k ] [ 1 ] , d p [ k ] [ 2 ] ) ) dp[i][0]=1+\sum min(dp[k][0],min(dp[k][1],dp[k][2])) dp[i][0]=1+min(dp[k][0],min(dp[k][1],dp[k][2]))
②当前结点 i i i没有被选择但被其父结点覆盖:这句话对应的意思就是当前结点的所有子结点肯定不会被其父结点覆盖,即 d p [ i ] [ 1 ] = ∑ m i n ( d p [ k ] [ 0 ] , d p [ k ] [ 2 ] ) dp[i][1]=\sum min(dp[k][0],dp[k][2]) dp[i][1]=min(dp[k][0],dp[k][2])
③当前结点 i i i没有被选择但被其子结点覆盖:这句话对应的意思就是当前结点的所有子结点肯定不会被其父结点覆盖,即 d p [ i ] [ 2 ] = i n c + ∑ m i n ( d p [ k ] [ 0 ] , d p [ k ] [ 2 ] ) dp[i][2]=inc+\sum min(dp[k][0],dp[k][2]) dp[i][2]=inc+min(dp[k][0],dp[k][2]).这里之所以要多出一个增量 i n c inc inc,对应一种特殊情况:如果对于所有的 k k k,都满足 d p [ k ] [ 0 ] > d p [ k ] [ 2 ] dp[k][0]>dp[k][2] dp[k][0]>dp[k][2],那么当前结点的所有子结点都不会被选择,这就与当前结点被其子结点覆盖矛盾!解决方案就是选择一个dp[k][0]-dp[k][2]最小的子树,把其修改成选择子结点进行覆盖。
边界条件就是叶子结点: d p [ i ] [ 0 ] = 1 , d p [ i ] [ 1 ] = 0 , d p [ i ] [ 2 ] = I N F dp[i][0]=1,dp[i][1]=0,dp[i][2]=INF dp[i][0]=1,dp[i][1]=0,dp[i][2]=INF.

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false)
using namespace std;
const int maxn=1e4+100;
vector<int> v[maxn];
int dp[maxn][3];
//0:选择了这个结点 1:没有选择这个结点,但其被其父节点覆盖
//2:没有选择这个结点,但其被其子节点覆盖
void DFS(int root,int fa)
{
    int size=v[root].size(),inc=INT_MAX;
    bool ok=true,isLeaf=true;dp[root][0]=1;
    for(int i=0;i<size;++i)
    {
        int cur=v[root][i];
        if(cur==fa) continue;
        isLeaf=false;DFS(cur,root);
        dp[root][0]+=min(dp[cur][0],min(dp[cur][1],dp[cur][2]));
        dp[root][1]+=min(dp[cur][0],dp[cur][2]);
        dp[root][2]+=min(dp[cur][0],dp[cur][2]);
        if(dp[cur][0]>dp[cur][2]) inc=min(inc,dp[cur][0]-dp[cur][2]);
        else ok=false;
    }
    if(isLeaf) dp[root][2]=INT_MAX;
    else if(ok) dp[root][2]+=inc;
}
int main()
{
    close;int n;cin>>n;
    for(int i=0;i<n-1;++i)
    {
        int x,y;cin>>x>>y;
        v[x].push_back(y);
        v[y].push_back(x);
    }
    DFS(1,-1);
    cout<<min(dp[1][0],dp[1][2]);
}

5:二叉苹果树

  给定一棵二叉树(注意:这里的二叉树中的每个结点,要不是叶子结点,要不然一定具有两个子结点),每条边都有一个权重,问在保留给定数量的树枝的条件下,最大值是多少。考虑到结点数不超过100个,因此边的数量也不超过99条,因此我们用 d p [ i ] [ j ] dp[i][j] dp[i][j]表示以结点 i i i的子树保留 j j j条边的最大权值。我们同时用 r e c [ i ] rec[i] rec[i]去记录以结点 i i i为根节点的子树中的边的条数,然后遍历1~ r e c [ i ] rec[i] rec[i],枚举边的来源。

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false)
using namespace std;
const int maxn=300;
struct Edge{
    int to,w,next;
}edge[maxn];
int head[maxn],tot=0,dp[maxn][maxn],rec[maxn],n,goal;
void addedge(int x,int y,int w)
{
    edge[++tot].to=y;edge[tot].w=w;edge[tot].next=head[x];
    head[x]=tot;
}
void DFS(int root,int fa)
{
    rec[root]=0;bool isLeaf=true;
    vector<int> son;son.clear();
    for(int i=head[root];i;i=edge[i].next)
    {
        int cur=edge[i].to;
        if(cur==fa) continue;
        DFS(cur,root);isLeaf=false;
        son.push_back(i);
        rec[root]+=1+rec[cur];
    }
    if(isLeaf) return;
    int x=edge[son[0]].to,y=edge[son[1]].to;
    int wx=edge[son[0]].w,wy=edge[son[1]].w;
    for(int j=rec[root];j>=1;--j)
    {
        dp[root][j]=max(wx+dp[x][j-1],wy+dp[y][j-1]);
        for(int k=j-2;k>=0;--k)
            dp[root][j]=max(dp[root][j],dp[x][k]+dp[y][j-2-k]+wx+wy);
    }
}
int main()
{
    close;cin>>n>>goal;
    for(int i=1;i<n;++i)
    {
        int x,y,w;cin>>x>>y>>w;
        addedge(x,y,w);
        addedge(y,x,w);
    }
    DFS(1,-1);
    cout<<dp[1][goal];
}

6:洛谷P2014 [CTSC1997]选课 (树上背包)

  相较于第5题,我们会发现原来二叉树变成了一棵多叉树。对于一棵根结点为 i i i的子树来说,我们可以想象其左侧有一棵空子树,然后我们枚举原来的子树将其合并上去。注意在枚举的时候需要注意两点:①倒序枚举防止重复计算收益;②课程具有先修关系,因此根节点必选!而且这道题建成的树可能是一片森林,我们可以通过虚拟一个0号节点来达成目的。

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false)
using namespace std;
const int maxn=400;
int val[maxn],dp[maxn][maxn],N,M;;
vector<int> v[maxn];
void DFS(int root)
{
    int size=v[root].size();
    for(int i=M;i>=1;--i) dp[root][i]=val[root];
    for(int i=0;i<size;++i)
    {
        int cur=v[root][i];
        DFS(cur);
        for(int j=M;j>0;j--)
            for(int k=j-1;k>0;k--)
                dp[root][j]=max(dp[root][j],dp[cur][k]+dp[root][j-k]);
    }
}
int main()
{
    close;cin>>N>>M;M++;
    for(int i=1;i<=N;++i)
    {
        int fa;cin>>fa>>val[i];
        v[fa].push_back(i);
    }
    DFS(0);
    cout<<dp[0][M];
    //注意:在这里0作为必修课被处理
}

7:树上子链 (树的直径)

  求解树的直径主要有两种方式:①树上DP;②两遍DFS.两遍DFS的做法就是随便选取一个结点 i i i出发找到距离当前结点距离最远的结点 j j j,然后再以结点 j j j为起点找到最长的结点。但此方法只适用于所有结点的权值非负的情况。
  另一种情况就是采用树上DP的解题策略。我们令 f [ i ] f[i] f[i]表示以节点 i i i为链的一端形成的最长链,针对每个结点,我们都假设一开始有一棵空树,然后我们每次去合并一个子结点的时候,都会在左边这棵合并的树中找到一条以 r o o t root root(父结点)为端点的最长链和以 k k k(子结点)为端点的最长链合并起来比较答案,然后合并这棵子树,意味着要更新以 r o o t root root为端点的最长链。

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false)
using namespace std;
const int maxn=1e5+100;
typedef long long ll;
ll val[maxn],dp[maxn];
vector<int> v[maxn];
ll maxnum=LONG_LONG_MIN;
void DFS(int root,int fa)
{
    dp[root]=val[root];
    maxnum=max(dp[root],maxnum);
    int size=v[root].size();
    for(int i=0;i<size;++i)
    {
        int cur=v[root][i];
        if(cur==fa) continue;
        DFS(cur,root);
        maxnum=max(maxnum,dp[root]+dp[cur]);//更新最长链的值
        dp[root]=max(dp[root],dp[cur]+val[root]);//更新以root为端点的最长链的值 
    }
}
int main()
{
    close;int n;cin>>n;
    for(int i=1;i<=n;++i) cin>>val[i];
    for(int i=1;i<n;++i)
    {
        int x,y;cin>>x>>y;
        v[x].push_back(y);
        v[y].push_back(x);
    }
    DFS(1,-1);
    cout<<maxnum;
}

8:Rinne Loves Edges

  题目的意思即为对于一棵以结点 s s s为根结点的树,如何让所有的叶子结点(度为1)到根结点的道路不通。我们用 f [ i ] f[i] f[i]表示根结点为 i i i的子树让所有叶子结点到结点 i i i的路径不通所需要的最小代价。状态转移方程为: f [ i ] = ∑ m i n ⁡ ( f [ k ] , c o s t [ i ] [ k ] ) f[i]=\sum min⁡(f[k],cost[i][k]) f[i]=min(f[k],cost[i][k]).即要不破坏根结点 i i i到子节点 k k k的路径,要不去破坏叶子结点到子结点 k k k的路径。边界条件是结点 i i i是叶子结点,此时 f [ i ] = I N F f[i]=INF f[i]=INF.

9:吉吉国王

  这道题跟上一题有一点类似,但是多了两个限制:①切断道路的总长度不超过 m m m;②切断道路中的最长道路的值尽可能小。后面的一个方法常见的是采用二分法,我们去二分可能的最长道路的长度,然后我们应用于对根节点到子结点所连边是否能断的判断。针对每一个枚举的长度,我们都跑一遍DFS,只要最后的答案满足不大于 m m m,说明当次枚举的答案是一个可行解。

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false)
using namespace std;
typedef long long ll;
const int maxn=1e3+100;
const int INF=0x3f3f3f3f;
int head[maxn],tot=0,n,m;
struct Edge{
    int to,next;ll w;
}e[maxn<<1];ll dp[maxn];
void addedge(int x,int y,ll w)
{
    e[++tot].to=y;e[tot].next=head[x];e[tot].w=w;
    head[x]=tot;
}
void DFS(int root,int fa,int limit)
{
    bool isLeaf=true;dp[root]=0;
    for(int i=head[root];i;i=e[i].next)
    {
        int cur=e[i].to;
        if(cur==fa) continue;
        DFS(cur,root,limit);isLeaf=false;
        if(e[i].w>limit) dp[root]+=dp[cur];
        else dp[root]+=min(dp[cur],e[i].w);
    }
    if(isLeaf) dp[root]=INF;
}
bool Judge(int limit){DFS(1,-1,limit);return (dp[1]<=m)?true:false;}
int main()
{
    close;cin>>n>>m;
    for(int i=1;i<n;++i)
    {
        int x,y,d;cin>>x>>y>>d;
        addedge(x,y,d);
        addedge(y,x,d);
    }
    int l=0,r=1e3;
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(Judge(mid)) r=mid;
        else l=mid;
    }
    if(Judge(r)) cout<<r;
    else cout<<-1;
}

Part.2 【牛客树形DP练习

Part.3 【算法竞赛入门到进阶练习】

1.HDU 2196 Computer (换根DP)

方法为两次扫描来求解:
  第一次扫描时,任选一个点为根,在“有根树”上执行一次树形dp,在回溯时,自底向上的状态转移。
  第二次扫描时,从第一次选的根出发,对整根树执行一个dfs,在每次递归前进行自顶向下的转移,计算出换根后的解。


  题目大意是给定一棵树,问你能否找到距离每个结点最远的结点与其的距离是多少。首先我们第一遍DFS,能够确定的是距离根节点最远的结点的距离以及次远的结点的距离;第二遍DFS的时候,我们需要找到反向最远距离:子结点的反向最远距离等于max(父节点的反向最远距离,父节点的正向最远距离/正向次远距离<取决于子结点是否经过父结点正向最远距离的链>)+父结点与子结点之间的距离。

#include<bits/stdc++.h>
#define close ios::sync_with_stdio(false)
using namespace std;
const int maxn=1e4+100;
typedef long long ll;
struct Edge{
    ll to,dis;
    Edge(ll a,ll b):to(a),dis(b){}
};
vector<Edge> e[maxn];
ll son[maxn],f[maxn][3];
//f[i][0]:表示点i的正向最长距离
//f[i][1]:表示点i的正向次长距离
//f[i][2]:表示点i的反向最长距离
void DFS1(int root)
{
    f[root][0]=f[root][1]=f[root][2]=0;
    int size=e[root].size();
    for(int i=0;i<size;++i)
    {
        int to=e[root][i].to,dis=e[root][i].dis;
        DFS1(to);
        if(f[to][0]+dis>f[root][0]){
            f[root][1]=f[root][0];
            f[root][0]=f[to][0]+dis;
            son[root]=to;
        }
        else if(f[to][0]+dis>f[root][1]){
            f[root][1]=f[to][0]+dis;
        }
    }
}
void DFS2(int root)
{
    int size=e[root].size();
    for(int i=0;i<size;++i)
    {
        int to=e[root][i].to,dis=e[root][i].dis;
        if(son[root]==to) f[to][2]=max(f[root][2],f[root][1])+dis;
        else f[to][2]=max(f[root][2],f[root][0])+dis;
        DFS2(to);
    }
}
int main()
{
    close;int n;
    while(cin>>n)
    {
        for(int i=1;i<=n;++i) e[i].clear();
        for(int i=2;i<=n;++i)
        {
            ll x,y;cin>>x>>y;
            e[x].push_back(Edge(i,y));
        }
        DFS1(1);DFS2(1);
        for(int i=1;i<=n;++i)
            cout<<max(f[i][0],f[i][2])<<endl;
    }
}

2.HDU 2242 考研路茫茫——空调教室
3.HDU 5834 Magic boy Bi Luo with his excited tree
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值