树形dp的应用

以节点从深到浅(子树从小到大)的顺序作为dp的阶段;状态表示中,第一维通常是节点的编号(代表以该节点为根的子树),第二维根据具体题目分析。大多数时候,采用递归的方式实现树形dp。对于每个节点x,先递归在他的每个子节点上进行dp,回溯时,从子节点向节点x转移。

目录

代码框架

一、状态机

1. 没有上司的舞会

2. 战略游戏

3. 皇宫看守

二、有依赖的背包

4. 二叉苹果树

5. 选课

三、二次扫描换根

6. 积蓄程度

7. 树的中心


代码框架

void dfs(int u, int father) {

    If (...) ...  //初始化、一些判断等

    for(){

        int v = e[i];   

        if(v == father) continue;  //保证无后效性,只向儿子遍历

        ①dp; //先更新后dfs,自顶向下

        dfs(v, u);

        ②dp; //先dfs后更新,自底向上

    }

    dp;  //此处是对每一个根节点已经遍历完了其所有的儿子后进行的操作,通常是用于对以u为根节点的子树的操作

}

一、状态机

1. 没有上司的舞会

题意:1~N个职员(点),每个点加入舞会后快乐为h[i]。在满足每个职员不与它的直接上司在一起时,求将一部分职员加入舞会的最大快乐和。

转化:一棵树上每个节点有权值w,求每条边上最多选择一个节点的情况下总权值最大值

dp分析:状态表示:f[x][1或0]表示在以x为根的子树内,x节点参加/不参加的最大快乐和(第二维为是否选该节点)

所求:max(f[root][1], f[root][0])

状态计算:不参会则下属可加可不加:f[x][0] = Σmax( f[son][0], f[son][1])

参会则下属不可加:f[x][1] = happy[x]+Σf[son][0]

初始化:如果根节点选了,那么其初始权值为根节点的权值,否则为0

​#include <bits/stdc++.h>
using namespace std;
const int N = 6010;

int e[N] , ne[N] , h[N] , idx;
int happy[N], n,f[N][2];
bool has_fa[N];


void add(int a , int b)
{
    e[idx] = b , ne[idx] = h[a] , h[a] = idx++;
}

void dfs(int u)
{
    f[u][1] = happy[u];

    for(int i = h[u] ; ~i ; i = ne[i])
    {
        int j = e[i];
        dfs(j);

        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0] , f[j][1]);
    }
}

int main()
{
    cin >> n;

    for(int i = 1 ; i <= n ; i++)    cin >> happy[i];

    memset(h , -1 , sizeof h);

    for(int i = 0 ; i < n - 1 ; i++)
    {
        int a ,b;
        cin >> b >> a;
        has_fa[b] = true;
        add(a , b);
    }

    int root = 1;
    while (has_fa[root]) root ++ ;

    dfs(root);

    printf("%d\n", max(f[root][0], f[root][1]));

}


​

2. 战略游戏

题意:每个节点的士兵可以观察到所有与该点相连的边,求所有的都能被看到,最少在多少个节点上放置士兵。

转化:一棵树上每个节点有权值1,求每条边上最少选择一个节点的情况下总权值最小值

dp分析:状态表示:f[x][1或0]表示在以x为根的子树内,x节点放/不放的方案(第二维为是否选该节点)

所求:min(f[root][1], f[root][0])

状态计算:不参会则下属可加可不加:f[x][0] = Σmin( f[son][0], f[son][1])

参会则下属不可加:f[x][1] = Σf[son][0]

初始化:如果根节点选了,那么其初始权值为根节点的权值,否则为0

#include <bits/stdc++.h>
using namespace std;
const int N = 1510;

int n;
int h[N], e[N], ne[N], idx;
int f[N][2];
bool not_root[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
    f[u][0] = 0, f[u][1] = 1;  //initialize
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += f[j][1];
        f[u][1] += min(f[j][0], f[j][1]);
    }
}
int main()
{
    while (~scanf("%d", &n))
    {
        memset(h, -1, sizeof h); idx = 0;
        memset(not_root, 0, sizeof not_root);
        for (int i = 0; i < n; i ++ )
        {
            int a, b, siz;
            scanf("%d:(%d) ", &a, &siz);
            while (siz -- )
            {
                scanf("%d", &b);
                add(a, b);
                not_root[b] = true;
            }
        }
        int root = 0;
        while (not_root[root]) root ++ ;
        dfs(root);
        printf("%d\n", min(f[root][0], f[root][1]));
    }
   
}

3. 皇宫看守

题意:每个节点的士兵可以看到该点和所有与该点相连的点,求所有的都能被看到,最少在多少个节点上放置士兵。

对于节点x,有两种情况:

放置x,可以到所有子节点,子节点可选可不选;

不放x,还要考虑其父节点放置情况的影响,再分成两种情况:

  1. 父节点放置,则x就能由父节点看到,其子节点就可选可不选;
  2. 父节点不放,则其子节点就至少要选一个

我们将以上三种情况作为第二维,用f[x][0], f[x][1], f[x][2]表示。

(待补)

二、有依赖的背包

当物品的依赖关系以树的形式进行存在时,如果是一棵树,那么通常的根节点为1;如果是森林,新建一个虚拟根节点0,连接所有的树。

下面复习一下分组背包:

 f[u][k]表示在以u为根节点 选k个点的值。我们进行递归自底向上更新,每个子树v看做一个组,在组内进行0 ~ k(k = min(sz[v], m)决策 (sz是以v为根节点的子树大小,m是通常可以供选择的体积上限) ,一般转移方程为 f[u][k] = min(f[u][k], f[u][j - k] + f[v][k] + val)

           此处f[u][j - k]是在以u为根节点,排除v这个子树选择j - k 个点的最值(因为枚举到v时还没有更新),而f[v][k]则已经被更新过了,此处代表的是一个值,即以v为根节点,选择k个的值。

4. 二叉苹果树

题意:一棵树节点1~n,求只保留树中的m条边,使得树根所在的连通块的所有边边权之和最大值

物品从点变为边,但由于对于一棵树来说任意节点的入边,即连向父节点的边都是唯一的,所以将入边价值作为点价值。

#include <bits/stdc++.h>
using namespace std;

const int N = 110, M = N << 1;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int f[N][N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u, int father)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int ver = e[i];
        if (ver == father) continue;
        dfs(ver, u);
        for (int j = m; j >= 0; j -- )
            for (int k = 0; k <= j - 1; k ++ ) 
             //枚举体积预留一条连向父节点的边,所以要-1
                f[u][j] = max(f[u][j], f[u][j - k - 1] + f[ver][k] + w[i]);
    }
}
int main()
{
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    for (int i = 1; i < n; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    dfs(1, -1);
    printf("%d\n", f[1][m]);
}

5. 选课

题意:有些课必须上了先修课(只有一门)才能上的情况下,求选m个课的最多学分。

对于任意课最多只有一个先修课,所以这是一个n棵树的森林。设一个虚拟根节点将所有树连起来形成一棵大树来求解。注意更新完子节点后再更新该节点。

注意:如果是选择不超过m个课程,那么最后更新自己的时候还要加上一步,清空没选上u的所有状态,因为选附件有价值的前提是至少要选上主件,我们是自下向上计算,如果背包容积不如当前物品的体积大,那就不能选择当前结点及其子节点,因此赋值为零 。

#include <bits/stdc++.h>
using namespace std;
const int N = 110;

int n, m, root;
int h[N], e[N], ne[N], idx;
int v[N], w[N], f[N][N];


void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
    
    for (int i = h[u]; ~i; i = ne[i])
    {
        int son = e[i];
        dfs(son); 
        for (int j = m ; j >= 0; -- j) 
        {
            for (int k = 0; k <= j; ++ k)   
            {
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
            }
        }
    }
   
    for (int j = m; j >= v[u]; -- j) f[u][j] = f[u][j - v[u]] + w[u];
    //for (int j = 0; j <  v[u]; ++ j) f[u][j] = 0;   //清空没选上u的所有状态
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= n; ++ i)
    {
        int p;
        cin >> v[i] >> w[i] >> p;
        if (p == -1) root = i;
        else add(p, i);
    }
    dfs(root);
    cout << f[root][m] << endl;
  
}

三、二次扫描换根

     一般的dp中,人为规定一个遍历方向,却不能方便直接更新所有由父节点转移过来的信息 (对于不定根问题,枚举所有目标节点,然后求解每个节点到其他节点的最大流量,O(n^2)超时)。由于并不是所有的u→v的信息都截然不同,所以换根就是利用已经求出的信息更新,速度更快。

先任选一点作为根,第一次扫描,进行树形DP,统计子树内的节点对当前节点的贡献(自底向上);第二次扫描,从刚才的根出发,对整棵树DFS(自顶向下),即统计父亲节点对当前节点的贡献并合并统计最终答案。

6. 积蓄程度

题意:一条河(边)的最大水流容量为c[i],整个水系向任一固定方向流动,交汇点流入量=流出量。求以哪一个节点作为源点可以让水系流量最大,求最大值。

在求一个节点的流量时,有两种情况:

  1. 从当前节点往下走
  2. 从当前节点往上走到其父节点,再从其父节点出发且不回到该节点

这样的方向才能保证无后效性(无环),才能使用dp求解。

设D[x]为x为根节点的子树的最大流量,F[x]为以x为根节点整个水系的最大流量,可以得到

状态计算:当y不是叶节点时F[y] = D[y] + min(F[x] - min(D[y], c[x,y])

当y是叶节点时  F[y] = D[y] + c[x, y]

(F[y]就是将根从x变为y的计算结果,这是一个自顶向下的递推方程)

#include <bits/stdc++.h>
using namespace std;
const int N = 200010, M = N*2;
int n;
int h[N], e[M], w[M], ne[M], idx; 
int d[N], f[N]; 
int deg[N], root; 

void add(int a, int b, int c) 
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dp(int u, int father) {
    d[u] = 0; 

    for(int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(j == father) continue;

        dp(j, u); 

       
        if(deg[j] == 1) d[u] += w[i];
        else d[u] += min(d[j], w[i]);
    }
}

void dfs(int u, int father)
{
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(j == father) continue;

        if(deg[u] == 1) f[j] = d[j] + w[i];
        else if(deg[j] == 1) f[j] = d[j] + min(f[u] - w[i], w[i]);
        else f[j] = d[j] + min(f[u] - min(d[j], w[i]), w[i]);

        dfs(j, u);
    }
}

int main()
{
    int T;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);

        memset(deg, 0, sizeof deg); //不要忘记清空
        memset(h, -1, sizeof h); 
        idx = 0;

        for(int i = 0; i < n - 1; i++)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c); 
            deg[a]++, deg[b]++; 
        }

        root = 1; //任选一个点作为根
        dp(root, -1); 

        f[root] = d[root]; 
        dfs(root, -1); 

        int res = 0; 
        for(int i = 1; i <= n; i++) res = max(res, f[i]);
        printf("%d\n", res);
    }


}

7. 树的中心

题意:求一棵树中的一个点,它到所有节点的最远距离最小

与一般换根不同,第一次扫描时要算出当前子树对于根的最大贡献和次大贡献,因为如果该点是其父节点子树的最大路径上的点,则父节点子树的最大贡献不能算作对节点的贡献,否则不符合无后效性。第二次扫描求解出每个节点的父节点对他的贡献后(即每个节点往上能到的最远路径)两者比较取最大即答案。

#include <bits/stdc++.h>
using namespace std;

const int N = 10010, M = N << 1, INF = 0x3f3f3f3f;

int n, h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], up[N];
int s1[N], s2[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs1(int u, int father)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == father) continue;
        dfs1(j, u);
        if (d1[j] + w[i] >= d1[u])
        {
            d2[u] = d1[u], s2[u] = s1[u];
            d1[u] = d1[j] + w[i], s1[u] = j;
        }
        else if (d1[j] + w[i] > d2[u])
        {
            d2[u] = d1[j] + w[i], s2[u] = j;
        }
    }
}
void dfs2(int u, int father)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == father) continue;
        if (s1[u] == j) up[j] = w[i] + max(up[u], d2[u]);   //son_u  = j,则用次大更新
        else up[j] = w[i] + max(up[u], d1[u]);              //son_u != j,则用最大更新
        dfs2(j, u);
    }
}
int main()
{
    memset(h, -1, sizeof h);
    scanf("%d", &n);
    for (int i = 1; i < n; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }

    dfs1(1, -1);
    dfs2(1, -1);

    int res = INF;
    for (int i = 1; i <= n; i ++ ) res = min(res, max(d1[i], up[i]));
    printf("%d\n", res);


}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值