准备开图论,复习一下基本算法。
1. 最短路算法
(V) : 点集
(E) : 边集
Floyd
应该从动态规划的角度去考虑这个算法,设 f[i][j]
表示从 i
到 j
的最短路径 (指某条路,而不是最小距离这样一个数字,距离是路径的一个属性)。
其实所有的最短路算法都应该落实到路径上
则很显然,有如下转移
这个 (+)理解成路径的拼接,而 min
表示按数值比较
复杂度分析
对于每一个
f[i][j]
都需要1 <= k <= n
去更新
(O(V^3))
抽象出来
struct node {
vector<int>path;
int dis;
bool operator < (const node& b)const {
return dis < b.dis;
}
node operator + (node b) {
node tmp = *this;
tmp.path.insert(tmp.path.end(), b.path.begin(), b.path.end());
tmp.dis += b.dis;
return tmp;
}
}f[N][N];
实例代码
/*
* @Author: zhl
* @Date: 2020-10-19 18:31:45
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 5e2 + 10;
struct node {
vector<int>path;
int dis;
bool operator < (const node& b)const {
return dis < b.dis;
}
node operator + (node b) {
node tmp = *this;
tmp.path.insert(tmp.path.end(), b.path.begin(), b.path.end());
tmp.dis += b.dis;
return tmp;
}
}f[N][N];
int n, m;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++)f[i][j] = node{ vector<int>(),0x3f3f3f3f };
for (int i = 1; i <= n; i++)f[i][i] = node{ vector<int>(),0 };
for (int i = 1; i <= m; i++) {
int a, b, c;
cin >> a >> b >> c;
//sb题目会有重边
if (c < f[a][b].dis)f[a][b] = node{ vector<int>(1,b),c };
if (c < f[b][a].dis)f[b][a] = node{ vector<int>(1,a),c };
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (f[i][k] + f[k][j] < f[i][j]) {
f[i][j] = f[i][k] + f[k][j];
}
}
}
}
int q;
cin >> q;
while (q--) {
int a, b;
cin >> a >> b;
cout << "Min_Dis : " << f[a][b].dis << endl;
cout << a;
for (int i : f[a][b].path)cout << "->" << i;
cout << endl;
}
}
做题直接存数值就完事了
Bellman-ford
(O(VE))
用边去进行松弛操作
与 Floyd
不同,Bellman-ford
处理的是 ‘单源最短路’ 。处理的是从源点 s
出发到任意一点 t
的最短路径
对于一条边 E[i]
,该边连接 a
, b
,边权是 w
有
f[b] = min(f[b],f[a] + w)
跑 n-1
次,每次都遍历所有的边进行松弛
再跑一次,如果松弛成功,则说明有负环。
n 个点的图上的路径最多 n-1 条边
代码懒得写了(跑
Dijkstra
时间复杂度 (O(VlgV)) , 一般正权图用 Dij就完事
struct Node{
double d;
int id;
bool operator < (const Node& b)const{
return d > b.d;
}
};
int n,m,s,t;
void Dijkstra(){
rep(i,0,n){
dis[i] = INF;
vis[i] = 0;
}
dis[s] = 0;
priority_queue<Node>Q;
Q.push(Node{0,s});
while(!Q.empty()){
Node tp = Q.top();Q.pop();
int u = tp.id;
double d = tp.d;
if(vis[u])continue;
vis[u] = 1;
repE(i,u){
int v = E[i].to;
if(dis[v] > d + E[i].x + E[i].y * A){
dis[v] = d + E[i].x + E[i].y * A;
Q.push(Node{dis[v],v});
}
}
}
}
三种状态
vis[u] == True
,表示s
到u
的最短路径已经被处理出来在队列中,表示该点目前可达但是不确定是否已经是最短路径
目前不可达
每次取队列中距离最短的那个点,给他打上标记。
因为这个点必定已经是最短路径。
因为
u
是目前所有可达的点中距离最短的一个,若还有另一条更短的路径,则肯定有一条路径s->t
和t->u
,s->t
的最短距离必定小于目前u
的最短路径,且t
必定会在队列中(这里想的也不是特别清楚,好像不是很严谨)
需要注意的是,这里的 vis
和 BFS
中的标记 vis
不一样
BFS 中需要入队的时候打上标记,不能取队首的时候打标记,否则会多次入队
而这里的
vis
表示的是已经处理完毕
SPFA
一种对 Bellman-ford
的优化
很显然 Bellman-ford
中做了很多次无用的松弛,例如第一次的时候只需要松弛和源点相连的边就可以。
每一个点入队,就表示有一个点的 dis
下降了,而在无负环的图中,必然存在最短路径。所以该算法一定能在有限次的执行后停止。
复杂度 : (O(kV)) ,(k) 是平均每个点的入队次数
在极端条件下可以被卡成朴素 bellman-ford
(O(EV))
void spfa() {
queue<int> q;
memset(dis, 0x3f, sizeof(int) * (n + 10));
memset(vis, 0, sizeof(int) * (n + 10));
q.push(s); dis[s] = 0; vis[s] = 1; //第一个顶点入队,进行标记
while (!q.empty()) {
int u = q.front();
q.pop(); vis[u] = 0;
for (int i = head[u]; ~i; i = E[i].next) {
int v = E[i].to;
if (dis[v] > dis[u] + E[i].w) {
dis[v] = dis[u] + E[i].w;
if (vis[v] == 0) {
//此处判环
//if(++cnt[v] >= n) 有负环
vis[v] = 1;
q.push(v);
}
}
}
}
}