介绍
英文名: Dijkstra
用处: 这是个用来在边有非负权的图中求单源最短路的算法。
(注:单源,即只有一个固定的起点)
流程
说白了,就一句话:每次找到离起点最近的点,然后用它到起点的最短路来更新其他的点到起点的最短路,以及,每个点只用一次。
证明和其他的在下面,这里先举个栗子!
比如说现在有这样一张
5
5
5 个点的图(起点是
1
1
1):
下面的
d
i
s
dis
dis 数组表示现在每个点到起点的距离,因为其他点一开始都不知道,所以我们需要初始化成一个比较大的数。
那么一开始离起点最近的肯定就是起点自己啦,于是用点
1
1
1 进行一次更新,点
2
,
3
2,3
2,3 会被更新。
为了方便,我们将
1
1
1 号点标成绿色,表示这个点已经用过了。
那么现在能用的点有:
2
,
3
,
4
,
5
2,3,4,5
2,3,4,5,其中离起点最近的是点
3
3
3。然后用点
3
3
3 进行一次更新,点
4
,
5
4,5
4,5 会被更新。
现在能用的有:
2
,
4
,
5
2,4,5
2,4,5,离起点最近的是点
2
2
2。
用点
2
2
2 进行一次更新,点
4
4
4 会被更新,而点
3
3
3 并不会,因为
d
i
s
[
3
]
<
d
i
s
[
2
]
+
2
dis[3]<dis[2]+2
dis[3]<dis[2]+2。
现在能用的有:
4
,
5
4,5
4,5,离起点最近的是点
4
4
4。
用点 4 4 4 进行一次更新,没有点会被更新。
最后用点 5 5 5 也更新不了其他点,于是我们就这样求出了起点到每个点的最短路。
正确性
为什么这样做是对的呢?
其实也很简单,因为边的权值都是非负的,所以我们每次选出的离起点最近的点,它的 d i s dis dis 已经不可能再被别人更新了,因为剩下的 d i s dis dis 都比他大。
限制
这是个 只能 用来在 边有非负权的图 中求 单源最短路 的算法。
没错,不只是负环,连权值都不能为负。
想想那个正确性的证明,只有当权值非负的时候,我们每次选出的离起点最近的点的 d i s dis dis 才不可能被别人更新,假如有负权边,那么它还是可能被更新。
例:
如果遇上这种情况,那么点
1
1
1 会先更新
2
,
3
2,3
2,3,此时:
d
i
s
:
0
,
2
,
3
,
∞
dis:0,2,3,\infty
dis:0,2,3,∞
然后,点
2
2
2 更新
4
4
4,得到:
d
i
s
:
0
,
2
,
3
,
3
dis:0,2,3,3
dis:0,2,3,3
然后,点
3
3
3 更新
2
2
2,得到:
d
i
s
:
0
,
1
,
3
,
3
dis:0,1,3,3
dis:0,1,3,3
显然 d i s [ 4 ] dis[4] dis[4] 的值不应该是 3 3 3,而应该是 2 2 2。
普通的模板
bool v[maxn];//记录每个点是否用过
int dis[maxn];
void dij(int st)
{
for(int i=0;i<=n;i++)//初始化
dis[i]=2147483647;
dis[st]=0;
for(int i=1,x;i<=n;i++)
{
x=0;
for(int j=1;j<=n;j++)//找出没用过的点里面离起点最近的点
if(!v[j]&&dis[j]<dis[x])x=j;
v[x]=true;//标记为用过
for(int j=first[x];j;j=e[j].next)//更新其他点
if(dis[e[j].y]>dis[x]+e[j].z)dis[e[j].y]=dis[x]+e[j].z;
}
}
AC代码:
#include <cstdio>
#include <cstring>
#define maxn 10010
int n,m,qidian;
struct edge{int y,z,next;};
edge e[1000010];
int first[maxn],len=0;
void buildroad(int x,int y,int z)
{
e[++len]=(edge){y,z,first[x]};
first[x]=len;
}
bool v[maxn];
int dis[maxn];
void dij(int st)
{
for(int i=0;i<=n;i++)
dis[i]=2147483647;
dis[st]=0;
for(int i=1,x;i<=n;i++)
{
x=0;
for(int j=1;j<=n;j++)
if(!v[j]&&dis[j]<dis[x])x=j;
v[x]=true;
for(int j=first[x];j;j=e[j].next)
if(dis[e[j].y]>dis[x]+e[j].z)dis[e[j].y]=dis[x]+e[j].z;
}
}
int main()
{
scanf("%d %d %d",&n,&m,&qidian);
for(int i=1,x,y,z;i<=m;i++)
scanf("%d %d %d",&x,&y,&z),buildroad(x,y,z);
dij(qidian);
for(int i=1;i<=n;i++)
printf("%d ",dis[i]);
}
我们发现这种不优秀的做法的时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的,这显然不符合它的名声,于是有了下一部分。
堆优化
其实也不是什么奇怪的东西,我们发现,每次要找的都是 d i s dis dis 最小的点,那么我们用堆维护一下最小值即可。
代码如下:
int dis[maxn];
struct par{
int x;
par(int xx):x(xx){};
bool operator <(const par &b)const{return dis[x]==dis[b.x]?(x<b.x):dis[x]<dis[b.x];}
};
set<par> s;
void dij(int st)
{
for(int i=1;i<=n;i++)
dis[i]=2147483647;
dis[st]=0; s.insert(par(st));
while(!s.empty())
{
int x=s.begin()->x; s.erase(par(x));
for(int i=first[x];i;i=e[i].next)
{
int y=e[i].y;
if(dis[y]>dis[x]+e[i].z)
s.erase(par(y)),dis[y]=dis[x]+e[i].z,s.insert(par(y));
//注意,这里一定要先erase,再修改dis[y],不然set里面因为关键字的改变会爆炸
}
}
}
AC代码:
#include <cstdio>
#include <cstring>
#include <set>
#include <algorithm>
using namespace std;
#define maxn 1000010
int n,m,qidian;
struct edge{int y,z,next;};
edge e[maxn<<2];
int first[maxn],len=0;
void buildroad(int x,int y,int z)
{
e[++len]=(edge){y,z,first[x]};
first[x]=len;
}
int dis[maxn];
struct par{
int x;
par(int xx):x(xx){};
bool operator <(const par &b)const{return dis[x]==dis[b.x]?(x<b.x):dis[x]<dis[b.x];}
};
set<par> s;
void dij(int st)
{
for(int i=1;i<=n;i++)
dis[i]=2147483647;
dis[st]=0; s.insert(par(st));
while(!s.empty())
{
int x=s.begin()->x; s.erase(par(x));
for(int i=first[x];i;i=e[i].next)
{
int y=e[i].y;
if(dis[y]>dis[x]+e[i].z)
s.erase(par(y)),dis[y]=dis[x]+e[i].z,s.insert(par(y));
}
}
}
int main()
{
scanf("%d %d %d",&n,&m,&qidian);
for(int i=1,x,y,z;i<=m;i++)
scanf("%d %d %d",&x,&y,&z),buildroad(x,y,z);
dij(qidian);
for(int i=1;i<=n;i++)
printf("%d ",dis[i]);
}