重刷树形背包计划!

本文详细探讨了树形背包问题,通过分析洛谷P3177、P1272、P1273等多个典型题目,阐述了树上分组背包、删除最少边、最大叶子节点选择等场景的解题思路。涉及动态规划和树形结构的结合,如定义状态、计算点对贡献等关键步骤,并给出了相应的AC代码模板。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.洛谷P3177
题目大意:

给你一颗含n个点的树,让你选出k个黑点,其他都为白点。然后贡献为同颜色点之间两两路径和.最大化贡献。
k ≤ n ≤ 2000 k \leq n \leq 2000 kn2000

题目思路:
1.首先定义状态

d p [ i ] [ j ] dp[i][j] dp[i][j]为节点i,有j个黑点的最大值
这样以来,我们可以将一颗子树的黑点个数看作是背包的体积。然后二元组<子树中黑点个数,点对贡献>就代表着一个物品对于每个子树来说,只能向根转移一种二元组(黑点个数0~min(子树大小,k)).

问题就转化为树上分组背包,体积恰好为k的最大价值.

2.计算点对贡献

由于这个题目中k是固定了的。所以对于一个子树x.若含有j个黑点,那么x的补集树就含有 k - j 个黑点,白点个数一样。所以贡献为:

( j ∗ ( k − j ) + ( s z [ x ] − j ) ∗ ( n − k − s z [ x ] + j ) ) ∗ w [ i ] (j * (k - j) + (sz[x] - j)*(n - k -sz[x]+j))*w[i] (j(kj)+(sz[x]j)(nksz[x]+j))w[i]

注意:一定要控制循环上界为树的大小sz。这样才能保证每个点对只在它们的LCA处计算一次.复杂度为O(n^2).

AC代码(模板):

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
const int maxn = 2000 + 5;
const int mod = 1e9 + 7;
ll dp[maxn][maxn];
ll sz[maxn] , n;
vector<pii> e[maxn];
ll tmp[maxn] , up;
void dfs (int u , int fa)
{
    sz[u] = 1;
    for (auto g : e[u]){
        int v = g.first;
        ll w = g.second;
        if (v == fa) continue;
        dfs(v , u);
        for (int i = 0 ; i <= sz[u] + sz[v]; i++) tmp[i] = dp[u][i];
        for (int i = 0 ; i <= min(sz[u] , up) ; i++){
            for (int j = 0 ; j <= min(sz[v] , up) ; j++){
                if (i + j > up) continue;
                ll val = 1ll * j * (up - j) /*黑点*/ + 1ll * (sz[v] - j) * (n - sz[v] - (up - j));
                val *= w;
                tmp[i + j] = max (tmp[i + j] , dp[u][i] + dp[v][j] + val);
            }
        }
        for (int i = 0 ; i <= sz[u] + sz[v] ; i++) dp[u][i] = tmp[i];
        sz[u] += sz[v];
    }
    return ;
}
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> up;
    for (int i = 1 ; i < n ; i++){
        int x , y , z; cin >> x >> y >> z;
        e[x].pb (mp(y , z));
        e[y].pb (mp(x , z));
    }
    dfs (1 , 0);
    cout << dp[1][up] << endl;
    return 0;
}
二.洛谷P1272
题目大意:

给你一棵树,让你删除最少的边使得生成一个含有 P P P个节点的连通块.

题目思路:

f ( i , j ) f(i,j) f(i,j)代表以 i i i节点为根,向上提供 j j j个节点的最少代价.
跑完树形背包后遍历每个节点即可。除了根节点,其他节点花费均需要 + 1 +1 +1
AC代码:

for (int i = 1 ; i <= sz[u] ; i++){
    for (int j = 0 ; j <= sz[v] ; j++){
        tmp[i + j] = min (tmp[i + j] , dp[u][i] + dp[v][j]);
    }
}
三.洛谷P1273
题目大意:

给你一棵树,每个叶子节点有它的价值。选择一个叶子节点将产生的花费为 【从叶子节点到根节点的路径权值和】. 一条边只会被算一次花费。问你在不亏本的情况下最多选多少个叶子节点.

题目思路:

问题结构:叶子节点的选择之间有影响。树上依赖背包.

首先我想的是: f ( i , j ) f(i,j) f(i,j)代表以 i i i为根节点,整棵树获得价值 j j j的情况下最多选择多少个叶子节点.
但是价值的状态太大,考虑换意:令 f ( i , j ) f(i,j) f(i,j)代表…,在选择了 j j j个叶子节点后所能得到的最大价值.
然后最后对根节点,求最大的 j j j使得 f ( i , j ) ≥ 0 f(i,j) \geq 0 f(i,j)0。即得到答案.
AC代码:

void dfs (int u , int fa)
{
    sz[u] = 1;
    dp[u][0] = 0;
    for (auto g : e[u]){
        int v = g.first;
        int w = g.second;
        if (v == fa) continue;
        dfs(v , u);
        for (int i = 1 ; i <= sz[u] + sz[v] ; i++)
            tmp[i] = -inf;
        for (int i = 0 ; i <= sz[u] ; i++){
            for (int j = 0 ; j <= sz[v] ; j++){
                tmp[i + j] = max (tmp[i + j] , dp[u][i] + dp[v][j] - (j == 0 ? 0 : w));
            }
        }
        for (int i = 0 ; i <= sz[u] + sz[v] ; i++)
            dp[u][i] = tmp[i];
        sz[u] += sz[v];
    }
    if (sz[u] == 1){
        dp[u][1] = a[u];
    }
    return ;
}
四.HDU6540
题目大意:

给你一棵树,然后有 m m m个关键点。有多少种选择方案使得他们两两之间的距离 ≤ k \leq k k
n , m , k ≤ 5000 n,m,k \leq 5000 n,m,k5000

题目思路:

容易想到 f ( i , j ) f(i,j) f(i,j)代表以i为根,且离 i i i最远的关键点离它距离为 j j j的方案数。
然后在转移的时候考虑将子树接在主树上时要满足条件 a + b + 1 ≤ k a+b+1 \leq k a+b+1k才能转移.

注意,这样的转移忽略了主树没选一个关键点 或者 子树没选一个关键点 的两种情况。我们在预处理 t m p tmp tmp数组的时候额外转移一下即可。

AC代码:

void dfs (int u , int fa)
{
    sz[u] = 1;
    dp[u][0] = a[u];
    for (auto v : e[u]){
        if (v == fa) continue;
        dfs(v , u);
        // 预处理 子树不选一个关键点的方案数
        for (int i = 0 ; i <= sz[u] + sz[v] ; i++)
            tmp[i] = dp[u][i];
        // 预处理 主树不选一个关键点的方案数
        for (int i = 0 ; i <= sz[v] ; i++)
            tmp[i + 1] = (tmp[i + 1] + dp[v][i])%mod;
        // 子树和主树都选了关键点,那么形成路径,跑dp即可.
        for (int i = 0 ; i <= sz[u] ; i++){
            for (int j = 0 ; j <= sz[v] ; j++){
                if (i + j + 1 > k) continue;
                tmp[max(i , j + 1)] = (tmp[max(i , j + 1)] + 1ll * dp[u][i] * dp[v][j] %mod)%mod;
            }
        }
        for (int i = 0 ; i <= sz[u] + sz[v] ; i++)
            dp[u][i] = tmp[i];
        sz[u] += sz[v];
    }
    return ;
}
五.牛客-杀树
题目大意:

给出一棵节点数为 n n n 的树,删去一个点 i i i 的代价为 a i a_i ai

一条链的长度定义为路径上 点 的个数。一棵树死了,满足不存在一条长度 ≥ l \geq l l 的链,牛牛希望用最少代价杀死这棵树。
n , l , a i ≤ 5000 n,l,a_i \leq 5000 n,l,ai5000

题目思路:

定义 f ( i , j ) f(i,j) f(i,j) r o o t = i root = i root=i,向上提供长度为j的链时,最小代价.

特判预处理:
1.当我们将 i i i节点删掉时,无论子树如何贡献长度,总体向上贡献长度还是0.
2.当 l = 1 l=1 l=1时,边界条件 f ( i , 1 ) = 0 f(i,1)=0 f(i,1)=0非法!

AC代码:

void dfs (int u , int fa)
{
    sz[u] = 1;
    dp[u][0] = a[u];
    if (k != 1) 
        dp[u][1] = 0;
    for (auto v : e[u]){
        if (v == fa) continue;
        dfs(v , u);
        for (int i = 0 ; i <= sz[u] + sz[v] ; i++)
            tmp[i] = inf;
        // 主树不向上提供链长
        for (int i = 0 ; i <= sz[v] ; i++){
            tmp[0] = min (tmp[0] , dp[u][0] + dp[v][i]);
        }
        for (int i = 1 ; i <= sz[u] ; i++){
            for (int j = 0 ; j <= sz[v] ; j++){
                if (i + j >= k) continue;
                tmp[max(i,j+1)] = min (tmp[max(i,j+1)] , dp[u][i] + dp[v][j]);
            }
        }
        for (int i = 0 ; i <= sz[u] + sz[v] ; i++)
            dp[u][i] = tmp[i];
        sz[u] += sz[v];
    }
    return ;
}
六.蓝桥杯-金属采集
题目大意:

给你一棵带边权的树,放 k k k个机器人在 s s s点。让你安排这 k k k个机器人遍历完整颗树,使得价值最小.
n ≤ 1 e 5 , k ≤ 10 n \leq 1e5 , k \leq 10 n1e5,k10

题目思路:

f ( i , j ) f(i,j) f(i,j)代表 r t = i rt = i rt=i,最终有 j j j个机器人停在该子树中的最小花费.

f ( i , 0 ) f(i,0) f(i,0)代表最终无人停留在子树中,也就是派了一个机器人遍历整棵树.那么边 * 2.(一来一回的花费)

因为机器人可以随意移动。它可以先进入一颗子树,再出来。

为什么不会有这样的情况:分配两个机器人进入子树 i i i,然后再让一个机器人出来.因为这种状态等同于 f ( i , 1 ) f(i,1) f(i,1)

AC代码:

void dfs (int u , int fa)
{
    sz[u] = 1;
    for (int i = 0 ; i < (int)e[u].size() ; i++){
        pii g = e[u][i];
        int v = g.first;
        int w = g.second;
        if (v == fa) continue;
        dfs(v , u);
        for (int i = 0 ; i <= min(sz[u] + sz[v] , k) ; i++)
            tmp[i] = inf;
        for (int i = 0 ; i <= min(sz[u] , k) ; i++){
            for (int j = 0 ; j <= min(sz[v] , k) ; j++){
                if (i + j > k) continue;
                tmp[i + j] = min (tmp[i + j] , dp[u][i] + dp[v][j] + (j == 0 ? 2 : j) * w);
            }
        }
        for (int i = 0 ; i <= min(sz[u] + sz[v] , k) ; i++)
            dp[u][i] = tmp[i];
        sz[u] += sz[v];
    }
    return ;
}
七.ACWING-有依赖的背包问题
题目大意:

给你一棵含有 N N N个点的树,每个节点是一个物品。每个物品有体积和价值。一个物品选了,那么他的所有祖先节点都需要被选。问你整棵树在容量为 V V V能够获得的最大价值
N , V ≤ 100 N,V \leq 100 N,V100

题目思路:

一样的套树上背包模板.令 f ( i , j ) f(i,j) f(i,j)为i点,体积为 j j j的最大价值.
千万注意,对于每个节点,先将其看作是必选了该节点的物品的背包.(如何实现 “必须选某个物品下”的最优解?初始化的时候,体积为 [ 0 , w [ i ] − 1 ] [0,w[i]-1] [0,w[i]1]的都赋值为 − i n f -inf inf, [ w [ i ] , V ] [w[i],V] [w[i],V]的都赋值为 v [ i ] v[i] v[i],然后转移即可. 或者,先背其他物品,最后的时候,将 w [ i ] w[i] w[i]的体积让出来放该物品,即 ∀ i ∈ [ w [ i ] , V ] , d ′ [ i ] = d [ i − w [ i ] ] + v [ i ] \forall i\in [w[i],V],d'[i]=d[i-w[i]]+v[i] i[w[i],V],d[i]=d[iw[i]]+v[i]

AC代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
const int maxn = 100 + 5;
const int mod = 1e9 + 7;
int v[maxn] , w[maxn];
vector<int> e[maxn];
int dp[maxn][maxn] , n , V;
int tmp[maxn];
void dfs (int u , int fa)
{
    // 本节点必选
    for (int i = 0 ; i <= V ; i++){
        if (i >= w[u])
            dp[u][i] = v[u];
        else
            dp[u][i] = -1e9;
    }
    for (auto v : e[u]){
        if (v == fa) continue;
        dfs(v , u);
        // 本节点必选
        for (int i = 0 ; i <= V ; i++)
            tmp[i] = -1e9;
        for (int i = 0 ; i <= V ; i++)
            for (int j = 0 ; i + j <= V ; j++)
                tmp[i + j] = max (tmp[i + j] , dp[u][i] + dp[v][j]);
        for (int i = 0 ; i <= V ; i++)
            dp[u][i] = tmp[i];
    }
    dp[u][0] = 0;
}
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> V;
    int rt;
    for (int i = 1 ; i <= n ; i++){
        cin >> w[i] >> v[i];
        int x; cin >> x;
        if (x == -1) rt = i;
        else e[x].pb (i);
    }
    dfs (rt , 0);
    cout << dp[rt][V] << endl;
    return 0;
}
八.ICPC2020南京M
题目大意:
题目思路:

AC代码:

九.HDU6769:二分答案+树上背包(较难)

https://www.jianshu.com/writer#/notebooks/44190750/notes/76658492

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值