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;
}
posted @ 2022-03-23 09:45  糖豆爸爸  阅读(79)  评论(0编辑  收藏  举报
Live2D