单源最短路径floyd、dijkstra、dijkstra+ heap、bellman-flod、spfa

畅通工程续

Problem Description

某省自从实行了很多年的畅通工程计划后,终于修建了很多路。不过路多了也不好,每次要从一个城镇到另一个城镇时,都有许多种道路方案可以选择,而某些方案要比另一些方案行走的距离要短很多。这让行人很困扰。

现在,已知起点和终点,请你计算出要从起点到终点,最短需要行走多少距离。

Input

本题目包含多组数据,请处理到文件结束。
每组数据第一行包含两个正整数N和M(0 接下来是M行道路信息。每一行有三个整数A,B,X(0<=A,B 再接下一行有两个整数S,T(0<=S,T

Output

对于每组数据,请在一行里输出最短需要行走的距离。如果不存在从S到T的路线,就输出-1.

Sample Input

3 3
0 1 1
0 2 3
1 2 1
0 2
3 1
0 1 1
1 2

Sample Output

2
-1

floyd 暴力算法,核心代码三个for

#include

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

int arr[N][N];
int n,m,st,ed;

inline void init()
{
    for(int i=0;i>n>>m)
    {
        init();//初始化
        for(int i=0;i>a>>b>>c;
            if(arr[a][b]>c)arr[a][b]=arr[b][a]=c;//因为是双向,所以尽量取最小
        }

        floyd();

        cin>>st>>ed;
        cout<<(arr[st][ed]==inf?-1:arr[st][ed])<

dijsktra算法

用于无负边权的 单元 最短路径问题。

#include

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

int arr[N][N];
int dis[N];//存储某个点到各个点的最短路径
bool vis[N];//不能成环,所以判断某个点是否走过
int n,m,st,ed;

inline void init()
{
    for(int i=0;i>n>>m)
    {
        init();
        for(int i=0;i>a>>b>>c;
            if(arr[a][b]>c)arr[a][b]=arr[b][a]=c;
        }

        cin>>st>>ed;

        for(int i=0;i

dijkstra+ heap算法

时间复杂度o((m+n)logn)级别小于n^2。利用优先队列自动排序,寻找最短边。这里有两种写法,一种用二维数组存储,另一种用邻接表存储。邻接表适用于边特别多的情况。

#include

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

typedef pairPII;

int arr[N][N];
int dis[N];
bool vis[N];
int n,m,st,ed;

inline void init()
{
    memset(dis,inf,sizeof dis);
    memset(vis,false,sizeof vis);

    for(int i=0;i,greater > q;
    q.push({0,st});

    while(q.size())
    {
        PII now=q.top();q.pop();//greater小顶堆自动排序队首为最小,less队首最大大顶堆。用这个来找最短边。
        int vi=now.second;//最短边的另一端端点
        if(vis[vi])continue;
        vis[vi]=true;
        for(int i=0;idis[vi]+arr[vi][i])//比较通过最短边是否为最短路径,更新到每个点的最短路径
            {
                dis[i]=dis[vi]+arr[vi][i];
                q.push({dis[i],i});
            }
        }
    }
}

int main()
{
    int a,b,c;
    while(cin>>n>>m)
    {
        init();//初始化应该在输入之前,否则数据没输入。
        for(int i=0;i>a>>b>>c;
            arr[a][b]=arr[b][a]=min(arr[a][b],c);
        }
        cin>>st>>ed;

        dijkstra();
        cout<<(dis[ed]==inf?-1:dis[ed])<
#include

//邻接表写法。存在大量边的情况下适合用邻接表

#define PUSH(x,y,z) G[x].push_back({y,z});

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

typedef pair P;

int n,m,st,ed;
vector

G[N]; int dis[N]; bool vis[N]; inline void init() { memset(dis,inf,sizeof dis); memset(vis,false,sizeof vis); for(int i=0;i,greater

> q; q.push({0,st}); while(q.size()) { P now=q.top(); q.pop();//选最短边,找到最短边的另一个端点 int u=now.second; if(vis[u])continue; vis[u]=true; for(int i=0;idis[u]+cost)//遍历更新,通过最短边到点G[u][i].first的最短距离。检测vis[v]是否走过,因为要更新通过u到各个点,防止成环。 { dis[v]=dis[u]+cost; q.push({dis[v],v});//仅c++11以上版本编译器支持。建议写成make_pair(int,int),make_pair作用是把参数变成pair类型,兼容所有版本,写法:q.push(make_pair); } } } } int main() { int a,b,c; while(cin>>n>>m) { init(); for(int i=0;i>a>>b>>c; PUSH(a,b,c);//不用判断G[a][b].second>c因为,首先第一次输入用不上,其次如果出现重复数据,在更新的时候不影响更新他的最小值。 PUSH(b,a,c); } cin>>st>>ed; dijkstra(); /*for(int i=0;i

bellman-flod算法

dijkstra无法解决负权问题,此算法可以,核心代码4行,循环n - 1次。

如果存在负边权,则无最短路径。

如何保证不成环,因为总共对每条边最多做n - 1次松弛,每次松弛只有两种可能情况(1)利用未用过的较短的边松弛(2)利用负边松弛,且只要用负边权,下次还会用用过的负边权。所以松弛n - 1次,最多利用n - 1条边,总共n个点所以不会成环。

#include

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e3 + 5;

int n,m,st,ed;
int pro[N],dis[N],u[N],v[N],w[N];

void Bellman_Ford()
{
    for( int k = 0;k < n - 1; ++ k )
    {
        for( int i = 0;i < n; ++ i )pro[i] = dis[i];
        for( int i = 0;i < m; ++ i )
        {
            dis[v[i]] = min(dis[v[i]],dis[u[i]] + w[i]);
            dis[u[i]] = min(dis[u[i]],dis[v[i]] + w[i]);//无向图需要反向更新一次
        }

        bool check = false;
        for( int i = 0;i < n; ++ i )
        {
            if(pro[i] != dis[i])
            {
                check = true;
                break;
            }
        }
        if(check)continue;
        else break;
    }

    bool check = false;
    for( int i = 0;i < m; ++ i )
    {
        if(dis[v[i]] > dis[u[i]] + w[i])
        {
            check = true;
            break;
        }
    }

    if(check)cout << "-1" << endl;
}

int main()
{
    while(cin >> n >> m)
    {
        for( int i = 0;i < m; ++ i )cin >> u[i] >> v[i] >> w[i];
        memset(dis,INF,sizeof dis);

        cin >> st >> ed;

        dis[st] = 0;

        Bellman_Ford();

        cout << ( dis[ed] == INF ? -1 : dis[ed]) << endl;
    }
    return 0;
}

Bellman-flod 算法 + 队列优化 (spfa算法)

在Bellman-flod 算法中可以发现,很多松弛是无法进行的,只有当某一条边松弛之后,他的邻边才有可能松弛。

每次成功松弛的点入队,每次循环用已经松弛过的边松弛其他的边。

判断有无负环:如果某个点进入队列的次数超过一定次数则存在负环(在一次循环中可能会更新多次,但只入队一次,更新次数不能决定是否成环,因为一条边可以同时由许多其他边更新,一旦其他边更新,对于当前要检测是否成环的边的更新次数会超过 n - 1,入队或者出队次数才能代表该边的松弛情况,当它入队超过n - 1时,表明成环)。也可以利用Bellman-flod中检测负边权的方法。

期望的时间复杂度O(kE), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。

SPFA 在形式上和宽度优先搜索(bfs)非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。

SPFA算法有两个优化算法 SLF 和 LLL。

  • SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)

  • LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。

  • 引用网上资料,SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。

#include
#define PUSH(x,y,z) G[x].push(make_pair(y,z))

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e3 + 5;

typedef pair P;
int n,m,st,ed;
int dis[N],arr[N][N],check[N];
bool vis[N];

inline void init()
{
    memset(vis,false,sizeof vis);
    memset(dis,INF,sizeof dis);
    memset(check,0,sizeof check);

    for( int i = 0;i < n; ++ i )
    {
        for( int j = i + 1;j < n; ++ j )
        {
            arr[i][j] = arr[j][i] = INF;
        }
        arr[i][i] = 0;
    }
}

void spfa()
{
    queue q;
    vis[st] = true;
    dis[st] = 0;
    q.push(st);

    while(q.size())
    {
        int now = q.front();
        q.pop();
        vis[now] = false;

        for( int i = 0;i < n; ++ i )
        {
            if(dis[i] > dis[now] + arr[now][i])
            {
                dis[i] = dis[now] + arr[now][i];
                if(!vis[i])//把松弛过但不在队列中的边推进去,进而松弛其他边
                {
                    check[i] ++;
                    if(check[i] > n - 1 )
                    {
                        //cout << i << endl;
                        dis[ed] = INF;
                        break;
                    }
                    vis[i] = true;
                    q.push(i);
                }
            }
        }

    }

}

int main()
{
    int a,b,c;
    while(cin >> n >> m)
    {
        init();
        for( int i = 0;i < m; ++ i )
        {
            cin >> a >> b >> c;
            arr[a][b] = arr[b][a] = min(arr[a][b],c);
        }

        cin >> st >> ed;
        spfa();

        cout << (dis[ed] == INF ? -1 : dis[ed]) << endl;
    }
    return 0;
}

//邻接表写法
#include
#define PUSH(x,y,z) G[x].push_back(make_pair(y,z))

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e3 + 5;

typedef pair P;

int n,m,st,ed,dis[N],check[N];
bool vis[N];

vector

G[N]; inline void init() { memset(dis,INF,sizeof dis); memset(vis,false,sizeof vis); memset(check,0,sizeof check); for( int i = 0;i < N; ++ i )G[i].clear(); } void spfa() { queue q; q.push(st); dis[st] = 0; while(q.size()) { int now = q.front(); q.pop(); vis[now] = false;//这里不能丢,如果vis[now]更新了,需要再次放入队列。 for( int i = 0;i < G[now].size(); ++ i ) { int v = G[now][i].first; if(dis[v] > dis[now] + G[now][i].second) { dis[v] = dis[now] + G[now][i].second; if(!vis[v]) { vis[v] = true; q.push(v); } } } } } int main() { int a,b,c; while(cin >> n >> m) { init(); for( int i = 0;i < m; ++ i ) { cin >> a >> b >> c; PUSH(a,b,c); PUSH(b,a,c); } cin >> st >> ed; spfa(); cout << (dis[ed] == INF ? -1 : dis[ed]) << endl; } return 0; }

前向星和链式前向星

前向星

是一种特殊的边集数组,我们把边集数组中的每一条边按照起点从小到大排序,如果起点相同就按照终点从小到大排序,

并记录下以某个点为起点的所有边在数组中的起始位置和存储长度,那么前向星就构造好了.

用len[i]来记录所有以i为起点的边在数组中的存储长度.

用head[i]记录以i为边集在数组中的第一个存储位置.

最终可以达到快速访问指定起点的边集。缺点需要排序,快排nlogn.

链式前向星

事实上根本不需要排序,我们的目的是在一堆数组中直接访问指定起点的边集,我们可以牺牲一些空间保存同一类集合的下标即可,对于每一条边需要保存其起点、终点、长度等。(如果是开太多数组,牺牲太多,又和邻接矩阵没差,小声bb)

对比:链式前向星和领接表的遍历速度都不如邻接矩阵,但存储的边的数量都比邻接矩阵要多,极端情况下邻接表可能会卡vector。

存边

int head[N];//一般初始化为-1

struct node{
    int next,to,w;
}edge[N];

写入

int cnt = 0;
void add(int u,int v,int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];//指向上一条同起点的边
    head[u] = cnt ++;//更新head[u]为当前边
}

其中edge[i].to表示第i条边的终点,edge[i].next表示与第i条边同起点的下一条边的存储位置,edge[i].w为边权值.

另外还有一个数组head[],它是用来表示以i为起点的第一条边存储的位置,实际上你会发现这里的第一条边存储的位置其实

在以i为起点的所有边的最后输入的那个编号.

整数快读模板

int rd()
{
    int res = 0,f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){
        if(ch == '-')f = -1;
        ch = getchar();
    }
    
    while(ch >= '0' && ch <= '9'){
        res = res * 10 + ch - '0';
        ch = getchar();
    }
    
    return f * res;
}

链式前向星 模板题 洛谷P4479

#include

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e5 + 5;
const int M = 2e5 + 5;

typedef pair P;

struct node{
    int next,to,w;
}edge[M];

int n,m,st,ed,cnt;
int head[N],dis[N];
bool vis[N];

inline void init()
{
    cnt = 0;
    memset(head,-1,sizeof head);
    memset(dis,INF,sizeof dis);
    memset(vis,false,sizeof vis);
}

int rd()
{
    int res = 0,f = 1;
    char ch = getchar();

    while(ch < '0' || ch > '9')
    {
        if(ch == '-')f = -1;
        ch = getchar();
    }

    while(ch <= '9' && ch >= '0')
    {
        res = res * 10 + ch - '0';
        ch =getchar();
    }

    return f * res;
}

inline void add(int u,int v,int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];
    head[u] = cnt ++;
}

void dijkstra()
{
    priority_queue,greater

> q; dis[st] = 0; q.push(make_pair(0,st)); while(q.size()) { P now = q.top(); q.pop(); int u = now.second; if(vis[u])continue; vis[u] = true; for( int i = head[u]; ~ i;i = edge[i].next) {//因为head初始化为-1,当i = -1 时结束,因为-1存储形式为111……111 按位去反后为0,所以可以写作 ~ i int v = edge[i].to; if(!vis[v] && dis[v] > dis[u] + edge[i].w) { dis[v] = dis[u] + edge[i].w; q.push(make_pair(dis[v],v)); } } } } int main() { int a,b,c; init(); n = rd(); m = rd(); st = rd(); for( int i = 0;i < m; ++ i ){ a = rd(); b = rd(); c = rd(); add(a,b,c); } dijkstra(); for( int i = 1;i <= n; ++ i ){ printf("%d ",dis[i]); } puts(""); return 0; }

你可能感兴趣的:(算法)