二分最大允许造价 T。对每个 T,只看二级造价不超过 T 的边是否能连通全图,再看一级造价不超过 T 的边最多能提供多少条一级公路,从而判断可行性。
OJ: luogu
题目 ID: P2323
难度:省选/NOI-
标签:图论二分答案最小生成树并查集贪心
日期: 2026-06-20 01:23
题意
有
每条候选公路有两种修法:
- 一级公路,花费
- 二级公路,花费
并且
你要选出
- 其中至少有
条是一级公路 - 所选公路里,花费最大的那一条尽量小
最后输出这个最小可能的“最大花费”,以及一组对应方案。
思路
先看一个小数据暴力:
// brute.cpp:小数据枚举所有生成树,再枚举一级/二级公路分配。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10;
const int MAXM = 20;
const int INF = 1e9;
struct Edge {
int id;
int u, v;
int c1, c2;
} edges[MAXM];
struct AnswerEdge {
int id;
int level;
};
int n, need_level1, m_input;
int edge_cnt;
int picked[MAXM];
int fa[MAXN];
int best_cost = INF;
vector<AnswerEdge> best_answer;
void init_dsu() {
for (int i = 1; i <= n; i++) {
fa[i] = i;
}
}
int find_root(int x) {
if (fa[x] == x) {
return x;
}
fa[x] = find_root(fa[x]);
return fa[x];
}
bool unite(int x, int y) {
x = find_root(x);
y = find_root(y);
if (x == y) {
return false;
}
fa[x] = y;
return true;
}
void update_answer(const vector<AnswerEdge> &answer, int cost) {
vector<AnswerEdge> cur = answer;
sort(cur.begin(), cur.end(), [](const AnswerEdge &a, const AnswerEdge &b) {
return a.id < b.id;
});
if (cost < best_cost) {
best_cost = cost;
best_answer = cur;
}
}
void enumerate_types(int pos, int cnt_level1, int cur_cost, vector<AnswerEdge> &answer) {
if (pos > n - 1) {
if (cnt_level1 >= need_level1) {
update_answer(answer, cur_cost);
}
return;
}
Edge &e = edges[picked[pos]];
answer.push_back({e.id, 1});
enumerate_types(pos + 1, cnt_level1 + 1, max(cur_cost, e.c1), answer);
answer.pop_back();
answer.push_back({e.id, 2});
enumerate_types(pos + 1, cnt_level1, max(cur_cost, e.c2), answer);
answer.pop_back();
}
void check_tree(int picked_cnt) {
if (picked_cnt != n - 1) {
return;
}
init_dsu();
for (int i = 1; i <= picked_cnt; i++) {
Edge &e = edges[picked[i]];
if (!unite(e.u, e.v)) {
return;
}
}
int root = find_root(1);
for (int i = 2; i <= n; i++) {
if (find_root(i) != root) {
return;
}
}
vector<AnswerEdge> answer;
enumerate_types(1, 0, 0, answer);
}
void dfs(int pos, int picked_cnt) {
if (picked_cnt > n - 1) {
return;
}
if (pos > edge_cnt) {
check_tree(picked_cnt);
return;
}
if (picked_cnt + (edge_cnt - pos + 1) < n - 1) {
return;
}
picked[picked_cnt + 1] = pos;
dfs(pos + 1, picked_cnt + 1);
dfs(pos + 1, picked_cnt);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> need_level1 >> m_input;
edge_cnt = 0;
int a, b, c1, c2;
while (cin >> a >> b >> c1 >> c2) {
edge_cnt++;
edges[edge_cnt] = {edge_cnt, a, b, c1, c2};
}
dfs(1, 0);
if (best_answer.empty()) {
cout << -1 << '\n';
return 0;
}
cout << best_cost << '\n';
for (AnswerEdge e : best_answer) {
cout << e.id << ' ' << e.level << '\n';
}
return 0;
}暴力直接枚举生成树,再枚举每条边修一级还是二级:
- 只保留至少有
条一级公路的方案 - 计算这组方案里的最大边权
- 取最小值
这个思路按定义完全正确,但显然只适合很小数据。
关键在于:目标不是最小化总花费,而是最小化所选边中的最大花费。
这类题的标准做法就是二分答案。
设我们猜测答案是
是否存在一棵生成树,使得每条被选中的边费用都不超过
,并且至少有 条一级公路?
对一条边 (u,v,c1,c2) 来说:
- 如果
,那么它至少可以按二级公路使用 - 如果
,那么它还可以按一级公路使用
于是把边分成两类:
- 可用边:
- 可作为一级公路的边:
先看“能不能连成树”:
- 只要所有可用边能把全图连通,就至少存在一棵生成树所有边费用都不超过
再看“最多能有多少条一级公路”:
- 把所有
的边单独拿出来看,它们会形成若干个连通块 - 在这些边内部,一棵极大生成森林最多能选出
条一级公路,其中 cnt是这些点集的连通块个数
这是上界,而且可达到:
- 先把所有一级可行边尽量选成一片森林
- 再用普通可用边把这些块接起来
所以对于给定的
的边能连通全图 的边最多能提供的一级公路数量不少于
这两个条件都能用并查集线性检查,因此整体可以二分
二分出最小可行
- 先用所有
的边做一片极大森林,并把这些边都当作一级公路 - 再用
的边把整张图补成生成树,这些边作为二级公路
因为极大森林已经保证一级公路数量尽量多,所以只要
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
const int MAXM = 20005;
struct Edge {
int id;
int u, v;
int c1, c2;
} edges[MAXM];
struct AnswerEdge {
int id;
int level;
};
int n, need_level1, m_input;
int edge_cnt;
int fa1[MAXN], fa2[MAXN], fa_build[MAXN];
void init_dsu(int fa[]) {
for (int i = 1; i <= n; i++) {
fa[i] = i;
}
}
int find_root(int fa[], int x) {
if (fa[x] == x) {
return x;
}
fa[x] = find_root(fa, fa[x]);
return fa[x];
}
bool unite(int fa[], int x, int y) {
x = find_root(fa, x);
y = find_root(fa, y);
if (x == y) {
return false;
}
fa[x] = y;
return true;
}
bool feasible(int limit_cost) {
init_dsu(fa1);
init_dsu(fa2);
int comp_all = n;
int comp_level1 = n;
for (int i = 1; i <= edge_cnt; i++) {
if (edges[i].c2 <= limit_cost && unite(fa1, edges[i].u, edges[i].v)) {
comp_all--;
}
if (edges[i].c1 <= limit_cost && unite(fa2, edges[i].u, edges[i].v)) {
comp_level1--;
}
}
int max_level1_edges = n - comp_level1;
return comp_all == 1 && max_level1_edges >= need_level1;
}
vector<AnswerEdge> build_answer(int limit_cost) {
vector<AnswerEdge> answer;
init_dsu(fa_build);
// 先尽量加入所有能加入的一级公路,得到一级公路子图的极大生成森林。
for (int i = 1; i <= edge_cnt; i++) {
if (edges[i].c1 > limit_cost) {
continue;
}
if (unite(fa_build, edges[i].u, edges[i].v)) {
answer.push_back({edges[i].id, 1});
}
}
// 再用二级可行的边把各个一级公路连通块接起来。
for (int i = 1; i <= edge_cnt; i++) {
if ((int)answer.size() == n - 1) {
break;
}
if (edges[i].c2 > limit_cost) {
continue;
}
if (unite(fa_build, edges[i].u, edges[i].v)) {
answer.push_back({edges[i].id, 2});
}
}
sort(answer.begin(), answer.end(), [](const AnswerEdge &a, const AnswerEdge &b) {
return a.id < b.id;
});
return answer;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> need_level1 >> m_input;
edge_cnt = 0;
vector<int> values;
int a, b, c1, c2;
while (cin >> a >> b >> c1 >> c2) {
edge_cnt++;
edges[edge_cnt] = {edge_cnt, a, b, c1, c2};
values.push_back(c1);
values.push_back(c2);
}
if (edge_cnt == 0) {
cout << -1 << '\n';
return 0;
}
sort(values.begin(), values.end());
values.erase(unique(values.begin(), values.end()), values.end());
if (!feasible(values.back())) {
cout << -1 << '\n';
return 0;
}
int left = 0;
int right = (int)values.size() - 1;
while (left < right) {
int mid = (left + right) >> 1;
if (feasible(values[mid])) {
right = mid;
} else {
left = mid + 1;
}
}
int best = values[left];
vector<AnswerEdge> answer = build_answer(best);
cout << best << '\n';
for (AnswerEdge e : answer) {
cout << e.id << ' ' << e.level << '\n';
}
return 0;
}复杂度
设实际读到的候选公路数为
每次二分检查:
- 扫一遍边做两个并查集,复杂度
二分次数是
总复杂度可以写成:
空间复杂度
总结
这题最关键的一步,是把“至少
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
