图论——最短路径问题
update:2023.07.19 更新了堆优化版本的 Dijkstra
update:2023.09.21 更新了时间复杂度讨论并修改了堆优化 Dij
Dijkstra
什么是Dijkstra
Dijkstra采用了贪心的策略。
在一张连通图中,我们将点分类为已知点和未知点。
什么是已知点?就是已经遍历过的,例如原点(出发点)就是一个已知点。
什么是未知点?顾名思义,就是未遍历到的点(除了已知点,剩下的就是未知点)
理解贪心策略
这里举一个简单的栗子:
夏福要去supermarket,他面前可以选择3个商店:
- 1.壹加壹超市,据夏福1000m
- 2.美宜佳商店,据夏福500m
- 3.711超市,据夏福200m
他该去哪个超市呢?
这里很容易看出来,肯定是选择第3种方案。
没错,这就是从夏福(原点)到超市(终点)的最短路径。也就是贪心策略。
思考实现代码
因为贪心只能保证局部最优,不能保证全局最优,所以我们还是需要去遍历,但是我们可以缩小遍历的范围。
假想现在从已知点可以到达三个中转站,最后都可以到终点。
那么从起点到终点的路径就是:起点到中转站的路径 + 中转站到终点的路径
我们想让起点到中转站的距离尽可能的小,那肯定是选择据起点最近的中转站作为新的起点(新的已知点)
我们就可以把那个点当作起点,继续找最短路径就好了。
原来那个点怎么办?
丢进垃~圾~桶~里
代码实现
题目:
输出从\(s\)到\(t\)的最短路,并换行输出最短路径
\(Input\)
5 7
1 2 10
1 4 30
1 5 100
2 3 50
3 5 10
4 3 20
4 5 60
1 5
\(Output\)
60
1 4 3 5
\(Code\)
#include <bits/stdc++.h>
using namespace std;
int n, m;
int edge[505][505];
int ss, tt;
bool vis[505];
pair<int, int> dis[505]; // 相当于一个变量里有两个成员:一个是距离,另一个是数组下标所连接的点
vector<int> q[505];
vector<int> ans;
bool cmp(int a, int b)
{
return a < b;
}
int main()
{
scanf("%d%d", &n, &m); // 输入点的个数n,边的数量m
for (int i = 1; i <= m; i++)
{
int l, r, v;
scanf("%d%d%d", &l, &r, &v); // 输入边的信息:边连接的两个点l、r,和边的权值v
edge[l][r] = v; // 表示从l到r这个点的权值是v
/* 因为这个代码是有向图的最短路问题,如果是无向图的话应该再加上下面这行代码:
edge[r][l] = v;
这个表示r到l的路径边权是v */
}
scanf("%d%d", &ss, &tt); // 输入ss(原点)和tt(终点),这个代码是输出从ss到tt的最短路径
for (int i = 1; i <= n; i++)
dis[i].first = 1e9; // 初始化从原点到第i个点的路径全部为正无穷(这里只要数据够大就行了)
fill(vis, vis + n + 1, 0); // 初始化所有的点都没被访问过
dis[ss].first = 0; // 从原点到原点的距离肯定是0
dis[ss].second = ss; // 默认原点和原点相连
for (int i = 1; i <= n; i++)
{
int tmp = -1; // 存储下标的临时变量
for (int j = 1; j <= n; j++)
{
if (!vis[j] && (tmp == -1 || dis[j] < dis[tmp]))
{
// 只要 (j未被访问过) 并且只要达到 [(tmp未赋值) 或 (找到离i点有更近的点)] 的条件之一
tmp = j;
// tmp就赋值为j
}
}
vis[tmp] = 1; // 完事之后,备注tmp已经被访问过了,丢进**垃~圾~桶~**里
for (int j = 1; j <= n; j++)
{
if (dis[tmp].first + edge[tmp][j] < dis[j].first && tmp != j && edge[tmp][j])
{
/*
如果 (到tmp的距离 + tmp到j的距离 < 原来到j的距离)
并且
(tmp和j不是同一个点)
并且
(tmp到j的距离不是0)
的话
*/
dis[j].first = dis[tmp].first + edge[tmp][j];
dis[j].second = tmp;
// 到j的距离重新赋值,与j相邻的点就变为tmp
}
}
}
printf("%d\n", dis[tt].first); // 输出终点的距离
int temp = tt;
while (dis[temp].second != ss) // 只要与temp这个临时变量相邻的不是起点,就进行
{
ans.push_back(temp); // 把temp存储进ans数组里
temp = dis[temp].second; // temp就重新赋值
}
ans.push_back(temp); // 最后一个点会跳出循环,所以要存储
ans.push_back(ss); // 把原点也放进去
for (int i = ans.size() - 1; i >= 0; i--)
{
printf("%d ", ans[i]); // 倒过来输出就行了
}
}
注意:
\(Dijkstra\)采用的是贪心的策略,所以遇上有负边的图时,它就会陷入自环中。
SPFA
什么是SPFA
相对\(Dijkstra\)来讲,在随机生成数据中,会比\(Dijkstra\)会更快一点。
它是基于邻接表的基础写的。
理解邻接表
在一张连通图中,一个点并不总是只连着一个点。
举个粒子:
小A家处于十字路口,可以从小A家到小B家、小C家、小D家等多个good friends的家。
而对于小\(A\)家的邻接表就是:
\(B->C->D\)
代码实现
题目:给一堆数据,输出1到\(n\)的最短路
\(Input\)
4 7
1 2 68
1 3 19
1 4 66
2 3 23
3 4 65
3 2 57
4 1 68
\(Output\)
66
\(Code\)
#include <bits/stdc++.h>
using namespace std;
int n, m;
struct edge
{
int s, e, val;
};
int maxx = INT_MIN;
int step;
vector<edge> b[5005];
queue<int> q;
int dis[5005];
bool vis[5005];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int x, y, v;
edge ee;
scanf("%d%d%d", &x, &y, &v);
ee.s = x, ee.e = y, ee.val = v;
b[x].push_back(ee); // 存储x的邻接表
}
fill(dis, dis + n + 1, 1e9); // 初始化路径全部为正无穷(数据足够大就行)
q.push(1);
dis[1] = 0;
vis[1] = 1;
while (!q.empty())
{ // STL宽搜
step++;
if (step > m)
{ // 出现负环,直接退出
printf("No Solution");
return 0;
}
int u = q.front();
q.pop();
for (int i = 0; i < b[u].size(); i++)
{ // 对于当前这个u点的邻接点
int vv = b[u][i].val;
int en = b[u][i].e;
if (dis[u] + vv < dis[en])
{ // 如果 (到u点距离) + (从u点到它的邻接点的距离) < (原来的到en的距离)
dis[en] = dis[u] + vv;
if (!vis[en])
{ // 在压入队列之前进行标记,否则会陷入死循环
vis[en] = 1;
q.push(en);
}
}
}
vis[u] = 0; // 标记这个点《 免 费 》了(free这个点)
}
printf("%d", dis[n]);
}
Floyd
什么是Floyd
就是运用枚举中间点进行松弛(专业术语,指令这个点距离最小)每个点的距离。
理解枚举中间点
还是举个梨子:
小A和小B有一定的亲密度,为了使他们的亲密度更近,需要一个中介来帮忙。通过中介的帮忙,他们是否能提升之间的亲密度呢?所以我们就枚举一下。
代码实现
\(Code\)
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (i != j && i != k && j != k)
{
if (dis[i][k] + dis[k][j] < dis[i][j])
{
dis[i][j] = dis[i][k] + dis[k][j];
// 这样写的话,无论是有向图还是无向图都成立
}
}
}
}
}
但是这个时间复杂度是
\(O(n^3)\)
《没 逝 , 就 慢 了 亿 点 点》
堆优化Dijkstra
思想
因为Dijkstra类似于贪心的策略,每次都选择边权最小的边。如果我们用小根堆来维护呢?
是的,所以堆优化Dijkstra用到了优先队列优化。
因为曾有“关于SPFA——他死了”的一说,所以这个就很吃香。更好的是,这个和SPFA代码实现很像,所以直接入手是很容易的。
代码实现
code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int MAXN=500+5,MAXM=500+5,INF=0x3f3f3f3f3f3f3f3f;
int n,m;
int dis[MAXN];
bool vis[MAXN];
int su,en[MAXM<<1],vl[MAXM<<1],hd[MAXN],lt[MAXM<<1];
struct node
{
int id,dis;
bool operator>(const node &T)const
{
return dis>T.dis;
}
};
void add(int u,int v,int w)
{
en[++su]=v,vl[su]=w,lt[su]=hd[u],hd[u]=su;
}
int Dij()
{
priority_queue<node,vector<node>,greater<node>> q;
memset(vis,0,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
dis[1]=0,vis[1]=1;
q.push({1,0});
while(!q.empty())
{
int u=q.top().id;q.pop();
for(int i=hd[u];i;i=lt[i])
{
int v=en[i],w=vl[i];
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
if(!vis[v])
{
vis[v]=1;
q.push({v,dis[v]});
}
}
}
vis[u]=0;
}
if(dis[n]==INF)
return -1;
return dis[n];
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
printf("%lld\n",Dij());
return 0;
}
时间复杂度讨论
堆优化Dij:\(O(m\log m)\)
朴素Dij:\(O(n^2)\)
SPFA:\(O(nm)\)(最坏情况)
Floyd:\(O(n^3)\)
一般来说,无负边权就选择 Dij,对于是否采用堆优化,取决于图的稀疏程度。
关于 SPFA……