题解:洛谷 P1948 Telephone Lines
【题目来源】
洛谷:P1948 [USACO08JAN] Telephone Lines S - 洛谷
【题目描述】
多年以后,笨笨长大了,成为了电话线布置师。由于地震使得某市的电话线全部损坏,笨笨是负责接到震中市的负责人。该市周围分布着 \(n\)(\(1\le n\le10^3\))根按 \(1\sim n\) 顺序编号的废弃的电话线杆,任意两根线杆之间没有电话线连接,一共有 \(p\)(\(1\le p\le10^4\))对电话杆可以拉电话线。其他的由于地震使得无法连接。
第 \(i\) 对电线杆的两个端点分别是 \(a_i,b_i\),它们的距离为 \(l_i\)(\(1\le l_i\le10^6\))。数据中每对 \((a_i,b_i)\) 只出现一次。编号为 \(1\) 的电话杆已经接入了全国的电话网络,整个市的电话线全都连到了编号 \(n\) 的电话线杆上。也就是说,笨笨的任务仅仅是找一条将 \(1\) 号和 \(n\) 号电线杆连起来的路径,其余的电话杆并不一定要连入电话网络。
电信公司决定支援灾区免费为此市连接 \(k\) (\(1\le k\le p\))对由笨笨指定的电话线杆,对于额外的那些电话线,需要为它们付费,总费用决定于其中最长的电话线的长度(每根电话线仅连接一对电话线杆)。如果需要连接的电话线杆不超过 \(k\) 对,那么支出为 \(0\)。
请你计算一下,将电话线引到震中市最少需要在电话线上花多少钱?
【输入】
输入文件的第一行包含三个数字 \(n,p,k\)。
第二行到第 \(p+1\) 行,每行分别都为三个整数 \(a_i,b_i,l_i\)。
【输出】
一个整数,表示该项工程的最小支出,如果不可能完成则输出 \(-1\)。
【输入样例】
5 7 1
1 2 5
3 1 4
2 4 8
3 2 3
5 2 9
3 4 7
4 5 6
【输出样例】
4
【核心思想】
-
问题分析:给定 \(n\) 个电话杆和 \(p\) 条可连接边,每条边有长度 \(l_i\)。可以免费连接 \(k\) 条边,其余边需付费,总费用为所付边中的最大长度。求连接 \(1\) 号到 \(n\) 号电话杆的最小费用。这是一个二分答案 + 0-1 BFS问题,关键在于将"最小化最大边权"转化为二分判定,用 0-1 BFS 验证给定费用 \(bound\) 下是否能通过不超过 \(k\) 条付费边到达终点。
-
算法选择:
- 整数二分:在边权范围 \([0, 10^6]\) 内二分查找最小可行费用
- 0-1 BFS 验证:对于给定 \(bound\),边权 \(\le bound\) 的边视为免费(代价 \(0\)),边权 \(> bound\) 的边视为付费(代价 \(1\))。用双端队列 BFS 求从 \(1\) 到 \(n\) 的最少付费边数,若 \(\le k\) 则 \(bound\) 可行
-
关键步骤:
- 读取输入:\(n, p, k\) 和 \(p\) 条边 \((a_i, b_i, l_i)\)
- 建图:无向图,链式前向星存储
- 二分查找(\(l = 0\),\(r = 10^6 + 1\)):
- \(mid = (l + r) / 2\)
- 若
check(mid)可行:\(r = mid\) - 否则:\(l = mid + 1\)
- 0-1 BFS 验证(
check(bound)):- 初始化 \(dist[i] = \infty\),\(dist[1] = 0\)
- 起点 \(1\) 入队
- 取出队首 \(t\),遍历邻接边:
- 若 \(w[i] > bound\):\(v = 1\)(付费边),否则 \(v = 0\)(免费边)
- 若 \(dist[j] > dist[t] + v\):更新距离,\(v = 0\) 则队首入队,\(v = 1\) 则队尾入队
- 返回 \(dist[n] \le k\)
- 若 \(r = 10^6 + 1\) 输出 \(-1\)(无解),否则输出 \(r\)
-
时间/空间复杂度:
- 时间复杂度:\(O(\log(10^6) \cdot (n + p))\),二分约 \(20\) 轮,每轮 0-1 BFS 为 \(O(n + p)\)
- 空间复杂度:\(O(n + p)\),邻接表、距离数组和队列
-
二分 + 0-1 BFS 的核心思想:
- 费用转化:将"最大边权最小化"转化为"给定阈值下最少超阈值边数"的判定问题,利用边权的单调性进行二分
- 0-1 边权 BFS:超阈值边代价为 \(1\),不超阈值边代价为 \(0\),双端队列保证 BFS 的正确性(代价 \(0\) 的边优先扩展)
- 判定与优化的分离:二分负责搜索最优值,0-1 BFS 负责验证可行性,两者结合实现高效求解
- 适用于"最小化最大边权/最大化最小边权 + 路径约束"类问题,核心是将原问题转化为可判定的二分问题
【算法标签】
普及+ #SPFA
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 1005; // 最大节点数
const int M = 20005; // 最大边数
int n, m, k; // 节点数、边数、允许的边数限制
int h[N], e[M], ne[M], w[M], idx; // 邻接表存储图
deque<int> q; // 双端队列用于0-1 BFS
int dist[N]; // 存储从起点到各点的最短距离
bool st[N]; // 标记数组,记录节点是否被访问过
/**
* 添加边到邻接表
* @param a 边的起点
* @param b 边的终点
* @param c 边的权重
*/
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
/**
* 检查是否存在路径满足条件
* @param bound 当前检查的边界值
* @return 是否存在满足条件的路径
*/
bool check(int bound)
{
memset(st, 0, sizeof st); // 初始化访问标记
memset(dist, 0x3f, N * 4); // 初始化距离为无穷大
dist[1] = 0; // 起点到自身距离为0
q.push_back(1); // 起点入队
while (q.size()) // 队列不为空时循环
{
int t = q.front(); // 取出队首节点
q.pop_front(); // 弹出队首节点
if (st[t]) continue; // 如果已访问则跳过
st[t] = true; // 标记为已访问
// 遍历所有邻接边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接节点
int v = w[i] > bound; // 判断当前边权重是否超过bound
// 松弛操作
if (dist[j] > dist[t] + v)
{
dist[j] = dist[t] + v; // 更新最短距离
// 根据v的值决定插入队列前端还是后端
if (!v) q.push_front(j);
else q.push_back(j);
}
}
}
// 判断终点距离是否满足条件
return dist[n] <= k;
}
int main()
{
cin >> n >> m >> k; // 输入节点数、边数、限制k
memset(h, -1, sizeof h); // 初始化邻接表
// 输入所有边(无向图)
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
add(b, a, c);
}
// 二分查找最小满足条件的bound
int l = 0, r = 1e6 + 1;
while (l < r)
{
int mid = l + r >> 1; // 取中间值
if (check(mid)) r = mid; // 如果满足条件则缩小右边界
else l = mid + 1; // 否则增大左边界
}
// 处理无解情况
if (r == 1e6 + 1) r = -1;
cout << r << endl; // 输出结果
return 0;
}
【运行结果】
5 7 1
1 2 5
3 1 4
2 4 8
3 2 3
5 2 9
3 4 7
4 5 6
4
浙公网安备 33010602011771号