Problem Description
给出一棵 nnn 个节点的无根树。你可以进行以下操作:
选择 kkk 个共同父节点的叶子节点,将 kkk 个节点和与父节点相连的边删去。
求最大操作次数。
Input
第一行输入一个整数 ttt (1≤t≤2×104)(1 \le t \le 2 \times 10^4)(1≤t≤2×104) ,表示测试组数。
接下来每组测试数据第一行输入两个整数 nnn , kkk (2≤n≤2×105,1≤k<n)(2 \le n \le 2 \times 10^5,1 \le k <n)(2≤n≤2×105,1≤k<n) ,表示节点个数和删除的节点限制个数。接下来 n−1n-1n−1 行每行输入两个整数 xi,yix_i,y_ixi,yi (1≤xi,yi≤n)(1 \le x_i,y_i \le n)(1≤xi,yi≤n) ,表示 xix_ixi 和 yiy_iyi 之间有一条边。
题目保证 ∑n≤2×105\sum n \le 2 \times 10^5∑n≤2×105 。
Output
输出最大操作次数。
Solution
首先考虑到删 kkk 个叶子节点的操作是从原树的叶子节点向内进行的,这是具有拓扑关系的。
那么我们可以去考虑每个节点,将其视作根节点即可以找出所以情况的答案,但是这样复杂度是 O(n2)O(n^2)O(n2) ,此时我们便考虑通过换根 dpdpdp 去转移根节点状态优化。
dpu,0dp_{u,0}dpu,0 表示为 uuu 是否为可删节点,如何为 000 即为可删节点,如果不为 000 则为不可删节点。
dpu,1dp_{u,1}dpu,1 表示为 uuu 的儿子节点中可删节点的个数。
ansuans_uansu 表示 uuu 为父节点的子树对答案的贡献。
第一遍 dfsdfsdfs 时,我们任意找一个节点为根,得到各个点的状态,状态转移为:
{dpu,1=∑[dpv,0==0],ansu=⌊dpu,1/k⌋+∑ansv,dpu,0=[dpu,1%k≠0]+∑dpv,0,\left\{\begin{array}{l} dp_{u,1}=\sum[dp_{v,0} == 0], \\ans_u=\left \lfloor dp_{u,1} / k\right \rfloor + \sum ans_v, \\dp_{u,0}=[dp_{u,1}\%k \not= 0]+\sum dp_{v,0}, \end{array} \right.⎩⎨⎧dpu,1=∑[dpv,0==0],ansu=⌊dpu,1/k⌋+∑ansv,dpu,0=[dpu,1%k=0]+∑dpv,0,
然后第二遍 dfsdfsdfs 时,首先要将 uuu 状态更新为儿子节点, ansu−=ansvans_u-=ans_vansu−=ansv , dpu,0−=dpv,0dp_{u,0}-=dp_{v,0}dpu,0−=dpv,0 。
然后考虑转移的节点 vvv 是否为可删节点 ,如果是,考虑以下两种情况:
- dpu,1%k==0dp_{u,1}\% k == 0dpu,1%k==0 ,说明 vvv 是对 uuu 的答案有贡献的,且 uuu 会变成不可删节点,我们需要令 ansu−−ans_u--ansu−− , dpu,0++dp_{u,0}++dpu,0++。
- (dpu,0−1)%k==0(dp_{u,0}-1)\%k==0(dpu,0−1)%k==0 ,说明在去掉这个节点后,uuu 的可删节点将没有残余,可能成为不可删节点,所以要 dpu,0−−dp_{u,0}--dpu,0−− 。
然后我们更新 vvv 的状态,首先判断一下是否 dpu,0==0dp_{u,0}==0dpu,0==0 ,如果是则 uuu 变成可删节点, dpv,1++dp_{v,1}++dpv,1++ 。之后类似第一遍的 dfsdfsdfs 转移即可,不过要注意需要先减去旧状态对状态的影响,下面中 be1be_1be1 表示 dpv,1dp_{v,1}dpv,1 为儿子节点时的状态:
{dpv,0=dpv,0+dpu,0+[dpv,1%k≠0]−[be1%k≠0],ansv=ansv+ansu+⌊dpv,1/k⌋−⌊be1/k⌋,\left\{ \begin{array}{l} dp_{v,0} = dp_{v,0}+dp_{u,0} + [dp_{v,1}\%k \not= 0]-[be_{1}\%k \not= 0], \\ans_v=ans_v+ans_u+\left \lfloor dp_{v,1}/k \right \rfloor-\left \lfloor be_1/k \right \rfloor,\end{array}\right.{dpv,0=dpv,0+dpu,0+[dpv,1%k=0]−[be1%k=0],ansv=ansv+ansu+⌊dpv,1/k⌋−⌊be1/k⌋,
然后遍历下一个节点统计最大值,之后记得回溯状态即可。
Code
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
constexpr int N = 2e5 + 10, M = 4e5 + 10;
int n, k;
int head[N], e[M], ne[M], idx;
inline void addedge(int a, int b) {
e[idx] = b, ne[idx] = head[a], head[a] = idx++;
}
int res, ans[N];
array<int, 2>dp[N];
void initial_dfs(int u, int fa) {
for (int i = head[u]; i != -1; i = ne[i]) {
int v = e[i];
if (v == fa)continue;
initial_dfs(v, u);
ans[u] += ans[v], dp[u][0] += dp[v][0];
if (!dp[v][0])dp[u][1]++;//儿子节点为可删节点才对答案有贡献
}
if (dp[u][1] % k)dp[u][0]++;//如果该节点操作后有不可删节点时
ans[u] += dp[u][1] / k;
}
void dfs(int u, int fa) {
res = max(res, ans[u]);
for (int i = head[u]; i != -1; i = ne[i]) {
int v = e[i];
if (v == fa)continue;
auto a = ans[u], b = ans[v];
auto l = dp[u], r = dp[v];
ans[u] -= ans[v], dp[u][0] -= dp[v][0];
if (!dp[v][0]) {//当转移节点是可删节点时
if (dp[u][1] % k == 0) ans[u]--, dp[u][0]++;//如果v对u的答案是有贡献的
if (--dp[u][1] % k == 0)dp[u][0]--;//如果v可能是限制u成为可删节点
}
if (!dp[u][0])dp[v][1]++;//如果u变成可删节点
//将v的状态转移成根节点状态
dp[v][0] += (dp[u][0] + (dp[v][1] % k != 0) - (r[1] % k != 0));
ans[v] += (ans[u] + dp[v][1] / k - r[1] / k);
dfs(v, u);
ans[u] = a, ans[v] = b, dp[u] = l, dp[v] = r;//状态回溯
}
}
void solve() {
cin >> n >> k;
res = idx = 0;
for (int i = 0; i <= n; i++) {
ans[i] = dp[i][0] = dp[i][1] = 0;
head[i] = -1;
}
for (int i = 1; i <= n - 1; i++) {
int a, b;
cin >> a >> b;
addedge(a, b), addedge(b, a);
}
initial_dfs(1, 0);
dfs(1, 0);
cout << res << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--)solve();
}