文章目录
一、单源最短路
1. Dijkstra(堆优化)
DJ 的思想是每次用一个点更新与其相邻的所有点,更新后将该点标记(每个点只用一次),从用过的点连通分量出发,找一条连通分量与外界的连边,用该边的终点继续更新与其相连点到起点的距离。 O(n^2)
因为复杂度主要由边决定,适合稀疏图
不能处理负边权
利用堆优化,取出最短的一条O(nlogn),用其终点继续向外更新。
模板
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int,int> PII; // first 存距离 second 存编号
const int maxn = 2e5+10;
int n, m;
int cnt = 0;
int dist[maxn];
bool st[maxn]; // 第 i 个点的最短路是否确定,是否需要更新
int head[maxn];
struct Edge {
int to, next, w;
}e[maxn];
void add(int u, int v, int w) {
e[++cnt].next = head[u];
e[cnt].to = v;
e[cnt].w = w;
head[u] = cnt;
}
void init() {
memset(dist, 0x3f3f3f3f, sizeof(dist)); // 将所有距离初始化为正无穷
memset(head, 0, sizeof(head));
memset(e, 0, sizeof(e));
cnt = 0;
}
int dijkstra() {
dist[1] = 0; // 第一个点到起点的距离
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆
heap.push({0,1});
while(heap.size()) { // 堆不空
PII t = heap.top();
heap.pop();
int ver = t.second, dis = t.first;
if(st[ver]) continue; // 重边(访问过的)就不用再更新了,DJ思想就是贪心的访问每条最短边
st[ver] = true; // 标记 t 已经确定为最短路
for(int i = head[ver]; i; i = e[i].next) {
int to = e[i].to;
if(dist[to] > dis + e[i].w) {
dist[to] = dis + e[i].w;
heap.push({dist[to], to});
}
}
}
}
int main() {
// freopen("test.in", "r", stdin);
while(~scanf("%d%d", &n, &m)) {
init();
for(int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
}
dijkstra();
int to; // 目标点
scanf("%d", &to);
if(dist[to] != 0x3f3f3f3f) {
printf("%d\n", dist[n]); // 到达目标的最短距离。 DJ计算一个起点,多个终点
}
else printf("无法到达!\n");
}
return 0;
}
例题
RE了好几次,总结一下RE原因
- 数组越界
- 站溢出
- 除0
- 函数格式(void int 是否有返回值)
AC代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
// RE:数组越界,站溢出,除0 , 函数格式(void int 是否有返回值)
using namespace std;
typedef pair<int,int> PII; // first 存距离 second 存编号
const int maxn = 5e5+10;
int n, m;
int cnt = 0;
int dist[10005];
bool st[maxn]; // 第 i 个点的最短路是否确定,是否需要更新
int head[maxn];
struct Edge {
int to, next, w;
}e[maxn];
void add(int u, int v, int w) {
e[++cnt].next = head[u];
e[cnt].to = v;
e[cnt].w = w;
head[u] = cnt;
}
void init() {
for(int i = 0; i <= n; i++) dist[i] = (1<<31)-1; // 将所有距离初始化为正无穷
memset(head, 0, sizeof(head));
memset(e, 0, sizeof(e));
cnt = 0;
}
void dijkstra(int s) {
dist[s] = 0; // 第一个点到起点的距离
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆
heap.push({0,s});
while(heap.size()) { // 堆不空
PII t = heap.top();
heap.pop();
int ver = t.second, dis = t.first;
if(st[ver]) continue; // 重边(访问过的)就不用再更新了,DJ思想就是贪心的访问每条最短边
st[ver] = true; // 标记 t 已经确定为最短路
for(int i = head[ver]; i; i = e[i].next) {
int to = e[i].to;
if(dist[to] > dis + e[i].w) {
dist[to] = dis + e[i].w;
heap.push({dist[to], to});
}
}
}
}
int main() {
// freopen("test.in", "r", stdin);
int s;
scanf("%d%d%d", &n, &m, &s);
init();
for(int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
}
dijkstra(s);
for(int i = 1; i <= n; i++) {
printf("%d", dist[i]); // 到达目标的最短距离。 DJ计算一个起点,多个终点
if(i == n) break;
else printf(" ");
}
return 0;
}
Dijkstra 线段树优化 // 手抄了一遍还是没懂,等我强了再来
2. SPFA
先说一下Bellman-Ford 算法:
每次取一条边加入连通块,然后从该边的终点作为中转点更新整个图的最短路,最坏情况就是每次加入一条边都能更新整个图的最短路, 即 V*E
复杂度 O(VE)
SPFA(优化Bellman-Ford)是把一个点周围所有点放进队列来更新最短值,优化就在有导向性,只用更新与松弛成功的点相连的点
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5+10;
int head[maxn], cnt = 0, vis[maxn];
int dis[maxn];
struct Edge{
int to, w, next;
}e[maxn];
void add(int u, int v, int w) {
e[++cnt].next = head[u];
e[cnt].to = v;
e[cnt].w = w;
head[u] = cnt;
}
void spfa(int s) {
memset(dis, 0x3f3f3f3f, sizeof(dis));
dis[s] = 0; vis[s] = 1;
queue<int> Q;
Q.push(s);
int u, v;
while(!Q.empty()) {
u = Q.front(); Q.pop(); vis[u] = 0;
for(int i = head[u]; i; i = e[i].next) {
v = e[i].to;
if(dis[u] + e[i].w < dis[v]) {
dis[v] = dis[u] + e[i].w;
if(!vis[v]) Q.push(v), vis[v] = 1; // 不在队列里就放进去更新别的点
}
}
}
}
void init() {
memset(head, 0, sizeof(head));
memset(vis, 0, sizeof(vis));
memset(e, 0, sizeof(e));
cnt = 0;
}
int main() {
// freopen("test.in", "r", stdin);
int n, m, s;
while(scanf("%d%d%d", &n, &m, &s) == 3) {
init();
int u, v, w;
for(int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), add(u, v, w); // 有向图连一次边就好
spfa(s);
for(int i = 1; i <= n; i++) printf("%d ", dis[i]);
}
return 0;
}
二、全源最短路
Floyd
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5+10;
int dis[maxn][maxn];
void Floyd(int n) {
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
}
void init() {
memset(dis, 0x3f, sizeof(dis));
}
int main() {
freopen("test.in", "r", stdin);
int n, m, s;
while(~scanf("%d%d%d", &n, &m, &s)){
init();
int u, v, w;
for(int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), dis[u][v] = w;
Floyd(n);
for(int i = 1; i <= n; i++) printf("%d ", dis[s][i]);
}
return 0;
}
三、k短路
四、判环(SPFA)
用 spfa 判断正负环都可以利用某个点被松弛 n 次以上作为依据
判正环
判断正环的例题
E - Currency Exchange POJ - 1860
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
using namespace std;
int N, M, S;
double V;
struct edge {
int to;
double R, C;
};
vector<edge> G[105];
int vis[105], cnt[105];
double d[105];
bool spfa() {
queue<int> Q;
Q.push(S);
vis[S] = 1;
d[S] = V;
while(Q.size()) {
int u = Q.front(); Q.pop();
vis[u] = 0; // 出队
for(int i = 0; i < G[u].size(); i++) {
edge e = G[u][i];
double val = (d[u] - e.C) * e.R;
if(val - d[e.to] > 1e-8) {
d[e.to] = val;
if(vis[e.to]) continue;
Q.push(e.to); // 每次更新完一个点,要把它周围的点加入队列继续往外更新其他点
vis[e.to] = 1;
if(++cnt[e.to] > N) return 1;// 如果一个点入队 N 次以上,说明出现环了
}
}
}
return 0;
}
int main() {
// freopen("test.in", "r", stdin);
ios::sync_with_stdio(false); // 打消iostream中输入输出缓存,节省时间。
cin.tie(0); cout.tie(0); // 可以通过tie(0)(0表示NULL)来解除cin与cout的绑定,进一步加快执行效率。
cin >> N >> M >> S >> V;
int a, b;
double Rab, Cab, Rba, Cba;
for(int i = 0; i < 105; i++) G[i].clear();
for(int i = 0; i < M; i++) {
cin >> a >> b >> Rab >> Cab >> Rba >> Cba;
edge e;
e.to = b, e.R = Rab, e.C = Cab;
G[a].push_back(e);
e.to = a, e.R = Rba, e.C = Cba;
G[b].push_back(e);
}
if(spfa()) { // 正环使钱变多
cout << "YES" << endl;
}
else cout << "NO" << endl;
return 0;
}
判负环
利用 SPFA 由于每个点记录的是从起点出发到达当前点的最短路,只有当一个点松弛成功它才能作为一个中转点去松弛其他点,所以如果一个点如果被松弛 n 次,那么存在负环,否则一个点至多被松弛 n-1 次
判负环例题
F - Wormholes POJ - 3259
只要出现一个负环,则沿这个负环不停走,一定能使时间不断前走,走一定次数以后一定能满足提前返回出发点
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<vector>
const int inf = 1e9+7;
using namespace std;
const int maxn = 3e3+10;
int N, M, K;
int num[maxn]; // 记录每个点出现的次数
int dis[600];
bool vis[600];
struct node {
int to, w;
};
vector<node> G[maxn];
void init() {
memset(vis, 0, sizeof(vis));
memset(num, 0, sizeof(num));
memset(dis, inf, sizeof(dis));
}
bool spfa(int s) {
queue<int> Q;
Q.push(s); vis[s] = 1;
dis[s] = 0; num[s] = 1;
while(!Q.empty()) {
int u = Q.front(); Q.pop(); vis[u] = 0;
for(int i = 0; i < G[u].size(); i++) {
node e = G[u][i];
if(dis[u] < inf && dis[e.to] > (dis[u] + e.w)) {
dis[e.to] = dis[u] + e.w; // 在队列中只是不重复添加,但是需要更新值
if(vis[e.to]) continue;
Q.push(e.to); vis[e.to] = 1;
if(++num[e.to] > N) return 1; // 出现负环
}
}
}
return 0;
}
int main() {
// freopen("test.in", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int F; cin >> F;
while(F--) {
cin >> N >> M >> K;
init();
for(int i = 1; i <= N; i++) G[i].clear();
int u, v, t;
node e;
for(int i = 1; i <= M; i++) {
cin >> u >> v >> t;
e.to = v; e.w = t;
G[u].push_back(e);
e.to = u; e.w = t;
G[v].push_back(e);
}
for(int i = 1; i <= K; i++) {
cin >> u >> v >> t;
e.to = v; e.w = -t;
G[u].push_back(e);
}
if(spfa(1)) cout << "YES" << endl;
else cout << "NO" << endl;
}
return 0;
}