最短路径求解方法

                          从简单到难的最短路径实现方法 测试都是 hdu 2544

DFS-就是深度遍历

这个不会可以自己百度一下原理或者看书,肯定比我讲的好很多(广度遍历 BFS 也是可以的,就不贴了)。直接上代码吧,用模版题检验是否写对,题目是

 

//
//  深度遍历实现最短路.cpp
//  c++ virtureTest
//
//  Created by 刘恒 on 2018/8/19.
//  Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
int map[333][333];
int minDis;
int visit[N];
void dfs(int vertex,int dis,int endNum)
{
    if(minDis < dis)
        return;
    if(vertex == endNum)
    {
        if(minDis > dis)
            minDis = dis;
        return;
    }
    else
    {
        for(int i = 1 ; i <= endNum ; i++)
        {
            if(visit[i] == 0 && map[vertex][i] != inf && i != vertex)
            {
                visit[i] = 1;
                dfs(i, dis+map[vertex][i], endNum);
                visit[i] = 0;//
            }
        }
        return;
    }
    
}
int main()
{
    int n,m;
    while (cin>>n) {
        cin>>m;
        if(n==0 || m==0)
            break;
        memset(map, inf,  sizeof(map));
        memset(visit, 0, sizeof(visit));
        minDis = inf;
        for(int i = 0 ; i< m ; i++)
        {
            int a,b,dis;
            cin>>a>>b>>dis;
            map[a][b] = dis;
            map[b][a] = dis;
        }
        visit[1] = 1;
        dfs(1, 0, n);
        cout<<minDis <<endl;
    }
}

 

弗洛伊德算法-Floyd

这个算法我认为是最好实现的,而且对于顶点少的效果还很好,但是这个算法很简单吗,其实我觉得还是比较难理解的,要知其然而知其所以然话的我觉得比Dijkstra、DFS 要难理解的多,可能你会写,但是问你为什么这样做能保证它是最短路,我觉得有很多人是不知道的。

首先肯定要先初始化,map[N][N] = 无穷大 ,同时输入每个点之间的路径。举个例子,A-F点,首先以A 为中介点,观察图上的任意两点在加入中介点A后距离能不能缩短,如果能,那么最短距离更新为K-A-S(KS为图上任意两点,K也是可以为A 的)实际例子:一开始A-B为无穷大,现在A-B = map[A][B],A-C = map[A][C]等。第二次循环,以B(不一定按顺序,除了A外的任意一个点都可以)为中介点,更新距离,发现map[A][C] > map[A][B] + map[B][C] , 更新。并且以后若以C为中介点更新距离A-C-K,实际路径则是A-B-C-K。

下面是百科的介绍:

1,从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。

2,对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。

把图用邻接矩阵G表示出来,如果从Vi到Vj有路可达,则G[i][j]=d,d表示该路的长度;否则G[i][j]=无穷大。定义一个矩阵D用来记录所插入点的信息,D[i][j]表示从Vi到Vj需要经过的点,初始化D[i][j]=j。把各个顶点插入图中,比较插点后的距离与原来的距离,G[i][j] = min( G[i][j], G[i][k]+G[k][j] ),如果G[i][j]的值变小,则D[i][j]=k。在G中包含有两点之间最短道路的信息,而在D中则包含了最短通路径的信息。

比如,要寻找从V5到V1的路径。根据D,假如D(5,1)=3则说明从V5到V1经过V3,路径为{V5,V3,V1},如果D(5,3)=3,说明V5与V3直接相连,如果D(3,1)=1,说明V3与V1直接相连。

Floyd算法适用于APSP(All Pairs Shortest Paths,多源最短路径),是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法,也要高于执行|V|次SPFA算法

优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。

缺点:时间复杂度比较高,不适合计算大量数据。(补充一下 复杂度为O(3) )

//
//  Floyd.cpp
//  c++ virtureTest
//
//  Created by 刘恒 on 2018/8/18.
//  Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <list>
#include <array>
using namespace std;
#define inf 0x3f3f3f3f
#define N 111
int map[N][N];
void Floyd(int n)
{
    for(int k =1 ; k<=n ;k++)
    {
        for(int i = 1; i<=n ; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                if(map[i][j]>map[i][k]+map[k][j])
                {
                    map[i][j]=map[i][k]+map[k][j];
                }
            }
        }
    }
}
int main()
{
    int n,m;
    while (cin>>n) {
        cin>>m;
        if(n==0 || m==0)
            break;
        memset(map, inf, sizeof(map));
        for(int i= 1; i<=m ; i++)
        {
            int a,b,c;
            cin>>a>>b>>c;
            map[a][b]=c;
            map[b][a]=c;
        }
        Floyd(n);

        cout<<map[1][n]<<endl;
    }
}

 

Dijkstra-迪杰斯特算法

这个反是让我觉得最好理解的一个算法,可是写起来却比较麻烦。这个算法用到了一个贪心的思想。先说用邻接矩阵实现Dijkstra。

邻接矩阵实现Dijkstra

建立两个集合S,V。S集合初始只有起点一个点,V集合有除了起点以外的所有点。初始化dis[N],dis[1] = 0;其余的为无穷大。遍历S和V集合,选择V集合当中到S集合中的点距离最小的那个点,让它从V集合中除去加入到S集合。这样做能保证,每一次找到的点,S-K(K为找到的点)dis[K] 都是所有路径中最小的,不知道你们能不能理解,我举个反例:如果存在某个中介点Q,使得S-Q-K < S-K,那么是不是证明S-Q<S-K,既然S-Q<S-K,那么根据我们的添加规则,Q肯定是先与K加入到S集合当中的。以此类推,最终找到终点就能停止了。

下面是百度百科的算法步骤

把顶点集合V分成两组:

(1)S:已求出的顶点的集合(初始时只含有源点V0)

(2)V-S=T:尚未确定的顶点集合

将T中顶点按递增的次序加入到S中,保证:

(1)从源点V0到S中其他各顶点的长度都不大于从V0到T中任何顶点的最短路径长度

(2)每个顶点对应一个距离值

S中顶点:从V0到此顶点的长度

T中顶点:从V0到此顶点的只包括S中顶点作中间顶点的最短路径长度

依据:可以证明V0到T中顶点Vk的,或是从V0到Vk的直接路径的权值;或是从V0经S中顶点到Vk的路径权值之和

反证法可证)

求最短路径步骤

算法步骤如下:

G={V,E}

1. 初始时令 S={V0},T=V-S={其余顶点},T中顶点对应的距离值

若存在<V0,Vi>,d(V0,Vi)为<V0,Vi>弧上的权值

若不存在<V0,Vi>,d(V0,Vi)为∞

2. 从T中选取一个与S中顶点有关联边且权值最小的顶点W,加入到S中

3. 对其余T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值缩短,则修改此距离值

重复上述步骤2、3,直到S中包含所有顶点,即W=Vi为止。

代码敬上!!

#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
int map[333][333];
int s[333];
int v[333];
int dis[N];

void Dijkstra(int n)
{
    int s_size =1;//S集合的大小
    int v_size = n-1;V集合的大小
    s[1] = 1;
    dis[1] = 0;

    for(int i = 2 ; i <= n ; i++)
    {
        v[i-1] = i;
    }
    for(int i = 1;i<=s_size ; i++)
    {
        int min = inf;
        int _min = 1;
        
        for(int q = 1 ; q<=s_size ; q++)
        {
            for(int j = 1;j<=v_size ; j++)
            {
                int k = map[s[q]][v[j]];
                if( k!= -1)
                {
                    int d = dis[s[q]] + k;
                    if(dis[v[j]] > d)
                    {
                        dis[v[j]] = d;
                    }
                    if(min > dis[v[j]])
                    {
                        min = dis[v[j]];
                        _min = j;
                    }
                }
            }
        }
        s_size ++;
        s[i+1] =v[_min];
        for(int kk = _min ; kk<=v_size-1 ; kk++)
        {
            v[kk] = v[kk+1];
        }
        v_size --;
        if(v_size == 0)
            break;
        i=0;
    }
}
int main()
{
    
    int n,m;
    while (cin>>n) {
        cin>>m;
        if(n==0 || m==0)
            break;
        memset(map, -1,  sizeof(map));
        
        for(int i = 0 ; i< m ; i++)
        {
            int a,b,dis;
            cin>>a>>b>>dis;
            map[a][b] = dis;
            map[b][a] = dis;
            
        }
        
        for(int i = 2 ; i<=n ; i++)//初始化dis
        {
            if(map[1][i]!= -1)
            {
                dis[i] = map[1][i];
            }
            else
            {
                dis[i] = inf;
            }
        }
        //上面全是初始化
        Dijkstra(n);
        cout<<dis[n]<<endl;
    }
}

其实这个代码我写的不好,大家就将就一下,因为第一个写的就是这个,思维比较混乱。

 

接下来讲下下如何用邻接表来实现Dijkstra

用邻接表来实现dijkstra 我觉得可能更加直观点。其实很像BFS上的一种改进。

先定义如下几个基本结构:

class Edge
{
public:
    int vertexId;
    int length;
    Edge *next = nullptr;
};
class Vertex
{
public:
    int id;
    int rank=-1;
    bool isVisit  =false;
    Edge *first = nullptr;
    Edge *last = nullptr;
    void AddEdge(Edge *edge);
};
void Vertex::AddEdge(Edge *edge)
{
    if(this->first == nullptr)
    {
        first = edge;
        last = edge;
    }
    else
    {
        last->next = edge;
        last = edge;
    }
}
class Map
{
public:
    vector<Vertex> vertexs;
    Map();
};
Map :: Map()
{
    for(int i = 0 ; i< N ; i ++)
    {
        Vertex v;
        v.id = i;
        vertexs.push_back(v);
    }
}
class Node
{
public:
    int id;
    int dis;
    friend bool operator < (const Node &node1,const Node &node2)
    {
        return node1.dis < node2.dis;
    }
    Node(int id,int minDis)
    {
        this->id = id;
        this->dis = minDis;
    }
    Node(){}
};

 

邻接表的优点我也就不多说了,省空间。如何创建邻接表。。。。emm看书吧,和DFS,BFS一样,基础内容我都建议看书。

思想其实就是贪心思想,和邻接矩阵是一样的。先说下结构,定义minDis[N] 存储起点到达各个点的最短距离。如果需要将路径输出还能定义一个parent[N]来存储路径。还有记得初始化邻接表。

再创建一个最小堆队列priority_queue<Node> q;Node 是自定义的结构。最小堆队列其实就是根结点永远是最小的,访问方式也只能是q.top()。一开始将起点推入q中。minDis[1] = 0;

例如A-F,遍历与A 相连的结点,将其推入q。由于最小堆,所以我们取出的每个结点都是最短的路径。接下来的思想其实都一样的,不多讲了=-=。下面代码奉上:

//
//  邻接表实现最短路径 Dijkstra.cpp
//  c++ virtureTest
//
//  Created by 刘恒 on 2018/8/16.
//  Copyright © 2018年 刘恒. All rights reserved.
//
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <list>
#include <array>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
class Edge
{
public:
    int vertexId;
    int length;
    Edge *next = nullptr;
};
class Vertex
{
public:
    int id;
    int rank=-1;
    Edge *first = nullptr;
    Edge *last = nullptr;
    void AddEdge(Edge *edge);
};
void Vertex::AddEdge(Edge *edge)
{
    if(this->first == nullptr)
    {
        first = edge;
        last = edge;
    }
    else
    {
        last->next = edge;
        last = edge;
    }
}
class Map
{
public:
    vector<Vertex> vertexs;
    Map();
};
Map :: Map()
{
    for(int i = 0 ; i< N ; i ++)
    {
        Vertex v;
        v.id = i;
        vertexs.push_back(v);
    }
}
class Node
{
public:
    int id;
    int dis;
    friend bool operator < (const Node &node1,const Node &node2) //定义最小堆用的,重载运算符
    {
        return node1.dis < node2.dis;
    }
    Node(int id,int minDis)
    {
        this->id = id;
        this->dis = minDis;
    }
    Node(){}
};

Node minDis[N];
int Parent[N];
priority_queue<Node> q; //最小堆和最大堆。 只能访问根节点
void CreatMap(int n, int m, Map &map)
{
    for(int i = 1 ; i<= m ; i++)
    {
        int a,b,dis;
        cin>>a>>b>>dis;
        Edge *edge1 = new Edge();
        edge1->vertexId = b;
        edge1->length = dis;
        map.vertexs[a].AddEdge(edge1);
        
        Edge *edge2 = new Edge();
        edge2->vertexId = a;
        edge2->length = dis;
        map.vertexs[b].AddEdge(edge2);
    }
}
void Dijkstra(Map &map,int start,int n)
{
    for(int i=1;i<=n ; i++)
    {
        minDis[i].id = i;
        minDis[i].dis = inf;
        Parent[i] = -1;
    }
    minDis[start].dis = 0;
    q.push(minDis[start]);
    while (!q.empty()) {
        Node node = q.top();
        q.pop();
        Edge *p = map.vertexs[node.id].first;
        while (p!=nullptr) {
            if(minDis[p->vertexId].dis > minDis[node.id].dis + p->length)
            {
                minDis[p->vertexId].dis = minDis[node.id].dis + p->length;
                q.push(minDis[p->vertexId]);
                Parent[p->vertexId] = node.id;//需要输出路径的时候可以用到
            }
            p = p->next;
        }
    }
}

int main()
{
    int n,m;
    while (cin>>n) {
        cin>>m;
        if(n==0 || m==0)
            break;
        Map map;
        CreatMap(n, m, map);
        Dijkstra(map, 1, n);
        cout<<minDis[n].dis<<endl;
    }
}

 下面是一些优缺点:

优点:O(N*N),加堆优化:O(N*logN)
缺点:    在单源最短路径问题的某些实例中,可能存在权为负的边。
如果图G=(V,E)不包含从源s可达的负权回路,
则对所有v∈V,最短路径的权定义d(s,v)依然正确,
即使它是一个负值也是如此。但如果存在一从s可达的负回路,
最短路径的权的定义就不能成立。S到该回路上的结点就不存在最短路径。
当有向图中出现负权时,则Dijkstra算法失效。当不存在源s可达的负回路时,
我们可用Bellman-Ford算法实现。

这里说一下为什么Dijkstra 不能有负权,我举个例子:我门每次是不是都要保证S集合中的每一个点都是当前的最短距离,假设V集合中存在点Q ,S集合中存在点A,B,C.dis(A-B) = 4,dis(B-Q) = -4;dis(A-C) = 5;dis(B-C) = 1;dis(C-Q) =1;按照我们的做法,Q是不和A直接相连的,所以第一遍Q肯定没有加入S集合,此时minDis[B] = A-B = 4;而实际上,minDis[B] = 5+1-4 = 2;???对吧如果我门要求的就是AB 的最短距离是不是就出现问题了。

BellMan-Ford 贝尔曼-福特

为了解决Dijkstra算法不能解决负权的问题,所以出现了了BellMan-Ford算法。注意点是,Floyd也是能解决负权边的,但是在顶点太多的时候,效率实在是太低了-0-,毕竟O(3),还有BellMan-Ford还能发现负权环。

BellMan-Ford 算法的实现和Floyd一样简单,简而言之,若有n个顶点,那么就对所有的边的进行n-1次松弛操作,面是松弛操作,其实就是

if(map[i][j]>map[i][k]+map[k][j])

 {

      map[i][j]=map[i][k]+map[k][j];

  }

类似于上面的操作,上面的是floyd的松弛操作。

因为没有前提条件,所以能解决负权边的问题。思想我觉得和floyd挺像的。对边进行n-1次松弛操作,实在懒得讲了,直接上代码,我觉得看了代码以后肯定有感觉!!

#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 333
int minDis[N];
typedef struct Edge
{
    int start = 0;
    int end = 0;
    int weight = inf;
}Edge;
void BellmanFord(Edge edge[],int v_num,int edge_num)
{
    minDis[1] = 0;
    for(int i = 0 ; i < v_num -1 ; i ++)
    {
        for(int j = 0 ; j < edge_num ; j++)
        {
            if(minDis[edge[j].end] > minDis[edge[j].start] + edge[j].weight)
            {
                minDis[edge[j].end] = minDis[edge[j].start] + edge[j].weight;
            }
        }
    }
}
int main()
{
    int n,m;
    while (cin>>n) {
        cin>>m;
        if(n==0 || m==0)
            break;
        Edge edge[100001];
        memset(minDis, inf,  sizeof(minDis));
        for(int i= 0 ; i< m ; i++)
        {
            int a,b,c;
            cin>>a>>b>>c;
            edge[i].start = a;
            edge[i].end = b;
            edge[i].weight = c;
        }
        BellmanFord(edge, n, m);
        cout<<minDis[n]<<endl;
    }
}

这里说下BellMan-Ford 的时间复杂度是O(VE),V是顶点数量,E是边的数量。所以在边数少,顶点数多的时候计算速度会比Floyd要快很多。

SPFA-Bellman的一种改进

SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素 Bellman-Ford 相同,为 O(VE)。(来自百科介绍)

根据上面的BellMan-Ford 我门知道,BellMan-Ford 要对每一条边都进行一次松弛,直到进行n-1次,每一次都能确定一个点的最短路径(我门并不知道是哪个点)。但是用每次都把所有边拿来松弛太浪费了,不难发现,只有那些已经确定了最短路径的点所连出去的边才是有效的,因为新确定的点一定要先通过已知(最短路径的)节点。 详细点的话可以看这两篇博客,我觉得讲的很好https://blog.csdn.net/mmy1996/article/details/52225893 和https://www.sohu.com/a/206351499_479559如果考虑一个随机图(点和边随机生成),除了已确定最短路的顶点与尚未确定最短路的顶点之间的边,其它的边所做的都是无用,因为已经确定的点之间的松弛操作是没有必要的 。具体思想参考一下Dijkstra算法,他们之间其实都是很类似的。

可能讲了那么多还不是很理解,博主我也一样,想了好几天,可能代码都很好实现,一下就写好了,刷刷题什么的都没有什么问题,但我觉得知道算法为什么要这么写还是比较重要的,需要自己多花时间,多看几篇博客。下面就代码奉上了

 

//
//  spfa.cpp
//  c++ virtureTest
//
//  Created by 刘恒 on 2018/8/20.
//  Copyright © 2018年 刘恒. All rights reserved.
//

#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
#define inf 0x3f3f3f3f
#define N 111
int minDis[N];
int map[N][N];
int sign[N];
void SPFA(int nodenum)
{
    queue<int> SP;
    minDis[1] = 0;
    SP.push(1);
    sign[1] = 1;
    while (!SP.empty()) {
        int u = SP.front();
        SP.pop();
        sign[u] = 0;
        for(int i = 1; i <= nodenum ; i++)
        {
            if(i!= u && minDis[i] > minDis[u] + map[u][i])
            {
                minDis[i] = minDis[u] + map[u][i];
                if(sign[i] == 0)
                {
                    SP.push(i);
                    sign[i] = 1;
                }
                
            }
        }
    }
}
int main()
{
    int n,m;
    while (cin>>n) {
        cin>>m;
        if(n==0 || m==0)
            break;
        memset(minDis, inf, sizeof(minDis));
        memset(map, inf, sizeof(map));
        memset(sign, 0, sizeof(sign));
        for(int i= 0 ; i< m ; i++)
        {
            int a,b,c;
            cin>>a>>b>>c;
            map[a][b] = c;
            map[b][a] = c;
        }
        SPFA(n);
        cout<<minDis[n]<<endl;
    }
}

 

总结

      求最短路,正常情况下用dijkstra 就够了,速度一般是最快,如果有负权,可以考虑使用Floyd 和Spfa;需要判断负权环的话就用 SPFA,至于BellMan-Ford看看就好,毕竟效率还是太差了,而且它能做的SPFA都可以,而且还更快是吧==。写的不好,但依旧希望能对你们有点帮助吧。

 

 

 

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值