目录
我们今天讲一讲另一种求单源最短路径(非负权图)的算法, Dijkstra 。
思路与过程
Dijkstra 也是要使用松弛操作的,如果不太懂松弛操作,可以看看 Bellman-Ford 中松弛操作的解释。
在讲解 Dijkstra 算法之前,我们先看看一些定义。
n 代表顶点的个数, m 代表边数。
w(u,v) 代表从 u 到 v 的边的边权;
s 表示起点,如果有终点则用 t 表示。
定义 dist ( u ) 代表我们当前求出的 s 到 u 的最短路径的长度。
我们还需要一个集合 A ,满足对于所有集合 A 中的顶点 x ,我们都已经计算出了从 s 到 x 的最短路径,dist ( x ) 存储的就是最短路径的长度。
我们再来看看 Dijkstra 算法的过程。
我们初始化 dist(s)=0 ,其他的点为无穷大。接下来,我们重复进行以下操作:
1.将一个距离起点最近的,同时还不在 A 里的顶点加入集合 A 。
2.同时,利用这个点所有连出去的边,通过松弛操作去尝试更新其他点的 dist 。
当没有新的顶点可以加入 A 时,算法结束。
那么,我们算法的时间复杂度是多少呢?
操作2的总时间复杂度是 。
我们在操作1中,要查距起点最近的顶点,有好几种做法,做法的不同直接导致了效率的不同。
查找做法1.暴力
我们直接暴力寻找距离起点最近的结点,也是最短路长度最小的结点。这样的话,操作1的时间复杂度为 ,总时间复杂度为 。
代码:
#include<bits/stdc++.h>
using namespace std;
struct node{
int v,w;
node(int _v,int _w)
{
v=_v;
w=_w;
};
};
int n,m;
vector<node> vec[1000010];
int dist[1000010];
bool vis[1000010];
void dijkstra(int s,int t)
{
memset(vis,false,sizeof(vis));
memset(dist,127,sizeof(dist));
dist[1]=0;
for(;;)
{
int x=-1;
for(int i=1;i<=n;i++)
{
if(!vis[i]&&dist[i]<1<<30)
{
if(x==-1||dist[i]<dist[x])
{
x=i;
}
}
}
if(x==-1||x==t)
{
break;
}
vis[x]=true;
for(auto i:vec[x])
{
dist[i.v]=min(dist[i.v],dist[x]+i.w);
}
}
if(dist[t]<1<<30)
{
cout<<dist[t]<<" ";
}
else
{
cout<<-1<<" ";
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
vec[u].push_back(node(v,w));
}
for(int i=1;i<=n;i++)
{
dijkstra(1,i);
}
return 0;
}
查找做法2.二叉堆
我们每成功松弛一条边 ( u , v ) ,就将 u 插入二叉堆(如果 u 已经在二叉堆里了,我们就修改相应元素的权值就行了)。操作1我们可以直接取堆顶点就行了。一共有 m 次修改或插入, n 次删除堆顶的操作,每次修改、插入或删除的时间复杂度都是 ,所以我们总的时间复杂度就是 。
查找做法3.优先队列
和二叉堆差不多,但使用优先队列时,如果同一个点的最短路被更新多次,因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,故优先队列内的元素个数有 m 个,时间复杂度为 。
代码:
#include<bits/stdc++.h>
using namespace std;
struct edge
{
int v,w;
};
struct node
{
int dist,x;
bool operator>(const node &a)const
{
return dist>a.dist;
}
};
priority_queue<node,vector<node>,greater<node> >q;
int n,m;
vector<edge>vec[100010];
int dist[100010];
bool vis[100010];
void dijkstra(int s)
{
memset(dist,127,sizeof(dist));
dist[s]=0;
q.push({dist[s],s});
while(!q.empty())
{
int x=q.top().x;
q.pop();
if(vis[x])
{
continue;
}
vis[x]=true;
for(auto i:vec[x]) {
int v=i.v;
int w=i.w;
if(dist[v]>dist[x]+w)
{
dist[v]=dist[x]+w;
q.push({dist[v],v});
}
}
}
for(int i=1;i<=n;i++)
{
cout<<dist[i]<<" ";
}
}
int main()
{
cin>>n>>m;
while(m--)
{
int u,v,w;
cin>>u>>v>>w;
vec[u].push_back({v,w});
}
dijkstra(1);
return 0;
}
查找做法4.set
就是把优先队列换成了 set 啦,其他的一模一样。
代码:
#include<bits/stdc++.h>
using namespace std;
struct node{
int v,w;
node(int _v,int _w)
{
v=_v;
w=_w;
};
};
int n,m;
vector<node> vec[1000010];
int dist[1000010];
set<pair<int,int> >ss;
void dijkstra(int s,int t)
{
memset(dist,127,sizeof(dist));
dist[1]=0;
ss.clear();
for(int i=1;i<=n;i++)
{
ss.insert({dist[i],i});
}
for(;!ss.empty();)
{
int x=ss.begin()->second;
ss.erase(ss.begin());
if(dist[x]>1<<30||x==t)
{
break;
}
for(auto i:vec[x])
{
if(dist[x]+i.w<dist[i.v])
{
ss.erase({dist[i.v],i.v});
dist[i.v]=dist[x]+i.w;
ss.insert({dist[i.v],i.v});
}
}
}
if(dist[t]<1<<30)
{
cout<<dist[t]<<" ";
}
else
{
cout<<-1<<" ";
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v,w;
cin>>u>>v>>w;
vec[u].push_back(node(v,w));
}
for(int i=1;i<=n;i++)
{
dijkstra(1,i);
}
return 0;
}
查找做法5.斐波那契堆
也和前面的也差不多,只不过是斐波那契堆插入的时间复杂度是 ,所以总时间复杂度是 ,但是它写起来比较困难。
查找做法6.线段树
还是和二叉堆差不多,插入操作改成在线段树上执行单点修改,而操作1就是线段树上的全局查询最小值。时间复杂度 。