1. 树上的动态规划(Tree DP)+贪心

我们在树上做一次后序遍历,把每个子树内部「尽可能凑满」的部分切掉(贪心切分大块),把余下的小块回传给父节点。

这是一种把「子问题余量」收集到一个列表、排序后再做贪心合并/切断的套路,常见于需要在树上按「容量/大小」来划分连通块的场景。

2. 小根/大根堆或排序合并

把所有子树回传的“剩余节点数”按升序排序,优先合并小的,剔除大的,这个思想也会出现在「多路合并」「小根堆维护残留」「合并子问题状态」等题里。

3. 下推上拉(push‑up)

子树先自己解决,然后把「无法形成完整块的残余」往上抛,这种「子解决——把余量带给父」的模式是典型的树形 DP 思路。

👇包含注释的代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;const int MAXN = 1000000 + 5;// ——前向星存图——
int n, m;
int h[MAXN], e[2 * MAXN], ne[2 * MAXN], idx;
ll ans = 0;int dfs(int u, int fa) {int sz = 1;              // ① 把当前节点 u 算进去,初始“余量”是 1vector<int> c;           // ② 局部动态数组,用来收集所有子树回传的“余量”// ③ 遍历 u 的每一条出边,找到子节点 vfor (int i = h[u]; i != -1; i = ne[i]) {int v = e[i];if (v == fa) continue;           // 跳过父节点,避免回头int sub = dfs(v, u);             // 递归:计算 v 子树的“剩余节点数”c.push_back(sub);                // 把 v 子树的余量收集到 c 里}// ④ 所有孩子的余量收集完毕后,升序排序,保证“余量小”的先合并sort(c.begin(), c.end());// ⑤ 贪心合并或切断for (int x : c) {if (sz + x <= m) {// 如果加上 x 仍不超过容量,就把这 x 个节点合并到 u 这一块sz += x;} else {// 如果加不下,就必须在 u–v 这条边“斩断”:// 让那 x 个节点单独成一块,块数 ans+1ans++;}}// ⑥ 返回剩下的 sz(≤m)给父节点,让父亲去做同样的合并/切断决策return sz;
}int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cin >> n >> m;// 初始化fill(h + 1, h + n + 1, -1);idx = 0;// 读边建图(无向)for (int i = 0; i < n - 1; i++) {int u, v;cin >> u >> v;e[idx] = v;   ne[idx] = h[u]; h[u] = idx++;e[idx] = u;   ne[idx] = h[v]; h[v] = idx++;}int rem = dfs(1, 0);if (rem > 0) ans++;     // 根节点最后的余量也当一块cout << ans << "\n";return 0;
}