P9962 [THUPC 2024 初赛] 一棵树
*P9962 [THUPC 2024 初赛] 一棵树
谁说点分治过不了?
思路来自 asmend。
从边的角度计算贡献,涉及黑点个数之差的绝对值,难以入手。
考虑从点的角度计算贡献。钦定黑点重心为
因为钦定
考虑最优方案对应的黑点重心
当
时间复杂度
当
实际上,我们断言,
- 对于不在
x\sim y 路径上的所有边,它们黑点个数较多的那部分子树又多出一个黑点,所以贡献增加1 ,即y 没有减少这些边的权值(一开始所有边的权值当成k 算,即方案的权值为(n - 1)k 减去每个黑点的贡献)。 - 对于在
x\sim y 路径上的所有边,由于x 是当前的重心,所以它们含y 的一侧子树至多有m 个黑点。如果黑点数恰好为m ,那么也没有减少这些边的权值。反之则减小了2 。
综合上述两种情况,
为什么会这样?
首先考虑
如果
否则
子树之间的贡献相对独立,容易证明
枚举所有
一个容易理解的性质是
- 要么
S\backslash S' 存在元素y 使得x, y 的 LCA 为d ,此时由于|S\backslash S'| \geq 2 ,挑一个不是y 的元素换成x 即可。 - 要么
S\cap S 存在元素x' 使得x, y 的 LCA 为d ,此时任选y\in S\backslash S' 换成x 即可。
所有工具已经准备完毕,只差最后一击了!设原来
- 如果
d 不是d' 的 LCA,那么将选择的m 个d 子树内深度最大的点换成原来的m 个黑点,则\sum_{i\in S} dep_i 不降(因为原来m 个黑点的深度是T 最大的m 个深度),且新的 LCA 变浅,即2dep_d 减小,于是总贡献增加。此时原来的m 个黑点现在依然是黑点。 - 如果
d 是d' 的 LCA,那么由于选择的m 个深度最大的点可以是任意m 个深度最大的点,所以同样的,原来的m 个黑点现在依然可以是黑点。
综上,原来的
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using pdi = pair<double, int>;
using pdd = pair<double, double>;
using ull = unsigned long long;
using LL = __int128_t;
#define ppc(x) __builtin_popcount(x)
#define clz(x) __builtin_clz(x)
bool Mbe;
// mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
mt19937_64 rnd(1064);
int rd(int l, int r) {
return rnd() % (r - l + 1) + l;
}
// ---------- templates above ----------
constexpr int N = 5e5 + 5;
int n, k;
ll ans = LONG_LONG_MAX;
vector<int> e[N];
int R, mx[N], sz[N], vis[N];
void findr(int id, int ff, int tot) {
sz[id] = 1, mx[id] = 0;
for(int it : e[id]) {
if(it == ff || vis[it]) continue;
findr(it, id, tot);
sz[id] += sz[it];
mx[id] = max(mx[id], sz[it]);
}
mx[id] = max(mx[id], tot - sz[id]);
if(mx[id] < mx[R]) R = id;
}
int dep[N], bel[N];
struct dat {
int dep, id;
bool operator < (const dat &z) const {
return dep < z.dep;
}
};
vector<dat> arr;
void findd(int id, int ff, int dp, int anc) {
bel[id] = anc;
arr.push_back({dep[id] = dp, id});
for(int it : e[id]) {
if(it == ff) continue;
findd(it, id, dp + 1, anc ? anc : it);
}
}
int f[N], cnt[N];
void dfs(int id, int ff) {
for(int it : e[id]) {
if(it == ff) continue;
dfs(it, id), f[id] += f[it];
}
}
void divide(int id) {
vis[id] = 1;
arr.clear();
findd(id, 0, 0, 0);
sort(arr.begin(), arr.end());
vector<int> to;
ll sum = 1ll * (n - 1) * k, lst = 0;
memset(cnt, 0, N << 2);
memset(f, 0, N << 2);
for(int _ = 1; _ <= k; _++) {
while(!arr.empty()) {
int tid = arr.back().id;
if((cnt[bel[tid]] + 1) * 2 <= k) break;
else to.push_back(bel[tid]);
arr.pop_back();
}
if(arr.empty()) break;
sum -= lst = 2 * arr.back().dep;
f[arr.back().id] = 1;
cnt[bel[arr.back().id]]++;
arr.pop_back();
if(_ == k) ans = min(ans, sum);
}
sort(to.begin(), to.end());
to.resize(unique(to.begin(), to.end()) - to.begin());
if(to.size() <= 1) {
for(int it : to) {
if(vis[it]) continue;
findr(it, id, n);
R = 0, findr(it, id, sz[it]);
divide(R);
}
return;
}
sum += lst, dfs(id, 0); // 撤回到 k - 1 的方案. 这里可以不清空 lst 对应 tid 的 f, 想一想为什么?
static int dis[N], mx = 0;
memset(dis, -1, N << 2);
queue<int> q;
for(int i = 1; i <= n; i++) {
if(f[i] >= k / 2) dis[i] = 0, q.push(i);
}
while(!q.empty()) {
int t = q.front();
q.pop();
if(!f[t]) mx = dis[t];
for(int it : e[t]) {
if(dis[it] == -1) {
dis[it] = dis[t] + 1;
q.push(it);
}
}
}
ans = min(ans, sum - 2 * mx);
}
void solve() {
cin >> n >> k;
if(k == 1) {
cout << n - 1 << "\n";
return;
}
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
mx[0] = N, findr(1, 0, n);
divide(R);
cout << ans << endl;
}
bool Med;
int main() {
fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
#ifdef ALEX_WEI
FILE* IN = freopen("1.in", "r", stdin);
FILE* OUT = freopen("1.out", "w", stdout);
#endif
int T = 1;
while(T--) solve();
fprintf(stderr, "%d ms\n", int(1e3 * clock() / CLOCKS_PER_SEC));
return 0;
}
/*
g++ a.cpp -o a -std=c++14 -O2 -DALEX_WEI
*/