最近一直忙,没有时间写算法博客(其实是去水数据结构去了),今天睡前写一篇图论的基础算法吧,叫Dijkstra,是由荷兰计算机科学家E.W.Dijkstra于1959 年提出的,因此又叫Dijkstra算法。是从一个顶点到其余各顶点的最短路径算法,即,单源最短路算法,解决的是有/无向图中最短路径问题
算法描述:原理就是每次找一个点作为中间点,来松弛每一条边,以寻得最短路,这是一个单源最短路算法,需要指定一个源点,所
求的是源点到其余各点的最短路径
那么,介绍一下松弛的概念:所谓松弛,即寻找一个顶点t,使得从s->e的最短路长度能通过t中转变成s->t->e,那么我们就说s->e的最短路通过t这个点松弛成功,比如1->2 有长为9的路径,1->3有长为1的路径,3->2有长为3的路径,则通过3松弛1->2,使得1->3->2变成长度为4,即此时1->2有两条路径,最短路是4
上述过程也叫作三角形优化,也许有人有疑问了,三角形两边之和大于第三边啊,为什么通过两边之和反而使得最短路比直接相连的边更短呢?因为三角形是两点之间均是直线,而路径可没有说是直线路径啊,考虑到此优化过程和数学的三角形类似,才称为三角形优化,准确地讲,应该是曲三角形才对
下图反应了Dijkstra算法的执行流程:
由上图我们可以知道,Dijkstra算法执行分为以下几个步骤:
1.将源点S到其余点的最短路径置为无穷远,源点到本身的距离设为0
2.寻找离源点最近的且未被使用过的点,则源点距该点的最短路径就确定了下来,并且通过该点松弛剩余所有点
3.重复2直至所有点均作为中间点松弛其余点完毕
思考这样一个问题:为什么2中,离源点最近的点,源点距其最短路确定呢?
因为如果源点到该点最短路未确定,则一定能通过一个中间点,松弛源点到该点的边,那么!该点一定不可能是离源点最近的点,因此离源点最近的点,源点距其最短路确定
那么,一共要松弛多少次呢?
一共是n-1次,n为顶点数量,因为每松弛一次,就会产生一个新的“中间点”,通过这个点进行下一次松弛操作,除去源点一共有n-1个“中间点”,因此松弛n-1次就好了
我们讨论一下边权全为正的情况:当边权全为正时,不可能存在一个点,使得已经松弛过的点最短路变得更短,这个矛盾前面刚讲过,所以松弛过的这些点最短路永远不再改变,因此保证了算法的正确性
然后,Dijkstra不能解决含有负权边的图!为什么?因为当扩展到负权边的时候,源点到所有边的最短路均改变,破坏了已经松弛过的点最短路不变的性质
说了这么多,怎么写Dijkstra算法呢? —— 我们介绍邻接矩阵和邻接表的形式
关于邻接矩阵和邻接表,可以去看看我写的邻接表和邻接矩阵
1.邻接矩阵写Dijkstra:
#include<cmath>
#include<cstring>
#include<iostream>
#define maxn 10000
#define inf 0x3f3f3f3f
using namespace std;
int e[maxn][maxn],dis[maxn],m,n; //dis[maxn]表示从源点到其它点的距离;
bool vis[maxn]; //标记一个点是否已经达到最短路/是否已经使用过;
void init() //初始化;
{
memset(vis,0,sizeof(vis));
memset(e,inf,sizeof(e));
for(int i = 1; i <= n; i++)
e[i][i] = 0;
}
void Dijkstra(int s) //s为源点;
{
int tag,minn;
vis[s] = 1,dis[s] = 0;
for(int i = 1; i <= n; i++) //预处理dis;
dis[i] = e[s][i];
for(int k = 0; k < n-1; k++) //最多松弛n-1次;
{
minn = inf;
for(int i = 1; i <= n; i++) //找到离源点最近的且未被使用过的点;
{
if(minn > dis[i] && !vis[i])
{
minn = dis[i];
tag = i;
}
}
vis[tag] = 1; //该点最短路已经确定了;
for(int i = 1; i <= n; i++)
dis[i] = min(dis[i],dis[tag]+e[tag][i]); //通过该点松弛其它所有点;
}
}
int main()
{
int u,v,w;
while(cin>>n>>m)
{
init();
for(int i = 0; i < m; i++)
{
cin>>u>>v>>w;
e[u][v] = w;
e[v][u] = w;
}
Dijkstra(1);
for(int i = 1; i <= n; i++) //输出最短路;
cout<<dis[i]<<" ";
cout<<endl;
}
return 0;
}
2.邻接表写Dijkstra:
#include<iostream>
#include<cstring>
#define inf 0x3f3f3f3f
#define maxn 100010
#define vertex 5050
using namespace std;
struct node
{
int to,v,next;
}e[maxn];
int head[vertex],dis[vertex],cnt,n,m;
bool vis[vertex];
void init()
{
cnt = 0;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
memset(dis,inf,sizeof(dis));
}
void addedge(int u,int v,int w)
{
e[cnt].to = v;
e[cnt].v = w;
e[cnt].next = head[u];
head[u] = cnt++;
}
void Dijkstra(int s)
{
dis[s] = 0;
vis[s] = 0;
int tag,minn;
for(int i = head[s]; i != -1; i = e[i].next) //预处理dis;
{
int to = e[i].to;
dis[to] = e[i].v;
}
for(int k = 0; k < n-1; k++) //松弛n-1次即可;
{
minn = inf;
for(int i = 1; i <= n; i++) //找到离源点最近的且未被使用过的点;
{
if(minn > dis[i] && !vis[i])
{
minn = dis[i];
tag = i;
}
}
vis[tag] = 1; //该点最短路已经确定;
for(int i = head[tag]; i != -1; i = e[i].next) //通过该点松弛其它点;
{
dis[e[i].to] = min(dis[e[i].to],dis[tag]+e[i].v);
}
}
}
int main()
{
int u,v,w;
while(cin>>n>>m)
{
init();
for(int i = 0; i < m; i++)
{
cin>>u>>v>>w;
addedge(u,v,w);
addedge(v,u,w);
}
Dijkstra(1);
for(int i = 1; i <= n; i++) //输出最短路;
cout<<dis[i]<<" ";
cout<<endl;
}
return 0;
}
或者用vector写邻接表版本Dijkstra:
#include<iostream>
#include<vector>
#include<cstring>
#define inf 0x3f3f3f3f
#define vertex 5050
using namespace std;
typedef pair<int,int> pir;
vector<pir> e[vertex]; //first代表to, second代表value;
int dis[vertex],m,n;
bool vis[vertex];
void init()
{
memset(dis,inf,sizeof(dis));
memset(vis,0,sizeof(vis));
for(int i = 0; i < vertex; i++)
e[i].clear();
}
void Dijkstra(int s)
{
dis[s] = 0;
vis[s] = 1;
int tag,minn;
for(int i = 0; i < e[s].size(); i++) //预处理dis;
{
int to = e[s][i].first;
dis[to] = e[s][i].second;
}
for(int k = 0; k < n-1; k++) //松弛n-1次即可;
{
minn = inf;
for(int i = 1; i <= n; i++) //找到离源点最近的且未被使用过的点;
{
if(minn > dis[i] && !vis[i])
{
minn = dis[i];
tag = i;
}
}
vis[tag] = 1;
for(int i = 0; i < e[tag].size(); i++) //通过该点松弛其它点;
{
int to = e[tag][i].first;
dis[to] = min(dis[to],dis[tag]+e[tag][i].second);
}
}
}
int main()
{
int u,v,w;
while(cin>>n>>m)
{
init();
for(int i = 0; i < m; i++)
{
cin>>u>>v>>w;
e[u].push_back(make_pair(v,w));
e[v].push_back(make_pair(u,w));
}
Dijkstra(1);
for(int i = 1; i <= n; i++) //输出最短路;
cout<<dis[i]<<" ";
cout<<endl;
}
return 0;
}
还能更好吗?——当然可以!
我们考虑算法的时间复杂度,用邻接矩阵存的图,时间复杂度应该是O(n^2)级别的,因为每次需要找点并松弛其余点,这是O(n),外层循环套松弛轮数O(n),因此综合时间复杂度为O(n^2),而我们用邻接表存图,更新最短距离只需要访问找到的那个点所有的边即可,这是O(m),但每次还是要枚举所有顶点来找离源点最近且未使用的点以供下次松弛其余点,时间复杂度是O(n),因此最后还是O(n^2)的复杂度,那我们考虑缩短找点的时间——优先队列!
优先队列是一种按优先级自动排序的数据结构,和队列相似,每次均从队首取元素,不同的是取的元素是按照一定优先级最高的元素,堆内部排序算法可以了解一下,为log(n),我们可以规定一个小根堆,即让小的元素优先级高,这样每次取出元素总是最小的,在之前的步骤中将符合条件的点放入堆中,取出的就是最小的符合条件的顶点,这样取最小值操作的复杂度就变成了O(1),加上之前的堆排序O(log n),和边更新的最多是O(2*m),每条边最多松弛两次,因为一条边有两个顶点,所以,总的时间复杂度为O((2*m+n)*log n),这样的优化对于稀疏图来说,极为有效,注意以上说的m为边数,n为顶点数
于是我们可以写一个,以STL实现的优先队列:
typedef pair<int,int> pir;
struct node
{
int to,v,next;
}e[maxn];
int head[vertex],dis[vertex],cnt;
bool vis[vertex];
struct cmp //比较函数,重载()运算符;
{
bool operator()(pir a,pir b)
{
return a.second > b.second;
}
};
void Dijkstra(int s)
{
dis[s] = 0;
priority_queue<pir,vector<pir>,cmp> q; //优先队列;
q.push(make_pair(s,dis[s]));
while(!q.empty())
{
pir tmp = q.top();
q.pop();
if(vis[tmp.first]) continue; //每个点只能入队一次;
vis[tmp.first] = 1;
int now = tmp.first; //队首的元素一定是最小的;
for(int i = head[now]; i != -1; i = e[i].next)
{
if(dis[e[i].to] > dis[now]+e[i].v)
{
dis[e[i].to] = dis[now]+e[i].v;
q.push(make_pair(e[i].to,dis[e[i].to]));
}
}
}
}
或者用pair<>存点,写一个:
typedef pair<int,int> pir;
struct cmp
{
bool operator()(pir a,pir b)
{
return a.second > b.second;
}
};
vector<pir> e[maxn]; //first代表to, second代表value;
int dis[vertex];
bool vis[vertex];
void Dijkstra(int s)
{
dis[s] = 0;
priority_queue<pir,vector<pir>,cmp> q; //优先队列;
q.push(make_pair(s,dis[s]));
while(!q.empty())
{
pir tmp = q.top();
q.pop();
if(vis[tmp.first]) continue; ////每个点只能入队一次;
vis[tmp.first] = 1;
int now = tmp.first; //队首元素一定是最小的;
for(int i = 0; i < e[now].size(); i++)
{
if(dis[e[now][i].first] > dis[now]+e[now][i].second)
{
dis[e[now][i].first] = dis[now]+e[now][i].second;
q.push(make_pair(e[now][i].first,dis[e[now][i].first]));
}
}
}
}
以上代码如果有阅读障碍可以去看看我写的C++ priority_queue的自定义比较方式,优先队列主要是掌握这个就好用了
那么接下来我们以一个非常经典的例子——HDU 1874,来看一看,Dijkstra的应用:
这就是一个裸题,给你一个从0~n-1编号的图,m条边,问是否存在一条路是S到T,若存在输出一条最短路,不存在输出-1,首先我们看到,题目给的边都是正向边,并且求的是单源最短路径,显然我们考虑Dijkstra算法,代码如下:
AC_1:
//HDU-1874
#include<iostream>
#include<cstring>
#include<queue>
#include<vector>
#define inf 0x3f3f3f3f
#define maxn 2050
#define vertex 205
using namespace std;
typedef pair<int,int> pir;
struct cmp
{
bool operator()(pir a,pir b)
{
return a.second > b.second;
}
};
struct node
{
int to,v,next;
}e[maxn];
int head[vertex],dis[vertex],cnt;
bool vis[vertex];
void init()
{
cnt = 0;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
memset(dis,inf,sizeof(dis));
}
void addedge(int u,int v,int w)
{
e[cnt].to = v;
e[cnt].v = w;
e[cnt].next = head[u];
head[u] = cnt++;
}
void Dijkstra(int s)
{
dis[s] = 0;
priority_queue<pir,vector<pir>,cmp> q;
q.push(make_pair(s,dis[s]));
while(!q.empty())
{
pir tmp = q.top();
q.pop();
if(vis[tmp.first]) continue;
vis[tmp.first] = 1;
int now = tmp.first;
for(int i = head[now]; i != -1; i = e[i].next)
{
if(dis[e[i].to] > dis[now]+e[i].v)
{
dis[e[i].to] = dis[now]+e[i].v;
q.push(make_pair(e[i].to,dis[e[i].to]));
}
}
}
}
int main()
{
int u,v,w,s,t,n,m;
while(cin>>n>>m)
{
init();
for(int i = 0; i < m; i++)
{
cin>>u>>v>>w;
addedge(u,v,w);
addedge(v,u,w);
}
cin>>s>>t;
Dijkstra(s);
if(dis[t] != inf) cout<<dis[t]<<endl;
else cout<<"-1"<<endl;
}
return 0;
}
AC_2:
//HUD-1874
#include<iostream>
#include<queue>
#include<vector>
#include<cstring>
#define inf 0x3f3f3f3f
#define maxn 2050
#define vertex 205
using namespace std;
typedef pair<int,int> pir;
struct cmp
{
bool operator()(pir a,pir b)
{
return a.second > b.second;
}
};
vector<pir> e[maxn]; //first代表to, second代表value;
int dis[vertex];
bool vis[vertex];
void init()
{
memset(dis,inf,sizeof(dis));
memset(vis,0,sizeof(vis));
for(int i = 0; i < maxn; i++)
e[i].clear();
}
void Dijkstra(int s)
{
dis[s] = 0;
priority_queue<pir,vector<pir>,cmp> q;
q.push(make_pair(s,dis[s]));
while(!q.empty())
{
pir tmp = q.top();
q.pop();
if(vis[tmp.first]) continue;
vis[tmp.first] = 1;
int now = tmp.first;
for(int i = 0; i < e[now].size(); i++)
{
if(dis[e[now][i].first] > dis[now]+e[now][i].second)
{
dis[e[now][i].first] = dis[now]+e[now][i].second;
q.push(make_pair(e[now][i].first,dis[e[now][i].first]));
}
}
}
}
int main()
{
int u,v,w,s,t,n,m;
while(cin>>n>>m)
{
init();
for(int i = 0; i < m; i++)
{
cin>>u>>v>>w;
e[u].push_back(make_pair(v,w));
e[v].push_back(make_pair(u,w));
}
cin>>s>>t;
Dijkstra(s);
if(dis[t] != inf) cout<<dis[t]<<endl;
else cout<<"-1"<<endl;
}
return 0;
}
从运行时间可以看出,堆优化的Dijkstra在时间复杂度上有很大的优势(虽然没有对比未优化的版本),而且两种邻接表在内存上也相差无几,可以说是非常实用的算法了,但是千万注意,存在负边权的图,不能用Dijkstra!
终于写完了,好菜,好累。。。