原题链接:F. Removing Leaves
题目大意:
给出一棵 n n n 个节点树, n − 1 n - 1 n−1 条边的树,再给出一个正整数 k k k,你要做以下操作:
- 定义叶子节点为只有一条边与其相连的节点。
- 选择一个节点,前提是与这个节点相连的的叶子数大于等于 k k k ,然后你可以任意把其中 k k k 个叶子从图中删去。
现在给出这张图,询问你最多能在图中做多少次这样的操作。
解题思路:
看到这题第一眼就想到拓扑排序了,根据题意模拟操作即可。
假设 c n t cnt cnt 数组为这个节点与其相邻的叶子节点数, d e g deg deg 为当前节点的度数 。
我们执行这样的操作,最初开始时,先让每个叶子让与其相连的节点的 c n t cnt cnt 加上一 ,将叶子的 d e g = 0 deg = 0 deg=0 ,然后将所有 c n t cnt cnt 大于等于 k k k 的所有节点都放入队列里,跑拓扑排序。
跑拓扑序时,我们每次让队头节点的 c n t cnt cnt 和 d e g deg deg 都减去一个 k k k, 如果 c n t cnt cnt 仍然大于等于 k k k, 我们则继续放入队列里,然后继续减。
减到最后,如果这个节点 c n t = 0 cnt=0 cnt=0 时,说明这个节点的叶子节点全部删完了,那么这个节点就会变成一个叶子。我们再让与其连边,且 d e g > 0 deg > 0 deg>0 的点的 c n t + 1 cnt+1 cnt+1 ,然后继续让这个叶子节点 d e g = 0 deg = 0 deg=0 。
想想为什么这样跑就是对的:
- 对于一个节点来说,叶子节点数 c n t cnt cnt 大于等于 k k k 时,那么显然我们可以执行一次操作,将 k k k 个叶子删除,同时度数 d e g deg deg 也减少 k k k 。
- 那么当叶子节点全部删除后我们只有两种情况,一种是图中只有自己一个节点(其余 n − 1 n - 1 n−1 个点全都被删了),或者我们一定还会有一个父亲节点( d e g = 1 deg = 1 deg=1)。后者的情况会让我们成为一个叶子,给父亲节点贡献 c n t + 1 cnt+1 cnt+1 。
- 在进行操作时,每个节点都保证了有 k k k 个叶子,即如果有节点不能执行删除操作,这个节点一定不会被入队。
- 从任意节点开始操作,按照任意顺序,删除之后最后的图中的状态一定都是相同的,因此我们只需要考虑该节点能否执行操作即可。
具体细节在代码内。
时间复杂度: O ( n ) O(n) O(n)
AC代码:
#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;
using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;
void solve() {
int n, k;
cin >> n >> k;
vector<vector<int>> g(n + 1);
for (int i = 1; i <= n - 1; ++i) {
int u, v;
cin >> u >> v;
g[u].emplace_back(v);
g[v].emplace_back(u);
}
//k == 1 直接输出即可
if (k == 1) {
cout << n - 1 << '\n';
return;
}
vector<int> deg(n + 1), cnt(n + 1);
//一个节点在队列里时就不用再多次放入队列里了
vector<int> inque(n + 1);
queue<int> que;
//从叶子开始,让其父亲的cnt+1
for (int i = 1; i <= n; ++i) {
deg[i] = g[i].size();
if (deg[i] == 1) {
deg[i] = 0;
++cnt[g[i][0]];
}
}
//找出 cnt >= k 的节点,从它们开始删
for (int i = 1; i <= n; ++i) {
if (cnt[i] >= k) {
inque[i] = 1;
que.push(i);
}
}
int ans = 0;
while (que.size()) {
int u = que.front(); que.pop();
inque[u] = 0;
//删除操作
++ans, cnt[u] -= k, deg[u] -= k;
//如果没删完 继续删
if (cnt[u] >= k) {
inque[u] = 1;
que.push(u);
}
if (cnt[u] != 0 || deg[u] != 1) continue;
//此时叶子以及全部被删完了
deg[u] = 0;
for (auto& v : g[u]) {
//找到没被删除的父亲节点 (被删除的点deg为0)
if (deg[v] != 0) {
++cnt[v];
if (cnt[v] >= k && !inque[v]) {
//能删就放入队列
que.push(v);
inque[v] = 1;
}
}
}
}
cout << ans << '\n';
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1; cin >> t;
while (t--) solve();
return 0;
}