最短路径:BellmanFord / SPFA / Dijkstra

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 0x3fffffff


typedef 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 0x3fffffff


typedef 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 0x3fffffff
int 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  ᐅ  ./spfa
7 9
1 2 10
1 3 2
2 5 1
3 4 2
3 6 11
4 5 4
4 6 6
5 7 7
6 7 3
Input a source point: 1
Vertex: 1
Dist to Source Point: 0
Path: 1
Vertex: 2
Dist to Source Point: 9
Path: 2<--5<--4<--3<--1
Vertex: 3
Dist to Source Point: 2
Path: 3<--1
Vertex: 4
Dist to Source Point: 4
Path: 4<--3<--1
Vertex: 5
Dist to Source Point: 8
Path: 5<--4<--3<--1
Vertex: 6
Dist to Source Point: 10
Path: 6<--4<--3<--1
Vertex: 7
Dist to Source Point: 13
Path: 7<--6<--4<--3<--1

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值