最短路旨在解决这样一种问题,在图中,需要知道从点i到点j的路径长度的最小值。
松弛操作:
在最短路中,我们需要做松弛操作,即当我们能够找到一个点k使得i->j能够通过k做中转变得更小,即满足i->j < i->k + k->j ,此时我们将i->j 更新为 i->k + k->j,此为松弛操作。
多源最短路
Folyd算法:
Folyd算法采用动态规划的算法,我们这样抽象问题:在考察n个点的情况下,求i->j的最短路径长度。当我们考察是否能够经过第k个点时使得i->j通过k这个中继变短,即i->j > i->k + k->j。算法原理是生成一系列中继矩阵,k表示考虑了1~k个点作为中继,那么自然有为最终的i->j最短路径长度。但是不知道大家有没有个疑问,最短路本身的路径顺序并不是按照从1~n的呀,那为什么Foldy还是正确的呢?
假设i-j的最小路径上有两个中继点k1、k2,(k1<k2),现在假设i->j的最短路径是i->...->k2->...k1->....->j,且当前考虑的中继点是k2,即在i->j的这条最短路径上,所有的点大小都满足<=k2,那么我们将这条路径分割成i->k2与k2->j两端,由于路径上(不包括端点)的点都满足<k,因此他们的最短路径大小就是与,故算法正确。
核心代码:
void Foldy(int[][] dist,int n){
for(int k=1;k<=n;++k){//考虑让每条关系通路都经过k 是否能变短
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
}
}
}
}
void initFoldy(){
int dist[410][410];
memset(dist,0x3f,sizeof(dist));//0x3f3f3f3f + 0x3f3f3f3f < INT_MAX
for(int i=1;i<=n;++i)dist[i][i]=0;
}
单源最短路
单源最短路是要求源点S到其余所有点的最短路径长度。
Bellman-Ford算法:
由图论的基本知识我们可以知道,在一个一共有n个点的连通图中,点x与源点S构成的最短路径上最多有n个点即n-1段(且这样的点只存在一个),即构成链式结构。因此,我们只需要对所有的边统统松弛n-1次,那么只要图本身不存在负环,则n-1后的松弛结果就是最短路。而且,根据松弛操作的更新规则,多余的松弛并不会影响最终结果。
当Bellman-Ford运行第n次时,如果有边还能被松弛,说明图中必定存在负环。
//单源最短路
//最短路的子路也是最短路
//最短路上的节点数最多为n个 由n-1条边构成 最多松弛n-1次 不需要考虑每次松弛哪些边,直接松弛所有边
void Bellman-Ford(int n,int m){//n:点个数 m:边条数
for(int i=1;i<n;++i){
for(int j=1;j<=m;++j){
dist[edge[j].to]]=min(dist[edge[j].to],dist[edge[j].from]+edge[j].w);
}
}
}
bool Bellman-Ford-judge(int n,int m){//判负环 n:点个数 m:边条数 第n次松弛时 有更新 则定有负环
for(int i=1;i<n;++i){
for(int j=1;j<=m;++j){
dist[edge[j].to]]=min(dist[edge[j].to],dist[edge[j].from]+edge[j].w);
}
}
for(int j=1;j<=m;++j){
if(dist[edge[j].from]+edge[j].w<dist[edge[j].to])return false;
}
return true;
}
SPFA算法:
SPFA是以Bellman-Ford算法为基础的优化,它基于这样的考虑:每次我们基于一个源点S进行松弛的时候,只能把那些S能够到达的点(被S更新了的点)进行更新,也就是说,只有这些被更新的点用于更新,才有可能使得他们能够达到的点与源点S之间的路径长度变小。因此SPFA采用一个队列进行优化,每次只将可能引起更新的点放入队列,并且保证队列里不会有重复的点。SPFA的原理如下:设S->x的最短路径为S->p1->p2->...->x,那么在对S能到达的所有点进行松弛时,p1一定在其中,而使用p1松弛时,p2也一定在其中,如此就可以得到S->x的最短路径。
//SPFA
//只把可能导致更新的起始点入队列
//存储结构采用链式前向星
void SPFA(int s){
queue<int> que;
que.push(s);
dist[s]=0;
int p;
while(!que.empty()){
p=que.front();
que.pop();
inque[p]=0;//表示该点不在队列中
for(int cur=head[p];cnt!=0;cnt=edge[cur].next){
int to=edge[cnt].to;
if(dist[to]<dist[p]+edge[cur].w){
//pre[to]=p;
dist[to]=dist[p]+edge[cur].w;
if(!inque[to]){//发生更新的点不在队列中
inque[to]=1;
que.push(to);
}
}
}
}
}
//判负环 每次松弛后,如果一个点入队列n次,则必有负环 否则最多入队列n-1次
bool SPFA-judge(int s){
queue<int> que;
que.push(s);
dist[s]=0;
count[s]=1;
int p;
while(!que.empty()){
p=que.front();
que.pop();
inque[p]=0;//表示该点不在队列中
for(int cur=head[p];cnt!=0;cnt=edge[cur].next){
int to=edge[cnt].to;
if(dist[to]<dist[p]+edge[cur].w){
dist[to]=dist[p]+edge[cur].w;
if(!inque[to]){//发生更新的点不在队列中
count[to]++;
if(count[to]==n)return true;
inque[to]=1;
que.push(to);
}
}
}
}
return false;
}
事实上,SPFA虽然保证了队列在某个时刻不会有两个相同的点,但是不能保证一个点多次入队列,这个过程持续到这个点的dist值被更新为最短路径。而SPFA和Bellman-Ford判负环的方式如出一辙,因为最短路径最长可以有n-1段,因此一个点最多会入队列n-1次,那么只需要统计一下入队次数,(在有负环的情况下SPFA是不会终止的,需要手动跳出),一旦有一个点入队了n次,即存在负环。
P3371 【模板】单源最短路径(弱化版)
提交177.81k
通过51.26k
时间限制1.00s
内存限制125.00MB
题目描述
如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
输入格式
第一行包含三个整数 n,m,sn,m,s,分别表示点的个数、有向边的个数、出发点的编号。
接下来 mm 行每行包含三个整数 u,v,wu,v,w,表示一条 u \to vu→v 的,长度为 ww 的边。
输出格式
输出一行 nn 个整数,第 ii 个表示 ss 到第 ii 个点的最短路径,若不能到达则输出 2^{31}-1231−1。
输入输出样例
输入
4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4
输出
0 2 4 3
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int to,w,next;
};
int cnt,head[10010],dist[10010];
Edge edge[500010];
void add(int from,int to,int weight){
edge[++cnt].to=to;
edge[cnt].w=weight;
edge[cnt].next=head[from];
head[from]=cnt;
}
void SPFA(int S){
memset(dist,0x3f,sizeof(dist));
queue<int> que;
que.push(S);
inque[S]=true;
dist[S]=0;
int e,to,w;
while(!que.empty()){
S=que.front();
que.pop();
inque[S]=false;
for(e=head[S];e!=0;e=edge[e].next){
to=edge[e].to;w=edge[e].w;
if(dist[to]>dist[S]+w){
dist[to]=dist[S]+w;
if(!inque[to]){
que.push(to);
inque[to]=true;
}
}
}
}
}
int main(){
int n,m,s,u,v,w,i;
cin>>n>>m>>s;
for(i=1;i<=m;++i){
cin>>u>>v>>w;
add(u,v,w);
}
SPFA(s);
for(i=1;i<=n;++i){
cout<<(dist[i]==0x3f3f3f3f?0x7fffffff:dist[i])<<" ";
}
return 0;
}
Dijkstra算法
基于贪心的思想,每次将松弛得到的最短路径值的点作为新的出发点做新的松弛。
1.在没有负环的条件下,首先我们用源点作为出发点松弛所有边,此时每个点的dist值保存的是由S直接到它的距离,其中拥有最小dist值的点,其dist值就是它与源点的最短路径的长度,因为不存在负环,所以不可能通过其他点使得它与源点的最短路径更短。
2.我们用上一次的最小dist值的点S'进行松弛,并使得该点无法再被使用,而此时松弛过的所有dist值可以分为由S直接到达和经过S'到达,同理,由于不存在负环,所以该次最小dist值的点一定是最终答案。后面继续该过程直到点使用完。
void Djkstra(int S,int n){
memset(dist,0x3f,sizeof(dist));
dist[S]=0;
int i,j,k,to,w,MinDist;
for(i=1;i<=n;++i){
MinDist=0x3f3f3f3f;
for(j=1;j<=n;++j){
if(!used[j]&&MinDist>dist[j]){
MinDist=dist[j];
k=j;
}
}
used[k]=true;
for(j=head[k];j!=0;j=edge[j].next){
to=edge[j].to;w=edge[j].w;
if(!used[to])dist[to]=min(dist[to],dist[k]+w);
}
}
}
朴素的DjkStra也可以过。。=-=
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int to,w,next;
};
int cnt,head[10010],dist[10010];
Edge edge[500010];
void add(int from,int to,int weight){
edge[++cnt].to=to;
edge[cnt].w=weight;
edge[cnt].next=head[from];
head[from]=cnt;
}
bool used[10010];
void Djkstra(int S,int n){
memset(dist,0x3f,sizeof(dist));
dist[S]=0;
int i,j,k,to,w,MinDist;
for(i=1;i<=n;++i){
MinDist=0x3f3f3f3f;
for(j=1;j<=n;++j){
if(!used[j]&&MinDist>dist[j]){
MinDist=dist[j];
k=j;
}
}
used[k]=true;
for(j=head[k];j!=0;j=edge[j].next){
to=edge[j].to;w=edge[j].w;
if(!used[to])dist[to]=min(dist[to],dist[k]+w);
}
}
}
int main(){
int n,m,s,u,v,w,i;
cin>>n>>m>>s;
for(i=1;i<=m;++i){
cin>>u>>v>>w;
add(u,v,w);
}
Djkstra(s,n);
for(i=1;i<=n;++i){
cout<<(dist[i]==0x3f3f3f3f?0x7fffffff:dist[i])<<" ";
}
return 0;
}
事实上,朴素的Dijkstra算法需要O(n^2)的复杂度。但是由于它是取最小值,因此可以采用堆优化的方式,将复杂度压低至O(nlogn)。然而Dijikstra的队列优化将不可避免地让同一时刻里可能存在多个重复的点,而他们拥有不同的dist值(因为是在不同的松弛过程中得到的),但是由于我们每次取得是dist最小的点,因此当我们使用完这个点后,会将它标记为不可用,因此即便我们后面又一次拿到了dist值更大的相同点,但是由于已经使用过dist值更小的点进行更新,因此直接忽略就好了,不会对算法造成任何影响。
//Dijkstra算法 队列优化
typedef pair<int,int> Polar;// dist、to 按照dist排序
void Dijkstra(int s){
dist[s]=0;
priority_queue<Polar,vector<Polar>,greater<Polar> > que;
que.push(make_pair(0,s));
int p,e,to;
while(!que.empty()){
p=que.top().second;
if(vis[p])continue;//如果p点在之前已经被用于更新过 则这次的答案并不是最优值 不再用于更新
vis[p]=1;
for(e=head[p];e!=0;e=edge[e].next){
to=edge[e].to;
if(dist[to]>dist[p]+edge[e].w){
dist[to]=dist[p]+edge[e].w;//松弛虽然可能进行多次 但是最短的那条一定会被最先用于更新别人,更新别人后vis=1,其他结果就失效了
pre[to]=p;
if(!vis[to])que.push(make_pair(dist[to],to));//没发生松弛则没必要放入队列 因为一开始出了源点 所有点的dist是INF 至少都会发生一次松弛
}
}//也就是说 队列里会出现有不同dist却有相同to的情形,但是那个dist更小的to会被用来更新别的点,所以无需担心,而且dist[]数组的答案一定是较小的那个
}
}
//路径打印 用pre数组指向父亲节点 原因:每一个节点的父亲都固定 但是每一个节点的儿子会有很多
void getPath(){
if(edge[e].w+dist[p]<dist[to]){//被更新
pre[to]=p;
dist[to]=edge[e].w+dist[p];
}
}
void printPath(int s,int v){
while(v!=s){
print("%d<-",v);
v=pre[v];
}
print("%d",s);
}
用队列优化的DjkStra过一下洛谷的P3371。
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int to,w,next;
};
int cnt,head[10010],dist[10010];
bool inque[10010];
Edge edge[500010];
bool used[10010];
void add(int from,int to,int weight){
edge[++cnt].to=to;
edge[cnt].w=weight;
edge[cnt].next=head[from];
head[from]=cnt;
}
typedef pair<int,int> Dist; //<dist,i>
void Djkstra(int S,int n){
memset(dist,0x3f,sizeof(dist));
dist[S]=0;
priority_queue<Dist,vector<Dist>,greater<Dist> > DistQue;//构造小根堆
DistQue.push(Dist{0,S});
Dist Top;
int distance,i,to,w;
while(!DistQue.empty()){
Top=DistQue.top();//取出一个拥有最小dist值的人 用他进行更新 并标记为已使用
DistQue.pop();
distance=Top.first;
S=Top.second;
if(used[S])continue;//used为真 说明已经用于更新了 所以队列中存储的这个dist值在没有负环的情况下一定不会小于上次用于更新的值
used[S]=true;//标记为使用
for(i=head[S];i!=0;i=edge[i].next){
to=edge[i].to;w=edge[i].w;
if(dist[to]>distance+w){
dist[to]=distance+w;
DistQue.push(Dist{dist[to],to});
}
}
}
}
int main(){
int n,m,s,u,v,w,i;
cin>>n>>m>>s;
for(i=1;i<=m;++i){
cin>>u>>v>>w;
add(u,v,w);
}
Djkstra(s,n);
for(i=1;i<=n;++i){
cout<<(dist[i]==0x3f3f3f3f?0x7fffffff:dist[i])<<" ";
}
return 0;
}
P4779 【模板】单源最短路径(标准版)
提交110.58k
通过36.43k
时间限制1.00s
内存限制125.00MB
题目描述
给定一个 n个点,m 条有向边的带非负权图,请你计算从 s出发,到每个点的距离。
数据保证你能从 ss 出发到任意点。
输入格式
第一行为三个正整数 n, m, sn,m,s。 第二行起 mm 行,每行三个非负整数 u_i, v_i, w_iui,vi,wi,表示从 u_iui 到 v_ivi 有一条权值为 w_iwi 的有向边。
输出格式
输出一行 nn 个空格分隔的非负整数,表示 ss 到每个点的距离。
输入输出样例
输入
4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4
输出
0 2 4 3
和上面的题目同一个意思,但是这题的数据卡掉了SPFA,使用队列优化的DjkStra可以过。
#include<bits/stdc++.h>
using namespace std;
struct Edge{
int to,w,next;
};
int cnt,head[100010],dist[100010];
Edge edge[200010];
bool used[100010];
void add(int from,int to,int weight){
edge[++cnt].to=to;
edge[cnt].w=weight;
edge[cnt].next=head[from];
head[from]=cnt;
}
typedef pair<int,int> Dist; //<dist,i>
void Djkstra(int S,int n){
memset(dist,0x3f,sizeof(dist));
dist[S]=0;
priority_queue<Dist,vector<Dist>,greater<Dist> > DistQue;//构造小根堆
DistQue.push(Dist{0,S});
Dist Top;
int distance,i,to,w;
while(!DistQue.empty()){
Top=DistQue.top();//取出一个拥有最小dist值的人 用他进行更新 并标记为已使用
DistQue.pop();
distance=Top.first;
S=Top.second;
if(used[S])continue;//used为真 说明已经用于更新了 所以队列中存储的这个dist值在没有负环的情况下一定不会小于上次用于更新的值
used[S]=true;//标记为使用
for(i=head[S];i!=0;i=edge[i].next){
to=edge[i].to;w=edge[i].w;
if(dist[to]>distance+w){
dist[to]=distance+w;
DistQue.push(Dist{dist[to],to});
}
}
}
}
int main(){
int n,m,s,u,v,w,i;
cin>>n>>m>>s;
for(i=1;i<=m;++i){
cin>>u>>v>>w;
add(u,v,w);
}
Djkstra(s,n);
for(i=1;i<=n;++i){
cout<<dist[i]<<" ";
}
return 0;
}