【JEOI-R1】树的上色 题解
题意简述
树上有两个黑点,在每个单位时间内,每个黑点可以把自己相邻的一个白点变为黑色,求把整棵树所有点变为黑色的最短时间。
解题思路
看到这道题的第一反应是:P2018。(可以看看这道题)
先来一个简化版问题:
树上有一个黑点,在每个单位时间内,每个黑点可以把自己相邻的一个白点变为黑色,求把整棵树所有点变为黑色的最短时间。
设
对于叶子结点,
对于非叶子结点,当仅有
引理
按照
证明
该贪心思路可由“邻项交换”证明。
设
若先对
假设交换顺序:先对
因此该证明变为比较:(前面的是交换前,后面的是交换后)
同时减去
因为
由
因此可得:
即
所以可知,在交换前更优,即要先染
证毕。
根据引理可知,对于非叶子结点,
至此,我们完成了简化版问题的求解。
回到本题,本题题目中是先把两个节点染成了黑色,我们需要把问题转化为上面的简化版问题。
(这里可以先思考一下在往下看)
方法:找到两个黑点间的所有树边,枚举选择其中一条树边断开,分为两颗子树,两个子树的根节点分别为两个黑点,时间复杂度
所以我们枚举改为
对于二分,我们需要进行单调性证明。
分别设两个黑点的子树为
由于是一颗树形结构,所以两个黑点间的路径是唯一的。
对于断边操作,相当于分别给子树
由于
随着选择的边远离
IL bool check(reg int p) {
vis[s[p]] = vis[s[p] ^ 1] = 1;
solve(a), solve(b);
vis[s[p]] = vis[s[p] ^ 1] = 0;
return f[a] >= f[b];
}
另一种理解
我们可以直接建立一个
部分分
(部分分还是很足的,优化的暴力大概
| 子任务编号 | 测试点 | 特殊性质 | 解法 | |
|---|---|---|---|---|
| 无 | 暴力枚举/搜索即可 | |||
| 无 | 暴力枚举/搜索 + 剪枝 | |||
| 分析链的答案性质,推导 |
||||
| 无 | 上文所述的枚举选择其中一条树边断开 | |||
| 无需枚举,分别求解两颗子树即可 | ||||
| 无 | 正解 |
特殊性质
特殊性质
代码
std:
#include <bits/stdc++.h>
#define reg register
#define IL inline
#define N 500500
IL int read() {
reg int x = 0;
reg char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
return x;
}
int n, a, b;
int first[N], nxt[N + N], to[N + N], num = 1;
IL void add(reg int u, reg int v) { nxt[++num] = first[u], to[num] = v, first[u] = num; }
IL void adde(reg int u, reg int v) { add(u, v), add(v, u); }
int in[N], s[N], tot;
void dfs(reg int u, reg int fa = 0) {
for (reg int i = first[u], v; i; i = nxt[i])
if ((v = to[i]) ^ fa) //不走父亲节点
in[v] = i, dfs(v, u);
}
bool vis[N + N];
std::vector<int> g[N];
int f[N];
IL void cmax(reg int &x, reg int y) { x < y ? x = y : 0; } //注意这里是引用
void solve(reg int u, reg int fa = 0) { //对子树 u 的 f 值进行递归计算
g[u].clear(), f[u] = 0;
for (reg int i = first[u], v; i; i = nxt[i])
if (!vis[i] && (v = to[i]) ^ fa)
solve(v, u), g[u].push_back(-f[v]);
std::sort(g[u].begin(), g[u].end()); //对子树的 f 值进行排序
for (reg int i = 0; i < g[u].size(); ++i) cmax(f[u], i + 1 - g[u][i]);
}
IL bool check(reg int p) {
vis[s[p]] = vis[s[p] ^ 1] = 1; //标记边断开
solve(a), solve(b); //分别计算两颗子树
vis[s[p]] = vis[s[p] ^ 1] = 0; //清除标记
return f[a] >= f[b];
}
int main() {
n = read(), a = read(), b = read();
for (reg int i = n; --i; adde(read(), read())) //读入,建双边
;
dfs(a); //从a点dfs建立有向图(树)
for (reg int x = b; x; s[++tot] = in[x], x = to[in[x] ^ 1]) //反向从b点走到a,记录他们之间的边
;
reg int l = 1, r = tot, mid, v1 = 2e9, v2 = 2e9;
while (l <= r) { //二分断开的边
mid = l + r >> 1;
if (check(mid))
l = mid + 1;
else
r = mid - 1;
}
if (l <= tot) {
vis[s[l]] = vis[s[l] ^ 1] = 1;
solve(a), solve(b), v1 = f[b];
vis[s[l]] = vis[s[l] ^ 1] = 0;
}
if (l > 1) {
--l;
vis[s[l]] = vis[s[l] ^ 1] = 1;
solve(a), solve(b), v2 = f[a];
vis[s[l]] = vis[s[l] ^ 1] = 0;
}
return printf("%d", v1 < v2 ? v1 : v2), 0; //输出最小值
}