P5021 [NOIP2018 提高组] 赛道修建 (二分+树上贪心)
upd 2024.11.14
求最大值最小,通常先从二分思考。
枚举完二分的值以后,也就限制了最小值的下届。我们要让满足下届的赛道尽可能多。
由于是树,解决这样的问题就有树上 dp 和贪心。
到底是哪一个?走 dp 的话,状态已经不好设计了,难以描述一个局面。
所以走贪心。树上贪心通常自底向上贪,因为每个子树最多向上给一条链,所以让子树内能拼的都拼完一定是最优的,不会等到上面再合并。拼完后就往上传剩下里面最优的。
怎么实现?直接用多重集维护当前链集是常见的手段。
在树上选 \(m\) 条不重合的路径(可以有交点),使得这些路径长度的最小值最大。
看到最小值最大,很自然想到二分模型:枚举最小值 \(L\),看大于等于 \(L\) 的路径能不能有 \(m\) 条。
如何在树上选出 \(m\) 条路径最优成为我们要思考的问题,考虑树上贪心。
普遍的思路是从叶子节点按子树合并,我们要让该子树对父亲的贡献最大,一定是在该子树已经无法拼出路径(贪心)后剩下的路径中取最大值(由于父亲唯一,所以只能留下最大值)。
设 \(f(u)\) 为 \(u\) 子树内的节点到 \(u\) 节点的路径中无法拼出赛道的最长路径,我们已知所有的 \(f(v)+w(u,v)\),对于 \(f(v)+w(u,v)\ge L\) 的,直接贡献+1,对于剩下的,我们可以先让小的路径得到匹配,无法匹配的就更新 \(f(u)\)。这个过程可以用 multiset 维护。
复杂度为 \(O(n\log n\log\sum l)\)。
总结:二分模型,树形贪心的基本思路,合适的数据结构维护。
#include <bits/stdc++.h>
typedef long long ll;
int read() {
int x = 0, f = 1;
char c = getchar();
while(!isdigit(c)) {
if(c == '-') f = -1;
c = getchar();
}
while(isdigit(c)) {
x = (x << 3) + (x << 1) + (c - '0');
c = getchar();
}
return x * f;
}
int n, m, cnt, L, ret, ans;
int h[50010], f[50010];
struct node {
int to, nxt, w;
} e[100010];
void add(int u, int v, int w) {
e[++cnt].to = v;
e[cnt].nxt = h[u];
e[cnt].w = w;
h[u] = cnt;
}
void dfs(int u, int fa) {
std::multiset<int> s;
for(int i = h[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa) continue;
dfs(v, u);
if(f[v] + e[i].w >= L) ret++;
else s.insert(f[v] + e[i].w);
}
while(!s.empty()) {
std::multiset<int>::iterator it = s.begin();
s.erase(it);
std::multiset<int>::iterator it2 = s.lower_bound(L - *it);
if(it2 == s.end()) {
f[u] = std::max(f[u], *it);
}
else {
s.erase(it2);
ret++;
}
}
}
bool check(int x) {
L = x;
ret = 0;
memset(f, 0, sizeof(f));
dfs(1, 0);
return ret >= m;
}
void Solve() {
n = read(), m = read();
int r = 0;
for(int i = 1; i < n; i++) {
int u = read(), v = read(), w = read();
add(u, v, w), add(v, u, w);
r += w;
}
int l = 0;
r = r / m;
while(l <= r) {
int mid = (l + r) >> 1;
if(check(mid)) l = mid + 1, ans = mid;
else r = mid - 1;
}
std::cout << ans << "\n";
}
int main() {
Solve();
return 0;
}