\(AcWing\) \(340\). 通信线路
一、题目描述
在郊区有 \(N\) 座通信基站,\(P\) 条 双向 电缆,第 \(i\) 条电缆连接基站 \(A_i\) 和 \(B_i\)。
特别地,\(1\)号基站是通信公司的总站 (起点),\(N\)号基站 (终点) 位于一座农场中。
现在,农场主希望对通信线路进行升级,其中升级第 \(i\) 条电缆需要花费 \(L_i\)
电话公司正在举行优惠活动
农产主可以指定一条从 \(1\) 号基站到 \(N\) 号基站的路径,并指定路径上不超过 \(K\) 条电缆,由电话公司 免费 提供升级服务
农场主只需要支付在该路径上 剩余的电缆中,升级价格最贵 的那条电缆的花费即可
求 至少用多少钱 可以完成升级
输入格式
第 \(1\) 行:三个整数 \(N,P,K\)。
第 \(2..P+1\) 行:第 \(i+1\) 行包含三个整数 \(A_i,B_i,L_i\)。
输出格式
包含一个整数表示最少花费。
若 \(1\) 号基站与 \(N\) 号基站之间不存在路径,则输出 \(−1\)。
二、题目解析
理解题意:找一条路径,边权最大的\(k\)条边忽略,第\(k + 1\)大的边权作为该条路径的代价,求最小代价
转换:从 起点 到 终点 的所有路径中,第\(k + 1\)大的 边权最小 是多少
三、二分解法【入门解法】
思考下二分一般应用于什么情况下:给一组数,先判断下要找的数在不在左半部分,在就在左半部分继续二分,不在就到右半部分去找
可以用二分取解决的问题要满足两个特性
-
解在一定的范围内,以便确定二分的左右端点
-
给定数据具有一定的单调性
这种单调性既可以是 显性 的,比如有序的数组,也可以是 隐性 的,只要知道中间数 满不满足条件 就可以 确定下一步查找的范围。
\(Q\):既然我们不知道这题的解如何求,那么是否有办法说:假定给我\(x\)元钱,我有没有办法确定这么多钱能否够升级一条路径呢?
\(A\):有办法! 题目给定的\(L\)在\(1\)到\(100w\)间,这说明本题的解一定在\(0\)到\(100w\)之间,否则就是无解,输出\(-1\)。
- 解为\(0\)的情况是这条路径上的边不超过\(k\)条,意味着不用花钱就可以升级线路
- 无解的情况是从起点无法到达终点 : 中间没有路,或者,需要的钱太多,就是减免\(k\)条最长的,剩下的第\(k+1\)条边还是比\(x\)要贵,办不成事
既然本题的解有一定的范围,并且,如果\(x\)元能够升级某条路径,那么解一定不会超过\(x\),这就是 单调性,也就意味着本题可以用 二分 解决
边权只有\(0,1\)的最短路
如何确定\(x\)元钱能否升级一条线路?
给我们一条路径,我们把这条路径上的边权与\(x\)比较,只要大于\(x\)的边不超过\(k\)条就说明可以用不超过\(x\)元去升级这条线路!
继续思考会发现,其实我们不关心这条路径上的每条边的具体权值是多少,只关心其与\(x\)的大小关系,因此整个图上的边就分为两类:
- 边权 大于\(x\)
- 边权不大于\(x\)
大于\(x\)的边我们将其边权视为\(1\),否则边权视为\(0\),只要从起点到终点的 最短路径长度 不超过\(k\),(也就是边权大于\(x\)的路线不超过\(k\)条),说明\(x\)元升级线路是可行的。
双端\(bfs\)
边权只有\(0\)和\(1\)两种情况的最短路问题可以用双端队列\(bfs\)解决,双端队列\(bfs\)相关的问题题解见\(AcWing\) \(175\) 电路维修,解释下双端队列\(BFS\)的思想:
\(dijkstra\)算法的思路是维护一个小根堆,堆顶元素的距离永远是最小的,然后不断取出堆顶元素去松弛周围点的距离。而边权只有\(01\)两种情况的图我们无需维护一个小根堆,只需要维护一个双端队列,保证双端队列的队头元素离起点的距离永远是最小的即可,为此,从起点开始我们将起点加入队列,然后尝试去松弛周围的点,只要周围的点还未出队过,并且可以被松弛,就将该点松弛后加入队列中,边权是\(0\)就加入队头,边权是\(1\)就加入队尾。这就是双端队列\(BFS\)的基本思路。
4.整理思路
-
在\(0\)到\(100w\)间二分答案,每次二分时通过双端队列\(bfs\)的方法判断 起点 到 终点 的 最短路径 是否不超过\(mid\),是的话就在左半部分继续二分,否则在右半部分二分,直到找到答案为止。
-
任意一次\(bfs\)的过程中,一旦发现\(bfs\)完 终点 离 起点 的距离还是 无穷大,说明终点不可达,直接输出\(-1\)终止程序。
二分+双端\(bfs\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1010; // 1000个点
const int M = 20010; // 10000条,记录无向边需要两倍空间
int idx, h[N], e[M], w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int n; //点数
int m; //边数
deque<int> q; //双端队列bfs模拟最短路径
bool st[N]; //记录是不是在队列中
int k; //不超过K条电缆,由电话公司免费提供升级服务
int d[N]; //记录最短距离
bool check(int cost) {
//多次检查,每次初始化
memset(d, 0x3f, sizeof d);
memset(st, false, sizeof st);
// 1号基站是通信公司的总站
q.push_front(1);
d[1] = 0;
while (q.size()) {
int u = q.front();
q.pop_front();
//以后这种continue写法应该优先选择,因为可以使下面的代码减少括号层数
if (st[u]) continue;
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
//如果边权大于二分值,视为1,否则为0,相当于利用最短路求大于cost的边个数
int dist = d[u] + (w[i] > cost);
if (dist < d[j]) {
d[j] = dist;
//大的靠后
if (w[i] > cost)
q.push_back(j);
else
//小的靠前
q.push_front(j);
}
}
}
//如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达
if (d[n] == 0x3f3f3f3f) {
puts("-1"); //不可达,直接输出-1
exit(0);
}
return d[n] <= k;
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m >> k;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
/*这里二分的是直接面对答案设问:最少花费
依题意,最少花费其实是所有可能的路径中,第k+1条边的花费
如果某条路径不存在k+1条边(边数小于k+1),此时花费为0
同时,任意一条边的花费不会大于1e6
整理一下,这里二分枚举的值其实是0 ~ 1e6*/
int l = 0, r = 1e6;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) // check函数的意义:如果当前花费可以满足要求,那么尝试更小的花费
r = mid;
else
l = mid + 1;
}
printf("%d\n", l);
return 0;
}
二分+\(Dijkstra\)
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
const int N = 1010; // 1000个点
const int M = 20010; // 10000条,记录无向边需要两倍空间
int idx, h[N], e[M], w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int n; //点数
int m; //边数
bool st[N]; //记录是不是在队列中
int k; //不超过K条电缆,由电话公司免费提供升级服务
int dist[N]; //记录最短距离
// u指的是我们现在选最小花费
bool check(int x) {
memset(st, false, sizeof st);
memset(dist, 0x3f, sizeof dist);
priority_queue<PII, vector<PII>, greater<PII>> q;
dist[1] = 0;
q.push({0, 1});
while (q.size()) {
PII t = q.top();
q.pop();
int d = t.first, u = t.second;
if (st[u]) continue;
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i], v = w[i] > x; //如果有边比我们现在选的这条边大,那么这条边对方案的贡献为1,反之为0
if (dist[j] > d + v) {
dist[j] = d + v;
q.push({dist[j], j});
}
}
}
//如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达
if (dist[n] == INF) {
puts("-1"); //不可达,直接输出-1
exit(0);
}
return dist[n] <= k; //如果有k+1条边比我们现在这条边大,那么这个升级方案就是不合法的,反之就合法
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m >> k;
int a, b, c;
for (int i = 0; i < m; i++) {
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
/*这里二分的是直接面对答案设问:最少花费
依题意,最少花费其实是所有可能的路径中,第k+1条边的花费
如果某条路径不存在k+1条边(边数小于k+1),此时花费为0
同时,任意一条边的花费不会大于1e6
整理一下,这里二分枚举的值其实是0 ~ 1e6*/
int l = 0, r = 1e6;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) // check函数的意义:如果当前花费可以满足要求,那么尝试更小的花费
r = mid;
else
l = mid + 1;
}
printf("%d\n", l);
return 0;
}
四、分层图解法
本题是一道比较祼的分层图题,只要比标准例题变化了一下,不是求累加路径和,而是求所有路径的最大值,其它都没有变化:
1、直接建图\(K+1\)大法
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10; //只要开不死,就往死里开!
const int INF = 0x3f3f3f3f;
int h[N], e[N], ne[N], idx, w[N];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int n, m, k;
int dist[N];
bool st[N];
void dijkstra() {
memset(dist, 0x3f, sizeof dist);
memset(st, false, sizeof st);
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, 1});
dist[1] = 0;
while (q.size()) {
PII t = q.top();
q.pop();
int u = t.second;
if (st[u]) continue;
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > max(dist[u], w[i])) { //农场主只需要支付在该路径上剩余的电缆中,升级价格最贵的那条电缆的花费即可。
dist[j] = max(dist[u], w[i]);
q.push({dist[j], j});
}
}
}
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m >> k;
while (m--) {
int a, b, c;
cin >> a >> b >> c;
//分层图建图
for (int i = 0; i <= k; i++) { //创建k+1层分层图
add(a + i * n, b + i * n, c), add(b + i * n, a + i * n, c); //无向图
if (i < k) //从第0层开始,到k-1层结束,都需要向下一层建立通道
add(a + i * n, b + (i + 1) * n, 0), add(b + i * n, a + (i + 1) * n, 0);
}
}
dijkstra();
// k+1个层中,都去找t的最短路径,再取最小值,就是答案
int ans = INF;
for (int i = 0; i <= k; i++) ans = min(ans, dist[n + i * n]);
if (ans == INF) ans = -1;
printf("%d\n", ans);
return 0;
}
2、二维数组省空间解法
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
//分层图+二维数组解法
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
const int N = 1010, M = 20010;
int n, m, k;
int h[N], e[M], w[M], ne[M], idx;
bool st[N][N];
int dist[N][N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dijkstra() {
priority_queue<PIII, vector<PIII>, greater<PIII>> q; //优先队列
memset(dist, 0x3f, sizeof dist);
dist[1][0] = 0; //起点
q.push({0, {1, 0}}); //距离(用于小顶堆排序,寻找最短距离),[节点,层](用于描述是哪个点)
while (q.size()) {
PIII t = q.top();
q.pop();
int u = t.second.first, p = t.second.second; // p层u节点
if (st[u][p]) continue; //如果p层u节点出过队列,则跳过
st[u][p] = true; //标识p层u节点已经出过队列
for (int i = h[u]; ~i; i = ne[i]) { //枚举u节点的每个连接边
int j = e[i]; // u节点的相邻接节点j
//先更新同层
if (dist[j][p] > max(dist[u][p], w[i])) {
dist[j][p] = max(dist[u][p], w[i]); //更新它
q.push({dist[j][p], {j, p}}); //被更新的点入队列
}
//更新下一层
if (p < k && dist[j][p + 1] > dist[u][p]) { // p<k以保证最后一层不继续下探
dist[j][p + 1] = dist[u][p]; //因为此边权为0
q.push({dist[j][p + 1], {j, p + 1}});
}
}
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d%d", &n, &m, &k);
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c); //无向边
}
//分层图+最短路
dijkstra();
int ans = INF;
for (int i = 0; i <= k; i++) ans = min(ans, dist[n][i]);
//最小值都是正无穷,说明从1出发无法到达n这个位置
if (ans == INF) ans = -1;
printf("%d\n", ans);
return 0;
}
五、\(DP+SPFA\)
下面采用\(DP\)的思想出发,来解决此题:
农场主只需要支付在该路径上剩余的电缆中,升级价格 最贵 的那条电缆的花费即可
注意:这里 不是所有电缆的累加和 ,而是找 最贵 的,也就是\(max\)
状态表示
令\(dist[u][p]\)为从\(1\)走到\(u\),花了\(p\)次机会的 答案。最终所求为$$\large min(dist[n][t]),t \in [0,k]$$
所谓的 答案 ,也就是这条路径中,最贵 的那段路的价格,也就是\(max(w[i])\)
再直白一点,就是\(dist\)里装的不是传统最短路径中的累加路径和,而是最长路径
状态计算
考虑一条边\((u,j,w[i])\)对\(dp\)的贡献:
- 在这条边 不使用机会
- 在这条边 使用机会
接下来直接\(dp\)就行了。但是码代码时不知道如何\(dp\),也就是 不知道\(dp\)顺序
回过头来看题,发现图并没有说是\(DAG\)(有向无环图),那么转移就有后效性了。借助\(spfa\)的思想——迭代思想。
\(spfa\)核心算法思想即不断松弛,直到松弛不了结束。
那我不知道\(dp\)顺序,我就一直\(dp\),\(dp\)到无法继续\(dp\)为止(也就是\(dp\)无法更新状态为止)
这两者是不是很像呢?于是我们将以前\(spfa\)中的松弛条件替换为\(dp\)转移方程就\(ok\)了。
这样就无需考虑\(dp\)顺序了, 完成
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;
const int M = 20010;
const int K = 1010;
const int INF = 0x3f3f3f3f;
int n, m, k, ans = INF;
int dist[N][K];
bool st[N][K];
//邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void spfa() {
queue<PII> q;
//出发点入队列
q.push({1, 0}); //节点号,使用了几次机会
dist[1][0] = 0; //距离也扩展为二维的,第二维记录使用了几次机会
while (q.size()) {
int u = q.front().first; //节点号
int p = q.front().second; //使用了几次机会
st[u][p] = false;
q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
//在这条边不使用机会
if (dist[j][p] > max(dist[u][p], w[i])) {
dist[j][p] = max(dist[u][p], w[i]);
if (!st[j][p]) { //注意这里不能使用continue,否则直接下一次循环了,后面的 分枝无法走到!
q.push({j, p});
st[j][p] = true;
}
}
if (p < k) {
//在这条边使用机会
if (dist[j][p + 1] > dist[u][p]) {
dist[j][p + 1] = dist[u][p];
if (!st[j][p + 1]) {
q.push({j, p + 1});
st[j][p + 1] = true;
}
}
}
}
}
}
int main() {
//初始化
memset(h, -1, sizeof h);
memset(dist, 0x3f, sizeof(dist));
//建图
scanf("%d %d %d", &n, &m, &k);
while (m--) {
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
//利用spfa的松驰操作,做DP的数据填充
spfa();
//最终的结果,一定出现在使用0,1,2,...,n次机会里面
for (int i = 0; i <= k; i++) ans = min(ans, dist[n][i]);
//如果无法到达n,则输出-1
if (ans == INF) ans = -1;
printf("%d\n", ans);
return 0;
}