最短路径:BellmanFord / SPFA / Dijkstra

来源:互联网 发布:淘宝花呗的客服电话 编辑:程序博客网 时间:2024/06/05 17:14

Dijkstra算法只适用于边的权值非负的情况,一般复杂度为O(n*n)。
Bellman-Ford是最简单最粗暴最慢的,复杂度为O(nm),比O(n*n)更复杂(因为边m比点n多),但是适用范围比Dijistra更广,可以应用于边权值为负的情况,但是不能出现负环,要不然就永远收敛不了了。
//BellmanFord只需要存储边,直接将边放到足够大的数组里#include<stdio.h>#include<string.h>#define N 100010#define INF 0x3ffffffftypedef struct Edge{    int begin, end, value;}ELink;ELink edge[N];                      //存储边int dist[N], n, m;void BellmanFord(int start){    int i, j;    dist[start] = 0;                //源点到自己的距离为0    for(i=1; i<=n; i++)             //外层是扫n遍,n个点    {        for(j=1; j<=m+m; j++)       //内层是扫2m遍,因为存储了2m条边        {            if(dist[ edge[j].end ] > dist[ edge[j].begin ] + edge[j].value)     //松弛            {                dist[ edge[j].end ] = dist[ edge[j].begin ] + edge[j].value;            }        }    }}int main(){    int i, a, b, c;        scanf("%d%d", &n, &m);    for(i=1; i<=n; i++)                 //存储        dist[i] = INF;    for(i=1; i<=m; i++)    {        scanf("%d%d%d", &a, &b, &c);        edge[i].begin = a;              //存储边        edge[i].end = b;        edge[i].value = c;        edge[m+i].begin = b;            //反向存储一遍,否则相当于存的是有向边!!!         edge[m+i].end = a;        edge[m+i].value = c;    }            BellmanFord(1);                     //不妨将点1作为源点            for(i=1; i<=n; i++)        printf("%d\n", dist[i]);            //输出源点到第n个点的最短距离    return 0;}

当然,这种无脑松弛会造成很多浪费,比如当前已经松弛完毕了,但是还没有达到n*m次,程序依旧会继续在n*m的循环里走下去,而此后并不会再发生松弛了。所以我们可以设置一个flag,一旦发现没有松弛,直接break,会大大提升效率。
void BellmanFord(int start){    int i, j;    dist[start] = 0;                //源点到自己的距离为0    for(i=1; i<=n; i++)             //外层是扫n遍,n个点    {        int change = 0;        for(j=1; j<=m+m; j++)       //内层是扫2m遍,因为存储了2m条边        {            if(dist[ edge[j].end ] > dist[ edge[j].begin ] + edge[j].value)     //松弛            {                dist[ edge[j].end ] = dist[ edge[j].begin ] + edge[j].value;                change = 1;            }        }        if(!change)        {            break;        }    }}

最后再说明强调一点——如果是无向图,存储边的话一定要正反存两遍,否则会导致无向图存成了有向图,这样就会导致最后的求解错误。
而且,如果想更简单一点的话,直接存成邻接矩阵也是可以的,会简单很多,不过浪费了不少存储空间。

SPFA是最好使相对来说写起来也不复杂,一般正常的情况下复杂度为O(km),也可以理解为O(m),性价比最好!力荐!!!想有更多更深的理解,可以参考如下文章http://blog.sina.com.cn/s/blog_a46817ff01015g9h.html。


SPFA是对Bellman-Ford的一种优化,其核心思路就是:
不必一直不停地松弛下去,如果当前节点A被松弛了(也就是说当前节点到Source Point的距离变小了),那么A的邻接点B(有可能B通过A得到到达Source Point的最短路径)就需要重新松弛一下:如果B经由A到达Source Point为最短路径,那么B一定会被松弛,否则B不会被松弛。

所以,我们需要构建一个队列,当A被松弛之后,所有A的邻接点都要被重新检测一下,看看需不需要被松弛。于是所有A的邻接点入队。之后再从队列中取出一个节点,进行相同的操作,直到队空,证明所有能松弛的都松弛完了,结束。这样就避免了Bellman-Ford盲目不停地松弛n*m次,使得算法效率大大提高。

//由于SPFA对BellmanFord进行了优化,只更新一个点周围的点,//所以需要知道和一个点相连的点都有哪些,因此采用邻接表的形式#include<stdio.h>#include<stdlib.h>#define V 200#define N 10010#define INF 0x3ffffffftypedef struct Edge                     //邻接表的链节点构造{    int to, value;    struct Edge *link;}ELink;typedef struct Ver                      //邻接表的顶点节点构造{    int vertex;    ELink *link;                        //链节点指针}VLink;VLink G[V];                             //顶点节点型数组,最多V个int queue[N], inqueue[N], dist[N];      //队;判断是否在队里;各个点到源点的距离void SPFA(int start){    int top = 0, rear = -1;    ELink *tmp;    dist[start] = 0;                    //此三句对源点进行操作    queue[++rear] = start;    inqueue[start] = 1;    while(top <= rear)                  //直到队空(有点儿像广度搜索)    {        for(tmp = G[queue[top]].link; tmp != NULL; tmp = tmp->link) //遍历队中顶点节点的所有链节点        {            if(dist[tmp->to] > dist[queue[top]] + tmp->value)       //符合条件则松弛            {                dist[tmp->to] = dist[queue[top]] + tmp->value;                if(inqueue[tmp->to] != 1)                           //如果没有入队则入队,并标记                {                    queue[++rear] = tmp->to;                    inqueue[tmp->to] = 1;                }            }        }        inqueue[queue[top]] = 0;               //出队,并更改标记        ++top;    }}void append(int a, int b, int c){    ELink *p, *q;    G[a].vertex = a;    p = (ELink *)malloc(sizeof(ELink));    p->to = b;    p->value = c;    p->link = NULL;    if(G[a].link == NULL)        G[a].link = p;    else    {        q = G[a].link;        while(q->link)            q = q->link;        q->link = p;    }}int main(){    int n, m, i, a, b, c;    scanf("%d%d", &n, &m);    for(i=1; i<=n; i++)                 //从1开始初始化,距离赋INF        dist[i] = INF;    for(i=0; i<m; i++)                  //构造邻接表    {        scanf("%d%d%d", &a, &b, &c);        append(a, b, c);                //eg:构造1-->3的链节点        append(b, a, c);                //eg:构造3-->1的链节点    }    SPFA(3);                            //所有点到第一个点的最短距离    for(i=1; i<=n; i++)                 //输出所有的最短距离        printf("%d ", dist[i]);    return 0;}

上面的算法因为用到了邻接表,所以在邻接表的构造上还是费了不少功夫的。当然不用邻接表也是可以的,使用邻接矩阵会使算法变得更简单易写,不过多浪费了存储空间:
#include <iostream>using namespace std;#define N 10000                     // 边的最大个数#define V 200                       // 顶点的最大个数#define INF 0x3fffffffint edge[N][N];                     // 邻接矩阵,记录边int dist[V], queue[V], inqueue[V];  // 到源点的距离;队列;是否入队int pre[V];                         // 记录每个点的前导点,用以输出路径int n, m;                           // 顶点个数;边的个数void init()                         // 初始化edge/dist/pre{    cin >> n >> m;    for(int i = 1; i <=n; i++){        dist[i] = INF;        pre[i] = i;                 // 每个点的终结点为自己    }    for(int i = 1; i <= n; i++){        for(int j = 1; j <=n; j++){            edge[i][j] = INF;        }    }        int row, col, value;    while(m--){        cin >> row >> col >> value;        edge[row][col] = edge[col][row] = value;    }}void SPFA(int start){    int top = 0, rear = -1;    dist[start] = 0;                // 初始化source point    queue[++rear] = start;    inqueue[start] = 1;    while(top <= rear){        int vertex = queue[top];        for(int i = 1; i <= n; i++){            int value = edge[vertex][i];            if(value < INF && dist[i] > dist[vertex] + value){                dist[i] = dist[vertex] + value;                pre[i] = vertex;                if(!inqueue[i]){                    queue[++rear] = i;                    inqueue[i] = 1;                }            }        }        ++top;        inqueue[vertex] = 0;    }}void printPath(int v)               // 按照pre逆序输出路径  {    while(v != pre[v]){        cout << v << "<--";        v = pre[v];    }    cout << v << endl;}int main(){    init();    cout << "Input a source point: ";    int start;    cin >> start;    SPFA(start);    for(int i = 1; i <=n; i++){        cout << "Vertex: " << i << endl            << "Dist to Source Point: " << dist[i] << endl            << "Path: ";        printPath(i);    }    return 0;}

算法运行结果如下:
pArch➜  tmp  ᐅ  ./spfa7 91 2 101 3 22 5 13 4 23 6 114 5 44 6 65 7 76 7 3Input a source point: 1Vertex: 1Dist to Source Point: 0Path: 1Vertex: 2Dist to Source Point: 9Path: 2<--5<--4<--3<--1Vertex: 3Dist to Source Point: 2Path: 3<--1Vertex: 4Dist to Source Point: 4Path: 4<--3<--1Vertex: 5Dist to Source Point: 8Path: 5<--4<--3<--1Vertex: 6Dist to Source Point: 10Path: 6<--4<--3<--1Vertex: 7Dist to Source Point: 13Path: 7<--6<--4<--3<--1

最后说的是Dijkstra算法,如果不优化则复杂度为O(n*n),相对来说最难写,且复杂度还不如SPFA好,所以性价比不是最好,但是跟Prim求最小生成树极其相似,两个算法可以想通。详情请参考http://blog.csdn.net/puppylpg/article/details/41440459,这里就不再贴代码了。

而且,在这些点存储的过程中,点从1到n,所以我更倾向于从数组的[1]开始存储,感觉这样的话在很多方面写起来都比较容易。

0 0
原创粉丝点击