AcWing 345 牛站 【BellmanFord算法,非正解】


题目传送门

  • \(Bellman-Ford\)算法求解经过不超过\(k\)条边的最短路

  • \(Bellman-Ford\)算法求解经过恰好\(k\)条边的最短路(可以重复经过)

前人的足迹,推荐阅读

一、解法I

#pragma GCC optimize(2) //累了就吸点氧吧~

#include 
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, E;

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[E];
}
int main() {
    cin >> k >> m >> S >> E;
    //开始节点为1号
    if (!id[S]) id[S] = ++n;
    //结束结点为2号,为了防止起点和终点是一个,采用了判断的办法
    if (!id[E]) id[E] = ++n;
    //修改原来S和E的离散化后的值
    S = id[S], E = id[E];
    // m条边
    for (int i = 0; i < m; i++) {
        int a, b, c;
        cin >> 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;
}

二、解法II

#pragma GCC optimize(2) //累了就吸点氧吧~

#include 
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) {
    return lower_bound(p, p + tot, x) - p;
}

int bellmanFord() {
    //最多经过 k 条边:在循环外初始化一次
    //恰好经过 k 条边:除在循环外初始化一次外
    //还需要在第一层循环中memcpy(backup)后面再次初始化
    memset(dist, 0x3f, sizeof dist);
    dist[s] = 0; //出发点

    for (int i = 0; i < k; i++) {
        // 恰好是k条边,迭代k次
        /*
        1、memset可以理解为是数组有多大,就执行多少次(实际性能比这个强,可以这样估算)
        2、其实就算不离散化,直接上来就BellmanFord也是可以的,就是会TLE
        3、为什么会TLE呢?因为n最大是1000,点数最大是1e6,那么将面对1000*1e6=1e9的时长
        ,C++1秒过不了。
        4、观察到点数虽多,但边数少,只有100条。满打满算边两头的端点都不一样的话,最多
        也就200个点。
        5、也就是说很多点没有边连着,是没用的,可以考虑去掉这些无用的点,保留有边的点。
        6、去的办法就是离散化,枚举边,找出边的两个端点,然后对端点集合进行离散化。
        7、离散化后,需要修改原来点与点之间的关联关系,比如原来是1000,2000这两个点之间
        有一条长度为100的边,现在1000映射成了1号点,2000映射成了2号点,需要修改为1到2有
        一条长度为100的边才对。
        8、我们需对点进行离散化,所以需要把点放到一个数组中,在本代码中就是p数组,
        point的意思。这里使用的存储图的方式为按点+边存储方式,所有点存储在p数组中,
        所有边存储在e数组中,每一条边包含三个信息:起点,终点,长度。
        这样存储的办法,使得我们可以离散化掉无用的点。

       BellmanFord算法的存图方式比较牛X(yxc大佬原话),用一个结构体Edge(u,v,w),
       然后开一个数组就行了,随便存,与之前学习的邻接表不同。
       原因很简单:因为BellmanFord需要遍历每一条边,怎么能快速遍历每一条边就是关键。
       我们以前学习过的邻接表,是按点做索引的,然后记录这个点出发到达哪些点。
       如果采用邻接表,就需要枚举每个点,然后二次循环找出每条边,就麻烦了,不如直接
       以边为索引来的快了。

        9、本题还有一个重要问题:bellmon-ford 算法解决的问题是最多经过k条边的最短路,
        而题目问的是恰好经过k条边的最短路径,怎么解决呢?
        答:bellmon-ford 每次强制转移即可,即把新的数组赋值为正无穷。
        这样每迭代一次数组中i代表的一定是恰好走k步的步数。
        这么做的原理是什么呢?什么地方讲过这个原理呢?是现想出来的吗?不可能吧~
        */
        // 每次在处理dist数组前备份一下到backup数组
        // 只用上一次的迭代结果,就不会发生串联了。
        // 上一次的迭代结果可以理解为在上一次x条边限定的情况下的意思
        // https://www.acwing.com/video/285/
        // 第18分钟讲到了关于串联的问题
        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() {
    cin >> k >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        int a, b, w;
        //一条边的边长以及构成边的两个点的编号
        cin >> w >> a >> b;
        edges[i] = {a, b, w};
        //记录有哪些点,准备离散化去重
        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;
}

相关