• 树形DP


      通过上一节的学习,应该对动态规划在树形结构上的实现方式有了初步的认识。给定一棵有N个节点的树(通常是无根树,也就是有N - 1条无向边),我们可以任选一个结点为根节点,从而定义出每个节点的深度和每棵子树的根。在树上设计动态规划算法时,一般就以节点从深到浅(子树从小到大)的顺序作为DP的“阶段”。DP的状态表示中,第一维通常是节点编号(代表以该节点为根的子树)。大多数时候,我们采用递归的方式实现树形动态规划。对于每个节点 x,先递归在它的每个子节点上进行DP,在回溯时,从子节点向节点 x进行状态转移。

    1、基本概念

      树形DP称为树形动态规划,顾名思义,就是在“树”的结构上做动态规划,通过有限次地遍历树,记录相关信息,以求解问题。通常,动态规划都是线性的或者是建立在图上的,线性动态规划的顺序有两种方向:即向前和向后,相应的状态转移方程有两种,即顺推与逆推,而树形动态规划是建立在树上的,树中的父子关系天然就是个递归(子问题)结构,所以也相应的有两个方向。

    1. 叶 -> 根,即根的子节点传递有用的信息给根,之后由根得出最优解的过程。这种方式DP的题目应用比较多。
    2. 根 -> 叶,即需要取所有点作为一次根节点进行求值,此时父节点得到了整棵树的信息,只需要去除这个儿子的PD值的影响,然后再转移给这个儿子,这样就能达到根 -> 叶的顺序。

      动态规划的顺序:一般按照后序遍历的顺序,即处理完儿子再处理当前结点,才符合树的子节点的性质。

      实现方式:树形DP是通过记忆化搜索实现的,因此采用的是递归方式。

      时间复杂度:树形动态规划的时间复杂度基本上是O(n);若有附加维m,则是O(n * m)。

      

    2、经典问题

    1. 树的重心

        对于一棵n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点数最小。换句话说,删除这个点后最大连通块的结点数最小,那么这个点就是树的重心。

        解法:任选一个结点为根,把无根树变成有根树,然后设 f[i] 表示以 i 为根的子树的结点个数。不难发现 f[i] = +1。程序实现思路:只需要一次DFS,在无根树转有根树的同时计算即可。其实在删除结点i后,最大的连通块有多少个结点呢?结点i的子树中最大的有max{f[j]}个结点,i的“上方子树”中有 n - f[i] 个结点,在动态规划中就可以根据定义顺便找出树的重心了。

    Acwing 846 树的重心

    给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。

    请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

    重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

    输入格式

    第一行包含整数n,表示树的结点数。

    接下来n-1行,每行包含两个整数a和b,表示点a和点b之间存在一条边。

    输出格式

    输出一个整数m,表示重心的所有的子树中最大的子树的结点数目。

    数据范围

    1n10^5

    输入样例

    9
    1 2
    1 7
    1 4
    2 8
    2 5
    4 3
    3 9
    4 6
    

    输出样例:

    4
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    const int N = 100010, M = 2 * N;
    int n, ans = N;
    int h[N], e[M], ne[M], idx;//n个单链表的头h[N];
    bool st[N];
    void insert(int a, int b)//插入以a为起点指向b的邻接表,插在a指向链表的开始位置h[a]
    {
        e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    int dfs(int u)
    {
        st[u] = true;
        int sum = 1, res = 0;//sum子树节点总数,初始化为根节点自己一个,res表示剩下连通块的大小,即所求
        for(int i = h[u]; i != -1;i = ne[i])
        {
            int j = e[i];
            if(!st[j])
            {
                int s = dfs(j);//以u为根节点子树的大小
                res = max(s, res);//每求得一个子树的大小,放入预定的最小池子里面
                sum += s;//把u的每个子树的大小加到s里面,得到的就是u为根的子树的大小,剩下的就是: n - s
            }
        }
        res = max(res, n - sum);//先求得去掉某一个数 剩余连通块的最大值
        
        ans = min(ans, res);//去掉n个数每一个数之后所剩连通块最大值的最小。
        
        return sum;
    }
    int main()
    {
        memset(h, - 1, sizeof h);
        cin>>n;
        for(int i = 0;i<n-1;i++)
        {
            int a,b;
            cin>>a>>b;
            insert(a, b), insert(b,a);//无向边,需要加入b->a, a->b。
        }
        dfs(1);
        cout<<ans<<endl;
        return 0;
    }

      2、树的最长路径

        给定一棵 n 个结点的边带权的树,找到一条最长路径。换句话说,要找到两个点,使得它们的距离最远,它们之间的路径就是树的最长路径。

        解法:一棵有根树的最长链,可能出现两种情况:1)从最下面的叶子结点到根节点。2)从一个叶子结点到另外一个叶子结点。

        要解决这个问题,我们只需要求出以每个结点为根的子树中的最长链,取其中的最大值即为该树的最长链。

        对于每个结点我们都要记录两个值:d1[i]表示以 i 为根的子树中,i 到叶子结点的距离最大值;d2[i]表示以 i 为根的子树中,除距离最大值所在子树,i 到叶子结点的距离最大值(也就是次大值); 

      令 j 是 i 的儿子。则:

      1)d1[j] + dist[i][j] > d1[i],则 d2[i] = d1[i]; d1[i] = d1[j] + dist[i][j];

      2)否则,若d1[j] + dist[i][j] > d2[i],则 d2[i] = d1[j] + dist[i][j];

      最后扫描所有的结点,找最大的d1[i] + d2[i]的值。

     Acwing 1072 树的最长路径:

    给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

    现在请你找到树中的一条最长路径。

    换句话说,要找到一条路径,使得使得路径两端的点的距离最远

    注意:路径中可以只包含一个点。

    输入格式

    第一行包含整数 n

    接下来 n1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

    输出格式

    输出一个整数,表示树的最长路径的长度。

    数据范围

    1n10000,
    1ai,bin,
    10^5ci10^5

    输入样例:

    6
    5 1 6
    1 4 5
    6 3 9
    2 6 8
    6 1 7
    

    输出样例:

    22

    代码如下:
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N = 10010, M = N * 2;
    int h[N], e[M], w[M], ne[M], idx, ans;
    void add(int a, int b, int c)
    {
        w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    
    int dfs(int u, int father)
    {
        int dist = 0;
        int d1 = 0, d2 = 0;
        for(int i = h[u]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(j == father) continue;
            int d = dfs(j, u) + w[i];
            dist = max(dist, d);
            
            if(d > d1) d2 = d1, d1 = d;
            else if(d > d2) d2 = d;
        }
        
        ans = max(ans, d1 + d2);
        
        return dist;
    }
    
    int main()
    {
        int n;
        cin>>n;
        
        memset(h, -1, sizeof h);
        for(int i = 0; i < n - 1;i++)
        {
            int a, b, c;
            cin>>a>>b>>c;
            add(a, b, c), add(b, a, c);
        }
        
        dfs(1, -1);
        
        cout<<ans<<endl;
    }

      3、树的中心问题

        给出一棵边带权的树,求树中的点,使得此点到树中的其他结点的最远距离最近。

        分析:从任意一点 i 出发的最长路径的可能形态有两种。

        1)从 i 点向上出发,即终点不在以 i 为根的子树中的最长路径长度为 u[i];

        2)从 i 点出发向下,即终点在以 i 为根的子树中的最长路径长度为 d1[i]。

        这里的关键是如何计算 u[i]。i 点向上的路径必经过(i, prt[i]),而 i 点的父结点 prt[i] 又引出了两条路径:一条是prt[i] 向上的最长路径,其长度为 u[prt[i]];另一条是 prt[i] 向下的路径,该路径不能途径 i 点,否则会产生重复计算。

        设 d1[i] 表示以 i 为根的子树中,i 到叶子结点的距离最大值。

        d2[i] 表示以 i 为根的子树中,i 到叶子结点的距离次大值;

        分别用 c1[i] 和 c2[i] 记录 d1[i], d2[i] 是从哪个子树更新来的。

        u[i] 表示出了以 i 为根的子树中的叶子结点外,其他的叶子结点到 i 的最大值。

        1)首先,一遍树形DP算出 d1, d2, c1, c2。令 j 是 i 的儿子,则:

          1. 若 d1[j] + dist[i][j] > d1[i],则 d2[i] = d1[i], d1[i] = d1[j] + dist[i][j];

          2. 否则,若 d1[j] + dist[i][j] > d2[i],则 d2[i] = d1[j] + dist[i][j];

        2) 设 prt[i] = x,

          若 c1[x] != i 即 d1[x] 不从 i 更新而来的,那么 u[i] = max{d1[x], u[x]} + dist[x][i];

          若 c1[x] = i 即 d1[x] 从 i 更新而来的,那么 u[i] = max{d2[x], u[x]} + dist[x][i];

        3)最后在 n 个结点中找到最大值,即:

          t[i] = max{u[i], d1[i]}(1 <= i <= n)

        4) 树的中心:ans = min{t[i]}(1 <= i <= n)

    Acwing 1073 树的中心

    给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。

    请你在树中找到一个点,使得该点到树中其他结点的最远距离最近

    输入格式

    第一行包含整数 n。

    接下来 n1 行,每行包含三个整数 ai,bi,ci,表示点 ai 和 bi 之间存在一条权值为 ci 的边。

    输出格式

    输出一个整数,表示所求点到树中其他结点的最远距离。

    数据范围

    1n10000,
    1ai,bin,
    1ci10^5

    输入样例:

    5 
    2 1 1 
    3 2 1 
    4 3 1 
    5 1 1
    

    输出样例:

    2
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    
    const int N = 10010, M = 2 * N, INF = 0x3f3f3f3f;
    int n, h[N], w[M], ne[M], e[M];
    int d1[N], d2[N], p1[N], p2[N], up[N], idx;
    bool is_leaf[N];
    
    void add(int a, int b, int c)
    {
        w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
    int dfs_d(int u, int father)
    {
        d1[u] = d2[u] = -INF;
        for(int i = h[u]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(j == father) continue;
            int d = dfs_d(j, u) + w[i];
            if(d >= d1[u]) 
            {
                d2[u] = d1[u], d1[u] = d;
                p1[u] = j;   
            }
            else if(d >= d2[u]) d2[u] = d;
            
        }
        if(d1[u] == -INF) 
        {
            is_leaf[u] = true;   
            d1[u] = d2[u] = 0;
        }
        return d1[u];
    }
    
    void dfs_u(int u, int father)
    {
        for(int i = h[u]; i != -1;i = ne[i])
        {
            int j = e[i];
            if(j == father) continue;
            if(p1[u] == j) up[j] = max(up[u], d2[u]) + w[i];
            else up[j] = max(up[u], d1[u]) + w[i];
            dfs_u(j, u);
        }
    }
    
    int main()
    {
        cin>>n;
        memset(h, -1, sizeof h);
        for(int i = 0; i < n - 1;i++)
        {
            int a, b, c;
            cin>>a>>b>>c;
            add(a, b, c), add(b, a, c);
        }
        
        dfs_d(1, -1);
         dfs_u(1, -1);
         int res = d1[1];
        for(int i = 2;i <= n;i++) 
        {
            if(is_leaf[i]) res = min(res, up[i]);
            else res = min(res, max(d1[i], up[i]));
        }
        
        cout<<res<<endl;
        
    }

       4、普通的树形DP

        给定一棵树,现在要从中选出最少的点,使得所有的边至少有一个端点在选中的集合中。

        分析:按照要求构建一棵树。对于这类最值问题,向来是用动态规划求解的。

        点的取舍可以看成一种决策,那么状态就是在某个点取得时候或者不取的时候,以它为根的子树的最小代价。分别可以用f[j][1]和f[j][0]表示。

        当这个点不取的时候,它的所有儿子都要取,所以f[i][0] = 

        当这个点要取得时候,它的所有儿子取不取无所谓,不过当然应该取最优的一种情况。所以

          

         普通的树形DP中,常常会采用叶 -> 根的转移形式,根据父结点的状态确定子结点的状态,若子结点有多个,则需要一一枚举,将子结点(子树)的DP值合并。

    AcWing 285. 没有上司的舞会

    Ural大学有N名职员,编号为1~N。

    他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

    每个职员有一个快乐指数,用整数 Hi 给出,其中 1iN

    现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

    在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

    输入格式

    第一行一个整数N。

    接下来N行,第 i 行表示 i 号职员的快乐指数Hi。

    接下来N-1行,每行输入一对整数L, K,表示K是L的直接上司。

    输出格式

    输出最大的快乐指数。

    数据范围

    1N6000,
    128Hi127

    输入样例:

    7
    1
    1
    1
    1
    1
    1
    1
    1 3
    2 3
    6 4
    7 4
    4 5
    3 5
    

    输出样例:

    5
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    const int N = 6010;
    
    int n, happy[N], h[N], ne[N], e[N], idx;
    int f[N][2];
    bool has_father[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!=-1;i = ne[i])
        {
            int j = e[i];
            dfs(j);
            
            f[u][0] += max(f[j][0], f[j][1]);
            f[u][1] += f[j][0];
        }
    }
    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>>a>>b;
            has_father[a] = true;
            add(b, a);
        }
        
        int root = 1;
        while(has_father[root]) root++;
        
        dfs(root);
        
        cout<<max(f[root][0], f[root][1])<<endl;
    }

        树形DP还有一个重要拓展是与各类树形数据结构结合。例如,Trie上的DP、AC自动机上的DP、后缀自动机上的DP等。

        有时我们的图可以不简单限制于树,在树的基础上进行简单扩展,也可以得到一些能用DP解决的例子,例如,环 + 外向树(在有根树的基础上,添加了一条某结点指向根的边的图)上的DP、仙人掌(每条边至多存在于一个简单环上)上的DP等。 

    Acwing 1074 二叉苹果树

    有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。

    这棵树共 N 个节点,编号为 1 至 N,树根编号一定为 1。

    我们用一根树枝两端连接的节点编号描述一根树枝的位置。

    一棵苹果树的树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

    这里的保留是指最终与1号点连通。

    输入格式

    第一行包含两个整数 N 和 Q,分别表示树的节点数以及要保留的树枝数量。

    接下来 N1 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。

    输出格式

    输出仅一行,表示最多能留住的苹果的数量。

    数据范围

    1Q<N100.
    N1,
    每根树枝上苹果不超过 30000 个。

    输入样例:

    5 2
    1 3 1
    1 4 10
    2 3 20
    3 5 20
    

    输出样例:

    21
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    const int N = 110, M = 2 * N;
    
    int n, m, h[N], e[M], ne[M], w[M], idx, f[N][N];
    
    void add(int a, int b, int c)
    {
        e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
    }
    
    void dfs(int u, int father)
    {
        for(int i = h[u]; ~i; i = ne[i])
        {
            if(e[i] == father) continue;
            dfs(e[i], u);
            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 - 1] + f[e[i]][k] + w[i]);
        }
    }
    
    int main()
    {
        cin>>n>>m;
        memset(h, -1, sizeof(h));
        for(int i = 1; i < n; i ++)
        {
            int a, b, c;
            cin>>a>>b>>c;
            add(a, b, c), add(b, a, c);
        }
        
        dfs(1, -1);
        
        cout<<f[1][m]<<endl;
    }

    Acwing 323 战略游戏

    鲍勃喜欢玩电脑游戏,特别是战略游戏,但有时他找不到解决问题的方法,这让他很伤心。

    现在他有以下问题。

    他必须保护一座中世纪城市,这条城市的道路构成了一棵树。

    每个节点上的士兵可以观察到所有和这个点相连的边。

    他必须在节点上放置最少数量的士兵,以便他们可以观察到所有的边。

    你能帮助他吗?

    例如,下面的树:

    1463_1.jpg.gif

    只需要放置1名士兵(在节点1处),就可观察到所有的边。

    输入格式

    输入包含多组测试数据,每组测试数据用以描述一棵树。

    对于每组测试数据,第一行包含整数N,表示树的节点数目。

    接下来N行,每行按如下方法描述一个节点。

    节点编号:(子节点数目) 子节点 子节点 …

    节点编号从0到N-1,每个节点的子节点数量均不超过10,每个边在输入数据中只出现一次。

    输出格式

    对于每组测试数据,输出一个占据一行的结果,表示最少需要的士兵数。

    数据范围

    0<N1500输入样例:

    4
    0:(1) 1
    1:(2) 2 3
    2:(0)
    3:(0)
    5
    3:(3) 1 4 2
    1:(1) 0
    2:(0)
    0:(0)
    4:(0)
    

    输出样例:

    1
    2
    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    const int N = 1510;
    int n, h[N], e[N], ne[N], idx, f[N][2];
    bool st[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;
        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(cin>>n)
        {
            memset(h, -1, sizeof h);
            idx = 0;
            memset(st, 0, sizeof st);
            for(int i = 0; i < n; i++)
            {
                int id, cnt;
                scanf("%d:(%d)", &id, &cnt);
                while(cnt--)
                {
                    int ver;
                    cin>>ver;
                    add(id, ver);
                    st[ver] = true;
                }
            }
            
            int root = 0;
            while(st[root]) root++;
            
            dfs(root);
            
            cout<<min(f[root][0], f[root][1])<<endl;
        }
    }

    Acwing 1077 皇宫看守

    太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。

    皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。

    已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。

    大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。

    可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。

    帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。

    输入格式

    输入中数据描述一棵树,描述如下:

    第一行 n,表示树中结点的数目。

    第二行至第 n+1 行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号 ii,在该宫殿安置侍卫所需的经费 k,该结点的子结点数 m,接下来 m 个数,分别是这个结点的 m 个子结点的标号 r1,r2,,rm

    对于一个 个结点的树,结点标号在 1 到 n 之间,且标号不重复。

    输出格式

    输出一个整数,表示最少的经费。

    数据范围

    1n1500

    输入样例:

    6
    1 30 3 2 3 4
    2 16 2 5 6
    3 5 0
    4 4 0
    5 11 0
    6 5 0
    

    输出样例:

    25
    

    样例解释:

    在2、3、4结点安排护卫,可以观察到全部宫殿,所需经费最少,为 16 + 5 + 4 = 25。

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    using namespace std;
    const int N = 1510;
    int n, w[N], e[N], ne[N], h[N], idx, f[N][3];
    bool st[N];
    
    void add(int a, int b)
    {
        e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
    }
    
    void dfs(int u)
    {
        f[u][2] = w[u];
        for(int i = h[u];~i; i = ne[i])
        {
            int j = e[i];
            dfs(j);
            f[u][0] += min(f[j][1], f[j][2]);
            f[u][2] += min(min(f[j][1], f[j][2]), f[j][0]);
        }
        
        f[u][1] = 1e9;
        for(int i = h[u]; ~i; i = ne[i])
        {
            int j = e[i];
            f[u][1] = min(f[u][1], f[j][2] + f[u][0] - min(f[j][1], f[j][2]));
        }
    }
    
    int main()
    {
        cin>>n;
        memset(h, -1, sizeof h);
        
        for(int i = 1; i <= n; i++)
        {
            int id, cost, cnt;
            cin>>id>>cost>>cnt;
            w[id] = cost;
            while(cnt--)
            {
                int ver;
                cin>>ver;
                add(id, ver);
                st[ver] = true;
            }
        }
        int root = 1;
        while(st[root]) root++;
    
        dfs(root);
        cout<<min(f[root][1], f[root][2])<<endl;
    }
     
  • 相关阅读:
    d2admin框架学习
    手机访问本地配置域名下的项目
    laydate 限制结束日期不能大于起始日期
    学习MVC之租房网站(十一)-定时任务和云存储
    学习MVC之租房网站(十)-预约和跟单
    学习MVC之租房网站(九)-房源显示和搜索
    学习MVC之租房网站(八)- 前台注册和登录
    学习MVC之租房网站(七)-房源管理和配图上传
    《程序员修炼之道》笔记(九)
    《程序员修炼之道》笔记(八)
  • 原文地址:https://www.cnblogs.com/longxue1991/p/12826627.html
Copyright © 2020-2023  润新知