【算法学习】最短路径

最短路径

最短路径算法思想包括 Floyd,Dijskra,Bellman-Ford三大类
学好算法思想,代码不就呈现在眼前了吗?
只需要代码的直接文末伺候
首先出场的是代码量最简洁[4行],但是复杂度最高[O(n^3)]的Floyd算法

Floyd

Floyd算法思想比较容易理解。
比如当我们在寻找两个节点之间最短距离的时候,最容易想到的就是列出所有可能的路径,然后选择路径最小值。
至于如何列出所有可能的路径,当然是一个一个增加节点的数量来列出路径,例如不经过任何一个节点 v0->vn,经过一个节点 v0 -> v1 ->vn,两个节点 v0 -> v1 -> v2 -> vn,直到n-1个节点 v0->v1->v2->…->vn。
当列出所有路径的权值时,就可以得到最短路径了。
对于上面的思想,我们容易理解,但是计算机不太好理解。我们需要转换一下,例如我们可以这么考虑,当考虑点 vi 到点 vj 的最短距离时:

假设edge[i][j] 记录点 i 到点 j 的距离。
(便于计算机循环,从点v0开始考虑)
经过一个节点[v0]时,判断 edge[i][j] 和 edge[i][0]+edge[0][j] 取最小值更新edge[i][j]路径信息 
经过两个节点[v0,v1],判断  edge[i][j] 和 edge[i][1]+edge[1][j] 取最小值更新edge[i][j]路径信息
...
经过n-1个节点[v0,v1,...,vn-2],判断  edge[i][j] 和 edge[i][n-2]+edge[n-2][j] 取最小值更新edge[i][j]路径信息

到这里不免会有疑问,为什么增加节点数目,只考虑后面增加的节点情况呢。
这也是Floyd算法精妙的地方,它利用动态规划的方法,遍历每一个经过点v0的路径进行更新。举个例子,假如刚开始如下图:
在这里插入图片描述
Floyd算法会首先遍历搜索所有经过点0的路径进行更新,更新结束后的结果如下图:
在这里插入图片描述
这样的话就容易理解了!!
所以Floyd的算法最最最最重要的就是对每一个点,遍历所有经过这个点的路径进行更新,然后相当于删除这个点。
这样的话,代码就呈现在眼前了~~

for(int k = 0; k < n; ++k)
    for(int i = 0; i < n; ++i)
        for(int j = 0; j < n; ++j) 
                edge[i][j] = ((edge[i][j] > edge[i][k] + edge[k][j])?(edge[i][k] + edge[k][j]) :  edge[i][j] );

其次向我们走来的Dijstra算法,它是处理单源点无负权的优先算法,算法复杂度[O(n^2)]

Dijskra

算法的原理是比较好理解的,举个例子可以更好的说明,如下:
还是用这个图举例,加入要计算出以 i 为源点到其它各点的最短距离
在这里插入图片描述

Round01j{ i }
1i->0 (6)i->1 (1)i->j (6){ i,1 }
2i->1->0 (2)i->1->j (5){ i,1,0 }
3i->1->0->j (4){ i,1,0,j }

算法的思想就是在最开始的路径中找出权值最小(与此点的最短路径)的点,然后更新路径,继续选择还没有收录点中的最小值(与此点的最短路径),更新路径,如此往复,直至把所有的点都收录。
算法思想倒是比较容易理解,但是代码却不如Floyd算法那么简洁,但其实只要我们把这个算法一个步骤一个步骤剖析出来,也就容易比较好写了。
首先,我们需要两个数组,一个数组记录点与点直接的最短距离,这个数组的原始值为源点到其它各个点的距离。另一个数组记录该点是否被收录。
接着就是在未收录的点中寻找最短路径、更新路径的循环中,直至所有点被收录(其实也就是n-1次)。
代码这不就出现了吗:

path[n];//记录源点到其他点的最短距离
flag[n];//记录结点是否被确认,1表示已被收录
//初始化 path 和 flag,假设源点为s
flag[n] = {0};
flag[s] = 1;
for(int i = 0; i < n; ++i) {
	//edge[][] 记录点与点之间的距离
    path[i] = edge[s][i];
}
//寻找n-1次
for(int i = 0; i < n-1; ++i) {
    int shorter = max;
    int node = 0;
    //寻找当前最短的路径
    for(int j = 0; j < n; ++j) {
        if(!flag[j] && shorter > path[j]) {
            shorter = path[j];
            node = j;
        }
    }
    //更新标志位
    flag[node] = 1;
    //更新路径
    for(int j = 0; j < n; ++j) {
    	//只更新未被收录的点的路径,这存在一个动态规划的思想,可以看上面Floyd算法
        if(!flag[j]) {
            if(path[j] > path[node] + edge[node][j]) {
                path[j] = path[node] + edge[node][j];
            }
        }
    }
}

如果当出现负权的时候,使用Dijskra算法可能会失去作用,例如
在这里插入图片描述
根据Dijskra,则会出现错误的结果。所以为了解决单源点存在负权(必须无负环,否则不存在最短路径)图中的最短路径,就有了Bellman-Ford算法,时间复杂的为O(n*m)。

Bellman-Ford

其实如果了解时间复杂的话,我们就可以知道这个算法与边数(m)相关。
对于边,可以创建一个结构记录起点,终点以及权值方便处理。

struct Edge{
    int start,end,weight;
};

Bellman-ford算法其实就是对起点不为空的边进行松弛优化,何为松弛,就是逐步缩小两点之间的距离。用数组dis[]记录从源点到节点的距离,初始值为Max,源点为0;
还是举例说明,就用上面的图吧,源点为0:
在这里插入图片描述
既然是对边进行循环迭代松弛,所以起始状态是独自分离的点,
dis[] = {0,Max,Max,Max};
在这里插入图片描述
遍历第一次后的情况为,
dis[] = {0,3,5,Max};
在这里插入图片描述
遍历第二次后的情况为,
dis[] = {0,3,5,6};
在这里插入图片描述
遍历第三次后的情况为,
dis[] = {0,3,3,6};
在这里插入图片描述
到此Bellman-ford算法就算是结束了。
下面看一下Bellman-ford算法的写法,相对于Dijskra,代码量是减少了一点,但是算法思想需要自己琢磨,其实也就是动态规划的问题。

for(int i = 0; i < n; ++i) {
    for(int j = 0; j < m; ++j) {
        if(dis[e[j].start] != M && dis[e[j].end] > dis[e[j].start] + e[j].weight) {
            dis[e[j].end] = dis[e[j].start] + e[j].weight;
            //如果迭代了n次 则存在负环
            if(i == n-1) {
                cout << "存在负环" << endl;
                return false;
            }
        }
    }
}
优化思想
1 设置flag当迭代时不再更新节点的时候,取消迭代;
2 设置队列来维护节点,只对松弛成功的节点进行迭代松弛,也就是SPFA算法

打印路径问题

上面的几个算法,都可以打印出最短路径数组。但是如果我们想要打印出路径的话,我们需要如何处理呢。
这里我们可以利用一个数组记录节点的直接前驱,当我们记录节点的前驱之后,则就可以用递归或者栈来实现路径打印了。

Floyd全部代码

#include <iostream>
#include <stack>

using namespace std;

int main() {
    int max = 999999;
    int n[5][5] = {0, 4, max, 2, max,
                   4, 0, 4, 1, max,
                   max, 4, 0, 1, 3,
                   2, 1, 1, 0, 7,
                   max, max, 3, 7, 0};
    //path[][] 记录每个节点的直接前驱
    int path[5][5];
    for(int i = 0; i < 5; ++i) {
        for(int j = 0; j < 5; ++j) {
            cout << n[i][j] << "\t";
            if(i != j && n[i][j] < max) path[i][j] = i;
            else path[i][j] = -1;
            }
        cout << endl;
    };
    //floyd算法
    for(int k = 0; k < 5; ++k) {
        for(int i = 0; i < 5; ++i) {
            for (int j = 0; j < 5; ++j) {
                if(n[i][j] > n[i][k] + n[k][j]) {
                    n[i][j] = n[i][k] + n[k][j];
                    path[i][j] = path[k][j];
                }
            }
        }
    }
    for(int i = 0; i < 5; ++i) {
        for(int j = 0; j < 5; ++j) {
            cout << n[i][j] << " ";
        }
        cout << endl;
    };
    for(int i = 0; i < 5; ++i) {
        for(int j = 0; j < 5; ++j) {
                cout << path[i][j] << " ";
        }
        cout << endl;
    };
    for(int i = 0; i < 5; ++i) {
        for(int j = 0; j < 5; ++j) {
            if(i == j) continue;
            cout << "from "<< i << " to " << j <<" is " << n[i][j] << " : " << i;
            stack<int> s;
            s.push(j);
            while(path[i][s.top()] != i) {
                s.push(path[i][s.top()]);
            }
            while(!s.empty()) {
                cout << "->" << s.top();
                s.pop();
            }
            cout << endl;
        }
    }
    return 0;
}

Dijskra全部代码

#include <iostream>
#include <stack>

using namespace std;

#define max 999999

void Dijskra(int* n, int start) {
    int nodes[5][5];
    for(int i = 0; i < 5; ++i) {
        for(int j = 0; j < 5; ++j) {
            nodes[i][j] = *n++;
            }
    };
    int path[5], prenode[5];
    bool flag[5] = {false};
    flag[start] = true;
    //初始化,path记录以start为源点到其他点的最短路径,flag记录节点是否找到,prenode记录节点前驱
    for(int i = 0; i < 5; ++i) {
        path[i] = nodes[start][i];
        prenode[i] = start;
    }
    //count为节点数-1
    for(int count = 0; count < 4; ++count) {
        int shortpath = max;
        int i = 0;
        //寻找最短路径节点
        for(int j = 0; j < 5; ++j) {
            if(!flag[j] && path[j] < shortpath) {
                shortpath = path[j];
                i = j;
            }
        }
        flag[i] = true;
        //更新路径信息
        for(int j = 0; j < 5; ++j) {
            if(!flag[j]) {
                if(path[j] > path[i] + nodes[i][j]) {
                    path[j] = path[i] + nodes[i][j];
                    prenode[j] = i;
                }
            }
        }
    }
    for(int i = 0; i < 5; ++i)
        cout << prenode[i] << "\t";
    cout << endl;
    for(int i = 0; i < 5; ++i) {
        if(start == i) continue;
        cout << "from " << start << " to " << i << " is "<< path[i] << " : " << start;
        stack<int> s;
        s.push(i);
        while(prenode[s.top()] != start) {
            s.push(prenode[s.top()]);
        }
        while(!s.empty()) {
            cout << "->" << s.top();
            s.pop();
        }
        cout << endl;
    }
}

int main() {
    int n[] = {0, 4, max, 2, max,
               4, 0, 4, 1, max,
               max, 4, 0, 1, 3,
               2, 1, 1, 0, 7,
               max, max, 3, 7, 0};
    cout << "info :" << endl;
    for(int i = 1; i < 26; ++i) {
        cout << n[i-1] << "\t";
        if(i % 5 == 0)cout << endl;
    };
    cout << "input your start node:" << endl;
    int start;
    while(cin >> start && start < 5) {
        //传数组
        Dijskra(&n[0], start);
    }
    return 0;
}

Bellman-Ford全部代码

#include <iostream>
#include <stack>

using namespace std;

#define M 999999

struct Edge{
    int start;
    int end;
    int weight;
};

bool BellmanFord(Edge *e,int n, int m) {
    for(int i = 0; i < m; ++i) {
            cout << e[i].start << "->" << e[i].end << " " << e[i].weight  << endl;
    };
    int begin;
    cout << "please input begin node:" << endl;
    cin >> begin;
    //初始化dis
    int dis[n], prenode[n];
    for(int i = 0; i < n; ++i) {
        dis[i] = M;
        prenode[i] = i;
    }
    dis[begin] = 0;
    //迭代松弛,正常情况下迭代n-1次
    for(int i = 0; i < n; ++i) {
        for(int j = 0; j < m; ++j) {
            if(dis[e[j].start] != M && dis[e[j].end] > dis[e[j].start] + e[j].weight) {
                dis[e[j].end] = dis[e[j].start] + e[j].weight;
                prenode[e[j].end] = e[j].start;
                //如果迭代了n次 则存在负环
                if(i == n-1) {
                    cout << "存在负环" << endl;
                    return false;
                }
            }
        }
    }
    for(int i = 0; i < n; ++i) {
        cout << dis[i] << " ";
    }
    cout << endl;
    for(int i = 0; i < n; ++i) {
        cout << prenode[i] << " ";
    }
    cout << endl;
    for(int i = 0; i < n; ++i) {
        if (i == begin || i == prenode[i]) continue;
        cout << "from " << begin << " to " << i << " is "<< dis[i] << " : " << begin;
        stack<int> s;
        s.push(i);
        while(prenode[s.top()] != begin) {
            s.push(prenode[s.top()]);
        }
        while(!s.empty()) {
            cout << "->" << s.top();
            s.pop();
        }
        cout << endl;
    }
    return true;
}

int main() {
    int n,m;
    cout << "please input nodes and edge number:" << endl;
    cin >> n >> m;
    Edge e[m];
    for(int i = 0; i < m; ++i) {
        cin >> e[i].start >> e[i].end >> e[i].weight;
    }
    BellmanFord(&e[0], n, m);
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值