最大值最小是二分答案的特征。二分完后每个公园可以覆盖距离不超过 \(k\) 的领域,要覆盖整棵树。
二分完后需要 check。最可能的路线是贪心和 dp。
好像本质上都存储了可能成为答案的组合的部分信息,但贪心确定了这个组合当前的唯一性,dp 并没有,只能保证最优解一定属于被划分出来的某个等价类中,保留所有可能成为最优解的组合转移,用转移边进行答案的构造。
那么可以 dp 吗,对什么 dp 呢,转移边象征什么呢,怎么压缩信息呢。首先转移方向只能从儿子到自己,如果转移是在当前点建公园,需要记子树内最深的未被覆盖的点,至少是 \(O(siz)\) 的,并且合并子树时还要更新这个值,需要另外记最浅的公园,这就开不下了。
那一定是把大量本不可能成为答案的东西记进去了。找性质剪剪枝,或者直接发现答案构造可以被唯一确定变成贪心吧。
赛时发现对于一片叶子,覆盖它的公园并不是越浅越好,可能要稍微深一点照顾另一片叶子。尝试给叶子钦定一个顺序让决策有单调性,发现好像可以从下到上合并相交的必要放置区域,直到再往父亲放置就不合法了。 然后就感觉很不可做了。 所以还是去放置公园的点上贪吧。
赛时思路卡在这里了,试图思考覆盖每个点的公园在哪里。但 dp 视角,知道覆盖一个点的公园在哪里也不好推覆盖另一个点的公园在哪里,贪心视角,它没有单调性,要最小化的公园数量也不好借助覆盖每个点的公园表示。因此,要直接贪在不在这个点建公园。
这个时候就有单调性了。如果在这个点建了公园,则,尽量往上放一定更优,即,如果把公园改建到当前点的父亲,子树仍然合法,则改建一定不劣。
于是只要判定在父亲建公园是否会导致子树内有点不合法就可以了。维护每个子树内最深未覆盖点和最浅公园,先更新最浅公园,用这个值更新最深未覆盖点,就可以了。
首先一定要排除 dp,要记的东西有点多。其次要转到公园本身的视角,因为这样才好表示公园到底有几个。后面的过程是自然的。
#include <bits/stdc++.h>using LL = long long;int main() {int n, k; scanf("%d %d", &n, &k);std::vector<std::vector<std::pair<int, int>>> e(n);for (int i = 1, u, v, w; i < n; i++) {scanf("%d %d %d", &u, &v, &w), --u, --v;e[u].emplace_back(v, w), e[v].emplace_back(u, w);}auto chk = [&](LL x) {std::vector<LL> mn(n, 1e18), mx(n, -1e18);std::vector<int> p;std::function<void(int, int, int)> dfs = [&](int u, int fa, int l) {for (const auto &[v, w] : e[u]) if (v != fa) dfs(v, u, w), mn[u] = std::min(mn[u], mn[v] + w);if (mn[u] > x) mx[u] = std::max(mx[u], 0ll);for (const auto &[v, w] : e[u]) if (v != fa && mn[u] + mx[v] + w > x) mx[u] = std::max(mx[u], mx[v] + w);if (mx[u] + l > x || (u == 0 && mx[u] != -1e18))p.push_back(u), mn[u] = 0, mx[u] = -1e18;};return dfs(0, -1, 0), p;};LL l = 0, r = 1e15, ans = 0;while (l <= r) {LL mid = l + r >> 1;if (chk(mid).size() <= k) ans = mid, r = mid - 1;else l = mid + 1;}auto s = chk(ans);std::vector<int> vis(n);for (auto x : s) vis[x] = 1;for (int i = 0; i < n && s.size() < k; i++) if (!vis[i])s.push_back(i);printf("%d\n", ans);for (auto x : s) printf("%d ", x + 1);
}