<数据结构>图的最短路径问题
最短路径问题
- 单源最短路径问题:Dijstra[不带负权边]\Bell_Ford(SPFA)[可带负权边]
- 多源最短路径问题:Floy
Dijstra算法:中介点优化
解决不带负权边的单源最短路问题
通过与源点s最短路径已知的点Vi为中介,优化Vi的邻接点与源点s的距离
基本步骤
MAXV:最大顶点数,源点s
数组d[MAXV]:各个顶点到源点s的距离。初始化为INF(无穷大,一般设为100,000,000),d[s]初始化为0。
数组vis[MAXV]:标记数组。 vis[Vi] == true 表示Vi结点已被访问过
- 取出未被标记过的顶点中与源点距离最短的顶点Vi,标记Vi
- 以Vi为中介点,更新Vi的邻接点到源点s的距离
- 将1.2循环n次(n为顶点数)
伪代码
//G为图,一般设成全局变量;数组d为源点到达各点的最短路径长度,s为起点
Dijstra(G, d[], s){
初始化;
for(循环n次){
u = 使d[u]最小的还未被访问的顶点的标号;
记u已被访问;
for(从u出发能到达的所有顶点v){
if(v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优){
优化d[v];
}
}
}
}
在实现过程中的关键问题
- “u = 使d[u]最小的还未被访问的顶点的标号;”————>如何找到d[u]最小值
遍历d:O(MAXV)
将d构建为Min_Heap:O(logMAXV) - "v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优"————>前者用标记数组vis解决;后者即"d[u] + G[u][v] < d[v]"(邻接矩阵实现时)
代码实现
邻接矩阵版
#include<stdio.h>
#include<algorithm>
using namespace std;
#define MAXV 100 //最大顶点数
#define INF 100000000 //一个很大的数
int n, G[MAXV][MAXV]; //n为顶点数
int d[MAXV]; //起点到各点的最短路径长度
bool vis[MAXV] = {false};//标记数组,vis[i] == false 表示已访问。初值均为false
void Dijstra(int s){ //s为起点
fill(d, d+MAXV*MAXV, INF); //fill函数将整个d数组赋为INF(慎用memset)
d[s] = 0; //起点s到自身的距离为0
for(int i = 0; i < n; i++){ //循环n次
int u = -1, MIN = INF; //u是d[u]最小,MIN存放最小的d[u]
for(int j = 0; j < n; j++){ //找到未访问的顶点中d[u]最小的,时间复杂度为O(MAXV)
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的 d[u], 说明剩下的顶点和起点s不连通
if(u == -1) return ;
vis[u] = true; //标记u为已访问
for(int v = 0; v < n; v++){
//如果v未访问 && u能到达v && 以u为中介点可以使d[v]更优
if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
d[v] = d[u] + G[u][v];
}
}
}
}
邻接表版
#include<stdio.h>
#include<algorithm>
#include<vector>
using namespace std;
#define MAXV 100 //最大顶点数
#define INF 100000000 //一个很大的数
struct Node
{
int v, dis; //v表示目标顶点, dis表示边权
};
vector<Node> Adj[MAXV];
int n; //n为顶点数
int d[MAXV]; //起点到各点的最短路径长度
bool vis[MAXV] = {false};//标记数组,vis[i] == false 表示已访问。初值均为false
void Dijstra(int s){ //s为起点
fill(d, d+MAXV*MAXV, INF); //fill函数将整个d数组赋为INF(慎用memset)
d[s] = 0; //起点s到自身的距离为0
for(int i = 0; i < n; i++){ //循环n次
int u = -1, MIN = INF; //u是d[u]最小,MIN存放最小的d[u]
for(int j = 0; j < n; j++){ //找到未访问的顶点中d[u]最小的
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的 d[u], 说明剩下的顶点和起点s不连通
if(u == -1) return ;
vis[u] = true; //标记u为已访问
/**********************只有下面这个与邻接矩阵不同**************************/
for(int i = 0; i < Adj[u].size(); i++){
int v = Adj[u][i].v; //通过邻接表直接获得u能到达的顶点v
if(d[u] + Adj[u][v].dis < d[v]){
d[v] = d[u] + Adj[u][v].dis; //优化d[v]
}
}
}
}
时间复杂度:O(VlogV+E)
外循环:O(V)无法避免
影响因素:
- d[MAXV]最小值的获取方式
遍历d:O(V)
最小堆:O(logV) - 图G的实现方式
邻接矩阵:内循环O(V)
邻接表:与外循环共同作用等价于遍历了所有边,故内外循环的总时间复杂度为O(E)
总结:
邻接表 | 邻接矩阵 | |
---|---|---|
遍历d | O(V* V+E) | O(V* (V+V)) |
最小堆 | O(V* logV+E) | O(V*(logV+V)) |
算法存在的问题:存在负权边时会失效
一个简单的例子:
在该例中,如果以A顶点为源点,那么A先被标。而后由于 A到B的距离-1 < A到C的距离1,故B到源点的最短距离更新为1,B被标记。
由于被标记过的顶点的距离值不会再被更新,所以根据Dijstra算法得出B到源点A的最短距离为-1,但实际上应该为-4,可见Dijstra算法在存在负权边的图中就出错了。
Bell_Ford和SPFA算法:遍历边优化
解决单源最短路问题,可检测负权边
存在负权边时函数返回false, 不存在时返回true且d[]存放源点到各顶点的最短路径长
Dijstra算法相比:Dijstra主要操作对象是结点,无法处理负权边;BF主要操作对象是边,可以通过一轮循环判断是否存在负权边
基本步骤
构建d[]数组
- 对边执行n-1轮操作
- 每轮操作遍历所有边
- 对每条边u->v,如果以u为中介使得d[v]更小,则更新d[v] 【边松弛】
寻找负权环
再对所有边执行一轮操作,判断是否仍有边满足 d[u]+G[u][v] < d[v]
有则说明存在负权边,无则说明不存在,d数组中所有值已达最优
伪代码
//构建d[]数组
for(int i = 0; i < n-1; i++){ //执行n-1轮操作,其中n为顶点数
for(each edge u->v){ //每轮操作都遍历所有边
if(d[u] + length[u->v] < d[v]) //以u为中介点可以使得d[v]更小
d[v] = d[u] + length[u->v]; //松弛操作
}
}
//检查负环
for(each edge u->v){ //对每条边进行判断
if(d[u] + length[u->v] < d[v]){ //如果仍可以被松弛
return false; //说明图中有从原点可达的负环
}
return true; //数组d的所有值已达最优
}
代码本质:构建最短路径树
- 最短路径树:如果将从源点s到其他各点的最短路径连接起来,必然会构成一棵树(整体连通&&无环),该树称为最短路径树【别和最小生成树搞混了】。原图和源点一旦确定,最短路径树就唯一确定了
- BF算法的每一轮循环实际上就是在确定树的一层,树的层数不超过V-1,故循环V-1次
代码实现
/*Bellman_Ford:可处理带有负权环的单源最短路径问题 O(V^2)*/
#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
#define MAXV 100
#define INF 100000000
struct Node{
int v, dis;
};
vector<Node> Adj[MAXV];
int n;
int d[MAXV];
bool Bellman(int s){ //s为源点
fill(d, d+MAXV, INF);
d[s] = 0;
for(int i = 0; i < n-1; i++){ //执行n-1 轮操作,n为顶点数
for(int u = 0; u < n; u++){ //每轮操作都遍历所有边
for(int j = 0; j < Adj[u].size(); j++){
int v = Adj[u][j].v; //邻接边的顶点
int dis = Adj[u][j].dis;//邻接边的边权
if(d[u] + dis < d[v]){ //以u为中介点可以使d[v]更小
d[v] = d[u] + dis;
}
}
}
}
for(int u = 0; u < n; u++){ //对每条边进行判断
for(int j = 0; j < Adj[u].size(); j++){
int v = Adj[u][j].v;
int dis = Adj[u][j].dis;
if(d[u] + dis < d[v]){
return false; //说明图中有从源点可达的负权环
}
}
}
return true;
}
复杂度分析:O(VE)
外循环:执行V-1次,无法改变
内循环(遍历每条边):执行E次(邻接表),执行V^2(邻接矩阵)
邻接表 | 邻接矩阵 | |
---|---|---|
时间复杂度 | O(VE) | O(V^3) |
优化:SPFA(Shortest Path Faster Algorithm)
切入点: 在BF算法中,当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]值才有可能改变,此时无需遍历所有边。
优化: 建立一个队列,每次将队首元素u取出,对u的所有邻接边u->v进行松弛操作,如果某个顶点v的d[v]改变了,就将v加入队列中。
循环直到 队列为空||某元素v入队次数大于n-1 退出循环。此时,队列为空,说明d数组已经最优,否则,说明存在负权边。
注:入队元素最大入队次数为n-1次: 顶点v最多与n-2个结点连通,如果每次边松弛时v都入队,则入队次数为n-2,如果v由恰好是源点,在初始化时入队1次,则总共入队n-1次。
伪代码
queue<int> q;
源点s入队;
while(队列非空){
取出队首元素u;
for(u的所有邻接边u->v){
if(d[u] + G[u][v] < d[v]){
if(v不在队列){
v入队;
if(v入队次数大于n-1){
说明有可达负环; return;
}
}
}
}
}
队列用stl的queue实现
v入队次数用数组num[MAXV]记录。(MAXV:最大顶点数)
代码实现:邻接表为例
/*基于Bellman-Ford的优化*/
/*无需遍历所有边,而只需遍历u的发出边(基于BFS思想) O(kE)*/
#include<stdio.h>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;
#define MAXV 100
#define INF 100000000
struct Node{
int v, dis;
};
vector<Node> Adj[MAXV];
int n, d[MAXV], num[MAXV]; //num数组记录入队次数
bool inq[MAXV]; //顶点是否在队列中
bool SPFA(int s){
//初始化部分
fill(inq, inq+MAXV, false);
fill(num, num+MAXV, 0);
fill(d, d+MAXV, INF);
//源点入队部分
queue<int> Q;
Q.push(s);
inq[s] = true; //源点已入队
num[s]++; //源点入队次数+1
d[0] = 0; //源点的d值为0
//主体部分
while(!Q.empty()){
int u = Q.front(); //队首顶点编号为u
Q.pop(); //出队
inq[u] = false; //设置u不在队列中
//遍历u所有的邻接边
for(int j = 0; j < Adj[u].size(); j++){
int v = Adj[u][j].v;
int dis = Adj[u][j].dis;
//松弛操作
if(d[u] + dis < d[v]){
d[v] = d[u] + dis;
if(!inq[v]){ //如果v不在队列中
Q.push(v); //v入队
inq[v] = true; //设置v在队列中
num[v]++; //v的入队次数+1
if(num[v] >= n) return false; //有可达负环
}
}
}
}
return true; //无可达负环
}
复杂度分析:O(kE)
证略。k是一个常数,一般不超过2。
- 经常优于堆优化的Dijstra算法
- 存在负权环时退化为O(VE)
Floy算法:待添加
<数据结构>图的最短路径问题的更多相关文章
- 数据结构 -- 图的最短路径 Java版
作者版权所有,转载请注明出处,多谢.http://www.cnblogs.com/Henvealf/p/5574455.html 上一篇介绍了有关图的表示和遍历实现.数据结构 -- 简单图的实现与遍历 ...
- [从今天开始修炼数据结构]图的最短路径 —— 迪杰斯特拉算法和弗洛伊德算法的详解与Java实现
在网图和非网图中,最短路径的含义不同.非网图中边上没有权值,所谓的最短路径,其实就是两顶点之间经过的边数最少的路径:而对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,我们称路径上第 ...
- <数据结构>图的最小生成树
目录 最小生成树问题 Prim算法:点贪心 基本思想:类Dijstra 伪代码 代码实现 复杂度分析:O(VlogV + E) kruskal算法:边贪心 基本思想: 充分利用MST性质 伪代码 代码 ...
- Python数据结构与算法之图的最短路径(Dijkstra算法)完整实例
本文实例讲述了Python数据结构与算法之图的最短路径(Dijkstra算法).分享给大家供大家参考,具体如下: # coding:utf-8 # Dijkstra算法--通过边实现松弛 # 指定一个 ...
- 数据结构-图-Java实现:有向图 图存储(邻接矩阵),最小生成树,广度深度遍历,图的连通性,最短路径1
import java.util.ArrayList; import java.util.List; // 模块E public class AdjMatrixGraph<E> { pro ...
- 数据结构(C#):图的最短路径问题、(Dijkstra算法)
今天曾洋老师教了有关于图的最短路径问题,现在对例子进行一个自己的理解和整理: 题目: 要求:变成计算出给出结点V1到结点V8的最短路径 答: 首先呢,我会先通过图先把从V1到V8的各种路径全部计算下来 ...
- 带权图的最短路径算法(Dijkstra)实现
一,介绍 本文实现带权图的最短路径算法.给定图中一个顶点,求解该顶点到图中所有其他顶点的最短路径 以及 最短路径的长度.在决定写这篇文章之前,在网上找了很多关于Dijkstra算法实现,但大部分是不带 ...
- 数据结构--图 的JAVA实现(上)
1,摘要: 本系列文章主要学习如何使用JAVA语言以邻接表的方式实现了数据结构---图(Graph),这是第一篇文章,学习如何用JAVA来表示图的顶点.从数据的表示方法来说,有二种表示图的方式:一种是 ...
- 数据结构--图 的JAVA实现(下)
在上一篇文章中记录了如何实现图的邻接表.本文借助上一篇文章实现的邻接表来表示一个有向无环图. 1,概述 图的实现与邻接表的实现最大的不同就是,图的实现需要定义一个数据结构来存储所有的顶点以及能够对图进 ...
随机推荐
- DBeaver客户端工具连接Hive
目录 介绍 下载安装 相关配置 1.填写主机名 2.配置驱动 简单使用 主题设置 字体背景色 介绍 在hive命令行beeline中写一些很长的查询语句不是很方便,急需一个hive的客户端界面工具 D ...
- 使用 Addressables 来管理资源
使用 Addressables 来管理资源 一.安装 打开Package Manager,在Unity Technologies的目录下找到Addressables,更新或下载. 二.配置 依次打开W ...
- 容器之分类与各种测试(三)——slist的用法
slist和forward_list的不同之处在于其所在的库 使用slist需要包含 #include<ext\list> 而使用forward_list则需要包含 #include< ...
- ORACLE profile含义,修改,新增
profiles文件是口令和资源限制的配置集合,包括CPU的时间.I/O的使用.空闲时间.连接时间.并发会话数量.密码策略等对于资源的使用profile可以做到控制会话级别或语句调用级别.oracle ...
- static JAVA
static 关键字:使用static修饰的变量是类变量,属于该类本身,没有使用static修饰符的成员变量是实例变量,属于该类的实例.由于同一个JVM内只对应一个Class对象,因此同一个JVM内的 ...
- springMVC WebApplicationInitializer 替代web.xml 配置Servlet 之原理
Servlet 3.0之前 ,xml 配置 在过去搭建spring + springMCV ,首先第一步要做的是什么 ,就是要配置web.xml 文件 ,把springMVC 中的Servlet 加 ...
- 如何使用redis作为缓存,增强用户访问数据的用户体验
/**完成步骤 1.创建关系型数据库mysql的Provice库,同时启动nosql系列的redis数据库 2.创建项目,导入相关的jar包 3.创建jedis/utils/domain/dao/se ...
- 端口占用,windows下通过命令行查看和关闭端口占用的进程
1.查找所有端口号对应的PID 端口号:8080 命令:netstat -ano|findstr "8080" 2.找到端口的PID并关闭 PID:1016 命令:taskkill ...
- 发布iOS应用(xcode5)到App Store(苹果商店) 详细解析
发布iOS应用(xcode5)到App Store(苹果商店) 详细解析 作者:Memory 发布于:2014-8-8 10:44 Friday IOS 此教程可能不太适合,请移步至最新最全的:201 ...
- jquery Ajax 不执行回调函数success的原因
jquery Ajax 不执行回调函数success的原因: $.ajax({ type: "post", contentType: "application/json& ...