• 【动态规划】树上背包,依赖背包的小技巧


    前排提醒,本文转载自 Sshwy's Notes

    转载仅供学习使用。

    可能大家都知道树上背包合并 (O(n^3)) 对子树大小取min可以优化到 (O(n^2)) 。但是对于树上依赖背包问题,背包合并的复杂度仍不能接受。考虑形式化的问题:

    一棵 (n)​ 个结点有根树,每个结点 (i)​ 有 (s_i)​ 个价格为 (c_i) ,价值为 (v_i) 的物品。除了根结点,要购买某个结点的物品必须在它的父节点购买至少一件。求总费用为x时的最大化价值。(x,v_i le m)

    一个朴素的 DP 是 $f(i,j) $​ 表示在结点 (i) 的子树中购买费用不超过 (j) 的物品的最大价值。转移时 (O(m^2)) 进行背包合并,总复杂度 (O(nm^2))

    我们考虑换一种 DP 的顺序。朴素 DP 是以递归子结构的形式进行 DP。考虑按照 DFS 序 DP。

    我们以后序遍历(先按序遍历子节点,再遍历根结点)的方式求出结点的 DFS 序。则对于结点 (u) ,设其 DFS 序为 (D_u),记它的子树大小为 (S_u)。同时我们记 DFS 序为i的结点为 (V(i))

    通俗地说,我们设 (f(i,j)) 表示在 DFS 序小于等于 (i) 的结点构成的连通块中选物品,总费用不超过 (j) 时的最大价值。转移的时候枚举 (V(i)) 上选不选物品,从 (f(i−1),f(i−S_{V(i)})) 转移。


    [QAQ ]


    如果上述状态定义不能理解,我们接下来做一些严谨的描述。首先给出后序遍历 DFS 序的一些性质:

    1. 如果 (u) 不是叶子结点,则 (V(D_u−1))(u) 的儿子(儿子序列中的最后一个儿子)
    2. 对于树中的任意结点 (u)​ ,如果 (D_u ge S_u)​​ ,则 (V(D_u−S_u)) 是离 (u) 最近的,存在前兄弟结点的祖先结点(也可能是 (u) 自己)的前兄弟结点。说的很绕口,可以自己画图理解一下。

    知道这个以后,我们定义 (P(i)=i−S_{V(i)})​ ,即 DFS 序为 (i) 的结点由性质 (2) 导出的结点;如果(D_u<S_u) 则令 (P(u)=0)

    因此更严谨地说,我们设 (f(i,j)) 表示在子树 (V(i), V(P(i)), V(P(P(i))), V(P(P(P(i)))), cdots) 构成的森林中按依赖关系选物品,总费用不超过 (j) 的最大价值。

    转移的时候,如果 (V(i)) 上选物品才能从 (f(i−1)) 转移(要选子节点必须选父节点上的东西)。此外在任何时候都可以从 (f(P(i))) 转移。

    对于上述多重依赖背包问题,我们首先可以从 (f(P(i)))​​ 转移到 (f(i))​(直接复制)。然后我们强制选一个物品,可以从 (f(i−1))​ 转移过来。然后对于剩下的 (s_{V(i)}−1)​ 个物品,使用单调队列优化多重背包或者二进制分组来更新 (f(i)) 即可。复杂度 (mathcal{O}(nm))(mathcal{O}(nm log_2m))

    const int N = 5000;
    struct QAQ {int nxt, t;} e[N << 1];
    int h[N], le;
    int n, m;
    int w[N], c[N], s[N];
    int totdfn, sz[N], f[N][N];
    
    void add(int f, int t) {e[++le] = (QAQ) {h[f], t}, h[f] = le;}
    int dfs(int u) {
        sz[u] = 1;
        for (int i = h[u], v; v = e[i].t, i; i = e[i].nxt)
            sz[u] += dfs(v);
        int i = ++totdfn;
        int lim = s[u];
        f[i][0] = 0;
    
        // ?f[i,j] <- f[i-sz[u],j] , f[i-1,j]
        for (int j = m; j >= 0; j--) {
            if (j >= c[u]) f[i][j] = max(f[i][j], f[i - 1][j - c[u]] + w[u]); // 至少选一个物品,才能从 i-1 转移
            f[i][j] = max(f[i][j], f[i - sz[u]][j]);
        }
        --lim;
        for (int k = 1; k <= lim; lim -= k, k <<= 1) { // 在之前 i-1 和 i-sz 转移的基础上,加入多个物品(二进制)
            for (int j = m; j >= k * c[u]; j--) {
                f[i][j] = max(f[i][j], f[i][j - k * c[u]] + k * w[u]);
            }
        }
        if (lim) { // 剩下一个二进制余项
            for (int j = m; j >= lim * c[u]; j--) {
                f[i][j] = max(f[i][j], f[i][j - lim * c[u]] + lim * w[u]);
            }
        }
        return sz[u];
    }
    int main() {
        cin.tie(nullptr)->sync_with_stdio(false);
        cin >> n >> m;
        for (int i = 2, x; i <= n; ++i) {
            cin >> x;
            add(x, i);
        }
        for (int i = 1; i <= n; ++i)
            cin >> w[i] >> c[i] >> s[i];
        dfs(1);
        for (int i = 1; i <= m; ++i) cout << f[totdfn][i] << " 
    "[i == m];
    }
    

    附:五类背包问题总结笔记之树上背包详解

    进击的Bill_Yang桑也在博客提出这个技巧,通过依赖关系优化树上背包问题

    另外附上一道模板题

    「bzoj4182」Shopping - 点分治+多重背包

    题目链接:Here

    The desire of his soul is the prophecy of his fate
    你灵魂的欲望,是你命运的先知。

  • 相关阅读:
    flowable camunda activiti 功能对比
    activiti与flowable的区别
    工作流框架flowable6与activiti7的选择
    NFS服务的简介及常见故障解决方法
    yum命令详解
    怎么把Chrome网页背景变成黑色模式
    时序数据库InfluxDB 2.0 alpha 发布:主推新的Flux查询语言,TICK栈将成为整体
    influxDB 2.0安装及使用说明
    InfluxDB和MySQL的读写对比测试
    时序数据库特点与对比
  • 原文地址:https://www.cnblogs.com/RioTian/p/15191607.html
Copyright © 2020-2023  润新知