在学习最短路径之前,我们需要知道图的比较常用的存储方式及最短路的判断方法(松弛操作)
1、邻接矩阵
2、邻接表
3、松弛操作 dis[v] = min(dis[v],dis[u]+cost);--(源点到v距离等源点到u的距离加上u到v的距离)
多源最短路径(Floyd)
floyd算法通常用于计算图中任意两点之间的距离,是一种求多源最短路径的算法。也可以处理有向图和有负权值的最短路径问题。算法时间复杂度O(n3),空间复杂度可以被优化为O(n2);
Floyd算法是一个经典的动态规划算法。从任意节点i到任意节点j的最短距离都可以视为一下情况:i 与 j中间经过若干个节点之后相连0至(n-2)个。
算法思想如下:
1、dp[i][j][k] 表示 i 和 j 经过前k个节点所能达到的最短距离
2、枚举1-k,然后在枚举i,j
3 、对于i,j经过点k存在转移方程: dp[i][j][k] = min(dp[i][j][k-1] , dp[i][k][k-1] + dp[k][j][k-1])
核心代码为:
void floyd()
{
//初始化状态
//grap[i][j]表示i,j两点之间的距离,不相连则为inf,自身到自身的距离为0
for(int i = 1; i <= n; i ++)
for(int j + 1; j <= n; j ++)
dp[i][j][0] = grap[i][j];
for(int k = 1; k <= n; k ++)
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
dp[i][j][k] = min(dp[i][j][k-1],dp[i][k][k-1]+dp[k][j][k-1]);
}
//使用自身空间进行迭代
void floyd()
{
for(int k = 1; k <= n; k ++)
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j])
}
//提供思路,未经测试
单源最短路径(Dijkstra)
Dijkstra算法是典型的用于计算无负权值图的最短路径的算法(存在负权值时,其运算结果可能是错的),它是基于贪心的思想,每次未访问且离源点最近的点作为新的起点,然后遍历其邻接点,更新邻接点到源点的最短距离。知道所有点都被访问。但是若当边的权值出现负值的情况时,就会出现下图这种情况:
假设1号点为源点,从图中我们可以看出到2好点的最短距离为0,但是若采用Dijkstra算法,我们计算2号店的距离是会从1号的邻接点中选出2好点算出最短距离为1,然后将其标记为已知点。因此当图中存在负权边时,dijkstra的计算结果是错误的。
下面为Dijkstra的流程:
dis【i】表示点 i 到源点的距离
vis【i】表示点 i 是否已经被标记,即点 i 到源点的最短距离已经确定
过程如下图所示
1、初始化状态,将源点的dis置0,其他置为无穷,vis初始化为未标记。
在dis中找到距离最小的且未被标记的点,这里为 1,将vis【1】置 1,之后遍历 1 的邻接点中未被标记的2、3、6
dis【2】 = min(dis【2】,dis【1】 + dp【1】【2】) = 7;
dis【3】 = min(dis【3】,dis【1】 + dp【1】【3】) = 9;
dis【6】 = min(dis【3】,dis【1】 + dp【1】【3】) = 5;
更新dis数组,得到下图状态
找到点 2, ,将vis【2】置 1,遍历3,4,
dis【3】 = min(dis【3】,dis【2】 + dp【2】【3】) = min(9,7+10) = 9;
dis【4】 = min(dis【4】,dis【2】 + dp【2】【4】) = 22;
更新dis数组,得到下图状态
找到点 3, ,将vis【3】置 1,遍历4,6,
dis【4】 = min(dis【4】,dis【3】 + dp【3】【4】) = min(22,9+11) = 20;
dis【6】 = min(dis【6】,dis【3】 + dp【3】【6】) = min(14,9+2) = 9
更新dis数组,得到下图状态
找到点 6, ,将vis【6】置 1,遍历5,
dis【5】 = min(dis【5】,dis【6】 + dp【6】【5】) = min(inf,9+11) = 20;
更新dis数组,得到下图状态
找到点 4, ,将vis【4】置 1,遍历5,
dis【5】 = min(dis【5】,dis【4】 + dp【4】【5】) = min(20,20+6) = 20;
更新dis数组,得到下图状态
找到点 6, ,将vis【6】置 1,无可遍历的点,结束。
得到dis【】数组为源点到个点的最短距离
分析复杂度 枚举1--n,在每次循环中又遍历一次dis数组,因此复杂度为O(N2)
代码如下
void dijkstra()
{
memset(vis,0,sizeof(vis));
for(int i = 1; i <= n; i ++)
dis[i] = inf; //inf为极大值
dis[1] = 0;
for(int i = 1; i <= n; i ++)
{
int mmin = inf;
int pos = -1;
for(int j = 1; j <= n; j ++)
{
if(mmin > dis[j] && vis[j] == 0)
{
mmin = dis[j];
pos = j; //找到距离最小的点
}
}
vis[j] = 1; // 标记
for(int j = 1; j <= n; j ++)
{
if(vis[j] == 0 && dis[j] > dis[pos] + dp[pos][j]) // 更新dis数组
dis[j] = dis[pos] + dp[pos][j];
}
}
}
// 未测试,仅供参考
优化 Dijkstra + 小根堆(优先队列)
用优先队列去存储以访问过点的距离,每次从队列里面去除一个最短的。复杂度为log(n)级别,总复杂度为O(N*log(N))级别
例题模板:HDU2544
给n个点,m条边,每条边的距离为v,问从起点1到终点n的最短距离是多少
#include<bits/stdc++.h>
using namespace std;
const int inf = 0x3f7f7f7f;
const int N = 1e4+5;
typedef pair<int,int> PI;
int n,m;
int dis[N];
int vis[N];
struct node
{
int u,v,cost;
node(){}
node(int u,int v,int cost):u(u),v(v),cost(cost){}
};
struct cmp{
bool operator()(const PI p1, const PI p2)
{
return p1.second > p2.second;
}
};
vector<node> grap[N];
void dijk()
{
for(int i = 0; i < N; i ++) dis[i] = inf;
memset(vis,0,sizeof(vis));
priority_queue< PI,vector<PI>,cmp> q; //优先队列
dis[1] = 0;
q.push(PI(1,0));
while(!q.empty())
{
PI p = q.top();
q.pop();
if(vis[p.first] == 1) continue;
vis[p.first] = 1;
int u = p.first;
for(int i = 0; i < grap[u].size(); i ++)
{
int v= grap[u][i].v;
int cost = grap[u][i].cost;
if((dis[v] > dis[u] + cost) )
{
dis[v] = dis[u] + cost;
q.push(PI(v,dis[v]));
}
}
}
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m))
{
if(n == 0 && m == 0) break;
for(int i = 1; i <= n; i ++) grap[i].clear();
int u,v,cost;
for(int i = 0; i < m; i ++){
scanf("%d%d%d",&u,&v,&cost);
node a(u,v,cost);
grap[u].push_back(a);
node b(v,u,cost);
grap[v].push_back(b);
}
dijk();
printf("%d\n",dis[n]);
}
return 0;
}
Bellman-ford算法
从上面的Dijkstra的介绍中我们可以知道dijkstra算法不能计算当边权值存在负值的情况。但是Belman-ford算法却能够解决这个问题,这是这算法的优势。
在最开始介绍Floyd算法是我们提到过:从任意节点i到任意节点j的最短距离都可以视为一下情况:i 与 j中间经过若干个节点之后相连0至(n-2)个。这是相对与多远最短路径的说法,但这里只用计算单元最短路径,那么可以翻译为:从任意节点到源点的最短距离的路径上包含至少1到n-1条边。那么对所有的边进行一次松弛操作就可以确认任意点到源点只经过一条边的最短距离,然后在对所有边进行一次松弛操作会得到任意点到源点经过至多2条边的最短距离,那么就可以得出结论对所有边进行n-1轮的松弛操作就可以得到任意点到源点的最短距离。
拿上图举个例子,dis【i】数组代表着i点到源点的最短距离,
对所有边进行一次松弛操作可以得到dis【2】 = 1,dis【3】 = 2,dis【4】 =?(?代表着不确定)。现在可能有人会有疑惑为什么不能得到dis【2】 = 0和dis【4】 = 2呢。我的理解是我们无法确定遍历的边的顺序,上图对于1->3和3->2这两条边的遍历顺序决定了dis【2】的最小值。因此我们可以在体会一“对所有的边进行一次松弛操作就可以确认任意点到源点只经过一条边的最短距离”的含义。
接下来再对所有边进行一次松弛操作,我们可以得到dis【2】 = 0;dis【4】= ?。为什么我们这里就可以确定dis【2】的距离了呢,我们之前说过1->3和3->2这两条边的遍历顺序决定了dis【2】的最小值。第一次遍历1-3这条边已经确认了dis【3】的最短距离,那么第二次遍历3->2这条边当然也能够确认dis【2】 = 0了。dis【4】的值取决于第二次遍历3->2和2->4的顺序,原因和之前的相同,这里不再描述。
分析:从上面的描述我们知道对于所有边要进行n-1轮的松弛,时间复杂度为O(N*E),N为点数,E为边数。因为复杂度和边有关,因此Bellam-ford算法更适合稀疏图。
优点:可以计算存在负权值边情况下的最短路(非负环图),存在负环时的最短距离为负无穷,因为每次松弛操作距离都会减少。但是Bellman-ford算法可以判断负环是否存在。
当图不存在负环时,任意点到源点的最短距离路径至多包含n-1条边,因此松弛n-1轮所有边之后一点能够计算任意点到源点的最短距离,那么第n轮松弛操作还能成功时,说明存在负环。
给出例题:POJ3259
大致题意:有n个农场,之前有m条路,每条路花费时间v。有w个虫洞,进入虫洞可以从点u到v,并回到val时间之前,问一个人经过这些路或者虫洞能否回到他出发之前。(模板题)
题解:
#include<cstdio>
#include<queue>
#include<iostream>
#include<cstring>
using namespace std;
typedef pair<int,int> PI;
const int inf = 0x3f7f7f7;
const int N = 1e4+5;
vector<PI> grap[N];
int n,m,w;
int dis[N];
bool bellmanford(int s)
{
for(int i = 0; i < N; i ++)
dis[i] = inf;
dis[s] = 0;
for(int i = 0; i < n-1; i ++)
{
//遍历所有边
for(int u = 1; u <= n; u ++){
if(dis[u] == inf) continue;
for(int j = 0; j < grap[u].size(); j ++){
int v = grap[u][j].first;
int val = grap[u][j].second;
if(dis[v] > dis[u] + val){ //松弛操作
dis[v] = dis[u] + val;
}
}
}
}
//判断负环
for(int u = 1; u <= n; u ++){
if(dis[u] == inf) continue;
for(int j = 0; j < grap[u].size(); j ++){
int v = grap[u][j].first;
int val = grap[u][j].second;
if(dis[v] > dis[u] + val)
return true; //存在负环
}
}
return false;
}
int main()
{
int t;
scanf("%d",&t);
while(t --)
{
scanf("%d%d%d",&n,&m,&w);
for(int i = 0; i <= n; i ++) grap[i].clear();
for(int i = 0; i < m; i ++)
{
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
grap[u].push_back(PI(v,val));
grap[v].push_back(PI(u,val));
}
for(int i = 0; i < w; i ++)
{
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
grap[u].push_back(PI(v,-val));
}
if(bellmanford(1))
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
SPFA算法(基于BFS)
Bellman-ford的思想在于所有的边进行进行扫描,然后松弛,一轮又一轮的迭代,直道没有松弛更新位置,但是在这n-1轮的扫描中对许多的面进行了无用的扫描,这些已经确定了最短距离的边不会扫描松弛后不会在对其他的距离产生影响。SPFA算法就是Bellman-ford算法的基础上使用了队列进行优化,减少冗余操作。相较于Bellman-ford算法,SPFA的思想主要是:每次只用更新过的点去更新这些邻接点的距离,而不是每次都去遍历所有的边。
如何做到:
1、每次从队列中取出队首,代表这是已经更新过的点
2、用这个点再去更新这个点的邻接点的最短距离,若这些邻接点的距离有被更新,则将更新的点加入队列
3、重复上述操作直道没有点可以更新距离。
注意:
1、某些点可能会有多次入队出队的操作
2、若邻接点有被更新且这个邻接点已经入队,则不必再入队了
3、若一个点进入队列
那么怎么做到上述的用已更新的点去更新其邻接点拿下图做例子:
首先初始化数组dis【】代表每个点到最短起点距离dis【1】 = 0;其他为inf(极大值)。vis【】代表每个点是否在队列中,初始化为0;cnt【】代表每个点松弛次数,若cnt【i】大于n代表存在负环
将起点1入队,得到以下数据
id | 1 | 2 | 3 | 4 |
dis | 0 | inf | inf | inf |
vis | 1 | 0 | 0 | 0 |
cnt | 1 | 0 | 0 | 0 |
将1出队,遍历1的所有邻接点,进行松弛操作,将2、3入队,,并更新表中数据得:
id | 1 | 2 | 3 | 4 |
dis | 0 | 1 | 2 | inf |
vis | 0 | 1 | 1 | 0 |
cnt | 1 | 1 | 1 | 0 |
然后将2出队,遍历其邻接节点,松弛判断,将4入队,得以下数据
id | 1 | 2 | 3 | 4 |
dis | 0 | 1 | 2 | 3 |
vis | 0 | 0 | 1 | 1 |
cnt | 1 | 1 | 1 | 1 |
将 3 出队,遍历邻接点,松弛判断,将 2 入队,得以下数据
id | 1 | 2 | 3 | 4 |
dis | 0 | 0 | 2 | 3 |
vis | 0 | 1 | 0 | 1 |
cnt | 1 | 2 | 1 | 1 |
然后将4出队,并未更新其他距离,之后将2出队,更新点4的距离,得下表
id | 1 | 2 | 3 | 4 |
dis | 0 | 0 | 2 | 2 |
vis | 0 | 0 | 0 | 1 |
cnt | 1 | 2 | 1 | 2 |
最后在将4出队,并未更新其他点距离,最后得到的dis【】数组为
dis | 0 | 0 | 2 | 2 |
总结:相较于Bellman-ford而言,SPFA减少了对许多无用边的遍历,如1->3这条边,Bellman-ford会遍历3次,而SPFA只会遍历一次。据网上言,SPFA的时间复杂度为O(K*E),K为一个较小的常数(一般为2或3),但会被一些特殊图或网格图退化成O(N*M)
例题:POJ3259
#include<cstdio>
#include<queue>
#include<iostream>
#include<cstring>
using namespace std;
typedef pair<int,int> PI;
const int inf = 0x3f7f7f7f;
const int N = 3000;
vector<PI> grap[N];
int cnt[N];
int vis[N];
int dis[N];
int n,m,w;
bool spfa(int s)
{
for(int i = 0; i < N; i ++){
dis[i] = inf,
vis[i] = 0;
cnt[i] = 0;
}
dis[s] = 0;
vis[s] = 1;
cnt[s] = 1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u = q.front();
q.pop();
for(int i = 0; i < grap[u].size(); i ++)
{
int v = grap[u][i].first;
int val = grap[u][i].second;
if(dis[v] > dis[u] + val){
dis[v] = dis[u] + val;
cnt[v] ++;
if(cnt[v] > n) return true;
if(vis[v] == 0){
vis[v] = 1;
q.push(v);
}
}
}
vis[u] = 0;
}
return false;
}
int main()
{
int cse;
scanf("%d",&cse);
while(cse --)
{
scanf("%d%d%d",&n,&m,&w);
for(int i = 0; i <= n; i ++) grap[i].clear();
for(int i = 0; i < m; i ++)
{
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
grap[u].push_back(PI(v,val));
grap[v].push_back(PI(u,val));
}
for(int i = 0; i < w; i ++)
{
int u,v,val;
scanf("%d%d%d",&u,&v,&val);
grap[u].push_back(PI(v,-val));
}
if(spfa(1)) printf("YES\n");
else printf("NO\n");
}
}