最短路+次短路
最短路算法
- 提供题单: 点我!!!
- 最短路分单源最短路(Bellman-Ford 和 dijkstra)和多源最短路(Floyd)
贝尔曼福特(Bellman-Ford)算法
- 可以证明,对于一个没有负环的图(边权可以为负),\(ans\) 最多更新 \(n - 1\)
- 可以看如果总共走了 \(n\) 条边,那么就说明有点重复走了,那么会延长路径
- 显然,\(O(nm)\)
模板 Code
void Bellman_Ford(){
fill(ans + 1, ans + 1 + n, 1e18);
ans[1] = 0;
for(int i = 1; i < n; i++){ // 类似广搜
for(int j = 1; j <= m; j++){ // 进行转移
ans[q[j].v] = min(ans[q[j].v], ans[q[j].u] + q[j].w);
}
}
}
- Bellman-Ford 可以用来判断是否有负环,负环是可以一直被更新最小值的
判负环 Code
void Bellman_Ford(){
fill(ans, ans + 1 + n, Inf);
ans[1] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(ans[eg[j].x] < Inf) ans[eg[j].y] = min(ans[eg[j].y], ans[eg[j].x] + eg[j].w); // 这个 if 语句是细节
}
}
for(int j = 1; j <= m; j++){
if(ans[eg[j].x] < Inf && ans[eg[j].x] + eg[j].w < ans[eg[j].y]){ // 如果答案还可以被更行,就说明有负环
cout << "有负环\n";
return;
}
}
cout << "没有负环\n";
}
求哪些点在负环上 Code
void Bellman_Ford(){
fill(ans, ans + 1 + n, Inf);
ans[1] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(ans[eg[j].x] < Inf) ans[eg[j].y] = min(ans[eg[j].y], ans[eg[j].x] + eg[j].w);
}
}
for(int i = 1; i <= n; i++){ // 因为有的点需要进行两轮才能判断到,所以再进行一轮
for(int j = 1; j <= m; j++){
if(ans[eg[j].x] < Inf && (vis[eg[j].x] || ans[eg[j].x] + eg[j].w < ans[eg[j].y])){ // 如果一个点可以从一个负环上的点转移来,那么这一个点一定是负环上的点
vis[eg[j].y] = 1;
}
}
}
for(int i = 1; i <= n; i++){
if(vis[i]) // 点 i 在负环上
}
}
迪杰斯特拉(dijkstra)算法
- 是单元最短路算法,面向边权非负的图(也可以处理,但是对于负边权,不保证路径长度上的拓扑序,时间复杂度很容易假)。
- 是在 Bellman-Ford 算法的基础之上,加入了一点贪心思想
- Bellman-Ford 算法的 \(i\) 循环严重超时了,为了减少 \(i\) 的循环次数,所以 dijkstra 算法出现了
- 利用了路径长度的拓扑序
- 每次对于 \(dp[i]\) 最小的 \(i\) 进行转移,可以使得每次 \(j\) 更新到的答案更加接近正确答案
- 总共 \(O(n^2 + m)\)
- 再堆优化,总共 \(O(n + (m \log n))\)
- 观察时间复杂度,发现当 \(n^2 \le m\) 的时候,不堆优化或许更快
模板 Code
void dijkstra(int s){
priority_queue<asd, vector<asd>, cmp> a; // 小根堆
for(int i = 0; i <= n; i++) dp[i] = 1e18;
dp[s] = 0, a.push({0, s});
while(!a.empty()){
asd i = a.top();
a.pop();
if(i.x > dp[i.id]) continue; // 答案不够优秀,可以不进行转移
for(qwe j : c[i.id]){
if(dp[j.x] > dp[i.id] + j.w){
dp[j.x] = dp[i.id] + j.w;
a.push({dp[j.x], j.x}); // 这里会是 j.x 重复出现,但是对时间复杂度没有大影响
}
}
}
}
dijkstra 优化 dp
实际上 dijkstra 复杂度的那个 \(m \log n\) 的 \(m\) 在是松弛次数(题目:https://www.luogu.com.cn/problem/P6681 )。
所以 dijkstra 跑权在点上的图,松弛次数只有 \(O(n)\),复杂度 \(O(n + n \log n)\)。
弗洛伊德(Floyd)算法
- 一种 \(dp\),可计算边权不相同的图中任意两点之间的最短距离
- 显然,\(O(n^3)\)
- 温馨提示:如果你不能确定自己的 Floyd 循环顺序正确,那么你可以让你的 Floyd 连续跑 3 遍,那么这样你的 Floyd 一定正确(这我也不清楚为啥)
Floyd 算法正确性证明如下:
- 你可以发现,最后枚举路径可以满足任意一条路径会不断扩张
- 枚举第一个中点,任意两点之间的最短路径为 两点之间长 \(1\) 的最短路径
- 枚举第二个中点,任意两点之间的最短路径为 两点之间长 \(1\) 或 \(2\) 的最短路径
- 枚举第三个中点,任意两点之间的最短路径为 两点之间长 \(1\) 或 \(2\) 或 \(3\) 的最短路径
- 你可以将最短路径视作 \(dp\) 代表的值
- 发现是依次顺延的,这就是拓扑序
- 那么只要枚举中点的循环按照任意一个长 \(n\) 的全排列枚举都可以
实现
点击查看代码
const int Inf = 1e9;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
dp[i][j] = 1ll * (i != j) * Inf;
}
}
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >>V >> W;
dp[U][V] = W, dp[V][U] = W; // 邻接矩阵
}
void Floyd(){
for(int h = 1; h <= n; h++){ // 这 3 个循环必须按照顺序进行
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(i != j && i != h && j != h && dp[i][h] + dp[h][j] < dp[i][j]){
dp[i][j] = dp[i][h] + dp[h][j];
}
}
}
}
}
无向图的最小环问题
https://www.luogu.com.cn/problem/P6175
这道题可以很好检验你是否了解了 Floyd。
考虑 Floyd,则在枚举到中点 \(k\) 的时候,任意两点间 当前最短路径中经过的点编号在 \([1,k-1]\) 范围中。
所以枚举到每个中点的时候,枚举 \(i,j \in [1, k-1]\)。这样统计的正确性显然,这可以保证环上没有重复的点出现,因为这样枚举环上的点都是 \([1,k]\) 组成的。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXN = 100 + 3;
int n, m;
int eg[MAXN][MAXN];
int dp[MAXN][MAXN];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++) eg[i][j] = 5e8 + 3, dp[i][j] = 5e8 + 3;
}
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U][V] = min(eg[U][V], W), eg[V][U] = min(eg[V][U], W);
}
int ans = 1e9;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++) dp[i][j] = eg[i][j];
}
for(int k = 1; k <= n; k++){
for(int i = 1; i < k; i++){
for(int j = i + 1; j < k; j++){
ans = min(ans, dp[i][j] + eg[i][k] + eg[k][j]);
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++) dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
}
}
if(ans > 5e8){
cout << "No solution.";
return 0;
}
cout << ans;
return 0;
}
SPFA 算法
面相任意边权的图,单源最短路。
本质是一种搜索!!!
- 最优性剪枝:每个点只记录当前最短的路径长度。
- 入队优化:点的最短路径长度被更新时,如果该点还在队列中,则不重复入队。
对于最短路径问题,宽搜会有更好的剪枝效果,深搜无法保证较低的时间复杂度。
每个点最坏入队 \(O(n)\) 次,对于随机图,期望 \(O(1)\) 次。
标准复杂度 \(O((n + m)n)\),对于随机图,期望复杂度 \(O(n + m)\)。
普通 SPFA 算法
模板 Code
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
int n, m, S;
bool vis[MAXN];
LL l[MAXN];
queue<int> que;
vector<PII> eg[MAXN];
void Record(int x, LL L){
if(l[x] > L){
l[x] = L;
if(!vis[x]){
vis[x] = 1, que.push(x);
}
}
}
void SPFA(){
for(int i = 1; i <= n; i++) l[i] = (1ll << 31) - 1;
Record(S, 0);
while(!que.empty()){
int i = que.front();
que.pop(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, l[i] + e.second);
}
}
}
int main(){
cin >> n >> m >> S;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U].push_back({V, W});
}
SPFA();
for(int i = 1; i <= n; i++){
cout << l[i] << " ";
}
return 0;
}
进化 SPFA
SLF 优化
SLF 优化 Code
#include <bits/stdc++.h> // SPFA + ios + O2 + SLF优化
/*
SLF(Small Label First) 优化:
将原队列改成双端队列,对要加入队列的点 p,
如果 dis[p] 小于队头元素 u 的 dis[u],
将其插入到队头,否则插入到队尾。
*/
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
int n, m, S;
bool vis[MAXN];
LL l[MAXN];
deque<int> que;
vector<PII> eg[MAXN];
void Record(int x, LL L){
if(l[x] > L){
l[x] = L;
if(!vis[x]){
vis[x] = 1;
if(!que.empty() && l[x] < l[que.front()]){
que.push_front(x);
}else{
que.push_back(x);
}
}
}
}
void SPFA(){
for(int i = 1; i <= n; i++) l[i] = (1ll << 31) - 1;
Record(S, 0);
while(!que.empty()){
int i = que.front();
que.pop_front(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, l[i] + e.second);
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> S;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U].push_back({V, W});
}
for(int i = 1; i <= n; i++){
random_shuffle(eg[i].begin(), eg[i].end());
}
SPFA();
for(int i = 1; i <= n; i++){
cout << l[i] << " ";
}
return 0;
}
LLL 优化
SLF 优化 + LLL 优化 Code
#include <bits/stdc++.h> // SPFA + ios + O2 + SLF优化 + LLL优化
/*
SLF(Small Label First) 优化:
将原队列改成双端队列,对要加入队列的点 p,
如果 dis[p] 小于队头元素 u 的 dis[u],
将其插入到队头,否则插入到队尾。
*/
/*
LLL(Large Label Last) 优化:
对每个要出队的队头元素 u,比较 dist[u] 和队列中点的 dist 的平均值,
如果 dist[u] 更大,将其弹出放到队尾,然后取队首元素进行相同操作,
直到队头元素的 dist 小于等于平均值。
*/
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
int n, m, S;
bool vis[MAXN];
LL l[MAXN], sum = 0;
deque<int> que;
vector<PII> eg[MAXN];
void Record(int x, LL L){
if(l[x] > L){
l[x] = L;
if(!vis[x]){
vis[x] = 1;
if(!que.empty() && l[x] < l[que.front()]){
que.push_front(x);
}else{
que.push_back(x);
}
sum += l[x];
}
}
}
void SPFA(){
for(int i = 1; i <= n; i++) l[i] = (1ll << 31) - 1;
Record(S, 0);
while(!que.empty()){
int i = que.front();
if(l[i] > (que.empty() ? 1e18 : sum / int(que.size()))){
que.pop_front(), que.push_back(i);
continue;
}
sum -= l[i], que.pop_front(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, l[i] + e.second);
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> S;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U].push_back({V, W});
}
for(int i = 1; i <= n; i++){
random_shuffle(eg[i].begin(), eg[i].end());
}
SPFA();
for(int i = 1; i <= n; i++){
cout << l[i] << " ";
}
return 0;
}
极限随机化
思想来自 https://blog.51cto.com/u_15127499/2673860
SLF 优化 + 普通随机化 Code
#include <bits/stdc++.h> // SPFA + ios + O2 + SLF优化 + 随机化
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
int n, m, S;
unsigned long long seed = time(0), ntime = seed;
bool vis[MAXN];
LL l[MAXN];
deque<int> que;
vector<PII> eg[MAXN];
int rnd(unsigned long long mod){ // 随机化
seed <<= 13, seed %= 111145147, seed *= 19198107, seed >>= 17;
seed += rand() % ntime, seed %= 180324927, seed += n + 73 * m + 1117 * S;
return seed % mod;
}
void Record(int x, LL L){
if(l[x] > L){
l[x] = L;
if(!vis[x]){
vis[x] = 1;
if(!que.empty() && l[x] < l[que.front()]){
que.push_front(x);
}else{
que.push_back(x);
}
}
}
}
void SPFA(){
for(int i = 1; i <= n; i++) l[i] = (1ll << 31) - 1;
Record(S, 0);
while(!que.empty()){
for(int www = 1; www <= 4 && que.size() > 1; www++){
int x = rnd(que.size() - 1) + 1;
if(l[que[0]] > l[que[x]]){
swap(que[0], que[x]);
}
}
int i = que.front();
que.pop_front(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, l[i] + e.second);
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> S;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U].push_back({V, W});
}
for(int i = 1; i <= n; i++){
random_shuffle(eg[i].begin(), eg[i].end());
}
SPFA();
for(int i = 1; i <= n; i++){
cout << l[i] << " ";
}
return 0;
}
SLF 优化 + 极限随机化 Code
轻松 AC 洛谷 P4779 【模板】单源最短路径(标准版)
#include <bits/stdc++.h> // SPFA + ios + O2 + SLF优化 + 随机化
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
int n, m, S;
unsigned long long seed = time(0), ntime = seed;
bool vis[MAXN];
LL l[MAXN];
deque<int> que;
vector<PII> eg[MAXN];
int rnd(unsigned long long mod){ // 随机化
seed <<= 13, seed += rand() % ntime, seed += n + 73 * m + 1117 * S;
return seed % mod;
}
void Record(int x, LL L){
if(l[x] > L){
l[x] = L;
if(!vis[x]){
vis[x] = 1;
if(!que.empty() && l[x] < l[que.front()]){
que.push_front(x);
}else{
que.push_back(x);
}
}
}
}
void SPFA(){
for(int i = 1; i <= n; i++) l[i] = (1ll << 31) - 1;
Record(S, 0);
while(!que.empty()){
for(int www = 1; www <= 6 && que.size() > 1; www++){
int x = rnd(que.size() - 1) + 1;
if(l[que[0]] > l[que[x]]){
swap(que[0], que[x]);
}
}
for(int www = 1; www <= 6 && que.size() > www; www++){
int x = www, y = que.size() - www;
if(l[que[0]] > l[que[x]]){
swap(que[0], que[x]);
}
if(l[que[0]] > l[que[y]]){
swap(que[0], que[y]);
}
}
int i = que.front();
que.pop_front(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, l[i] + e.second);
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> S;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U].push_back({V, W});
}
for(int i = 1; i <= n; i++){
random_shuffle(eg[i].begin(), eg[i].end());
}
SPFA();
for(int i = 1; i <= n; i++){
cout << l[i] << " ";
}
return 0;
}
差分约束
不等式可 \(x - y < c\) 变形为 \(x < c + y\),这很像最短路不等式 \(l_x < l_y + c\),即更新到最短路不能再更新,所以有负环就是无解。
乘法运算可以取 \(\log\) 变为加法运算(https://www.luogu.com.cn/problem/P4926 )
模板 Code
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
int n, m, S, cnt[MAXN];
bool vis[MAXN];
LL l[MAXN];
queue<int> que;
vector<PII> eg[MAXN];
void Record(int x, LL L){
if(l[x] > L){
l[x] = L;
if(!vis[x]){
cnt[x]++, vis[x] = 1, que.push(x);
if(cnt[x] >= n){
cout << "NO";
exit(0);
}
}
}
}
void SPFA(){
for(int i = 0; i <= n; i++) l[i] = (1ll << 31) - 1;
Record(0, 0);
while(!que.empty()){
int i = que.front();
que.pop(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, l[i] + e.second);
}
}
}
int main(){
cin >> n >> m;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[V].push_back({U, W});
}
for(int i = 1; i <= n; i++) eg[0].push_back({i, 0});
SPFA();
for(int i = 1; i <= n; i++){
cout << l[i] << " ";
}
return 0;
}
Johnson 算法
介绍
- 是一个全源最短路算法,面向任意边权的图,时间复杂度 \(O(nm + n + (m\log n))\),即:差分约束 + dijkstra
- 对于无负边权的图,求全源最短路,可以跑 \(n\) 遍 dijkstra,但是负边权呢?
- 可以跑 \(n\) 遍 SPFA !但是 \(O(n^2 m)\) 太不划算了。
- 思考如何使得负边权消失。
模板 Code
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 5e5 + 3;
struct Node{
int x;
LL l;
bool operator< (Node j) const {
return l > j.l;
}
};
int n, m, cnt[MAXN];
bool vis[MAXN];
LL w[MAXN], dis[MAXN];
queue<int> que;
vector<PII> eg[MAXN];
void Record(int x, LL L){
if(w[x] > L){
w[x] = L;
if(!vis[x]){
cnt[x]++, vis[x] = 1, que.push(x);
if(cnt[x] >= n){
cout << -1; // 有负环
exit(0);
}
}
}
}
void SPFA(){
for(int i = 0; i <= n; i++) w[i] = (1ll << 31) - 1;
Record(0, 0);
while(!que.empty()){
int i = que.front();
que.pop(), vis[i] = 0;
for(PII e : eg[i]){
Record(e.first, w[i] + e.second);
}
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
eg[U].push_back({V, W});
} // w[U] - w[V] + W >= 0
// w[U] + W >= w[V]
// w[V] <= w[U] + W
for(int i = 1; i <= n; i++) eg[0].push_back({i, 0});
SPFA();
for(int S = 1; S <= n; S++){
for(int i = 1; i <= n; i++) dis[i] = 1e9;
priority_queue<Node> pq;
dis[S] = 0, pq.push({S, 0});
while(!pq.empty()){
Node i = pq.top();
pq.pop();
if(dis[i.x] < i.l) continue;
for(PII e : eg[i.x]){
int nxt = e.first, nw = dis[i.x] + e.second + w[i.x] - w[nxt];
if(dis[nxt] > nw){
dis[nxt] = nw, pq.push({nxt, nw});
}
}
}
LL ans = 0;
for(int i = 1; i <= n; i++){
LL _ans = dis[i] - w[S] + w[i];
ans += 1ll * i * (dis[i] >= 1e9 ? 1e9 : _ans);
}
cout << ans << "\n";
}
return 0;
}
次短路
非严格次短路:https://www.luogu.com.cn/problem/P1491
严格次短路:https://www.luogu.com.cn/problem/P2865
对于非严格次短路,复杂度 \(O(n(n+m)n)\) 的:
- 先求出任意一条最短路。
- 枚举删除最短路上的哪一条边,删除后再跑一遍最短路,答案去最小值。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 200 + 3, MAXM = 20000 + 3;
double ew[MAXM];
int to[MAXM], tot = 0, start[MAXN], top[MAXN], nex[MAXM];
inline void ADDeg(int u, int v, double w){
tot++, to[tot] = v, ew[tot] = w, nex[top[u]] = tot;
start[u] = (!start[u] ? tot : start[u]), top[u] = tot;
}
void No(){ cout << -1; exit(0); }
int n, m;
PII a[MAXN];
int from[MAXN], frome[MAXN], vis[MAXN];
double dp[MAXN];
double SPFA(){
queue<int> que;
for(int i = 1; i <= n; i++) dp[i] = 1e9, from[i] = 0, vis[i] = 0;
dp[1] = 0, que.push(1);
while(!que.empty()){
int i = que.front();
que.pop(), vis[i] = 0;
for(int e = start[i]; e > 0; e = nex[e]){
double nw = dp[i] + ew[e];
if(nw < dp[to[e]]){
dp[to[e]] = nw, from[to[e]] = i, frome[to[e]] = e;
if(!vis[to[e]]){
vis[to[e]] = 1, que.push(to[e]);
}
}
}
}
return dp[n];
}
int main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i].first >> a[i].second;
}
for(int i = 1, U, V; i <= m; i++){
cin >> U >> V;
double w = sqrt((a[U].first - a[V].first) * (a[U].first - a[V].first) + (a[U].second - a[V].second) * (a[U].second - a[V].second));
ADDeg(U, V, w), ADDeg(V, U, w);
}
vector<int> vt;
if(SPFA() >= 1e9) No();
for(int x = n; x > 1; x = from[x]){
vt.push_back(frome[x]);
}
double ans = 1e9;
for(int e : vt){
double tmp0 = 1e9, tmp1 = 1e9;
swap(tmp0, ew[e]), swap(tmp1, ew[e^1]);
ans = min(ans, SPFA());
swap(tmp0, ew[e]), swap(tmp1, ew[e^1]);
}
if(ans >= 1e9) No();
cout << fixed << setprecision(2) << ans;
return 0;
}
对于严格次短路或非严格次短路,复杂度 \(O(n + m \log n)\) 的:
- dijkstra,dp 状态存两个值,分情况更新。