#4. 图的存储、最短路(未完结)
bro高一才开始自学图论(未完结警告)
图的存储
建议无脑用链式前向星
0x01. 什么是链式前向星
定义(摘自OI wiki)
本质上是用链表实现的邻接表
具体来说:
以有向边的形式 ,\(head\) 数组存当前边的编号,\(e[i].nxt\) 数组存上一次加的以 \(u\) 为起点的边的编号,这样就能实现用 \(head[u]\) 和 \(e[i].nxt\) 遍历所有出边;\(v\) 存边的终点; \(w\) 存边权
加边操作:
void add (int u, int v, int w) {
e[++tot].nxt = head[u];
e[tot].v = v;
e[tot].w = w;
head[u] = tot;
}
找是否存在 \(u\) 到 \(v\) 的边:
bool find(int u, int v) {
for(int i = head[u]; i; i = e[i].nxt) {
if (e[i].v == v) {
return true;
}
}
return false;
}
遍历一个节点连向的所有出边:
for(int i = head[u]; i; i = e[i].nxt){
int v = e[i].v, w = e[i].w;
...
}
遍历图:
void dfs(int u) {
if(vis[u]) return;
vis[u] = true;
for(int i = head[u]; i; i = e[i].nxt) {
...
dfs(e[i].v);
}
}
0x02. 性质与复杂度
·可以通过每次多存一条反边来存储树或无向图,复杂度 \(O(m)\)
·可以查询是否存在 \(u\) 到 \(v\) 的边,复杂度 \(O(u)\)
·可以遍历点 \(u\) 的所有出边,复杂度 \(O(u)\)
·可以遍历整张图,复杂度 \(O(n+m)\)
·空间复杂度 \(O(m)\)
非负权图单源最短路: Dijkstra 算法
0x01. 算法思路
用 \(s\) 表示起点节点编号,\(dist\) 数组记录起点到每个点的最短路径,实现流程如下:
- 初始化起点 \(dist[s]\) 为 \(0\),其他节点 \(dist[i]\) 为无穷大;
- 找出未被遍历的,\(dist\) 值最小的 \(dist[u]\),标记 \(u\);
- 扫描 \(dist[u]\) 所有出边 \((w,v)\),对于 \(dist[v]\),若 \(dist[u] + w_i < dist[v]\) 则更新 \(dist[v]\);
- 重复 \(2、3\) 步骤直到所有节点被标记。
\(Dijkstra\) 基于贪心思想,它只适用于边权非负的图。当边权 \(w_i\) 出现负数时,全局最小值 \(dist[u]\) 还可能被更新,不再符合贪心的思路;而在 \(w_i\) 非负时则不会出现这种情况,\(dist[u]\) 已经成为 \(s\) 到 \(u\) 的最短路,这样便可以保证每次更新一定在拓展最短路,最终一定能得到 \(s\) 到每个节点的最短路(保证图连通的情况下)。
0x02. 算法实现与复杂度
P3371 【模板】单源最短路径(弱化版)
对于朴素的 \(Dijkstra\),每次遍历所有节点找到 \(dist[u]\) 最小值 \(O(n)\),保证每次更新能确定 \(s\) 到一个节点的最短路,则总复杂度 \(O(n^2)\);
AC代码:
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int f = 1, otto = 0;
char a = getchar();
while(!isdigit(a)) {
if(a == '-') f = -1;
a = getchar();
}
while(isdigit(a)) {
otto = (otto << 1) + (otto << 3) + (a ^ 48);
a = getchar();
}
return f * otto;
}
const int maxn = 1e4 + 10;
int n, m, s;
struct edge{
int v, nxt, w;
}e[maxn<<6];
int tot = 0, head[maxn];
void add(int u, int v, int w) {
e[++tot].nxt = head[u];
head[u] = tot;
e[tot].v = v;
e[tot].w = w;
}
const int inff = 2147483647;
int d[maxn];
bool vis[maxn];
void dijkstra() {
for(int i = 0; i <= n; i++) d[i] = inff;
d[s] = 0;
for(int i = 1; i < n; i++) {
int u = 0;
for(int j = 1; j <= n; j++) {
if(!vis[j] && (u == 0 || d[j] < d[u])) u = j;
}
vis[u] = 1;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
d[v] = min(d[v], d[u] + w);
}
}
return;
}
int main() {
n = read(), m = read(), s = read();
for(int i = 1; i <= m; i++) {
int u = read(), v = read(), w = read();
add(u, v, w);
}
dijkstra();
for(int i = 1; i <= n; i++) {
printf("%d ", d[i]);
}
return 0;
}
P4779 【模板】单源最短路径(标准版)
发现每次找最小 \(dist[u]\) 时不必一个一个遍历,可以把起点和每次更新过的 \(dist[v]\) 放在一个小根堆里,每次取出堆顶更新,复杂度 \(O(mlogn)\) 。
使用STL库中的优先队列实现,每次以 \(-dist[v]\) 为第一关键字把大根堆变成小根堆。
AC代码:
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int f = 1, otto = 0;
char a = getchar();
while(!isdigit(a)) {
if(a == '-') f = -1;
a = getchar();
}
while(isdigit(a)) {
otto = (otto << 1) + (otto << 3) + (a ^ 48);
a = getchar();
}
return f * otto;
}
const int maxn = 1e5 + 10;
struct edge {
int nxt, v, w;
}e[maxn<<1];
int n, m, s;
priority_queue<pair<int, int> >q;
int tot = 0, head[maxn];
void add(int u, int v, int w) {
e[++tot].nxt = head[u];
head[u] = tot;
e[tot].v = v;
e[tot].w = w;
}
const int inff = 2e9;
int d[maxn];
bool vis[maxn];
void dijkstra() {
for(int i = 1; i <= n; i++) d[i] = inff;
d[s] = 0;
q.push(make_pair(0, s));
while(!q.empty()) {
int u = q.top().second; q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if(d[u] + e[i].w < d[v]) {
d[v] = d[u] + e[i].w;
q.push(make_pair(-d[v], v));
}
}
}
return;
}
int main() {
n = read(), m = read(), s = read();
for(int i = 1; i <= m; i++) {
int u = read(), v = read(), w = read();
add(u, v, w);
}
dijkstra();
for(int i = 1; i <= n; i++) {
printf("%d ", d[i]);
}
return 0;
}
注意:不要盲目选择优先队列优化 \(dijkstra\) ,当 \(m\) 达到 \(n^2\) 数量级时,优先队列可能成为负优化
可检测负环单源最短路: Bellman–Ford 算法(以及SPFA)
0x01. 算法思路
0x02. 算法实现与复杂度
P3371 【模板】单源最短路径(弱化版)
AC代码:
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int f = 1, otto = 0;
char a = getchar();
while(!isdigit(a)) {
if(a == '-') f = -1;
a = getchar();
}
while(isdigit(a)) {
otto = (otto << 1) + (otto << 3) + (a ^ 48);
a = getchar();
}
return f * otto;
}
const int maxn = 1e4 + 10;
const int maxm = 5e5 + 10;
int m, n, s;
struct edge{
int v, w, nxt;
}e[maxm];
int tot = 0, head[maxn];
void add(int u, int v, int w) {
e[++tot].nxt = head[u];
head[u] = tot;
e[tot].v = v;
e[tot].w = w;
}
const int inff = 2147483647;
queue<int> q;
int d[maxn];
bool vis[maxn];
void spfa() {
for(int i = 1; i <= n; i++) d[i] = inff;
d[s] = 0;
vis[s] = 1;
q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
vis[u] = 0;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v, w = e[i].w;
if(d[v] > d[u] + w) {
d[v] = d[u] + w;
if(!vis[v]) vis[v] = 1, q.push(v);
}
}
}
}
int main() {
n = read(), m = read(), s = read();
for(int i = 1; i <= m; i++) {
int u = read(), v = read(), w = read();
add(u, v, w);
}
spfa();
for(int i = 1; i <= n; i++) printf("%d ", d[i]);
}