【模板】单源最短路径(标准版)
题目背景
2018 年 7 月 19 日,某位同学在 NOI Day 1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。
然后呢?
100 → 60 100 \rightarrow 60 100→60;
Ag → Cu \text{Ag} \rightarrow \text{Cu} Ag→Cu;
最终,他因此没能与理想的大学达成契约。
小 F 衷心祝愿大家不再重蹈覆辙。
题目描述
给定一个 n n n 个点, m m m 条有向边的带非负权图,请你计算从 s s s 出发,到每个点的距离。
数据保证你能从 s s s 出发到任意点。
输入格式
第一行为三个正整数
n
,
m
,
s
n, m, s
n,m,s。
第二行起
m
m
m 行,每行三个非负整数
u
i
,
v
i
,
w
i
u_i, v_i, w_i
ui,vi,wi,表示从
u
i
u_i
ui 到
v
i
v_i
vi 有一条权值为
w
i
w_i
wi 的有向边。
输出格式
输出一行 n n n 个空格分隔的非负整数,表示 s s s 到每个点的距离。
样例 #1
样例输入 #1
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
样例输出 #1
0 2 4 3
提示
样例解释请参考 数据随机的模板题。
1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1≤n≤105;
1 ≤ m ≤ 2 × 1 0 5 1 \leq m \leq 2\times 10^5 1≤m≤2×105;
s = 1 s = 1 s=1;
1 ≤ u i , v i ≤ n 1 \leq u_i, v_i\leq n 1≤ui,vi≤n;
0 ≤ w i ≤ 1 0 9 0 \leq w_i \leq 10 ^ 9 0≤wi≤109,
0 ≤ ∑ w i ≤ 1 0 9 0 \leq \sum w_i \leq 10 ^ 9 0≤∑wi≤109。
本题数据可能会持续更新,但不会重测,望周知。
2018.09.04 数据更新 from @zzq
前言
- SPFASPFASPFA算法由于它上限 O(NM)=O(VE)O(NM) = O(VE)O(NM)=O(VE)的时间复杂度,被卡掉的几率很大.在算法竞赛中,我们需要一个更稳定的算法:dijkstradijkstradijkstra.
什么是dijkstradijkstradijkstra?
- dijkstradijkstradijkstra是一种单源最短路径算法,时间复杂度上限为O(n2)O(n^2)O(n2)(朴素),在实际应用中较为稳定;;;加上堆优化之后更是具有O((n+m)log2n)O((n+m)\log_{2}n)O((n+m)log2n)的时间复杂度,在稠密图中有不俗的表现.
dijkstradijkstradijkstra的原理/流程?
- dijkstradijkstradijkstra本质上的思想是贪心,它只适用于不含负权边的图.
- 我们把点分成两类,一类是已经确定最短路径的点,称为"白点",另一类是未确定最短路径的点,称为"蓝点"
- dijkstradijkstradijkstra的流程如下:::
- 1.1.1. 初始化dis[start]=0,dis[start] = 0,dis[start]=0,其余节点的disdisdis值为无穷大.
- 2.2.2. 找一个disdisdis值最小的蓝点x,x,x,把节点xxx变成白点.
- 3.3.3. 遍历xxx的所有出边(x,y,z),(x,y,z),(x,y,z),若dis[y]>dis[x]+z,dis[y] > dis[x] + z,dis[y]>dis[x]+z,则令dis[y]=dis[x]+zdis[y] = dis[x] + zdis[y]=dis[x]+z
- 4.4.4. 重复2,32,32,3两步,直到所有点都成为白点...
- 时间复杂度为O(n2)O(n^2)O(n2)
dijkstradijkstradijkstra为什么是正确的
- 当所有边长都是非负数的时候,全局最小值不可能再被其他节点更新.所以在第222步中找出的蓝点xxx必然满足:dis[x]:dis[x]:dis[x]已经是起点到xxx的最短路径...我们不断选择全局最小值进行标记和拓展,最终可以得到起点到每个节点的最短路径的长度
图解
- (令start=1start = 1start=1)
- 开始时我们把dis[start]dis[start]dis[start]初始化为000,其余点初始化为infinfinf
- 第一轮循环找到disdisdis值最小的点111,将111变成白点,对所有与111相连的蓝点的disdisdis值进行修改,使得dis[2]=2,dis[3]=4,dis[4]=7dis[2]=2,dis[3]=4,dis[4]=7dis[2]=2,dis[3]=4,dis[4]=7
- 第二轮循环找到disdisdis值最小的点222,将222变成白点,对所有与222相连的蓝点的disdisdis值进行修改,使得dis[3]=3,dis[5]=4dis[3]=3,dis[5]=4dis[3]=3,dis[5]=4
- 第三轮循环找到disdisdis值最小的点333,将333变成白点,对所有与222相连的蓝点的disdisdis值进行修改,使得dis[4]=4dis[4]=4dis[4]=4
- 接下来两轮循环分别将4,54,54,5设为白点,算法结束,求出所有点的最短路径
- 时间复杂度O(n2)O(n^2)O(n2)
为什么dijkstradijkstradijkstra不能处理有负权边的情况?
- 我们来看下面这张图
- 222到333的边权为−4-4−4,显然从111到333的最短路径为−2-2−2 (1−>2−>3).(1->2->3).(1−>2−>3).但在循环开始时程序会找到当前disdisdis值最小的点333,并标记它为白点.
- 这时的dis[3]=1,dis[3]=1,dis[3]=1,然而111并不是起点到333的最短路径.因为333已经被标为白点,所以dis[3]dis[3]dis[3]不会再被修改了.我们在边权存在负数的情况下得到了错误的答案.
dijkstradijkstradijkstra的堆优化?
-
观察dijkstradijkstradijkstra的流程,发现步骤222可以优化
-
怎么优化呢?
-
我会zkw线段树!我会斐波那契堆! -
我会堆!
-
我们可以用堆对disdisdis数组进行维护,用O(log2n)O(\log_{2}n)O(log2n)的时间取出堆顶元素并删除,用O(log2n)O(\log_{2}n)O(log2n)遍历每条边,总复杂度O((n+m)log2n)O((n+m)\log_{2}n)O((n+m)log2n)
-
范例代码:
#include<bits/stdc++.h>
const int MaxN = 100010, MaxM = 500010;
struct edge
{
int to, dis, next;
};
edge e[MaxM];
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;
inline void add_edge( int u, int v, int d )
{
cnt++;
e[cnt].dis = d;
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
struct node
{
int dis;
int pos;
bool operator <( const node &x )const
{
return x.dis < dis;
}
};
std::priority_queue<node> q;
inline void dijkstra()
{
dis[s] = 0;
q.push( ( node ){0, s} );
while( !q.empty() )
{
node tmp = q.top();
q.pop();
int x = tmp.pos, d = tmp.dis;
if( vis[x] )
continue;
vis[x] = 1;
for( int i = head[x]; i; i = e[i].next )
{
int y = e[i].to;
if( dis[y] > dis[x] + e[i].dis )
{
dis[y] = dis[x] + e[i].dis;
if( !vis[y] )
{
q.push( ( node ){dis[y], y} );
}
}
}
}
}
int main()
{
scanf( "%d%d%d", &n, &m, &s );
for(int i = 1; i <= n; ++i)dis[i] = 0x7fffffff;
for( register int i = 0; i < m; ++i )
{
register int u, v, d;
scanf( "%d%d%d", &u, &v, &d );
add_edge( u, v, d );
}
dijkstra();
for( int i = 1; i <= n; i++ )
printf( "%d ", dis[i] );
return 0;
}
例题
- 入门模板:P3371
- 进阶模板:P4779
- 其余例题请右转洛谷题库,搜索"最短路"