跳转至

250430 D10 模拟赛 T1 题解

题意

给定一棵以 \(1\) 为根的有根树,每个节点对应一个物品,有两个属性:价值 \(a_i\) 和代价 \(b_i\)。一个物品依赖于它的父节点(根节点除外)。你需要选择一些物品,总代价不能超过 \(m\),求最大价值和。

\(n,m\le 2000,\ 1\le |a_i|,b_i\le 10^4\)

题解

树上背包只能做到 \(O(n^3)\)(假设 \(n,m,b_i\) 同价)。考虑将问题用 dfn 序拍到序列上。

\(f_{i,j}\) 表示考虑了 dfn 序的前 \(i\) 项,钦定选择第 \(i\) 项,总共消耗了 \(j\) 的背包容量,能产生的最大价值。这种状态的 dp 转移量非常大,因为 \(f_{i,j}\) 能贡献到所有 \(i'>i\)\(f_{i'}\)

考虑减少转移数量。我们可以给转移添加更多约束。然而这种状态似乎没办法添加约束,因为后缀 \([i,n]\) 太自由了。

另一种方法就是重新设计状态,我们将 \(i\) 的转移区域用尽量少的状态覆盖,这样就只需要进行很少的转移了。考虑将后缀的设为状态,记 \(f_{i,j}\) 表示考虑 \([i,n]\) 后缀的所有节点,消耗 \(j\) 的背包容量,能产生的最大价值。

转移顺序也需要改变,我们从后往前 dp。考虑转移,如果当前节点 \(i\) 被选中了,则后缀 \([i+1,n]\) 转换为一个独立的子问题(因为 \([i,n]\) 的节点一定都挂在 \(1\rightarrow i\) 的链下面),因此直接从 \(f_{i+1,j-b_i}+a_i\) 转移。

如果 \(i\) 不选,则 \(i\) 子树内部的任何节点都不能选,其它节点则仍然没有任何(由 \(i\) 产生的)约束。此时我们直接跳过该子树,从 \(f_{i+sz[i],j}\) 转移即可。

时间复杂度 \(O(nm)\)

代码
#include<iostream>
#include<cstring>
#include<set>
#include<cassert>
using namespace std;
const int N = 2010;

struct Edge {
    int v, next;
} pool[2 * N];
int ne, head[N];

void addEdge(int u, int v) {
    pool[++ne] = {v, head[u]};
    head[u] = ne;
}

int n, m;

int a1[N], b1[N], a[N], b[N];
int sz[N], dfn[N], dt;

void dfs(int u, int fa) {
    dfn[u] = ++dt;
    sz[dt] = 1;
    a[dt] = a1[u];
    b[dt] = b1[u];
    for(int i = head[u]; i; i = pool[i].next) {
        int v = pool[i].v;
        if(v == fa) continue;
        dfs(v, u);
        sz[dfn[u]] += sz[dfn[v]];
    }
}

int f[N][N];

// #define FIO

int main() {

    #ifdef FIO
    freopen("b.in", "r", stdin);
    freopen("b.out", "w", stdout);
    #endif

    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a1[i] >> b1[i];
    for(int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        addEdge(u, v);
        addEdge(v, u);
    }

    dfs(1, 0);

    for(int i = n; i >= 1; i--) {
        for(int j = 0; j <= m; j++) {
            f[i][j] = f[i + sz[i]][j];
            if(j >= b[i]) f[i][j] = max(f[i][j], f[i + 1][j - b[i]] + a[i]);
        }
    }

    cout << f[1][m] << '\n';

    return 0;
}