通过 WQS 二分为白边附加额外权值,利用凸性将恰好 need 条白边的生成树转化为无约束的带权最小生成树。
OJ: roj
题目 ID: 1491
难度:提高
标签:最小生成树WQS二分并查集
创建: 2026-09-30 14:26
更新: 2026-10-07 11:01
形式化题目
给定一个包含
- 树中包含的白色边数恰好为
; - 树上所有边的权值和最小。
保证图中至少存在一棵满足要求的生成树。
正解
思路
1. 朴素瓶颈与凸性观察
直接寻找“恰好包含
设函数
2. WQS 二分(带权二分)转化
既然点集
在现实意义中,给所有的白色边权值统一加上一个附加权值 bonus):
- 当
时,白边变贵,算法在选边时会减少白边的数量; - 当
时,白边变便宜,算法在选边时会增加白边的数量。
对于任意固定的附加权值
因此,我们可以对白边的额外权值
- 如果运行 Kruskal 选出的白边数量
,说明当前白边仍然偏多(或刚好达标),白边还可以更贵一些,我们记录当前偏移量 ,并向右半区间 继续二分; - 如果选出的白边数量
,说明白边太贵了导致选少了,应向左半区间 缩小偏移量。
graph LR
subgraph WQS 二分过程
A["二分白边偏移量 bonus"] --> B["白边权值统一加 bonus"]
B --> C["Kruskal 跑无约束最小生成树"]
C --> D{"白边数量 cnt 与 need 比较"}
D -- "cnt >= need" --> E["记录候选 bonus,尝试增大 bonus"]
D -- "cnt < need" --> F["减小 bonus 使得白边更优先"]
end
3. 共线处理与答案还原
在实际计算中,可能存在多个点在凸包上共线(即存在某个斜率
为了保证二分收敛与截距正确:
- 我们在 Kruskal 排序/选边策略中约定:当黑边与加权后的白边权值相同时,优先选取白边。
- 这样,在共线段上我们总会贪心选取最右侧的端点(即白边数最大),保证只要线段覆盖了
,就能判定出 。 - 关键在于:共线段上所有点在切线下的截距都是相同的常数
。因此,哪怕最终生成树选取的白边数 ,原问题的答案依然可以直接通过截距还原:
4. 双指针归并优化
如果在每次二分判定中都对所有
代码
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-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
# 边信息类型:(u, v, weight)
Edge = tuple[int, int, int]
def kruskal(n: int, whites: list[Edge], blacks: list[Edge], bonus: int) -> tuple[int, int]:
"""计算给白边增加 bonus 权值后的最小生成树白边数与总权值。
同权时优先选白边,便于二分出满足 need 条白边的最小额外偏移。
"""
parent = list(range(n))
def find(x: int) -> int:
curr = x
while parent[curr] != curr:
curr = parent[curr]
while x != curr:
parent[x], x = curr, parent[x]
return curr
# 双指针归并白边(带 bonus)与黑边,因两组各自有序,归并只需 O(E)
total_weight = white_cnt = chosen = 0
w_idx = b_idx = 0
w_len, b_len = len(whites), len(blacks)
while chosen < n - 1 and (w_idx < w_len or b_idx < b_len):
take_white = (
b_idx >= b_len
or (
w_idx < w_len
and whites[w_idx][2] + bonus <= blacks[b_idx][2] # 权值相同时白边优先
)
)
u, v, w, is_white = (
(whites[w_idx][0], whites[w_idx][1], whites[w_idx][2] + bonus, 1)
if take_white
else (blacks[b_idx][0], blacks[b_idx][1], blacks[b_idx][2], 0)
)
if take_white:
w_idx += 1
else:
b_idx += 1
ru, rv = find(u), find(v)
if ru != rv:
parent[ru] = rv
total_weight += w
white_cnt += is_white
chosen += 1
return white_cnt, total_weight
def solve() -> None:
raw = sys.stdin.buffer.read().split()
if not raw:
return
data = iter(raw)
n, m, need = int(next(data)), int(next(data)), int(next(data))
whites: list[Edge] = []
blacks: list[Edge] = []
for _ in range(m):
u, v, c, col = int(next(data)), int(next(data)), int(next(data)), int(next(data))
if col == 0:
whites.append((u, v, c))
else:
blacks.append((u, v, c))
whites.sort(key=lambda e: e[2])
blacks.sort(key=lambda e: e[2])
# WQS 二分 / 带权二分:二分白边额外权值 bonus ∈ [-105, 105]
low, high = -105, 105
best_bonus = 0
while low <= high:
mid = (low + high) // 2
cnt, _ = kruskal(n, whites, blacks, mid)
if cnt >= need: # 白边选多了或恰好,说明 bonus 还可以更大(惩罚不够或恰好)
best_bonus = mid
low = mid + 1
else:
high = mid - 1
_, total = kruskal(n, whites, blacks, best_bonus)
# 真实最优生成树权值和 = 调整后的生成树权值和 - need * best_bonus
ans = total - need * best_bonus
print(ans)
if __name__ == "__main__":
solve()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-10-05 04:11
* update_at: 2026-10-05 04:11
*/
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int MAXE = 100005; // 边数上限
const int MAXN = 50005; // 点数上限
struct Edge {
int u;
int v;
int w;
};
int n, m;
ll need; // 生成树中必须包含的白边条数
Edge whites[MAXE]; // 白边列表,按边权升序
Edge blacks[MAXE]; // 黑边列表,按边权升序
int white_num; // 白边条数
int black_num; // 黑边条数
int fa[MAXN]; // 并查集父节点
// 边按权值升序,用于提前把白边、黑边各自排好序
bool cmp_edge(const Edge &a, const Edge &b) {
return a.w < b.w;
}
// 并查集查找,带路径压缩
int find_root(int x) {
while (fa[x] != x) {
fa[x] = fa[fa[x]];
x = fa[x];
}
return x;
}
// 给每条白边额外附加权值 bonus 后跑一次无约束 Kruskal。
// 白边、黑边两组已各自有序,用双指针归并成整体有序,单次只需 O(E)。
// 返回选入的白边条数,adjusted_weight 带回调整后的生成树总权值。
int kruskal(ll bonus, ll &adjusted_weight) {
for (int i = 0; i < n; i++) {
fa[i] = i;
}
int chosen = 0; // 已选入生成树的边数
int white_used = 0; // 已选入的白边条数
ll total = 0; // 调整后的生成树总权值
int wi = 0; // 白边指针
int bi = 0; // 黑边指针
while (chosen < n - 1 && (wi < white_num || bi < black_num)) {
// 白边用尽时只能取黑边;白边权值不超过黑边时取白边
// 权值相同时固定优先取白边,保证二分的单调性
bool take_white = true;
if (wi >= white_num) {
take_white = false;
} else if (bi < black_num && blacks[bi].w < whites[wi].w + bonus) {
take_white = false;
}
int u, v;
ll w;
if (take_white) {
u = whites[wi].u;
v = whites[wi].v;
w = whites[wi].w + bonus;
wi++;
} else {
u = blacks[bi].u;
v = blacks[bi].v;
w = blacks[bi].w;
bi++;
}
int ru = find_root(u);
int rv = find_root(v);
if (ru == rv) {
continue; // 两端已连通,这条边舍弃
}
fa[ru] = rv;
total += w;
chosen++;
if (take_white) {
white_used++;
}
}
adjusted_weight = total;
return white_used;
}
int main() {
scanf("%d %d %lld", &n, &m, &need);
white_num = 0;
black_num = 0;
for (int i = 0; i < m; i++) {
int s, t, c, col;
scanf("%d %d %d %d", &s, &t, &c, &col);
if (col == 0) {
whites[white_num].u = s;
whites[white_num].v = t;
whites[white_num].w = c;
white_num++;
} else {
blacks[black_num].u = s;
blacks[black_num].v = t;
blacks[black_num].w = c;
black_num++;
}
}
// 白边权值只比黑边大/小有限范围,一次排序后二分内部无需再排序
sort(whites, whites + white_num, cmp_edge);
sort(blacks, blacks + black_num, cmp_edge);
// WQS 二分:找“选出的白边条数 >= need”的最大附加权值 best_bonus
int low = -105;
int high = 105;
int best_bonus = 0;
ll dummy = 0;
while (low <= high) {
int mid = low + (high - low) / 2;
int cnt = kruskal(mid, dummy);
if (cnt >= need) {
best_bonus = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
// 用 best_bonus 再跑一次,截距还原出恰好 need 条白边的最小权值和
ll adjusted = 0;
kruskal(best_bonus, adjusted);
ll ans = adjusted - need * best_bonus;
printf("%lld\n", ans);
return 0;
}复杂度
- 时间复杂度:
- 预处理对白边和黑边各自排序:
; - 二分区间长度为
,总迭代次数约为 次; - 每次迭代双指针扫描归并
,并查集操作 ; - 整体时间复杂度为
,运行极为迅速。
- 预处理对白边和黑边各自排序:
- 空间复杂度:
- 存储白边与黑边列表需
空间; - 并查集父节点数组需
空间; - 整体空间复杂度为
。
- 存储白边与黑边列表需
总结
本题是 WQS 二分 / 带权二分(Aliens Trick) 的典中典。它将带数量限制的图论优化问题转化为无约束的最优化问题,其本质利用了目标函数关于限制参数的凸性,通过二分拉格朗日乘子(斜率)完成降维求解。同时,预先分流排序配合二分内部线性双指针归并,大幅降低了时间常数。