形式化题目
给定一棵以
- 根
随时可染; - 除根外,点
被染色之前,它的父亲 必须已经染过色,即 。
若点
最小。
原题把权值直接叫「权值」,下文沿用。
暴力解法
思路
每次在
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-11-10 09:55
* update_at: 2026-11-10 09:55
*/
// brute.cpp:小数据暴力解,枚举所有满足「父亲先染色」的染色顺序,取最小代价。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 12;
int n, root;
ll w[maxn];
int fa[maxn];
bool used[maxn]; // 是否已染色
ll best;
void dfs(int dep, ll cost) {
if (dep > n) {
best = min(best, cost);
return;
}
for (int i = 1; i <= n; i ++ ) {
if (used[i]) continue;
if (i != root && !used[fa[i]]) continue; // 父亲必须先染色
used[i] = true;
dfs(dep + 1, cost + (ll)dep * w[i]);
used[i] = false;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n >> root) {
if (n == 0 && root == 0) break;
for (int i = 1; i <= n; i ++ ) cin >> w[i], used[i] = false;
for (int i = 1, a, b; i < n; i ++ ) {
cin >> a >> b;
fa[b] = a;
}
best = LLONG_MAX;
dfs(1, 0);
cout << best << '\n';
}
return 0;
}复杂度
时间
瓶颈
搜索把「选谁」和「约束」混在一棵巨大的搜索树里,
正解
思路
先看没有「父亲先染」限制的版本:每轮可任意选点,那就是把
关键性质:加上限制后,权值最大的那个(非根的)点
合并与等效权值:既然必须相邻,就可以把
- 若顺序为
:块 内每个点相对于自身内部排位被延后了 个时刻,产生额外代价 ; - 若顺序为
:块 内每个点被延后 个时刻,产生额外代价 。
先排
代价计算:贡献法动态累加(无需带权并查集): 许多题解会用带权并查集去维护每个点的绝对染色时刻,最后再统一求和,这把问题不必要地复杂化了。实际上可以直接用贡献法在合并时动态结算代价:
- 初始基准代价:假设所有点都在第 1 个时刻染色,基准总代价为:
- 合并时的延后增量:每一步挑选当前平均权值最大的非根块
,将其绑定在其父节点所在的块 之后。这意味着块 内的所有点在时序上都被整整推迟了 个时刻,因此对总答案产生的确切增量就是: 我们只需在合并时直接累加: - 并查集维护:仅需最普通的并查集维护连通块代表元即可(无需维护任何边权):
进行
用样例(
| 步 | 当前最大平均权值的非根块 |
并入父块 |
产生代价增量 |
累计代价 |
合并后新块信息 |
|---|---|---|---|---|---|
| 1 | |||||
| 2 | |||||
| 3 | |||||
| 4 | 全树合并完毕 |
最终答案为
代码
同一算法的 C++ 与 Python 实现,逻辑一致。
C++ 版本:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-11-10 09:55
* update_at: 2026-11-10 12:10
*/
// MTREECOL - Color a tree
// 贪心:每次挑选等效权值(平均值)最大的非根块,并入其父节点所在的块;
// 贡献法:块 u 接在父块 f 后面时,u 内所有点整体延后 siz[f] 个时刻,代价增量为 siz[f] * w[u]。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1005;
int n, root;
ll w[MAXN]; // 连通块当前的总点权和
int siz[MAXN]; // 连通块当前包含的点数
int fa[MAXN]; // 原树上每个点的父节点
int p[MAXN]; // 普通并查集:定位每个点所属的连通块代表元
int find(int x) {
return p[x] == x ? x : p[x] = find(p[x]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n >> root && (n || root)) {
ll ans = 0;
for (int i = 1; i <= n; i++) {
cin >> w[i];
ans += w[i]; // 初始基准代价:假定所有点都在第 1 个时刻染色
siz[i] = 1;
p[i] = i;
}
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
fa[v] = u;
}
// 不断将当前平均权值最大的非根块并入其父块,共合并 n - 1 次
for (int step = 1; step < n; step++) {
int u = -1;
for (int i = 1; i <= n; i++) {
if (i == root || find(i) != i) continue;
// 十字相乘比较平均值:w[i]/siz[i] > w[u]/siz[u]
if (u == -1 || w[i] * siz[u] > w[u] * siz[i]) {
u = i;
}
}
int f = find(fa[u]);
// 物理意义:块 u 紧随块 f 染色,u 内所有点被延后 siz[f] 个时刻
ans += (ll)siz[f] * w[u];
// 并查集合并
p[u] = f;
siz[f] += siz[u];
w[f] += w[u];
}
cout << ans << "\n";
}
return 0;
}Python 版本:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-11-10 12:15
# update_at: 2026-11-10 12:15
import sys
def solve_case(n: int, root: int, w: list[int], edges: list[tuple[int, int]]) -> int:
"""求树上染色的最小代价(平均权值贪心 + 贡献法延后增量)。"""
fa = [0] * (n + 1)
for u, v in edges:
fa[v] = u
p = list(range(n + 1))
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x])
return p[x]
weight = [0] + w
siz = [1] * (n + 1)
ans = sum(w)
for _ in range(n - 1):
# 挑选平均值最大的非根代表元
best_u = -1
for i in range(1, n + 1):
if i == root or find(i) != i:
continue
if best_u == -1 or weight[i] * siz[best_u] > weight[best_u] * siz[i]:
best_u = i
f = find(fa[best_u])
ans += siz[f] * weight[best_u]
# 并查集合并
p[best_u] = f
siz[f] += siz[best_u]
weight[f] += weight[best_u]
return ans
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
out: list[str] = []
while True:
token = next(data, None)
if token is None:
break
n = int(token)
root = int(next(data))
if n == 0 and root == 0:
break
w = [int(next(data)) for _ in range(n)]
edges = [(int(next(data)), int(next(data))) for _ in range(n - 1)]
out.append(str(solve_case(n, root, w, edges)))
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
共进行
总结
- 等效权值:加上“先染父亲”约束后,最大权值块必然紧随父亲染色,两者可捆绑合并,用平均权值作为等效权值统一排序;
- 贡献法结算:合并块
到父块 后面时,块 的所有元素整体延后 个时刻,代价增量天然就是 ; - 极简实现:无需带权并查集、无需记录具体染色序列、无需浮点数,普通并查集 + 整数十字相乘即可优雅求解。