AcWing 345 牛站 【BellmanFord算法,非正解】
\(AcWing\) \(345\) 牛站
一、题目描述
给定一张由 \(T\) 条边构成的无向图,点的编号为 \(1\)∼\(1000\) 之间的整数。
求从起点 \(S\) 到终点 \(E\) 恰好 经过 \(N\) 条边(可以重复经过)的 最短路。
注意: 数据保证一定有解。
输入格式
第 \(1\) 行:包含四个整数 \(N\),\(T\),\(S\),\(E\)。
第 \(2..T+1\) 行:每行包含三个整数,描述一条边的边长以及构成边的两个点的编号。
输出格式
输出一个整数,表示最短路的长度。
二、\(bellmanFord\)之恰好\(k\)条边
有边数限制的最短路径问题,第一反应自然是 \(BellmanFord\)算法,只需对\(BellmanFord\)算法的逻辑稍加修改,再吸吸氧应该就可以\(AC\)了
\(BellmanFord\)算法的存图方式比较牛\(X\)(\(yxc\)大佬原话),用一个结构体\(Edge(u,v,w)\),然后开一个数组就行了,随便存,与之前学习的邻接表不同。
原因:因为\(BellmanFord\)需要 遍历每一条边,怎么能快速遍历每一条边就是关键。
以前学习过的邻接表,是按点做索引的 ,记录这个点出发到达哪些点。
如果采用邻接表,就需要枚举每个点,然后二次循环找出每条边,就麻烦了,不如直接以边为索引来的快了。
问题
\(Q_1\):\(bellmonFord\) 算法解决的问题是 最多 经过\(k\)条边的最短路,而题目问的是 恰好经过 \(k\)条边的最短路径,怎么解决呢?
\(A\):\(bellmonFord\) 每次 强制转移 即可,即把新的距离数组赋值为正无穷。
这样每迭代一次数组中\(i\)代表的一定是恰好走\(k\)步的步数。
\(Q_2:\)为什么要加上 备份数组 ?
\(A\):每次在处理\(dist\)数组前备份一下到\(backup\)数组,是为了每次迭代只使用上一次的结果,本次的迭代结果不用于本轮次的其它迭代,这样才能保证使用边数的限制有效,就不会发生串联了。
#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
const int M = 1000010;
int dist[N], backup[N];
struct Edge {
int a, b, c;
} edges[M];
int n, k, m, s, t;
int bellmanFord() {
//初始化
memset(dist, 0x3f, sizeof(dist));
dist[s] = 0;
//经过k次迭代
for (int i = 0; i < k; i++) {
memcpy(backup, dist, sizeof(dist)); //使用备份,防止串联
memset(dist, 0x3f, sizeof(dist)); //这句是精髓,恰好需要在循环内进行全新初始化
for (int j = 0; j < m; j++) {
int a = edges[j].a, b = edges[j].b, c = edges[j].c;
dist[b] = min(dist[b], backup[a] + c);
dist[a] = min(dist[a], backup[b] + c);
}
}
return dist[t];
}
int main() {
scanf("%d %d %d %d", &k, &m, &s, &t);
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d %d %d", &c, &a, &b);
edges[i].a = a, edges[i].b = b, edges[i].c = c;
}
printf("%d", bellmanFord());
return 0;
}
三、离散化
本题没有离散化的必要!有的博文说必须需要离散化,纯粹是扯淡,人云亦云而已。
原因:
- \(bellmanFord\)是按边来枚举的,而且枚举的是\(Edge\)数组,不管你是不是离散化,边的数量都不会变化。
- 不超过\(k\)条 或者 恰好是\(k\) 条,都是说\(bellmanFord\)的外层循环是\(0 \sim k\),你离不离散化都没有影响。
- 非要说离散化有用的话,就是原来\(993\)这样的大号,被你搞成了\(3\)这样的小号,看着方便而已,其它毫无影响,该吸氧通过的,还得吸氧;该过不了的,还是过不了。
- 当然,只是说本题离散化没用,不是说图论的离散化没用,可别理解错了。
附上两种离散化的代码,起到学习的作用吧:
离散化技巧一
#pragma GCC optimize(2) //累了就吸点氧吧~不吸氧,通过8/12个数据
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 205, M = 105;
//现在看来,这个代码写的好垃圾啊,离散化用的不好。代码长
//而且,在用旧的号码记录到邻接表中,然后再去修改,逻辑太长,垃圾!
//相对于另外两套代码,都是在使用hashTable或者unordered_map进行映射后
//再放入邻接表(邻接矩阵),就方便多了~
int m; //图的边数
int k; //恰好是k条边
int s, t; //起点和终点
struct Edge {
int a; //起点
int b; //终点
int w; //边长
} edges[M];
int p[N]; //使用了哪些点,为了照顾后面的离散化,肯定要尽可能的把点都存储起来,
int tot;
int dist[N], backup[N];
//通过STL将原来的x找到现在新的下标位置
int get(int x) {
int l = 0, r = tot;
while (l < r) {
int mid = (l + r) >> 1;
if (p[mid] >= x)
r = mid;
else
l = mid + 1;
}
return l;
}
int bellmanFord() {
//最多经过 k 条边:在循环外初始化一次
//恰好经过 k 条边:除在循环外初始化一次外,还需要在第一层循环中memcpy(backup)后面再次初始化
memset(dist, 0x3f, sizeof dist);
dist[s] = 0; //出发点
//恰好是k条边,迭代k次
for (int i = 0; i < k; i++) {
memcpy(backup, dist, sizeof dist);
memset(dist, 0x3f, sizeof dist);
for (int j = 1; j <= m; j++) { //枚举m条边
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b], backup[a] + w);
dist[a] = min(dist[a], backup[b] + w);
}
}
//计算在n条限定下的最短距离
return dist[t];
}
int main() {
//加快读入
scanf("%d %d %d %d", &k, &m, &s, &t);
for (int i = 1; i <= m; i++) {
int a, b, c;
scanf("%d %d %d", &c, &a, &b); //一条边的边长以及构成边的两个点的编号
edges[i] = {a, b, c};
//记录有哪些点,准备离散化去重
p[tot++] = a, p[tot++] = b;
}
//离散化
sort(p, p + tot);
tot = unique(p, p + tot) - p;
//枚举每条边,将第i条边的起点和终点转化为离散化后的新序号
for (int i = 1; i <= m; i++)
edges[i].a = get(edges[i].a), edges[i].b = get(edges[i].b);
//将起点和终点也转化为离散化后的新点号
s = get(s), t = get(t);
//调用bellmanFord算法
printf("%d\n", bellmanFord());
return 0;
}
离散化技巧二
#pragma GCC optimize(2) //累了就吸点氧吧~
#include <bits/stdc++.h>
using namespace std;
int id[1010];
const int N = 210;
const int M = 1000010;
int d[N], backup[N];
struct Edge {
int a, b, c;
} edges[M];
int n, k, m, s, t;
int bellmanFord() {
memset(d, 0x3f, sizeof(d));
d[s] = 0;
for (int i = 0; i < k; i++) {
memcpy(backup, d, sizeof(d));
//与最多经过k条边这里不同!
memset(d, 0x3f, sizeof(d));
for (int j = 0; j < m; j++) {
int a = edges[j].a, b = edges[j].b, c = edges[j].c;
//与最多经过k条边这里不同!
d[b] = min(d[b], backup[a] + c);
d[a] = min(d[a], backup[b] + c);
}
}
return d[t];
}
int main() {
//加快读入
scanf("%d %d %d %d", &k, &m, &s, &t);
//起点分配1号
if (!id[s]) id[s] = ++n;
//结束结点为2号,为了防止起点和终点是一个,采用了判断的办法
if (!id[t]) id[t] = ++n;
//修改原来s和t的离散化后的值
s = id[s], t = id[t];
// m条边
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d %d %d", &c, &a, &b);
if (!id[a]) id[a] = ++n;
if (!id[b]) id[b] = ++n;
edges[i].a = id[a], edges[i].b = id[b], edges[i].c = c;
}
//上面的离散化简直太牛B了!
printf("%d", bellmanFord());
return 0;
}