模板 --单源最短路
模板 --单源最短路
求最短路一般有两种方法,dij,SPFA;
大多数情况下最常用并且最稳妥的就是dij,SPFA一般用于判断负权值和负环,并且如果边较多,SPFA容易被卡死。所以一般情况下都是使用dij。
首先先介绍dij
dij算法的主要思想是先寻找一个点A1,将这个点并入一个集合,然后找出与这个点相连的点中路径最小的点A2,然后将A2并入集合,将与A2相连点A3的路径加上A1到A2点的路径与A1到A3的路径比较,如果前者小于后者,就将A1到A3路径更新。
以此类推,直到将所有点加入集合中。我们用一个数组储存点A到所有点的最短路径。
图的存储也有多种方式,例如二维数组储存(邻接矩阵),C++中STL里的vector储存,还有链式前向星(邻接表)。其中用二维数组储存只能使用与数据范围较小的题,如果数据范围超过1e5,基本都是使用vector和链式前向星储存。
下面先放一个二维数组储存的代码模板;
void Dij(int s) // s为起点
{
memset(vis, 0, sizeof(vis));
dis[s]=0;
for(int i = 1; i <= n; i++)dis[i] = INF;//INF为一个很大的数
int v,mn;
for(int i = 1; i <= n; i++)
{
mn = INF;
for(int j = 1; j <= n; j++)//找最小值的点
{
if(!vis[j] && dis[j] < mn)
{
mn = dis[j];
v = j;
}
}
vis[v] = 1;
for(int j = 1; j <= n; j++)
{
if(!vis[j] && (mn + mp[v][j]) < dis[j])dis[j] = mn + mp[v][j];
}
}
}
一般情况下不推荐使用邻接矩阵存图,很容易TLE;
下面是使用C++STL中的vector存图;
#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<algorithm>
#include<vector>
#include<map>
#include<cmath>
#define long long ll;
using namespace std;
const int MAXN= 0x3f3f3f3f;
const int INF =2147483647;
struct node{
int v;
int w;
};//创建一个结构体,到达点和距离。
node make_out(int x,int y)
{
node t;
t.v = x;
t.w = y;
return t;
//将到达点和距离存入vector。
}
vector<node> q[500001];
int n, m, s;
int d[500001];
int vis[500001];
void dij(int x)
{
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n;i++)
{ d[i] = INF;}//初始化赋值为最大值
for (int i=0; i < q[x].size();i++)
{
int h = q[x][i].v;
d[h] = q[x][i].w;
}//更新与X到与X相邻点的距离
d[x] = 0;//到本身的距离为0
int pos;
for (int i = 1; i < n; i++)//寻找路程最小的点
{
int mm = INF;
for (int j = 1; j <= n; j++)
{
if (!vis[j] && mm > d[j])
{
mm = d[j];
pos = j;
}
}
vis[pos] = 1;
for (int k = 0; k < q[pos].size(); k++)//松弛操作
{
int h = q[pos][k].v;
if (d[h]>q[pos][k].w +d[pos])
{
d[h] = q[pos][k].w +d[pos];
}
}
}
}
int main()
{
cin >> n >> m >> s;
for (int i = 1; i <= m; i++)
{
int x, y, z;
cin >> x >> y >> z;
q[x].push_back(make_out(y, z));
}
dij(s);
for (int i = 1; i <= n;i++)
{
cout << d[i]<<' ';
}
return 0;
}
然后就是使用链式前向星,跟Vector一样都是邻接表储存方式。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<algorithm>
#include<vector>
#include<map>
#include<cmath>
using namespace std;
const int INF= 0x3f3f3f3f;
const int MAXN = pow(2, 31) - 1;
int head[500001];//head是储存最新起始点的位置
int ans = 0;
queue<int> q;
struct edge
{
int u, v, w, next;//next是储存上一条边的位置
}e[500001];
int n, m;
void add(int u,int v,int w)//链式前向星存图
{
e[++ans].next = head[u];
e[ans].v = v;
e[ans].w = w;
head[u] = ans;
}
int d[500001];
int vis[500001];
void DIJ(int x)
{
memset(vis, 0, sizeof(vis));//跟vector类似
for (int i = 1; i <= n;i++)
{
d[i] = MAXN;
}
d[x] = 0;
int pos = x;
while (!vis[pos])//这里的意思是是否访问所有点的
{for (int i = head[pos]; i != 0;i=e[i].next)
{
int h = e[i].v;//松弛操作
if(d[h]>d[pos]+e[i].w)
{
d[h] = d[pos] + e[i].w;
}
}
vis[pos] = 1;
int mn = MAXN;
for (int j = 1; j <= n; j++)//寻找最大值的点
{
if (!vis[j] && mn > d[j])
{
mn = d[j];
pos = j;
}
}
}
}
int main()
{
int s;
cin >> n >> m>>s;
for (int i = 1; i <= m;i++)
{
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
}
DIJ(s);
for (int i = 1; i <= n;i++)
{
cout << d[i] << " ";
}
return 0;
}
以上三种方式都是使用普通的dij,但是有时候会遇到一些数据很大的,dij还是可以优化的,最常用的就是dij的堆优化。
所谓的堆优化就是使用C++STL中的优先队列来维护dij,dij中有一步是寻找距离最小的点。使用堆优化就可以减少时间复杂度。
下面放堆优化模板
#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<algorithm>
#include<vector>
#include<map>
using namespace std;
const int MAXN= 0x3f3f3f3f;
struct node{
int u;
int s2;
bool operator <(const node& r)const//重载优先队列,最小堆
{
return r.s2 < s2;
}
};
struct MS
{
int u, v, w, next;
}e[200001];
int head[200001];
int ans = 0;
void add(int u,int v,int w)//链式前向星
{
e[++ans].next = head[u];
e[ans].v = v;
e[ans].w = w;
head[u] = ans;
}
priority_queue<node> q;//优先队列
int d[200001];
int n, m,s;
void dij()
{
for (int i = 1; i <= n;i++)
{ d[i] = 2147483647;}
d[s] = 0;
q.push((node){s, 0});//将距离和点放入优先队列中
while(!q.empty())
{
node t = q.top();//取堆顶元素,距离一定是最小的
q.pop();
int u = t.u;
int s1 = t.s2;
if(s1!=d[u])//这步是堆优化的基本操作,防止没更新的进入循环,减少时间复杂度
continue;
for (int i = head[u]; i != 0;i=e[i].next)
{int v1 = e[i].v;
if (d[v1]>d[u]+e[i].w)//链式前向星基本遍历操作,松弛操作
{
d[v1] = d[u] + e[i].w;
q.push((node){v1, d[v1]});//入队
}
}
}
}
int main()
{
cin >> n >> m >> s;
for (int i = 1; i <= m;i++)
{
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
}
dij();
for (int i = 1; i <= n;i++)
{ cout << d[i]<<' ';}
return 0;
}
dij算法差不多结束了,接下来就是SPFA.
SPFA也是一种求单源最短路的算法。
其实它的思路跟dij差不多,但是不同的是dij的入集合的点是不会再变动,而SPFA中集合中的点一直有变动,并且SPFA是使用队列来维护,已经入集合的点也是有可能出来。
下面放代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<algorithm>
#include<vector>
#include<map>
#include<cmath>
using namespace std;
const int INF= 0x3f3f3f3f;
const int MAXN = pow(2, 31) - 1;
struct node{
int u, v, w, next;
}e[500001];
int head[500001];
int ans = 0;
void add(int u,int v,int w)//链式前向星
{
e[++ans].next = head[u];
e[ans].v = v;
e[ans].w = w;
head[u] = ans;
}
int n, m, s;
queue<int> q;
int vis[500001];
int d[500001];
void SPFA(int x)
{
for (int i = 1; i <= n;i++)
d[i] = MAXN;
d[x] = 0;
q.push(x);
vis[x] = 1;//入队标记为1
while(!q.empty())
{
int h = q.front();
q.pop();
vis[h] = 0;//出队标记为0
for (int i = head[h]; i!=0;i=e[i].next)
{
int t = e[i].v;
if(d[t]>d[h]+e[i].w)//松弛操作
{ d[t] = d[h] + e[i].w;
if(!vis[t])//如果队里不存在相同的点,则入队。
{
q.push(t);
}}
}
}
}
int main()
{
cin >> n >> m >> s;
memset(vis, 0, sizeof(vis));
memset(d, 0, sizeof(d));
for (int i = 1; i <= m;i++)
{
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
}
SPFA(s);
for (int i = 1; i <= n;i++)
{
cout << d[i] << ' ';
}
return 0;
}