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题单为本文提供思路

  • 相关阅读:
    Mvc+三层(批量添加、删除、修改)
    js中判断复选款是否选中
    EF的优缺点
    Git tricks: Unstaging files
    Using Git Submodules
    English Learning
    wix xslt for adding node
    The breakpoint will not currently be hit. No symbols have been loaded for this document."
    Use XSLT in wix
    mfc110ud.dll not found
  • 原文地址:https://www.cnblogs.com/whenc/p/13957014.html
Copyright © 2011-2022 走看看