2020 CCPC 秦皇岛 K. Kingdom‘s Power(树形DP)

传送门


题目大意

给出一棵根节点为 1 1 1的树,其中树根上面有无穷的士兵,每分钟我们能调取一队士兵走一条边,如果该节点没有被占领那么会占领该节点,问占领所有节点需要的最少时间。

解题思路

首先要能简单证明如下几个关键想法:

  • 对于某节点 u u u,它要么被其兄弟子树上的士兵折返回来占领,要么就从根节点下来一队新的士兵占领。

  • 一队士兵到达节点 u u u的子节点 v v v后一定会继续向下走,要么停在叶子节点上,要么等到 u u u的整个子树遍历完成后可能前往 u u u的兄弟节点。

  • 对于每个节点来说,从根节点下来的士兵的路径长度是唯一的,但是其兄弟子树的士兵往返的路径是变化的,如何最小化折返的路径长度?考虑一个节点占领一棵子树时,最优的做法是除了最长链只走一次外,其他链都会走两次。这启发我们对每个节点按最长链的长度从小到大排序,这样再进行DP的结果是最优的。

  • 如果我们要用到第 i i i次操作的那队士兵,那么一定只会在 i + 1 i+1 i+1次操作时可能用到它,因为如果其兄弟节点都不会用到,那么在 i + x ( x > 1 ) i+x(x>1) i+x(x>1)次操作回溯到了距离根更近的父亲节点时更不会用到。

有了以上的思路后,显然我们只需要记录上一次走到叶子节点的那队士兵,然后比较上队士兵到当前节点的距离和当前节点的深度来确定是否新派士兵。一开始一直纠结如何从上次的叶子结点转移到当前的叶子节点,写了几次发现很难维护(对于蒟蒻来说)。然后突然发现每次不管如何,都只能是一队士兵在走,我们可以不单单考虑叶子节点,而是对每个节点来看,求出要用到的士兵走到该节点需要的距离,不断更新维护当前的士兵的位置,这样本题就迎刃而解了!

附上测试的图,答案为 13 13 13
在这里插入图片描述


//
// Created by Happig on 2021/3/3.
//
#include <bits/stdc++.h>
#include <unordered_map>

using namespace std;
#define ENDL "\n"
#define lowbit(x) (x & (-x))
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<double, double> pdd;
const double eps = 1e-8;
const double pi = acos(-1.0);
const int inf = 0x3f3f3f3f;
const double dinf = 1e300;
const ll INF = 1e18;
const int Mod = 1e9 + 7;
const int maxn = 1e6 + 10;

ll ans = 0;
vector<int> G[maxn];
int maxd[maxn], d[maxn];
int pre;

bool cmp(int &p, int &q) {
    return maxd[p] < maxd[q];
}

void dfs1(int u, int deep) {
    d[u] = maxd[u] = deep;
    for (auto v:G[u]) {
        dfs1(v, deep + 1);
        maxd[u] = max(maxd[u], maxd[v]);
    }
    sort(G[u].begin(), G[u].end(), cmp);
}

void dfs2(int u, int fa) {
    if (u != 1) {
        if (!pre) ans += d[u];
        else {
            if (d[pre] - d[fa] + 1 <= d[u]) ans += d[pre] - d[fa] + 1;
            else ans += d[u];
        }
        pre = u;
    }
    for (auto v:G[u]) {
        dfs2(v, u);
    }
}

int main() {
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    //ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t, n, kase = 0;
    scanf("%d", &t);
    while (t--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) G[i].clear();
        for (int i = 2, u; i <= n; i++) {
            scanf("%d", &u);
            G[u].push_back(i);
        }
        dfs1(1, 0);
        ans = pre = 0;
        dfs2(1, 0);
        printf("Case #%d: %lld\n", ++kase, ans);
    }
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值