二分答案最大边权,将大于阈值的边赋权为 1、其余赋权为 0,通过 0-1 BFS 判定不超过 K 条长边的最短路径可行性。
OJ: roj
题目 ID: 1496
难度:普及+/提高-
标签:二分最短路0-1 BFS
创建: 2026-09-30 14:38
更新: 2026-10-07 11:01
形式化题目
给定一个无向带权图
求一条从顶点
样例解释
对于输入样例:
- 节点数
,边数 ,免费边数 。 - 选择路径
: - 边
权值为 ; - 边
权值为 ; - 边
权值为 。
- 边
- 允许免费升级
条边,指定最贵的边 (花费 )免费; - 剩余边为
(花费 )与 (花费 ),最大花费为 。 - 若改选路径
: - 边包含
权值 、 权值 、 权值 、 权值 。 - 免费一条边仍无法将最大花费压到
。
- 边包含
- 重新审视更优路径:选择路径
: - 边
权值为 ; - 边
权值为 ; - 边
权值为 (免费该边); - 边
权值为 。此时剩余边最大值为 。
- 边
- 实际上选择路径
时也可以免费边 吗?不是。 - 观察更优路径:
(边权 ),免费 ,花费 。 - 观察路径
: 若选择路径 ; 是否存在一条路径使得除 1 条边外其余边均 ? 检查 (权值 4),接下来从 无法只用 的边走到 且只有一条 的边: 路径 (两条边权均 ),再从 到 (权值 ),在此路径上权值分别为 。 将最长的边 (权值 )免费掉,剩余边权最大值为 。 因此最小花费为 。
graph LR
1((1)) ---|4| 3((3))
3 ---|3| 2((2))
2 ---|9 [免费]| 5((5))
classDef free stroke:#f00,stroke-width:2px,stroke-dasharray: 5 5;
正解
思路
要求最小化路径上“除去最大的
-
单调性分析: 假设所求的花费上限为
。如果存在一条从 到 的路径,在其上权值大于 的边数不超过 条,那么我们只要将这些大于 的边指定为免费,其余边的花费均不超过 ,总花费便不超过 。 显然,当 增大时,满足条件的路径只会更多不会更少。因此判定结果关于 具有单调性,可以二分答案 。 -
判定问题转化(0-1 赋权与最短路): 对于固定的阈值
: - 若某条边的边权
,说明无需占用免费名额,权重记为 ; - 若某条边的边权
,说明必须消耗一次免费名额,权重记为 。
此时,问题转化为:在边权只有
和 的新图上,求从起点 到终点 的最短路径长度 。 - 若
,说明可以通过不超过 次免费操作使得最大边权不超过 ,该 可行; - 若
,说明至少需要消耗超过 次免费机会,该 不可行。
- 若某条边的边权
-
0-1 BFS 高效求解: 由于新图中边权只有
和 ,无需使用带 复杂度的 Dijkstra 堆优化,直接使用双端队列( deque)进行 0-1 BFS:- 转移边权为
时,将新节点压入队头( appendleft); - 转移边权为
时,将新节点压入队尾( append)。 单次判定时间复杂度仅为。
- 转移边权为
-
无解处理: 若原图上起点
与终点 根本不连通,无论多少免费名额都无法到达,判定直接输出 。
代码
Python 版:
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-09-30 14:45
# update_at: 2026-09-30 14:45
import sys
from collections import deque
INF = 10**9
def min_cost_edges(graph: list[list[tuple[int, int]]], threshold: int, n: int) -> int:
"""计算从 1 到 n 路径上边权大于 threshold 的最少边数(0-1 BFS)。"""
dist = [INF] * (n + 1)
dist[1] = 0
dq = deque([1])
while dq:
u = dq.popleft()
if u == n:
break
d = dist[u]
for v, w in graph[u]:
cost = 1 if w > threshold else 0
if d + cost < dist[v]:
dist[v] = d + cost
if cost == 0:
dq.appendleft(v)
else:
dq.append(v)
return dist[n]
def solve() -> None:
tokens = sys.stdin.buffer.read().split()
if not tokens:
return
data = iter(map(int, tokens))
n = next(data)
p = next(data)
k = next(data)
graph: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
max_len = 0
for _ in range(p):
u, v, w = next(data), next(data), next(data)
graph[u].append((v, w))
graph[v].append((u, w))
if w > max_len:
max_len = w
# 若 1 与 n 之间连通性不足,直接判 -1
if min_cost_edges(graph, INF, n) == INF:
print(-1)
return
# 二分寻找最小的阈值 mid,使得大于 mid 的边数 <= k
low, high, ans = 0, max_len, max_len
while low <= high:
mid = (low + high) // 2
if min_cost_edges(graph, mid, n) <= k:
ans = mid
high = mid - 1
else:
low = mid + 1
print(ans)
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* 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:26
* update_at: 2026-10-05 04:26
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1005; // 基站数上限(N <= 1000)
const int MAXM = 20005; // 无向边,正向反向各存一条(P <= 10000)
const ll INF = 1e18;
// 题目数据
int n, p, k;
// 链式前向星存无向图
int head[MAXN], nxt[MAXM], to[MAXM], edge_cnt;
ll w[MAXM]; // w[i] 为第 i 条边的升级花费
ll dist[MAXN]; // dist[u]:1 到 u 路径上边权大于阈值的边的最少条数(0-1 BFS 距离)
// 加一条 u <-> v、花费 c 的无向边。
void add_edge(int u, int v, ll c) {
edge_cnt++;
to[edge_cnt] = v;
w[edge_cnt] = c;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// 0-1 BFS:判定阈值 limit,返回 1 到 n 路径上权值大于 limit 的边的最少条数;
// 不可达返回 INF。
ll bfs01(ll limit) {
for (int i = 1; i <= n; i++) dist[i] = INF;
dist[1] = 0;
deque<int> dq; // 0-1 BFS 的双端队列:0 边进队头,1 边进队尾
dq.push_back(1);
while (!dq.empty()) {
int u = dq.front();
dq.pop_front();
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
// 边权不超过阈值的花 0 个免费名额,超过阈值的花 1 个
ll cost = (w[i] > limit) ? 1 : 0;
if (dist[u] + cost < dist[v]) {
dist[v] = dist[u] + cost;
if (cost == 0) dq.push_front(v);
else dq.push_back(v);
}
}
}
return dist[n];
}
void read_input() {
cin >> n >> p >> k;
edge_cnt = 0;
for (int i = 1; i <= p; i++) {
int a, b;
ll l;
cin >> a >> b >> l;
add_edge(a, b, l);
add_edge(b, a, l);
}
}
void solve() {
// 先判连通性:阈值取极大值时所有边花费都是 0,等价于普通 BFS
if (bfs01(INF) == INF) {
cout << -1 << endl;
return;
}
// 找到最大的边权作为二分上界
ll max_len = 0;
for (int i = 1; i <= edge_cnt; i++) {
if (w[i] > max_len) max_len = w[i];
}
// 二分答案:最小的阈值 mid,使得路径上大于 mid 的边数不超过 k
ll low = 0, high = max_len, ans = max_len;
while (low <= high) {
ll mid = (low + high) / 2;
if (bfs01(mid) <= k) {
ans = mid; // mid 可行,尝试更小的花费
high = mid - 1;
} else {
low = mid + 1;
}
}
cout << ans << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:
- 二分区间为
,二分判定次数为 ,在 时约 20 次。 - 每次判定运行一次 0-1 BFS,时间复杂度为
。 - 总体时间复杂度为
。当 时,总操作次数在 左右,可在几十毫秒内完成。
- 二分区间为
- 空间复杂度:
,用于存储图的邻接表、距离数组以及双端队列。
总结
本题是「二分答案 + 0-1 BFS」的经典模型:
- 将复杂的最优费用函数(第
大边权)转化为可行性判定; - 将“阈值以上/以下”映射为图上的“0/1 权值”,将判定问题转化为边权为 0/1 的单源最短路;
- 利用双端队列 0-1 BFS 将每次判定的复杂度优化到线性
。