最短路径

问题背景:地图上有很多个城市,已知各城市之间距离(或者是所需时间,后面都用距离了),一般问题无外乎就是以下几个:

  • 从某城市到其余所有城市的最短距离【单源最短路径】
  • 所有城市之间相互的最短距离【任意两点最短路径】
  • 各城市距离一致,给出需要最少中转方案 【最少中转】

深度优先搜索

适用范围:啥都不适用,只能处理n<10的情况

深搜求最短路径的思想和用深搜迷宫寻路有一点像,找出所有的从起点目标点的路径,选出其中最短的一条。

此算法仅供娱乐参考,实际不会用它的,因为算法复杂度是$O(n!)$

深度优先搜索:

const int inf =  << ;

int M[][];
bool fuck[];
int n, res; //cur-当前所在城市编号,dis-当前已走过的路径
void dfs(int cur, int dis) {
//若当前的路径值已比之前找到的最短路大,没必要继续往下搜索了,其实没什么必要,深搜本来就属于暴力算法,这个小优化属于杯水车薪
if (dis > res)
return;
//当前已到达目的城市,更新min
if (cur == n) {
res = min(res, dis);
return;
} //对1~n号城市依次尝试
for (int i = ; i <= n; i++) {
//若cur与i之间有路,且i没有在已走过的路径中
if (M[cur][i] != inf && !fuck[i]) {
fuck[i] = true; //标记i为已走的路径
dfs(i, dis + M[cur][i]); //继续搜索
fuck[i] = false; //回溯
}
}
}
#include <iostream>
#include <algorithm>
#include <fstream>
#include <cstdio>
#include <queue> using namespace std;
const int inf = << ; int M[][];
int path[];
bool fuck[];
int n, m, res = inf, cnt; //cur-当前所在城市编号,dis-当前已走过的路径
void dfs(int cur, int dis,int destination,int k) {
//若当前的路径值已比之前找到的最短路大,没必要继续往下搜索了,其实没什么必要,深搜本来就属于暴力算法,这个小优化属于杯水车薪
//if (dis > res)
// return;
//当前已到达目的城市,更新min
if (cur == destination) {
res = min(res, dis);
//cnt++;
for (int i = ; i < k; i++) {
cout << path[i] << ' ';
}
cout << endl;
return;
} //对1~n号城市依次尝试
for (int i = ; i <= n; i++) {
//若cur与i之间有路,且i没有在已走过的路径中
if (M[cur][i] != && M[cur][i] != inf && !fuck[i]) {
fuck[i] = true; //标记i为已走的路径
path[k] = i;
dfs(i, dis + M[cur][i], destination, k + ); //继续搜索
fuck[i] = false; //回溯
}
}
} int main() {
#ifdef LOCAL
fstream cin("data.in");
#endif // LOCAL
cin >> n >> m;
for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
if (i != j)
M[i][j] = inf;
}
} for (int i = ; i < m; i++) {
int c1, c2, c3;
cin >> c1 >> c2 >> c3;
M[c1][c2] = c3;
M[c2][c1] = c3;
}
cnt = ;
res = inf;
fuck[] = true;
path[] = ;
dfs(, , , ); return ;
}

完整测试代码

宽度优先搜索

适用范围:最少中转方案,处理n的级别看脸

假如现在是最少中转方案问题(或者所有边的权值一致) ,问从城市1到城市4需要经过的最少中转城市个数。

这类问题和宽搜求迷宫的最短路径思想完全一样,从开始点逐层扩展,找到目标停止。

宽搜的算法复杂度也是$O(n!)$,不过看脸,如果在前面几层就找到目标了,就比较快。

也就是目标点需要中转几次,如果一次都不要中转,那么第二层就能搜索到;如果需要中转n-2次,那就得搜索到最后一层,就也是$O(n!)$了

宽度优先搜索:

int M[][];
int path[];
bool fuck[];
int n, m, res = inf, cnt; int bfs(int start, int destination){
queue<pair<int, int>> q; //城市编号、当前是第几座城市
q.push({ start, }); //把起始点加入队列
fuck[start] = true; //标记为已在路径中
while (!q.empty()){
int cur = q.front().first, dis = q.front().second;
q.pop();
for (int i = ; i <= n; i++) {
//如果当前点到i点有路,并且当前还没有加入队列中
if (M[cur][i] != inf && !fuck[i]) {
q.push({ i,dis + });
fuck[i] = true;
if (i == destination) //如果发现了目标点
return dis;//这里具体是算多少步看题目咋问了
}
}
}
}
#include <iostream>
#include <algorithm>
#include <fstream>
#include <cstdio>
#include <queue> using namespace std;
const int inf = << ; int M[][];
int path[];
bool fuck[];
int n, m, res = inf, cnt; int bfs(int start, int destination){
queue<pair<int, int>> q; //城市编号、当前是第几座城市
q.push({ start, }); //把起始点加入队列
fuck[start] = true; //标记为已在路径中
while (!q.empty()){
int cur = q.front().first, dis = q.front().second;
q.pop();
for (int i = ; i <= n; i++) {
//如果当前点到i点有路,并且当前还没有加入队列中
if (M[cur][i] != inf&& !fuck[i]) {
q.push({ i,dis + });
fuck[i] = true;
if (i == destination) //如果发现了目标点
return dis;//这里具体是算多少步看题目咋问了
}
}
}
} int main() {
#ifdef LOCAL
fstream cin("data.in");
#endif // LOCAL
cin >> n >> m;
for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
if (i != j)
M[i][j] = inf;
}
} for (int i = ; i < m; i++) {
int c1, c2, c3;
cin >> c1 >> c2 >> c3;
M[c1][c2] = c3;
M[c2][c1] = c3;
} cout << bfs(, ); return ;
}

完整测试代码

这两个算法我觉得算是迷宫寻路算法的延伸,可以看下迷宫寻路问题全解,用在求最短路径中的话,效率太低,无法解决实际问题。

接下来才是重点。

迪杰斯特拉(Dijkstra)算法

适用范围:不含负权边的单源最短路径、最少中转

不含负权边就是所有路径长度大于0,牵扯到负权边,请参考 Bellman-Ford算法

思路图解

维护一个$dis$数组记录起点(按题目要求来,这里取$1$) 到达的所有节点的距离。(规定到自己的路径长度0,到不了的点是 inf(极大值))

找出当前距离$1$最近的结点:$4$。(已经访问过的,我们标记为红色,不再次访问)

借助$4$节点,对$dis$数组进行更新(就是如果结点$1$借助结点$4$到别的结点有更短的路径,就对$dis$数组进行值替换)

找出当前距离$1$最近的结点:$2$。

走到$2$,无法更新$dis$数组,无操作。

找出当前距离$1$最近的结点:$3$。

借助$3$节点,对$dis$数组进行更新,最后走到$5$节点,退出。(实际过程中,走到最后一个节点,别的节点都访问过,进行标记了,什么也不会做)。

这个时候$dis$数组就是从起点$1$到所有节点的最短路径了,如果还有$inf$表示不是连通图。

简单版(邻接矩阵+优先级队列):

测试题目:http://acm.hdu.edu.cn/showproblem.php?pid=2544 (数据很弱,建议再做后面一题)

#include <fstream>
#include <iostream>
#include <queue>
#include <algorithm>
#include <string.h>
using namespace std; const int inf = << ;
int n, m;
bool book[];
int M[][];
int dis[]; class P {
public:
int to, dis;
P(int t, int d) :to(t), dis(d) {} bool operator< (P a) const {
return a.dis < dis;
}
}; priority_queue<P>q;
void initialize() {
fill(book, book + n + , false);
fill(dis, dis + n + , inf);
for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
if (i != j)M[i][j] = inf;
}
}
}
void dijkstra() {
dis[] = ;
q.push({ , });
while (!q.empty()) {
int v = q.top().to; q.pop();
if (book[v])continue;
book[v] = true;
for (int i = ; i <= n; i++) {
if (!book[i] && dis[i] > dis[v] + M[v][i]) {
dis[i] = dis[v] + M[v][i];
q.push({ i, dis[i] });
}
}
}
} int main() {
#ifdef LOCAL
fstream cin("data.in");
#endif // LOCAL
while (cin >> n >> m) {
if (n == && m == )break;
initialize();
for (int i = ; i < m; i++) {
int A, B, C;
cin >> A >> B >> C;
M[A][B] = C;
M[B][A] = C;
}
dijkstra();
cout << dis[n] << endl;
}
return ;
}

正式版(邻接表+优先级队列)

测试题目:https://www.luogu.org/problemnew/show/P4779

#include <fstream>
#include <iostream>
#include <stdio.h>
#include <queue>
using namespace std;
int dis[];
bool fuck[];
const int inf = << ;
int n, m, s;
struct ENode {
int to, dis;
ENode* next = NULL;
ENode() {}
ENode(int t, int d) :to(t), dis(d) {}
void push(int t, int d) {
ENode* p = new ENode(t, d);
p->next = next;
next = p;
} bool operator<(ENode e)const {
return e.dis < dis;
}
}head[]; void dijkstra() {
priority_queue<ENode>q;
fill(dis, dis + n + , inf);
dis[s] = ;
q.push(ENode(s, ));
while (!q.empty()) {
//获得当期距离 源点 最近的点
int v = q.top().to, d = q.top().dis; q.pop();
if (fuck[v])continue;
fuck[v] = true;
ENode* p = head[v].next;
while (p) {
int to = p->to;
if (!fuck[to] && dis[to] > d + p->dis) {
dis[to] = d + p->dis;
q.push(ENode(to, dis[to]));
}
p = p->next;
}
} } int main() {
#ifdef LOCAL
fstream cin("data.in");
#endif // LOCAL
int c1, c2, c3;
cin >> n >> m >> s;
for (int i = ; i < m; i++) {
//cin >> c1 >> c2 >> c3;
scanf("%d%d%d", &c1, &c2, &c3);
head[c1].push(c2, c3);
}
dijkstra();
for (int i = ; i <= n; i++) {
printf("%d ", dis[i]);
}
cout << endl;
return ;
}

提一句如果是要求找最少中转方案,那么就把每个边的权值都设为1,在求最短路径即可。

时间复杂度分析

一般默认迪杰斯特拉算法复杂度为$O(n^2)$,也就是每次从$dis$中获取路径最短的结点,需要花费线性的时间$O(n)$,但这是普通情况下。【$n$为顶点数】使用优先级队列后,从$dis$中获取路径最短的结点只需要$O(logn)$(因为我们用了一个标记数组,所以堆中的数据个数不可能会超过$n$,所以是$O(logn)$,如果没有加这个复杂度是$O(logm)$,m为边的个数)。所以,堆优化的迪杰斯特拉算法时间复杂度为$O((m+n)logn)$。

关于负权边

$Dijkstra$是一种基于贪心策略的算法。每次新扩展一个路径最短的点,更新与它相邻的所有点。当所有边权为正时,由于不会存在一个路程更短的没扩展过的点,所以这个点的路程就确定下来了,这保证了算法的正确性。但也正因为这样,这个算法不能处理负权边,因为扩展到负权边的时候,某个点会产生更短的路径,但可能该点已被标记。

比如这张图,按照Dijkstra算法,假如起点是A,一定会先找到C,并且认为已经找到A到C最短路径,在没有负边的时候是这样的,但现在B到C是-2,这就出现错误了。

Floyd算法

Floyd算法属于动态规划,实现容易,好理解,但缺点就是时间复杂度高是$O(n^3)$。

$M [ j ] [ k ]$ 表示从$ j$ 到 $k$ 的路径,而 $i$ 表示当前 $j$ 到 $k$ 可以借助的点;红色部分表示,如果 $j$ 到 $i$ ,$i$ 到 $k$ 是通的,就将 $j$ 到 $k$ 的值更新为$min(M[j][i] + M[i][k],M[j][k] )$

for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
for (int k = ; k <= n; k++) {
if (j != k && M[j][i] != inf && M[i][k] != inf)
M[j][k] = min(M[j][i] + M[i][k], M[j][k]);
}
}
}

给个题目链接,可以交试一下:http://www.dotcpp.com/oj/problem1709.html

#include <iostream>
#include <queue>
using namespace std; #define inf 2147483647
int M[][]; int main() {
int n;
queue<int>q;
cin >> n;
for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
cin >> M[i][j];
if (M[i][j] == && i != j)M[i][j] = inf;
}
}
for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
for (int k = ; k <= n; k++) {
if (M[j][k] != ) {
if (M[j][i] != inf && M[i][k] != inf) {
M[j][k] = M[j][i] + M[i][k] < M[j][k] ? M[j][i] + M[i][k] : M[j][k];
}
}
}
}
} for (int i = ; i <= n; i++) {
for (int j = ; j <= n; j++) {
if (M[i][j] == inf)cout << - << " ";
else
cout << M[i][j] << " ";
}
cout << endl;
} return ;
}

完整代码

Dijkstra & Floyd 对比

$Dijkstra$算法的复杂度为$O(n^2)$【不考虑堆优化的情况】,如果采用Dijkstra算法来计算图中任两点之间的最短距离,复杂度也为$O(n^3)$,虽然复杂度相同,但是看代码,两个算法运算量差了很多,也就是$Dijkstra$算法输在了常数项。但是堆优化后的$Dijkstra$算法,还是要完全优于$Floyd$算法的。

对比:

图论篇3——最短路径 Dijkstra算法、Floyd算法的更多相关文章

  1. 算法学习笔记(三) 最短路 Dijkstra 和 Floyd 算法

    图论中一个经典问题就是求最短路.最为基础和最为经典的算法莫过于 Dijkstra 和 Floyd 算法,一个是贪心算法,一个是动态规划.这也是算法中的两大经典代表.用一个简单图在纸上一步一步演算,也是 ...

  2. 多源最短路径算法—Floyd算法

    前言 在图论中,在寻路最短路径中除了Dijkstra算法以外,还有Floyd算法也是非常经典,然而两种算法还是有区别的,Floyd主要计算多源最短路径. 在单源正权值最短路径,我们会用Dijkstra ...

  3. Dijkstra与Floyd算法

    1. Dijkstra算法 1.1 定义概览 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点 ...

  4. [链接]最短路径的几种算法[迪杰斯特拉算法][Floyd算法]

    最短路径—Dijkstra算法和Floyd算法 http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html Dijkstra算 ...

  5. JS实现最短路径之弗洛伊德(Floyd)算法

    弗洛伊德算法是实现最小生成树的一个很精妙的算法,也是求所有顶点至所有顶点的最短路径问题的不二之选.时间复杂度为O(n3),n为顶点数. 精妙之处在于:一个二重初始化,加一个三重循环权值修正,完成了所有 ...

  6. 图的最短路径算法-- Floyd算法

    Floyd算法求的是图的任意两点之间的最短距离 下面是Floyd算法的代码实现模板: ; ; // maxv为最大顶点数 int n, m; // n 为顶点数,m为边数 int dis[maxv][ ...

  7. 最短路-SPFA算法&Floyd算法

    SPFA算法 算法复杂度 SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环. SPFA一般情况复杂度是O(m)最坏情况下复杂度和朴素 ...

  8. 只有5行代码的算法——Floyd算法

    Floyd算法用于求一个带权有向图(Wighted Directed Graph)的任意两点距离的算法,运用了动态规划的思想,算法的时间复杂度为O(n^3).具体方法是:设点i到点j的距离为d[i][ ...

  9. Dijkstra and Floyd算法

    Dijkstra算法 算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集 ...

随机推荐

  1. 面向对象的理解 抽象类&接口

    一.关于面向对象 1.什么是面向对象     在解释面向对象之前,先说说面向过程.学过C的同学都知道,C就是面向过程的一种语言.那什么是面向过程呢?比方说组装主机,对于面向过程,需要从0开始.买cpu ...

  2. IE浏览器卡死提示是否停止运行此脚本的解决办法

    IE浏览器经常卡死,报是否停止运行此脚本,严重影响使用体验,下面小编教大家怎么解决这个问题,供大家参考! 1.启动IE浏览器,点击上方菜单栏位的工具,如下图所示 2.在工具栏位选择internet选项 ...

  3. cmder是一个增强型命令行工具,不仅可以使用windows下的所有命令,更爽的是可以使用linux的命令,shell命令。

    cmder使用简介 Cmder is a software package created out of pure frustration over the absence of nice conso ...

  4. List Map Set的线程安全

    常见的ArrayList  LinkedList  HashMap TreeMap LinkedHashMap HashSet TreeSet LinkedHashSet 都是线程不安全的.如果要使用 ...

  5. Apache Kafka - How to Load Test with JMeter

    In this article, we are going to look at how to load test Apache Kafka, a distributed streaming plat ...

  6. Element 表单验证,不清空数据,仅仅取消表单字段校验

    重置表单 this.$refs['ageForm'].resetFields() // 表单重置 仅清空校验 this.$refs['ageForm'].clearValidate() // 清除验证

  7. 66 网络编程(五)——TCP多线程实现多人聊天室

    思路 客户端读写各一个类,可以使内部类,实现Runnable.读写类都与服务器端建立连接,一个收,一个发. 客户端实现接收和转发.多线程实现每个客户端的连接(使与各客户端的连接独立). 服务器端中创建 ...

  8. Docker 搭建简单 LVS

    LVS简介 LVS(Linux Virtual Server)即Linux虚拟服务器,是由章文嵩博士主导的开源负载均衡项目,目前LVS已经被集成到Linux内核模块中.该项目在Linux内核中实现了基 ...

  9. 使用预编译库PREBUILT LIBRARY官方说明

    使用预编译库 NDK 支持使用预编译库(同时支持静态库和共享库).此功能有以下两个主要用例: 向第三方 NDK 开发者分发您自己的库(而不分发您的源代码). 使用您自己的库的预编译版本来提升编译速度. ...

  10. Java分布式唯一ID生成方案——比UUID效率更高的生成id工具类

    package com.xinyartech.erp.core.util; import java.lang.management.ManagementFactory; import java.net ...