最短路径的算法主要是解决两个问题:
1.单源最短路
2.任意两点间的最短路
主要有三个算法来解决这两个问题:
1.单源最短路:dijkstra算法,spfa算法
2.任意两点间的最短路:floyd算法
----------------------------------------dijkstra单源最短路算法----------------------------------------------------------------
应用范围:边权非负的有向图和无向图均可
算法思想:
1.因为边权非负,所以任意两条边相加的和一定不小于其中任意一条边,我们设源点为s,那么要求所有点到达s的最短路径,那么我们定义一个点集S,定义一个数组dis[],表示某个点只与点集中的点连接的情况到达源点的最短距离,如果暂时不能到达,记为INF,初始情况下点集中只有源点s,那么与源点直接相连的点中边权最小的点一定是最短路径,设最小边连接的点为v,u为除s和v外的任意一点,因为dis[v] < dis[u]且边权非负,所以dis[v]<dis[u]+一条或多条边的边权,所以当前的决策一定是最短路,且之后一定不能更新出更短的路径。
2.那么点v的最短路径已经得到,我们可以把它添加到S点集,然后还要继续维护dis[]数组的性质,也就是如果当前的v点与S点集外的一点u相连,如果dis[v]+edge[v,u] < dis[u],那么点u可以dis[u]就应该更新,因为定义中是定义的经过点集中的点得到的最短的路径,然后这个S点集我们就可以缩点为一个点了,然后又变为了1的情况,知道处理的所有点都计入S点集,也就是整个图缩成了一个点,那么整个图到达s点的最短路径就处理出来了
下面给出O(n^2)的实现方法
邻接矩阵的实现方法:
void dijkstra(int s){ memset(vis , 0 , sizeof(vis)); memset(father , 0 , sizeof(father)); /*初始化dis数组*/ for(int i = 1 ; i<= n ; i++) dis[i] = INF; dis[s] = 0; for(int i = 1 ; i <= n ; i++){/*枚举n个顶点*/ int pos; pos = -1; for(int j = 1 ; j <= n ;j++){/*找到未加入集合的最短路点*/ if(!vis[j] && (pos == -1 || dis[j] < dis[pos])) pos = j; } vis[pos] = 1;/*把这个点加入最短路径集合*/ for(int j = 1 ; j <= n ; j++){/*更新dis数组*/ if(!vis[j] && (dis[j] > dis[pos] + value[pos][j])){ dis[j] = dis[pos] + value[pos][j]; father[j] = pos; } } } }
主要是两个操作:
1.找出当前S集外的dis[v]最小的点v(可以利用堆优化)
2.将v加入点集S,然后更新dis数组
下面是利用堆优化的O(n*loge)
struct cmp { bool operator ( ) ( const int&a , const int&b ) const { return dis[a] > dis[b]; } }; void dijkstra ( ) { memset ( dis , 0x3f , sizeof ( dis ) ); memset ( used , 0 , sizeof ( used ) ); priority_queue<int , vector<int> , cmp > q; dis[s] = 0; q.push ( s ); while ( !q.empty( ) ) { int u = q.top(); q.pop(); used[u] = true; for ( int v = 1 ; v <= n ; v++ ) if ( !used[v]&&dis[v] > dis[u] + mp[u][v] ) { dis[v] = dis[u] + mp[u][v]; q.push ( v ); } } }
适用范围:有负权的图,可以判断负环,如果某个点松弛次数大于等于n,那么说明有负环
算法思想:很直观的宽搜的思想,近似于暴力,复杂度O(ke),k是每个点平均入队次数,可以证明出K<=2
dis数组,记录到源点的最短路径,如果当前不是最短,那么一定能够被某个点点松弛,相反,如果当前点不能够被松弛,那么它就一定是最短路径了
首先科普一个操作:就是松弛操作,这个操作就是利用当前的点的的dis值取更新它能够到达的点的dis值
在宽搜的过程中我们维护一个队列,这个队列中存放的刚刚被松弛过路径的点,初始的时候将源点放入队列中,因为当一个点松弛过后,它的最短路径变短,可能会导致其他点能够被松弛,所以当前点入队,当队列为空也就是没有点能够松弛其他点了,也就是说一个点最多直接间接地被其他n-1个点松弛一遍,所以当松弛数大于等于n的时候证明出现了负环,因为负环能够不断的松弛一条最短路径
代码如下:
struct Edge { int v,w,next; }e[MAX*MAX]; int head[MAX]; int cc; void add ( int u , int v , int w ) { e[cc].v = v; e[cc].w = w; e[cc].next = head[u]; head[u] = cc++; } bool used[MAX]; int dis[MAX]; int cnt[MAX]; bool spfa ( int s , int d ) { memset ( used , false , sizeof ( used ) ); memset ( dis , -1 , sizeof ( dis ) ); memset ( cnt , 0 ,sizeof ( cnt ) ); queue<int> q; q.push ( s ); cnt[s]++; used[s] = true; dis[s] = 0; while ( !q.empty()) { int u = q.front(); q.pop ( ); used[u] = false; for ( int i = head[u] ; ~i ; i = e[i].next ) { int v = e[i].v; if ( dis[v] != -1 && dis[v] <= dis[u] + e[i].w ) continue; dis[v] = dis[u] + e[i].w; if ( used[v] ) continue; used[v] = true; q.push ( v ); if ( ++cnt[v] > n ) return false; } } return true; }---------------------------------------------floyd求任意两点间的最短路-----------------------------------------------
适用范围:存在负权的图不能用
算法思想:n^3的复杂度不算优秀,但是容易实现。
首先floyd是一个传递闭包的思想,比如A,B,C三个点,A和B存在边,B和C存在边,那么A和C也可以推出直接相连的边
我们枚举这个n个点,然后对于每个点,枚举任意两点,然后更新dis[i][j],就相当于传递闭包当中利用两条边去推导出第三条边,但是floyd求最短时,每两点之间只保留一条边,最小那条,也就是如果本身存在边,那么新添的边与其进行比较,大的那条边被覆盖。整个floyd过程下来相当于整个传递闭包求了一遍,然后所有的边保留下来均是在每次覆盖操作后留下来的最小的,可能有的人会问?如果当前利用k点更新了dis[i][j] = dis[i][k] + dis[k][j],之后又出现了一个v更新了dis[i][k],还是最短的吗?其实是这样的,因为k在更新dis[i][j]的同时一定也会更新dis[v][j],那么到枚举v的时候dis[i][v]和dis[v][j]更新dis[i][j],就相当于用更小的dis[i][k]进行了松弛。
代码如下:
for ( int k = 0 ; k < n ; k++ ) for ( int i = 0 ; i < n ; i++ ) for ( int j = 0 ; j < n ; j++ ) dis[i][j] = max ( dis[i][j] , dis[i][k] + dis[k][j] );