Codeforces Round 595 (Div. 3) F. Removing Leaves(树形DP)

原题链接:F. Removing Leaves


题目大意:


给出一棵 n n n 个节点树, n − 1 n - 1 n1 条边的树,每一个点 i i i 有一个权值 a i a_i ai ,再给出一个数字 k k k

现在你可以选出一些点构成一个点集,且对于点集中任意两点 x , y x,y x,y 的最短距离 d d d 来说都满足 d > k d > k d>k

现在要你回答在所有选出点集的方案中,选出点的点权之和最大是多少。

数据范围: ( 1 ≤ n , k ≤ 200 , 1 ≤ a i ≤ 1 0 5 ) (1\leq n,k \leq 200, 1 \leq a_i \leq 10^{5}) (1n,k200,1ai105)

解题思路:


这题看起来贪心没有什么入手点,考虑树形 D P DP DP

我们设计这样一个状态:

d p u , d dp_{u,d} dpu,d 代表 u u u 节点的子树中,选出一些节点 v v v 且所有选中节点中深度最小的节点深度至少为 d d d 的最大权值和。(注意是 > k >k >k 而不是 ≥ k \geq k k,那么我们这里转化为 ≥ k + 1 \geq k+1 k+1 方便理解)

那么还是和传统的树形 D P DP DP 的思考方式一样,我们把整棵树的问题转化到一棵棵子树中,从而再从子树中的答案转移到自己身上。

假设节点 u u u 深度为 0 0 0 ,我们枚举这个最小深度 d d d 进行转移:

  • 如果 d = 0 d=0 d=0,那么我们此时必选根节点,转移方程就为:
    d p [ u ] [ 0 ] = a [ u ] + ∑ v ∈ s o n u d p [ v ] [ k ] \begin{array}{c} dp[u][0] =a[u]+\sum_{v \in son_u}dp[v][k] \end{array} dp[u][0]=a[u]+vsonudp[v][k]

这个转移方程很好理解,假设我们必须选 u u u 节点,那么我们就在从距离点 u u u k + 1 k+1 k+1 的点的子树中选剩下能选的节点即可。

而我们是枚举点 u u u 的子节点 v v v 的,此时我们自带了 1 1 1 的距离,所以我们查询的是在 v v v 的子树中,深度最小为 k k k 的子节点的最大权值和。

  • 如果 d ≠ 0 d \neq 0 d=0,那么我们此时会选择点 u u u 的子节点,即点 v v v 子树中的一些节点,在其中取一个最大值。

d p [ u ] [ d ] = max ⁡ ( d p [ u ] [ d ] , d p [ v ] [ d − 1 ] + ∑ v ′ ∈ s o n u ∖ { v } d p [ v ′ ] [ max ⁡ ( d e p − 1 , k − d e p ) ] ) \begin{array}{c} dp[u][d] = \max(dp[u][d],dp[v][d-1]+\sum_{v' \in son_u∖\{v\}}dp[v'][\max(dep-1,k-dep)]) \end{array} dp[u][d]=max(dp[u][d],dp[v][d1]+vsonu{v}dp[v][max(dep1,kdep)])

看起来很复杂,解释一下转移方程:

最外层的 max ⁡ \max max 就是在所有方案中取最大值,我们看内层的转移。

d p [ v ] [ d − 1 ] + ∑ v ′ ∈ s o n u ∖ { v } d p [ v ′ ] [ max ⁡ ( d − 1 , k − d ) ] \begin{array}{c} dp[v][d-1]+\sum_{v' \in son_u∖\{v\}}dp[v'][\max(d-1,k-d)] \end{array} dp[v][d1]+vsonu{v}dp[v][max(d1,kd)]

假设我们要在 u u u 的子树中取一个深度为 d d d 的节点,即我们要在其中一个子节点 v v v 的子树中取深度为 d − 1 d-1 d1 的节点,然后再剩余子节点 v ′ v' v 的子树中取深度为 max ⁡ ( d − 1 , k − d ) \max(d-1,k-d) max(d1,kd) 的节点,解释一下这个 max ⁡ \max max 为什么这样写。

  • k − d > d − 1 k-d > d-1 kd>d1 时,我们只能在深度为 k − d k-d kd 的子树中取点,才能使得我们的节点 v v v 和其他节点 v ′ v' v 的距离 d > k + 1 d > k + 1 d>k+1,这个很好理解。
  • k − d ≤ d − 1 k - d \leq d-1 kdd1 ,如果我们从 v ′ v' v 子树中取深度为 k − d k-d kd 的节点,那么这个节点的深度加上 1 1 1 会比我们的 d d d 更小,虽然满足了 v , v ′ v,v' v,v 两点间距至少为 k k k ,但这和我们状态设计相冲突。我们要的是 深度最小的节点深度至少为 d d d ,因此此时我们只能在 v ′ v' v 的子树中取深度为 d − 1 d-1 d1 的节点。

看起来很难懂,我们直接上图解释一下。

在这里插入图片描述

  • 左图要使得选的点距离 d ≥ k + 1 d \geq k+1 dk+1 那么我们选了橙色点后,我们只能选黄色点来进行转移,即 d p [ u ] [ 1 ] = max ⁡ { d p [ v ] [ 0 ] + d p [ v ′ ′ ] [ 1 ] + d p [ v ′ ] [ 1 ] } dp[u][1] = \max\{dp[v][0]+dp[v''][1]+dp[v'][1]\} dp[u][1]=max{dp[v][0]+dp[v′′][1]+dp[v][1]}

  • 右图我们选了橙色点后,在满足两点间距大于 k k k 的前提下,我们是可以选图上绿色的这两个点的,但注意我们的状态设计:所有选中节点中 深度最小的节点深度至少为 d d d 的最大权值和 ,绿色节点深度 d ′ = 2 d'=2 d=2 显然不满足 d ′ ≥ d = 3 d' \geq d=3 dd=3 那么我们此时便只能继续选黄色点而不能向上选绿色节点,即 d p [ u ] [ 3 ] = max ⁡ { d p [ v ] [ 2 ] + d p [ v ′ ′ ] [ 2 ] + d p [ v ′ ] [ 2 ] } dp[u][3] = \max\{dp[v][2]+dp[v''][2]+dp[v'][2]\} dp[u][3]=max{dp[v][2]+dp[v′′][2]+dp[v][2]}

再看回这个方程,浅显易懂了。
d p [ v ] [ d − 1 ] + ∑ v ′ ∈ s o n u ∖ { v } d p [ v ′ ] [ max ⁡ ( d − 1 , k − d ) ] \begin{array}{c} dp[v][d-1]+\sum_{v' \in son_u∖\{v\}}dp[v'][\max(d-1,k-d)] \end{array} dp[v][d1]+vsonu{v}dp[v][max(d1,kd)]

回到题目中,我们要的并不是所选节点深度恰好为 d d d 的最大值,而是深度 至少 d d d 的最大值。

但是显然,我们在深度浅的子树中是可以取深度更大的节点的,而不是必须取深度为 d d d 的节点,那么我们在最后只要再倒着从 d = n → 0 d=n \rightarrow 0 d=n0 max ⁡ { d p [ u ] [ d ] } \max\{dp[u][d]\} max{dp[u][d]} 即可获得 至少 的最大值了。

我们最终答案会在 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0] 中取得。

具体细节看代码实现即可。

时间复杂度: O ( n 3 ) O(n^3) O(n3)

AC代码:


#include <bits/stdc++.h>

#define NO return void(cout << "No\n")
#define YES return void(cout << "Yes\n")
using namespace std;

using i64 = long long;
using PII = pair<int, int>;

const int N = 200 + 10;

i64 dp[N][N], a[N], n; int k;

vector<int> g[N];

void DFS(int u, int ufa) {
    dp[u][0] = a[u];
    //先将子树的 dp[v][d] 求出
    for (auto& v : g[u]) {
        if (v == ufa) continue;
        DFS(v, u);
    }

    //当 d == 0 时必选根节点
    for (auto& v : g[u]) {
        if (v == ufa) continue;
        dp[u][0] += dp[v][k];
    }

    //否则 d != 0 我们会取子树中的节点
    for (int d = 1; d <= n; ++d) {

        //先取一个 v 子树中的点
        for (auto& v : g[u]) {
            if (v == ufa) continue;

            i64 sum = dp[v][d - 1];

            //再从剩下的子树 v2 中取出能够满足的点
            for (auto& v2 : g[u]) {
                if (v2 == v || v2 == ufa) continue;
                sum += dp[v2][max(d - 1, k - d)];
            }

            //在所有选取 v 的方案中取一个最大值
            dp[u][d] = max(dp[u][d], sum);
        }
    }

    //深度小的子树中可以取深度大的节点
    //倒着更新一下小深度可以取得的最大值
    for (int d = n - 1; d > 0; --d) {
        dp[u][d - 1] = max(dp[u][d - 1], dp[u][d]);
    }
}

void solve() {
    cin >> n >> k;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }

    for (int i = 1; i <= n - 1; ++i) {
        int u, v; cin >> u >> v;
        g[u].emplace_back(v);
        g[v].emplace_back(u);
    }

    DFS(1, 0);

    cout << dp[1][0] << '\n';
}

signed main() {

    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    int t = 1; //cin >> t;
    while (t--) solve();

    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值