0
点赞
收藏
分享

微信扫一扫

AcWing 340. 通信线路


​​题目传送门​​

分析:

本题的题意是找到一条路径,边权最大的\(k\)条边忽略,第\(k + 1\)大的边权就作为该条路径的代价,求最小代价是多少,换而言之,就是求从起点到终点的所有路径中第\(k + 1\)大的边权最小是多少。虽然最后写出来很简单,但是想到这个思路是相当不容易的。算法竞赛进阶指南上给出了动态规划的思路和二分的思路,本题也可以用分层图去做。相对于动态规划和分层图的思路,二分的解法思维的难度和求解的时间复杂度都是比较低的。

拿到一道陌生的题,没思路的情况下首先考虑​暴力求解​的思路,求第\(k + 1\)长的边​最小​是多少,暴力求解我们只能去​枚举所有的路径​,然后依次求出各种路径中第\(k + 1\)长的边是多少,比较下求出最小的是多少。显然要想求从起点到终点的所有路径是很不容易的,而且复杂度也相当高,暴力做法实现起来不容易。

本题的难点有两个,第一个是想到用​二分​去求解,第二个是将题目化为​双端队列​\(BFS\)问题。

首先,我们思考下二分一般应用于上面情况下,很简单,就是给一组数,先判断下要找的数在不在左半部分,在就在左半部分继续二分,不在就到右半部分去找。可以用二分取解决的问题要满足两个特性,其一是解在一定的范围内,以便我们确定二分的左右端点;其二是这组数据具有一定的单调性。这种单调性既可以是显性的,比如有序的数组,也可以是隐性的,只要知道中间数满不满足条件就可以确定下一步查找的范围。既然我们不知道这题的解如何求,那么是否有办法说给我\(x\)元钱,我有没有办法确定这么多钱能否去升级一条路径呢?肯定是有办法的。题目给定的\(L\)在\(1\)到\(100w\)间,这就说明本题的解一定在\(0\)到\(100w\)之间,否则就是无解,输出\(-1\)。解为\(0\)的情况是这条路径上的边不超过\(k\)条,意味着不用花钱就可以升级线路;无解的情况是从起点无法到达终点。既然本题的解有一定的范围,并且如果\(x\)元能够升级某条路径,那么解一定不会超过\(x\),这就是单调性,也就意味着本题可以用二分解决。

接着来谈第二个难点,我们如何去确定\(x\)元钱能否升级一条线路,给我们一条路径,我们把这条路径上的边权与\(x\)意义比较,只要大于\(x\)的边不超过\(k\)条就说明可以用不超过\(x\)元去升级这条线路。

继续思考会发现,我们不关心这条路径上的每条边的具体权值是多少,只关心其与\(x\)的大小关系,因此整个图上的边就分为两类,边权大于\(x\)的和不大于\(x\)的,大于\(x\)的边我们将其边权视为\(1\),否则边权视为\(0\),只要从起点到终点的最短路径长度不超过\(k\)就说明\(x\)元升级线路是可行的。

边权只有\(0\)和\(1\)两种情况的最短路问题可以用双端队列\(BFS\)解决,双端队列\(BFS\)相关的问题题解见\(AcWing\) \(175\) 电路维修,如果不想再去看那么多的文字,我这里简单的解释下双端队列\(BFS\)的思想。\(dijkstra\)算法的思路是维护一个小根堆,堆顶元素的距离永远是最小的,然后不断取出堆顶元素去松弛周围点的距离。而边权只有\(01\)两种情况的图我们无需维护一个小根堆,只需要维护一个双端队列,保证双端队列的队头元素离起点的距离永远是最小的即可,为此,从起点开始我们将起点加入队列,然后尝试去松弛周围的点,只要周围的点还未出队过,并且可以被松弛,就将该点松弛后加入队列中,边权是\(0\)就加入队头,边权是\(1\)就加入队尾。这就是双端队列\(BFS\)的基本思路。(详细介绍还是看之前的题解吧)。

整理下本题的求解思路,在\(0\)到\(100w\)间二分答案,每次二分时通过双端队列\(BFS\)的方法判断起点到终点的最短路径是否不超过\(mid\),是的话就在左半部分继续二分,否则在右半部分二分,直到找到答案为止。在其中任意一次\(BFS\)的过程中,一旦发现\(BFS\)完终点离起点的距离还是无穷大。说明终点不可达,直接输出\(-1\)终止程序。

实现代码

#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 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]; //记录最短距离
// u指的是我们现在选最小花费
bool check(int x) {
memset(st, false, sizeof st);
memset(d, 0x3f, sizeof d);
priority_queue<PII, vector<PII>, greater<PII>> q;
d[1] = 0;
q.push({0, 1});

while (q.size()) {
PII t = q.top();
q.pop();
int dist = 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 (d[j] > dist + v) {
d[j] = dist + v;
q.push({d[j], j});
}
}
}
//如果按上面的方法计算后,n结点没有被松弛操作修改距离,则表示n不可达
if (d[n] == 0x3f3f3f3f) {
puts("-1"); //不可达,直接输出-1
exit(0);
}
return d[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;
}



举报

相关推荐

0 条评论