P3000 [USACO10DEC]Cow Calisthenics G
二分 + \(dp\) 的好题。
首先,要求最大直径最小,我们肯定会想到二分最大直径。 难点就在于怎么 \(check\)。由于正确性过于显然,二分可行性证明就略了吧。
记录数组 \(dp_u\) 表示在 \(u\) 号点,其子树中所有点到达 \(u\) 号点的最大链长。转移时,考虑其和子树之间的关系。
可以发现,\(u\) 号点就类似于一个 \(lca\),两个最长的儿子拼起来的最长链就是这棵子树的最大直径。注意,这里的 \(dp_v\) 中所存最长链均已保证子树内部合法。
接着考虑子树中直径超过二分的上限怎么办?贪心地想,我们肯定是要断掉最长的那条链,保留次长的。因为这样可以使得我们的总长度尽可能小,从而达到最优次数。
最后,如果必须断边的次数大于上限 \(s\), 则不合格,应该调大区间,否则一定合格。想一想为什么。(题目中不是要求我们断掉 \(s\) 条边吗)
具体实现看代码:
#include <bits/stdc++.h>
using namespace std;
#define N 1000010
const int inf = 2e9;
template <class T>
inline void read(T& a){
T x = 0, s = 1;
char c = getchar();
while(!isdigit(c)){ if(c == '-') s = -1; c = getchar(); }
while(isdigit(c)){ x = x * 10 + (c ^ '0'); c = getchar(); }
a = x * s;
return ;
}
struct node{
int v, next;
} t[N << 1];
int f[N];
int bian = 0;
inline void add(int u, int v){
t[++bian] = (node){v, f[u]}, f[u] = bian;
return ;
}
int n, s;
int dp[N];
int sum = 0; // 断了多少次
void dfs(int now, int father, int lim){
int maxn = 0, minn = inf;
for(int i = f[now]; i; i = t[i].next){
int v = t[i].v;
if(v == father) continue;
dfs(v, now, lim);
if(dp[v] + maxn > lim) sum++, maxn = min(maxn, dp[v]); // 保留最小值
else maxn = max(maxn, dp[v]); // 否则更新成最大值
}
dp[now] = maxn + 1;
return ;
}
bool check(int lim){
sum = 0; // 记得清空
dfs(1, 0, lim);
return sum <= s;
}
int main(){
read(n); read(s);
for(int i = 1; i < n; i++){
int x, y;
read(x), read(y);
add(x, y); add(y, x);
}
int l = 1, r = n, ans = 0; // 最多断 n 次
while(l <= r){
int mid = l + r >> 1;
if(check(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans << endl;
return 0;
}