最开始想的是贪心瞎搞,拿到了 52pts 后以为有前途,结果一看讨论区发现假了。

正解是树形dp吼。

可以先看一下 洛谷的第一篇题解。写的很好,但是他写的一些东西我开始看不懂。因此在此记录一些问题与我的理解。

#include <bits/stdc++.h>
#define rep(i, a, b) for(register int i = a; i <= b; ++i)
using namespace std;
constexpr int N = 3e5 + 5;
bool vis[N];
int n, u, v, L, R, father[N], son[N], dp[N];
vector<int> e[N];
void check(int s, int k) {
	dp[s] = son[s] - k;
	for(const register int to : e[s]) {
		if(to == father[s]) continue;
		check(to, k);
		dp[s] += max(dp[to], 0);
	}	
}
void dfs(int s, int fa) {
	father[s] = fa;
	for(const register int to : e[s]) {
		if(to == fa) continue;
		++son[s];
		dfs(to, s);
	}
}
int main() {
	
	cin >> n;
	rep(i, 1, n - 1) {
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1, 0);
	L = 0, R = n + 1;
	while(L <= R) {
		int mid = L + R >> 1;
		check(1, mid);
		if(dp[1] <= 0) {
			R = mid - 1;
		}	
		else {
			L = mid + 1;
		}
	}
	cout << L;
	return 0;
}

\(dp_i\) 的含义是染黑 \(i\)

然后这里就分两种情况。如果一个节点的 \(dp\)

这就是check部分为什么不能这么写。

void check(int s, int k) {
	dp[s] = max(son[s] - k, 0);//dp_s即使是负的也有意义,因此错误
	for(const register int to : e[s]) {
		if(to == father[s]) continue;
		check(to, k);
		dp[s] += dp[to];
	}	
}

然后是关于贪心为啥不行。我的贪心代码如下。

#include <bits/stdc++.h>
#define rep(i, a, b) for(register int i = a; i <= b; ++i)
using namespace std;
constexpr int N = 3e5 + 5;
bool vis[N];
int n, u, v, L, R, dep[N], father[N], sum[N], son[N];
vector<int> e[N];
inline bool check(int s, int k) {
	bool ans = true;
	if(k * dep[s] < sum[s] + son[s]) {
		ans = false;
	}
	for(const register int to : e[s]) {
		if(to == father[s]) continue;
		if(!check(to, k)) {
			ans = false;
		}
	}
	return ans;
}
void dfs(int s, int fa) {
	father[s] = fa;
	dep[s] = dep[fa] + 1;
	for(const register int to : e[s]) {
		if(to == fa) continue;
		++son[s];
	}
	for(register int to : e[s]) {
		if(to == fa) continue;
		sum[to] = sum[s] + son[s];
		dfs(to, s);
	}
}
int main() {
	cin >> n;
	rep(i, 1, n - 1) {
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1, 0);
	L = 0, R = n + 1;
	while(L <= R) {
		int mid = L + R >> 1;
		if(check(1, mid)) {
			R = mid - 1;
		}	
		else {
			L = mid + 1;
		}
	}
	cout << L;
	return 0;
}

疑似还挺对的,但其实一点也不对。hack 数据如下。

10
1 2
2 3
2 4
3 5
3 6
3 7
4 8
4 9
4 10

贪心的思路是对于每个节点,根据深度判断已经可以染黑多少节点,然后这一路过来一共要染黑多少节点之后判断大小。

然而,由于是涂完色再走,你完全无法确定对面会怎么走。更具体的说,两种路径有可能有一个前缀是一样的,但是染色方法是完全不同的。这就导致你完全无法判断对面要怎么走,也不知道如何染色。