NOIP2012提高组.疫情控制
题目
357. 疫情控制
算法标签: 树, 二分, 倍增, l c a lca lca, 贪心
思路
- 除了首都, 军队都不应该向下移动, 因为向下移动会使管辖的区域变小, 且军队往上移动越多越好, 也就是管辖的区域变大
- 考虑每个军队最多往上移动多少, 二分
检查
t
t
t时间内军队是否能移动到既定位置, 进行二分
假设二分出来最优的时间是 t t t, 就可计算每个军队在 t t t时间内最多向上走到哪个位置, 然后判断这些位置是否保证每个根节点到叶子结点上路径有军队, 可以倍增进行优化
- 对于无法走到根节点的军队, 使其向上能走到哪就走到哪
- 然后统计有哪些根节点的子树未被完全覆盖, 记为集合 A A A
- 对于能走到根节点的军队, 首先走到根节点的一个儿子上, 然后军队有两种选择原地驻扎或者先走到首都再走到其他城市, 记为集合 B B B
那么问题就转化为, 哪些
B
B
B集合中军队必须原地驻扎
假设橙色子树节点未完全覆盖, 并且时间不足以从当前位置走到根节点再回到当前位置, 那么该位置军队是不能移动的
考虑 A A A集合中某个点(也就是未被覆盖的根节点的某个儿子), 选出到达该位置剩余时间最少的军队, 如果该军队走到首都再走回来时间不够, 那么必然存在一组最优解使得该军队在该节点位置原地驻扎
去除已经原地驻扎的军队覆盖的城市 A ′ A' A′, 剩余军队 B ′ B' B′, 剩余军队就是从当前位置出发走到根节点再走到其它城市, 因此可以统计一下 B ′ B' B′军队走到根后剩余的时间, 然后计算每个 A ′ A' A′中城市到根节点的距离, 将上述两个时间和距离排序, 也就是一个贪心问题, 一定存在一组最优解使得是保序覆盖
可以将交叉方案调整为保序覆盖, 使得不交叉
时间复杂度 O ( n log 2 n ) O(n\log ^ 2n) O(nlog2n)
代码
#pragma GCC optimize(2)
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 50010, K = 16;
const LL INF = 1e18;
int n, m;
vector<PII> head[N];
int fa[N][K], pos[N], tmp[N];
LL dist[N];
bool has_army[N], vis[N];
struct Node {
int ver;
LL rest;
bool is_del;
} nodes[N];
void add(int u, int v, int w) {
head[u].push_back({v, w});
}
void dfs_fa(int u, int pre, LL d) {
dist[u] = d;
for (auto [v, w]: head[u]) {
if (v == pre) continue;
fa[v][0] = u;
for (int k = 1; k < K; ++k) fa[v][k] = fa[fa[v][k - 1]][k - 1];
dfs_fa(v, u, d + w);
}
}
bool is_leaf(int u) {
return u != 1 && head[u].size() == 1;
}
void dfs_g(int u, int pre, bool has) {
has |= has_army[u];
if (is_leaf(u)) vis[u] = has;
else {
vis[u] = true;
for (auto [v, w]: head[u]) {
if (v == pre) continue;
dfs_g(v, u, has);
if (!vis[v]) vis[u] = false;
}
}
}
bool check(LL mid) {
memset(has_army, 0, sizeof has_army);
int cnt = 0;
for (int i = 0; i < m; ++i) {
int u = pos[i];
for (int k = K - 1; k >= 0; --k) {
if (fa[u][k] > 1 && dist[pos[i]] - dist[fa[u][k]] <= mid) u = fa[u][k];
}
if (dist[pos[i]] <= mid) {
nodes[cnt++] = {u, mid - (dist[pos[i]] - dist[u]), false};
}
// 能向上跳的最大距离
else has_army[u] = true;
}
memset(vis, 0, sizeof vis);
dfs_g(1, -1, false);
// 存储A集合中剩余时间最少的军队编号
int army_idx[N];
memset(army_idx, -1, sizeof army_idx);
// 求每个城市剩余时间最少的军队
for (int i = 0; i < cnt; ++i) {
int ver = nodes[i].ver;
if (army_idx[ver] == -1 || nodes[army_idx[ver]].rest > nodes[i].rest) army_idx[ver] = i;
}
// 枚举集合A中的城市
for (auto [v, w]: head[1]) {
if (!vis[v]) {
int k = army_idx[v];
// 不能走到根节点再走回来
if (k != -1 && nodes[k].rest < dist[nodes[k].ver] * 2) {
nodes[k].is_del = true;
vis[v] = true;
}
}
}
// 存储未被删除的军队
int ncnt = 0;
for (int i = 0; i < cnt; ++i) {
if (!nodes[i].is_del) nodes[ncnt++] = nodes[i];
}
int gcnt = 0;
for (auto [v, w]: head[1]) {
if (!vis[v]) tmp[gcnt++] = v;
}
sort(nodes, nodes + ncnt, [](const Node &a, const Node &b) {
return a.rest - dist[a.ver] < b.rest - dist[b.ver];
});
sort(tmp, tmp + gcnt, [](int a, int b) {
return dist[a] < dist[b];
});
// 贪心最大匹配
int k = 0;
for (int i = 0; i < ncnt && k < gcnt; ++i) {
if (nodes[i].rest - dist[nodes[i].ver] >= dist[tmp[k]]) k++;
}
return k == gcnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 0; i < n - 1; ++i) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w), add(v, u, w);
}
dfs_fa(1, -1, 0);
cin >> m;
for (int i = 0; i < m; ++i) cin >> pos[i];
LL l = 0, r = INF;
while (l < r) {
LL mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
if (r == INF) r = -1;
cout << r << "\n";
return 0;
}