基础之最短路

关于我知识点全忘了需要从头来过这件事

最短路

Floyed

DP的角度。

从节点 \(\large A\) 到节点 \(\large B\) 最短路径只有两种情况,要么直接从 \(\large A\)\(\large B\) ,要么经过若干个点再到 \(\large B\)

\(\large dis(A,B)\) 为从节点 \(\large A\) 到节点 \(\large B\) 的最短路径长,那么枚举 \(\large A,B\) 间断点 \(\large K\) ,若有 \(\large dis(A,K)+dis(K,B) < dis(A,B)\) ,那么更新 \(\large dis(A,B)\) 。遍历完所有的断点 \(\large K\) 后,\(\large dis(A,B)\) 就是我们要的答案。

由这个思路直接得出的代码:

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
        for (int k = 1; k <= n; k++) {
            (dis[i][k] + dis[k][j] < dis[i][j]) and (dis[i][j] = dis[i][k] + dis[k][j]);
        }
    }
}

然而,上面的代码是错误的,这里要注意循环的嵌套顺序, \(\large K\) 放最里面是错的。

因为这样的枚举方式会过早地将 \(\large i,j\) 间的最短路径确定下来,后面存在更短的路径时,就会无法更新。

有些抽象??举个例子

floyd1

如果将 \(\large K\) 放在最内层,那么 \(\large A->B\) 只能更新一条路径,即 \(\large A->B\) (枚举其他两个点时,与 \(\large A\)\(\large B\) 的连边边权为 \(\large INF\)),但很显然是错误的。把 \(\large K\) 放在中间也是一样的道理,这里不再细说。

那么把 \(\large K\) 放在最外层呢?当我们枚举到断点 \(\large C\) 时,会更新 \(\large B->D\)\(\large B->C->D\) ,这样接下来枚举到断点 \(\large D\) ,又会更新 \(\large A->B\) ,答案正确。

for (int k = 1; k <= n; k++) {
    for (int i = 1; i <= n; i++) {
         for (int j = 1; j <= n; j++){
            (dis[i][k] + dis[k][j] < dis[i][j]) and (dis[i][j] = dis[i][k] + dis[k][j]);
        }
    }
}

但是这玩意儿 \(\large O(n^3)\) 的,谁闲着没事写这玩意儿

路径的保存自己探索吧,我懒得打了。。。

Dijkstra

额。。。上面用的是 \(\large DP\) ,这边用的是贪心。

用的范围挺广的,有必要好好总结一下。

算法特点:

单源最短路,即可解决固定一点到其他任意点的最短路径问题。最终得到的是一个最短路径树。

他往往,是其他图论算法的子模块。

算法策略:

\(\large Dijkstra\) 采用贪心策略,声明 数组 \(\large dis\) 保存源点到各个顶点的最短路径长度 和 一个保存已经找到了最短路径的顶点集合 \(\large T\)

下面设源点为 \(\large s\) .

初始状态,\(\large dis[s]=0\) 。查找出边(我习惯用链表存图,虽然某些情况下有些慢),将与其直接相连的顶点 \(\large m\) 间的距离设为此边边权,即 \(\large dis[m]=w_{s\rightarrow m}\) ,同时把与 \(\large s\) 不直接相连的点间的距离设为无穷大。

初始时,集合 \(\large T\) 内只有 \(\large s\) ,然后,从 \(\large dis\) 中选出最小值,则该值就是当前源点 \(\large s\) 到该值对应的顶点的最短路径,将该点加入 \(\large T\) 中。

然后,看看新加入的顶点是否可以到达其他顶点并且看看通过这个新加的顶点后,到达其他顶点的路径是否更优,是,那就替换。

重复上述操作,直到 \(\large T\) 中包含所有点。

\(\large ps\) :一开始选最小值选出来的为什么就是最短路径?(课下思考

但他无法解决带有负边权的图。

毕竟只有在确定当前达到最短情况下才会将顶点加入 \(\large T\) ,但很显然,这玩意儿太贪了,他会在一个负边上反复横跳。。。。

code:

上代码(带堆优化

#include <bits/stdc++.h>

#define _ 0
#define N 100010
#define int long long
//防爆好习惯

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		(ch == '-') and (f = 0);
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
		ch = getchar ();
	}
	a = f ? x : -x;
}

struct blanc {
    int to, w, net;
} e[N << 1]; // 在无向图的情况下,边数 >=n <=2n
int tot, head[N];

inline void add (int u, int v, int w) {
    e[++tot].to = v;
    e[tot].w = w;
    e[tot].net = head[u];
    head[u] = tot;
}

priority_queue <pair <int, int>, vector <pair <int, int> >, greater <pair <int, int> > > q;
bool vis[N];
int dis[N << 1];

inline void dij () {
    while (! q.empty ()) {
        int y = q.top ().second;
        q.pop ();
        if (vis[y]) continue ;
        vis[y] = 1;
        for (int i = head[y]; i; i = e[i].net) {
            int v = e[i].to;
            if (dis[v] > dis[y] + e[i].w) {
                dis[v] = dis[y] + e[i].w;
                q.push (make_pair (dis[v], v));
            }
        }
    }
}

int n, m, u, v, w;
int start;

signed main () {
    read (n), read (m);
    read (start);
    
    for (int i = 1; i <= n; i++) {
        dis[i] = 2147483647;
    }
    
    for (int i = 1; i <= m; i++) {
        read (u), read (v), read (w);
        add (u, v, w);
        //add (v, u, w);
    }
    dis[start] = 0;
    q.push (make_pair (0, start));
    dij ();
    for (int i = 1; i <= n; i++) {
        printf ("%lld\n", dis[i]);
    }
    return ~~(0^_^0);
}

没了(这就是你说的好好总结??

SPFA

秉着公平公正的原则让这玩意儿出来诈诈尸

然鹅你也可以看出来我的这部分写得极不负责。。。

我更喜欢叫它 SPA

可以直接跳过!

算法简介:

\(\large SPFA\ \ \ (Shortest\ Path\ Faster\ Algorithm)\) 算法,是一种死掉的算法(bushi,是一种单源最短路算法,是对 \(\large Bellman-ford\) 的队列优化。正常情况下挺快的,但只是正常。。。

这玩意儿能处理负边权(\(\large Dij\) 表示羡慕。复杂度大约是 \(\large O(kE)\)\(\large k\) 为每个点的平均入队次数,稀疏图中小于 \(\large 2\) 。但在稠密图中,这玩意儿复杂度是。。。。 \(\large O(过不了)\) 。。。。

算法实现:

建一个队列,初始时队列只有起点,再建立一个表格记录起点到所有点的最短路径(就跟 \(\large Dij\) 一样。然后执行松弛操作(术语去死,也跟 \(\large Dij\) 那个贪法差不多。然后没了。

但他其实是通过队列的收敛性得到答案的。

还可以判负环,即一个点入队次数超过 \(\large N\)

算法具体化:

就是给图举例子

十分经典的一个图(照着别人的重画一遍

\(\large A\rightarrow E\) 的最短路

下面不画图了(画图实在太难了

但我可以偷图啊

源点入队

扩展与 \(\large A\) 相连的边, \(\large B,C\) 入队

\(\large B,C\) 再扩展,\(\large D\) 入队

下面操作自己描述

\(\large E\) 出队,队列为空,算法结束

code:

#include <bits/stdc++.h>

#define N 100010
#define _ 0

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		(ch == '-') and (f = 0);
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
		ch = getchar ();
	}
	a = f ? x : -x;
}

struct blanc {
    int to, net, w;
} e[N << 1];
int head[N], tot;

inline void add (int u, int v, int w) {
    e[++tot].to = v;
    e[tot].w = w;
    e[tot].net = head[u];
    head[u] = tot;
}

int dis[N], in[N], n, m; // in 存某点入队次数,判负环
bool vis[N];

inline bool spfa (int s) {
    memset (dis, 0x3f, sizeof dis);
    int u, v;
    queue <int> q;
    q.push (s);
    vis[s] = 1;
    dis[s] = 0;
    while (! q.empty ()) {
        u = q.front ();
        q.pop ();
        vis[u] = 0;
        for (int i = head[u]; i; i = e[i].net) {
            v = e[i].to;
            if (dis[v] > dis[u] + e[i].w) {
                dis[v] = dis[u] + e[i].w;
                if (! vis[v]) {
                    q.push (v);
                    vis[v] = 1;
                    if (++ in[v] > n) return 0;
                }
            }
        }
    }
    return 1;
}

int s, x, y, z;

signed main () {
    read (n), read (m), read (s), read (ed);
    for (int i = 1; i <= m; i++) {
        read (x), read (y), read (z);
        add (x, y, z);
        //add (y, x, z);
    }
    if (! spfa (s)) {
        puts ("FALSE!");
    } else {
        for (int i = 1; i <= n; i++) {
            printf ("%d ", dis[i]);
        }
    }
    return ~~(0^_^0);
}

这个交到 luogu 上会 T 掉......

还没完。。。

posted @ 2021-07-22 18:46  aleph_blanc  阅读(108)  评论(0编辑  收藏  举报