自學最短路算法

自學最短路算法

Coded by Jelly_Goat on 2019/4/13.
All rights reserved.

來自一個被智能推薦了一萬年\(\text{【模板】單源最短路徑}\)的蒟蒻小金羊。html

1、floyd算法

說實話這個算法是用來求多源最短路徑的算法。node

可是這個時候咱們能夠引用一下來先作一個鋪墊。ios

算法原理:

動態規劃。git

typedef long long int lli;
lli map[5001][5001];
int n;

void floyd()
{
    for (register int i=1;i<=n;i++)
        for (register int j=1;j<=n;j++)
            for (register int k=1;k<=n;k++)
                if (map[i][j]>map[i][k]+map[k][j])
                    map[i][j]=map[i][k]+map[k][j];
}

最後程序中的map[i][j]就是i->j的最短路徑長度。算法

其餘:

時間複雜度:\(O(n^3)\),空間複雜度:\(S(n^2)\)(用的是鄰接表)。數組

以及若是題目中數據範圍\(n\leq5000\),通常就是Floyd沒跑了。學習

2、Bellman-Ford算法

最經典的最短路算法之一,衍生品(其實主要就是SPFA)簡直不計其數。測試

可是咱們如今仍是先回到這個算法自己。優化

算法原理:

咱們用一個結構體存一下邊。注意,這個時候鏈式前向星尚未用到。ui

struct edge{
    int from,to;
    lli dis;
    edge(){
        from=to=dis=0;
    }
}Edge[500001];

通俗易懂的變量名字

而後就能夠進行比較寬泛的「鬆弛」操做。

「鬆弛」是什麼?就是咱們利用三角形不等式的方法逼近更新最短路。

這就是這個算法的核心。

爲何這個算法是對的?

不行,我不證實

咱們從一個點出發,一定會更新到其餘節點。再從其餘節點更新其餘節點,一定會獲得其餘節點的鬆弛操做。

由於咱們每個節點都鬆弛了邊數遍,因此鬆弛一定會獲得其餘節點的最短路(距離源點)。

因此這個算法是正確的。

可是這個不是嚴謹的證實,證實仍是要上網右轉百度。

代碼實現?

struct edge
{
    int from, to;
    lli dis;
    edge()
    {
        from = to = dis = 0;
    }
} Edge[500001];
lli dis[10001];
int nodenum, edgenum, origin_node, querynum;

bool Bellman_Ford()
{
    dis[origin_node] = 0;
    //核心:鬆弛操做
    for (int i = 1; i <= nodenum - 1; i++)
    {
        for (int j = 1; j <= edgenum; j++)
        {
            if (dis[Edge[j].to] > dis[Edge[j].from] + Edge[j].dis)
                dis[Edge[j].to] = dis[Edge[j].from] + Edge[j].dis;
        }
    }
    for (int i = 1; i <= edgenum; i++)
        if (dis[Edge[i].to] > dis[Edge[i].from] + Edge[i].dis)
            return false;
    return true;
}

3、SPFA(Bellman-Ford算法的隊列優化)

這就是上文所說的衍生物。

可是在這以前咱們還得學習一下鏈式前向星——一種存儲圖的方式。

前置知識:鏈式前向星

咱們會發現,bf之因此跑得慢,就是由於對於邊的更新次數太多了!因此咱們想要像搜索那樣更新每個節點。

可是咱們會發現,咱們只須要一個節點的全部出邊就夠了。

這樣存圖有什麼好處?

對於空間上:對比鄰接矩陣,咱們會發現咱們若是存儲有向邊的話,矩陣的空間損耗太大,並且二位數組容易爆。

對於時間上:O(1)訪問某一個節點的第一個出邊。

那麼怎麼實現?

首先咱們須要開一個節點個數大小的數組head[10001],用來存儲每個節點的「第一條」出邊,而後加上一個變量cnt用來轉移下標

而後咱們須要在結構體內部作修改——改爲一個「出邊」:出邊的終點、出邊的長度、下一條出邊這三項就能夠了。

struct edge{
    int to,next;
    lli dis;
    edge(){
        to=next=dis=0;
    }
}Edge[500001];
int cnt=1;//存儲的其實是下一條邊的下標

而後咱們進行添加邊的操做。

inline void add_edge(int from,int to,lli dis)
{
    Edge[cnt].to=to;//先給要添加的邊進行一個賦值
    Edge[cnt].dis=dis;
    //重點理解:鏈接到上一條from的出邊
    Edge[cnt].next=head[from];
    //將head[from]——from的最後一條出邊改爲這個邊,而後到下一條邊的下標(下標++)
    head[from]=cnt++;
}

看起來就很簡單

上面說:head[10001]`,用來存儲每個節點的「第一條」出邊

其實是最後一條出邊,可是咱們遍歷全部出邊的時候哪管那個是頭上的邊......

咱們對這個算法進行一個測試,看看到底正確性如何。剪貼板子

而後進入正題——

SPFA算法原理:

引自GGBeng大佬的blog:原文(P.S:解釋的我以爲還不錯)

咱們學過了Bellman-Ford算法,如今又要提出這個SPFA算法,爲何呢?

考慮一個隨機圖(點和邊隨機生成),除了已肯定最短路的頂點與還沒有肯定最短路的頂點之間的邊,其它的邊所作的都是無用的,大體描述爲下圖(分割線以左爲已肯定最短路的頂點):

img

其中紅色部分爲所作無用的邊,藍色部分爲實際有用的邊。既然只需用到中間藍色部分的邊,那就是SPFA算法的優點之處了。

就是由於咱們要捨棄沒有用的邊的遍歷,咱們引進了SPFA的思想。

首先先給一個全局定義:

#include <iostream>
#include <cstdio>
#include <bitset>
#include <queue>
#include <cstring>

using namespace std;
typedef long long int lli;
const lli oo = 2147483647LL, INF = oo;
struct edge
{
    int to, next;
    lli dis;
    edge()
    {
        to = next = 0, dis = oo;
    }
} Edge[500001];
int nodenum, edgenum, origin_node, end_node, cnt = 1;
int dis[10001], head[400001];
bitset<10005> vst; //bool vst[10005];
queue<int> q;

而後咱們須要進行SPFA!

1.BFS版本

BFS?聽起來挺熟悉......

然而還真的是借用了這個思想。

隊列裏面要放須要擴展的節點,而後還要用vst[]表示是否在隊列中。

對於每個取出來的節點,咱們擴展全部的出邊的終點,咱們發現能夠進行判斷&鬆弛——沒錯,仍是那個鬆弛。

若是在隊列中,就不須要重複加入隊列。

可是若是每個節點重複加入了n次,就說明這個地方有一個能夠無限鬆弛的負環。

#include <iostream>
#include <cstdio>
#include <cctype>
#include <bitset>
#include <queue>
#include <cstring>

using namespace std;
typedef long long int lli;
const lli oo = 2147483647LL, INF = oo;
struct edge
{
    int to, next;
    lli dis;
    edge()
    {
        to = next = 0, dis = oo;
    }
} Edge[500001];
int nodenum, edgenum, origin_node, end_node, cnt = 1;
int dis[10001], head[400001], cnt2[10001];
bitset<10005> vst; //bool vst[10005];
queue<int> q;
//quick input of num
template <typename T_>
inline T_ getnum();

inline void add_edge(int from, int to, lli dis)
{
    Edge[cnt].dis = dis;
    Edge[cnt].to = to;
    Edge[cnt].next = head[from];
    head[from] = cnt++;
}

bool spfa()
{
    q.push(origin_node);
    vst[origin_node] = true;
    dis[origin_node] = 0;
    while (!q.empty())
    {
        int t = q.front();
        q.pop();
        vst[t] = false;
        for (register int i = head[t]; i != 0; i = Edge[i].next)
        {
            int to = Edge[i].to;
            if (dis[to] > dis[t] + Edge[i].dis)
            {
                dis[to] = dis[t] + Edge[i].dis;
                if (!vst[to])
                {
                    q.push(to);
                    vst[to] = true;
                    if (++cnt2[to] > nodenum)
                        return false;
                }
            }
        }
    }
    return true;
}

int main()
{
#ifdef WIN32
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w+", stdout);
#endif
    nodenum = getnum<int>(), edgenum = getnum<int>(), origin_node = getnum<int>();
    for (register int i = 1; i <= nodenum; i++)
    {
        dis[i] = oo;
    }
    for (register int i = 1; i <= edgenum; i++)
    {
        int from = getnum<int>(), to = getnum<int>();
        lli dis = getnum<lli>();
        add_edge(from, to, dis);
    }
    if (!spfa())
    {
        cout << "Exist a minus circle." << endl;
        return 0;
    }
    else
    {
        for (register int i = 1; i <= nodenum; i++)
        {
            printf("%d ", dis[i]);
        }
    }
    return 0;
}

//quick input of num
template <typename T_>
inline T_ getnum()
{
    T_ res = 0;
    bool flag = false;
    char ch = getchar();
    while (!isdigit(ch))
    {
        flag = flag ? flag : ch == '-';
        ch = getchar();
    }
    while (isdigit(ch))
    {
        res = (res << 3) + (res << 1) + ch - '0';
        ch = getchar();
    }
    return flag ? -res : res;
}

代碼如上。

相關文章
相關標籤/搜索