首页 > 编程语言 >最短路径算法

最短路径算法

时间:2023-05-20 20:22:14浏览次数:32  
标签:weight int 路径 dijkstra 最短 算法 edge

最短路径问题

这是一类最基本的图论问题,给定一个图,求从某一个源节点到某一个目的节点的最短路径。
比较常见的算法有 dijkstra, floyd, SPFA。

在开始之前我们先说一说“松弛”这个词。
在描述最短路径算法的时候,我们经常可以看到松弛(relaxtion)一词,通常来说,所有的最短路径算法都可通过边的松弛求得,以下的所有算法中也都用到了松弛操作。

松弛是一种数学术语,描述的是一些求解方法,这些方法会通过逐步接近的方式获得相关问题的最佳解法。在最短路径算法中,松弛的含义是检查并更新某个顶点到源点的估计距离,使其满足三角不等式约束。也就是说,如果从源点经过另一个顶点到达目标顶点的距离比直接到达目标顶点的距离要短,那么就用前者替换后者,这样就可以逐渐缩小误差,得到最短路径。(from New Bing)

可能这种降低节点之间距离的操作很像将绷紧的皮筋放松,算法创造者因此赋予了它松弛的名字。

dijkstra

int n;                      //节点数量
int[n][n] map;              //邻接矩阵
int path[n] = {0};          //最短路径上该节点的前驱节点
int dijkstra(int s) {
    char found[n] = {0};        //标记该节点是否找到
    int weight[n];
    for (int i = 0; i < n; i++) {
        weight[i] = INFINITY;
    }
    for (int i = 0; i < n; i++) {
        weight[i] = map[s][i];
        path[i] = s;
    }
    found[s] = 1;
    weight[s] = 0;
    //初始化完成
    for (int i = 0; i < n - 1; i++) {
        int minWeight = INFINITY;
        int v = -1;
        for (int j = 0; j < n; j++) {
            if (found[j] == 0 && weight[j] < minWeight) {
                minWeight = weight[j];
                v = j;
            }
        }
        if (v != -1) {
            found[v] = 1;
            for (int j = 0; j < n; j++) {
                if (found[j] == 0 && weight[j] > weight[v] + map[v][j]) {
                    weight[j] = weight[v] + map[v][j];
                    path[j] = v;
                }
            }
        }
        else {
            return -1;
        }
    }
    return 0;
}

这是一种朴素的 dijkstra 算法,也是数据结构课上的内容。观察朴素 dijkstra 算法,我们会发现如下特征:

  • 集中的体现了贪心算法的思想,最短路径的规划是选择当前可达的所有节点中最短的边
  • 采用了邻接矩阵来存储图,适用于稠密图,而对于稀疏图,开销比较大
  • 寻找最短边时采用了遍历的办法,开销比较巨大,但实际上只需要寻找到一个最小权重路径即可,可以考虑维护一个小顶堆
    朴素的 dijkstra 算法比较适合稠密图的情况,时间复杂度为 O(n2)。

我们可以基于 dijkstra 算法进行很多改进

//按边遍历的 dijkstra
const int M;          //边上限
const int N;          //点上限
const int INFINITY;

typedef struct Edge {
    int next;   //所有以同一节点作为起点的边串成一个链,起点存储在 head[] 中
    int to;     //边的终点
    int w;      //权重
} Edge;
Edge edge[M];       //边
int head[N];        //存储以 i 为起点的第一条边的编号
int weight[N];
char found[N];
int tot;            //自增量,用于给边编号

void add(int form, int to, int w) {
    edge[tot].to = to;
    edge[tot].w = w;
    edge[tot].next = head[from];    //将新边插入到最前面,如果 next 为 -1, 则其代表了该节点的最后一条有效边
    head[from] = tot++;
}

int dijkstra(int s) {
    int min, v;
    while (1) {
        min = INFINITY;
        v = -1;
        for (int i = 0; i < n; i++) {
            if (found[i] == 0 && weight[i] < min) {
                min = weight[i];
                v = i;
            }
        }
        if (v == -1) {
            break;          //跳出条件
        }
        found[v] = 1;
        for (int i = head[v]; i != -1; i = edge[i].next) {      //i是边的编号,当i=-1时,即到了最后一条边的下一条边,需要跳出循环
            int to = edge[i].to;
            if (found[to] == 0 && weight[to] > weight[v] + edge[i].w) {
                weight[to] = weight[v] + edge[i].w;             //更新最短路径长度
            }
        }
    }
}

这种 dijkstra 采用了所谓“链式向前星”的数据结构来存储图,实际上存储的是图的边的信息,比较适合稀疏图或者两点之间有多条路径的情况。
链式向前星可以实现和邻接表类似的效果,但是实现起来比邻接表简单,应用比较广泛。

//堆优化版的 dijkstra
const int M;
const int N;
const int INFINITY;
typedef struct Node {
    int to;
    int w;
} Node;                 //用于构建小顶堆
Node heap[M * M];       //没有办法便捷的动态增加,一次性申请比较大的堆容量,如果使用 C++ 或者 java 可以直接使用 priority_queue/PriorityQueue
int size;               //堆的大小

void adjust(int i) {
    int left = i * 2 + 1;
    int right = i * 2 + 2;
    int min;
    int index;
    if (right > size - 1) {
        min = heap[left].w;
        index = left;
    }
    else {
        min = (heap[left].w < heap[right].w)? heap[left].w : heap[right].w;
        index = (heap[left].w < heap[right].w)? left : right;
    }
    if (min < heap[i].w) {
        swap(index, i);
        adjust(index);
    }
}

typedef struct Edge {
    int next;   
    int to;     
    int w;      
} Edge;
Edge edge[M];       
int head[N];        
int weight[N];
char found[N];
int tot;            

void add(int form, int to, int w) {
    edge[tot].to = to;
    edge[tot].w = w;
    edge[tot].next = head[from];    
    head[from] = tot++;
}

void dijkstra(int s) {
    int min, v;
    for (int i = 0; i < n - 1; i++) {
        while (1) {
            min = heap[0].w;
            v = heap[0].to;
            if (found[v] == 1) {
                swap(0, size - 1);
                size--;
                adjust(0);
            }
            else {
                break;
            }
        }
        found[v] = 1;
        weight[v] = min;
        swap(0, size - 1);
        size--;
        adjust(0);
        for (int i = head[v]; i != -1; i = edge[i].next) {     
            heap[size].to = edge[i].to;
            heap[size].w = egde[i].w + min;
            size++;
            swap(0, size - 1);
            adjust(0);
        }
    }
}

这是一种综合了边遍历和堆优化的 dijkstra 算法。可以看见即使没有到完全能够运行的程度,其码量也已经显著增加了。这样的 dijkstra 算法更适用于稀疏图,时间复杂度为 O((n + m)log(n))。当图十分稀疏时,m可以忽略不计,于是其具有近似的时间复杂度 O(nlog(n))。但当图十分稠密时,其性能可能不如朴素的 dijkstra。

floyd

int n;
int map[][];
int path[][];
int floyd() {
    for (int i = 0; i < n; i++) {       //被选中的中间值
        for (int j = 0; j < n; j++) {
            for (int k = 0; k < n; k++) {
                if (map[j][k] > map[j][i] + map[i][k]) {
                    map[j][k] = map[j][i] + map[i][k];      //更新路径值
                    path[j][k] = i          //更新需要经过的中间值
                }
            }
        }
    }
}

floyd 算法的能够处理多源最短路径,其核心思想可以这样总结:
最开始只允许通过 1 号节点进行中转,然后允许通过 1,2 号节点进行中转......最终允许通过 1,2,...,n 号节点进行中转,实际上是求得了从 i 到 j 节点只通过前 k 个节点的最短路径,本质上是动态规划的思想。
floyd 算法的正确性并不那么容易见得,但是我们可以直观的感受到:两个点之间要么直接相连就是最短路径,要么可以通过多个中转点连接形成最短路径。floyd 算法完成之后,路径的信息存放在 path 当中。如:path[i][j] = k(其中 k != i,j) 表明从 i 到 j 的最短路径或通过中间点 k,但是从 i 到 k 和从 k 到 j 的最短路径并不能确定是直接连通的,需要再检查 path[i][k] 和 path[k][j] 确定。
floyd 算法可以容忍有负边,但是不能容忍有负环;
floyd 算法的时间复杂度为 O(n3)

SPFA

const int INFINITY;
const int M;
const int N;
int dis[N];         //记录最小路径的数组
bool vis[N];        //标记点是否在优先队列队列中
int pre[N];         //记录前驱结点,用于输出路径
int head[N];
int tot;    
typedef struct Edge {
    int next;   
    int to;     
    int w;      
} Edge;
Edge edge[M];       

void add(int form, int to, int w) {
    edge[tot].to = to;
    edge[tot].w = w;
    edge[tot].next = head[from];    
    head[from] = tot++;
}

void spfa(int s) { 
    for(int i = 0; i < N; i++) {
        vis[i] = 0;
        dis[i] = INFINITY;
    }
    dis[s]=0; 
    vis[s]=1;
    queue<int> q;
    q.push(s);
 
    while(!q.empty()){
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(int i = head[u]; i != -1; i = edge[i].next){
            int v = edge[i].to;
            int w = edge[i].w;
            if(dis[v] > dis[u] + w){
                dis[v] = dis[u] + w;
                if(!vis[v]){
                    vis[v] = 1;
                    pre[v] = u; 
                    q.push(v);
                }
            }
        }
    }
}

算法步骤:

  1. 将除源点之外的所有的点当前距离初始化为无穷,并标记为未入队。源点的当前距离为0,将源点入队。
  2. 取出队首u,遍历u的所有出边,检查是否能更新所连接的点v的当前距离。如果v的当前距离被更新并且v不在队中,则将v入队。重复2操作直到队列为空。

SPFA 可以检查是否存在负权环:记录每个点的入队次数,如果超过V-1次说明存在负权环,因为最短路径上除自身外至多V-1个点,故一个点不可能被更新超过V-1次。
SPFA 是一种对于 bellmon-ford 算法的优化。bellmon-ford 是通过不断的对于所有边进行松弛操作最终找到最短路径的,这种做法存在盲目性,所以 SPFA 维护了一个队列,队列中存储了可能具有可松弛边的所有点,只需要对这些点所连接的所有边进行松弛操作即可。
SPFA 的时间复杂度为 O(mn)

标签:weight,int,路径,dijkstra,最短,算法,edge
From: https://www.cnblogs.com/topady/p/17417740.html

相关文章

  • 算法学习day25回溯part02-216、17
    packageLeetCode.backtrackpart02;importjava.util.ArrayList;importjava.util.LinkedList;importjava.util.List;/***216.组合总和III*找出所有相加之和为n的k个数的组合,且满足下列条件:*只使用数字1到9*每个数字最多使用一次*返回所有可能的有效......
  • 《数据结构与算法》之十大基础排序算法
    一.冒泡排序什么是冒泡排序?冒泡排序是一种交换排序,它的思路就是在待排序的数据中,两两比较相邻元素的大小,看是否满足大小顺序的要求,如果满足则不动,如果不满足则让它们互换。然后继续与下一个相邻元素的比较,一直到一次遍历完成。一次遍历的过程就被成为一次冒泡,一次冒泡的结束至......
  • 代码随想录算法训练营第十一天|20. 有效的括号、1047. 删除字符串中的所有相邻重复项
    【参考链接】20.有效的括号【注意】1.括号匹配是使用栈解决的经典问题。2.这个命令最后进入a目录,系统是如何知道进入了a目录呢,这就是栈的应用(其实可以出一道相应的面试题了)。3.有三种不匹配的情况,第一种情况,字符串里左方向的括号多余了;第二种情况,括号没有多余,但是括号的......
  • 基于Graph-Cut算法的彩色图像深度信息提取matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:2.算法涉及理论知识概要Graphcuts是一种十分有用和流行的能量优化算法,在图像处理领域普遍应用于前后背景分割(Imagesegmentation)、立体视觉(stereovision)、抠图(Imagematting)等,目前在医学图像领域应用较多。GraphCut(图形切割)应用于......
  • 基于GA遗传优化的CDVRP,CVRP,DVRP,TSP以及VRPTW常见路径优化问题求解matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:        TSP最优路径TSP最优路径TSP最优路径BestRoute:0->2->10->5->3->6->9->1->4->7->8->0TotalDistance=95.275km  DVRP最优路径DVRP最优路径DVRP最优路径总路程=19......
  • 基于Graph-Cut算法的彩色图像深度信息提取matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:     2.算法涉及理论知识概要       Graphcuts是一种十分有用和流行的能量优化算法,在图像处理领域普遍应用于前后背景分割(Imagesegmentation)、立体视觉(stereovision)、抠图(Imagematting)等,目前在医学图像领域应用较......
  • m基于低复杂度高性能BP译码算法的LDPC编译码性能matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:2.算法涉及理论知识概要LDPC码是麻省理工学院RobertGallager于1963年在博士论文中提出的一种具有稀疏校验矩阵的分组纠错码。几乎适用于所有的信道,因此成为编码界近年来的研究热点。它的性能逼近香农极限,且描述和实现简单,易于进行理论分......
  • 基于GA遗传优化的CDVRP,CVRP,DVRP,TSP以及VRPTW常见路径优化问题求解matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:TSP最优路径TSP最优路径TSP最优路径BestRoute:0->2->10->5->3->6->9->1->4->7->8->0TotalDistance=95.275kmDVRP最优路径DVRP最优路径DVRP最优路径总路程=198.801kmBestRoute:0->10->......
  • m基于低复杂度高性能BP译码算法的LDPC编译码性能matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:       2.算法涉及理论知识概要       LDPC码是麻省理工学院RobertGallager于1963年在博士论文中提出的一种具有稀疏校验矩阵的分组纠错码。几乎适用于所有的信道,因此成为编码界近年来的研究热点。它的性能......
  • 算法学习记录:[NOIP2011]铺地毯
    题目链接:https://ac.nowcoder.com/acm/contest/20960/1016解题思路:最直观的方法,因为编号大的地毯一定更靠后,所以直接用编号进行标记。时间复杂度分析:该代码时间复杂度为\(O(N^2)\),有\((10^5)^2\),评测oj每1秒能接受的时间复杂度为:\([10^8,10^9]\)所以下代码一定TLE。TLE......