zoukankan      html  css  js  c++  java
  • 树形DP--从入门到入土

    树形依赖背包

    一般形式:给定一颗(n)个节点的点权树,要求选出(m)个节点使得这些选出的节点的点权和最大,一个节点能选当且仅当其父亲节点被选中,根节点能直接选。

    一般解法:

    (f_{u,i})表示在(u)的子树中选择(i)个节点(包括本身)的最大价值,转移方程为:$$f_{u,i} = max(f_{u,j} + f_{v, i - j} + d_v) [j = 1 ... i - 1]$$

    其中(d_v)表示(v)的点权,(i-j)表示在子树(v)中选择(i - j)个节点。

    遍历复杂度(O(n)),总复杂度(O(nm^2))

    优化:

    一般有两种方式可以优化到(O(nm))

    1. 树的孩子兄弟表示法:一种将多叉树变为二叉树的常用方法,就是将每个点与它的第一个儿子连边,然后将它的儿子依次连接起来,如图:

      (f_{i,j})为以(i) 为根的子树中用大小为(j)的包能取到的最大价值,那么转移方程为:$$f_{i, j} = max(f_{left[i],j-w[i]} + v[i], f_{right[i],j})$$

      其中,(left_i)(i)在原树中的第一个儿子(即二叉树中的左儿子),(right_i)(i)在原树中的下一个兄弟(即二叉树中的右儿子)

    2. DFS序法:对整棵树求出(DFS)序与子树大小(siz),那么若根节点为(u),第一个儿子即为(dfn_u + 1),第二个儿子为(dfn_u + siz_{firstson} + 1)

      (f_{i,j})为当前DP到(DFS)序为(i)的点,目前已选(j)个点,则转移方程为:

      • 选当前点:(f_{i + 1,j + 1} = f_{i,j} + d_i)

        因为(i+1)号节点为(i)的儿子或者兄弟,在选(i)之后都是可选的

      • 不选当前节点:(f_{nx[i],j} = f_{i, j})

        其中(nx[i])表示下一颗子树,因为没有选(i),所以不能选(i)的子节点

    以上优化都是将转移降到了(O(1)),但它们只适用于点权问题。

    分组树形背包

    例如:Luogu1272 重建道路

    此时,父亲与儿子之间并不存在依赖关系,我们设(f_{k,i,j})为以(i)为根的子树,在前(t)个儿子中,分离出一个大小为(j)的子树的最小代价,则对于每一个儿子(v)

    [f_{t + 1,i,j} = min(f_{t,i,j - k} + f_{fullson[v],v,k} - 2) ]

    其中,(fullson[v])表示(v)的儿子个数。

    有了这个转移方程,我们就可以在(DFS)时DP了。

    不过,这样的空间开销太大了,我们可以参照01背包的降维优化,通过逆序枚举(j)来把(t)那一维消掉

    [f_{i,j} = min(f_{i,j - k} + f_{v,k} - 2) ]

    初始化时,将(f_{i,1} = ind[i]),其中,(ind_i)为与(i)有连边的节点数。因为将两个点合并到一个点集中时每一个点都会少一条出边,所以要(-2)

    参考代码:

    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int maxn = 1e4 + 10;
    int f[200][200],n,head[maxn],num,ind[maxn],K;
    struct Edge{
        int then,to;
    }e[maxn];
    
    void add(int u, int v){e[++num] = (Edge){head[u], v}; head[u] = num;}
    
    void DFS(int u, int fa){
        f[u][1] = ind[u];
        for(int i = head[u]; i; i = e[i].then){
            int v = e[i].to;
            if(v == fa) continue;
            DFS(v, u);
            for(int j = K; j; -- j)
                for(int k = 0; k <= j; ++ k)
                    f[u][j] = min(f[u][j], f[u][j - k] + f[v][k] - 2);
        }
    }
    
    int Ans = 0x3f3f3f3f;
    int main(){
        scanf("%d%d", &n, &K); 
        for(int i = 1; i < n; ++ i){
            int u,v; scanf("%d%d", &u, &v);
            add(u, v); add(v, u);
            ind[v]++; ind[u]++;
        }
        memset(f,0x3f,sizeof(f));
        DFS(1, 0);
        for(int i = 1; i <= n; ++ i) Ans = min(Ans, f[i][K]);
        printf("%d
    ", Ans);
        return 0;
    }
    

    不过,这一方法只适用于处理边权,用于点权就效率太低下了。

    换根DP(二次扫描法)

    一般来说,我们会默认(1)为树的根,但是有些题目要求计算以每一个节点为根时的内容

    朴素想法是枚举每一个点作为根时的情况,复杂度(O(n^2)),显然太高了;

    正解:换根DP,复杂度(O(n))

    大致思路:

    1. (1)为根(DFS)一遍,统计出所需要的数据和以(1)为根的答案;
    2. (1)开始再次(DFS),每次从节点(u)(v)时,计算出树根从(u)转移到(v)时的贡献变化。

    例题:Luogu3748 [POI2008]STA-Station

    题意:给定一个 (n)个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

    一个结点的深度之定义为该节点到根的简单路径上边的数量。

    思路:按照刚才的想法,我们先(DFS)一遍,求出每个节点的子树大小(siz),并求出以(1)为根的答案(f_1)

    第二遍(DFS),当根从(u)转移到(v)时,(v)子树内(含(v))节点的深度(-1),其他节点的深度(+1),所以可以得到下面的转移方程:

    [f_v = f_u + n - 2 imes siz_v ]

    最后统计最大值就可以了。

    参考代码:

    #include <cstdio>
    #define LL long long
    
    using namespace std;
    
    const int maxn = 1e6 + 10;
    int n,head[maxn << 1],num;
    LL f[maxn];
    struct Edge{
        int then,to;
    }e[maxn << 1];
    
    void add(int u, int v){e[++num] = (Edge){head[u], v}; head[u] = num;}
    
    int siz[maxn];
    void DFS1(int u, int fa, int deep){
        siz[u] = 1;
        for(int i = head[u]; i; i = e[i].then){
            int v = e[i].to;
            if(v == fa) continue;
            DFS1(v, u, deep + 1); 
            siz[u] += siz[v]; f[u] += deep + 1;
        }
    }
    
    void DFS2(int u, int fa){
        for(int i = head[u]; i; i = e[i].then){
            int v = e[i].to;
            if(v == fa) continue;
            f[v] = f[u] + n - 2 * siz[v];
            DFS2(v, u);
        }
    }
    
    int main(){
        scanf("%d", &n);
        for(int i = 1; i < n; ++ i){
            int u,v; scanf("%d%d", &u, &v);
            add(u, v); add(v, u);
        }
        DFS1(1, 0, 0); DFS2(1, 0);
        int Max = 0;
        for(int i = 1; i <= n; ++ i)
            if(f[i] > f[Max]) Max = i;
        printf("%d
    ", Max);
        return 0;
    }
    

    基环树DP

    一般思路:断环为链,再分类讨论

    例题:Luogu1453 城市环路

    题意:给定(N)个点,(N)条边,保证任意两点间至少存在一条路径。其中每个点均有其权值(val_i),问如何选择点,使得在保证任意直接相连的两点不会同时被选中的情况下,被选中的点的权值和最大?

    思路:首先,假如本题不是基环树,那么普通树形DP就可以搞定。而基环树DP的核心就是把基环树上问题转化为普通树上问题。考虑删去基环上的边(E_i),边的端点为(u)(v)。我们将(u)作为新根,可以求得(f_{u,0})(f_{u,1})(其中(f_{i,0/1})表示以(i)为根节点的子树选/不选(i)时的最大权值和)。在实际的图中,(u)(v)是不能同时取到的,为了保证答案合法,我们把(f_{u, 0})记为临时答案

    显然,这个答案并不能保证是最优的,因为答案可能要包含(u)

    如何解决这个问题?我们可以再以(v)作为新根,最做一遍树规,取上次的临时答案与(f_{v,0})(max)即可。

    如何保证答案最优?

    当我们以(u)为根进行树规时,已经把除选择点(u)以外的最优情况求出,而当以(v)为根时,又将选择(u)的情况求出了。由于不能同时选择(u)(v),所以答案必然最优

    参考代码:

    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int maxn = 1e5 + 10;
    int n,f[maxn][2],head[maxn << 1],num,val[maxn];
    int root,x,y;
    double k;
    struct Edge{
        int then,to;
    }e[maxn << 1];
    
    void add(int u, int v){e[++num] = (Edge){head[u], v}; head[u] = num;}
    
    int vis[maxn],flag;
    void DFS1(int u, int fa){
        vis[u] = 1;
        for(int i = head[u]; i; i = e[i].then){
            int v = e[i].to;
            if(v == fa) continue;
            if(flag) return;
            if(vis[v]){
                x = u, y = v; flag = 1;
                return;
            } 
            DFS1(v, u);
        }
    }
    
    void DFS2(int u, int fa){
        f[u][1] = val[u];
        for(int i = head[u]; i; i = e[i].then){
            int v = e[i].to;
            if(v == fa) continue;
            if((u == x && v == y) || (u == y && v == x)) continue;
            DFS2(v, u);
            f[u][0] += max(f[v][0], f[v][1]);
            f[u][1] += f[v][0];
        }
    }
    
    int main(){
        scanf("%d", &n);
        for(int i = 1; i <= n; ++ i) scanf("%d", val + i);
        for(int i = 1; i <= n; ++ i){
            int u,v; scanf("%d%d", &u, &v);
            u += 1, v += 1;
            add(u, v); add(v, u);
        } DFS1(1, 1);
        scanf("%lf", &k);
        memset(f,0,sizeof(f));
        root = x, DFS2(root, root);
        int Ans = f[root][0];
        memset(f,0,sizeof(f));
        root = y, DFS2(root, root);
        Ans = max(Ans, f[root][0]);
        printf("%.1lf
    ", Ans * k);
        return 0;
    }
    

    虚树

    有时候,题目会给出一颗(n)(1e5)级别的树,每次指定(m)个节点,给它们一些性质,然后求答案,保证(sum m)(n)为同一级别。如果我们用朴素的树形DP,复杂度是基于(n)的,会(T)到飞起。

    因此,我们可以用单调栈建出一颗“虚树”,它的节点数是(m)级别的,同时又能保证答案的正确性。

    建树:OI-Wiki上讲得很详细(因为略麻烦,这里不展开阐述当然不是我懒

    模板题:Luogu2495 [SDOI2011]消耗战

    思路:每次建出虚树,DP即可(注意在每一次初始化时保证效率

    参考代码:

    #include <cstdio>
    #include <algorithm>
    #include <vector>
    #include <cstring>
    #define LL long long
    
    using namespace std;
    
    const int maxn = 6e5 + 10;
    int n,m,k,now[maxn];
    int head[maxn << 1],num,cur[maxn],cnt;
    struct Edge{
        int then,to;
        LL val;
    }t[maxn << 1];
    vector<int> e[maxn];
    
    void Add(int u, int v, LL val){t[++cnt] = (Edge){cur[u], v, val}; cur[u] = cnt;}
    
    LL fa[maxn][30],dep[maxn],val[maxn],id[maxn],_num;
    void DFS(int u, int f){
        dep[u] = dep[f] + 1;
        fa[u][0] = f; id[u] = ++_num;
        for(int i = 1; i <= 18; ++ i) fa[u][i] = fa[fa[u][i - 1]][i - 1];
        for(int i = cur[u]; i; i = t[i].then){
            int v = t[i].to;
            if(v == f) continue;
            val[v] = min(val[u], t[i].val);
            DFS(v, u);
        }
    }
    
    int LCA(int x, int y){
        if(dep[x] < dep[y]) swap(x, y);
        for(int i = 18; i >= 0; -- i)
            if(dep[fa[x][i]] >= dep[y]) x = fa[x][i];
        if(x == y) return x;
        for(int i = 18; i >= 0; -- i)
            if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
        return fa[x][0];
    }
    
    bool cmp(int x, int y){return id[x] < id[y];}
    
    int sta[maxn],top;
    void build(){
        sort(now + 1, now + 1 + k, cmp);
        sta[1] = 1, top = 1; e[1].clear();
        for(int i = 1; i <= k; ++ i){
            if(top == 1){sta[++top] = now[i]; continue;}
            int pos = now[i];
            int L = LCA(pos, sta[top]);
            if(L == sta[top]) continue;
            while(id[L] <= id[sta[top - 1]] && top > 1) e[sta[top - 1]].push_back(sta[top]), top--;
            if(L != sta[top]) e[L].push_back(sta[top]), sta[top] = L;
            sta[++top] = pos;
        }
        while(top > 0) e[sta[top - 1]].push_back(sta[top]), top--;
        return;
    }
    
    LL dfs(int u, int fa){
        if(e[u].size() == 0) return val[u];
        LL tmp = 0;
        for(int i = 0; i < e[u].size(); ++ i) tmp += dfs(e[u][i], u);
        e[u].clear();
        return min(tmp, (LL)val[u]);
    }
    
    int main(){
        scanf("%d", &n);
        memset(val,0x7f,sizeof(val));
        for(int i = 1; i < n; ++ i){
            int u,v; LL val; scanf("%d%d%lld", &u, &v, &val);
            Add(u, v, val); Add(v, u, val);
        } 
        DFS(1, 1); scanf("%d", &m);
        while(m--){
            scanf("%d", &k);
            for(int i = 1; i <= k; ++ i) scanf("%d", &now[i]);
            build();
            printf("%lld
    ", dfs(1, 1));
        }
        return 0;
    }
    

    练习题

    [HAOI2009]毛毛虫 题解

    [POI2013]LUK-Triumphal arch 题解

    CF219D 题解

    Luogu1131 时态同步

    Noip2019 括号树

    以上习题都较为基础,请结合自身情况使用

    参考博客

    https://blog.csdn.net/weixin_30278311/article/details/95302702?utm_medium=distribute.pc_relevant.none-task-blog-title-10&spm=1001.2101.3001.4242

    http://blog.csdn.net/no1_terminator/article/details/77824790

    https://www.cnblogs.com/wlzhouzhuan/p/12643056.html

    https://blog.csdn.net/DorMOUSENone/article/details/54971697

    https://blog.csdn.net/wu_tongtong/article/details/79219822

    特别鸣谢

    UltiMadowLuogu题单为本文提供思路

  • 相关阅读:
    【LeetCode】N数和
    用PHP写一个双向队列
    PHP的几种遍历方法
    thinkphp中dump()方法
    【转】PHP对象在内存中的分配
    【转】PHP的执行原理/执行流程
    从头到尾彻底解析哈希表算法
    【转】TCP通信的三次握手和四次撒手的详细流程(顿悟)
    springmvc中拦截器的定义和配置
    springmvc中的异常处理方法
  • 原文地址:https://www.cnblogs.com/whenc/p/13957014.html
Copyright © 2011-2022 走看看