#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\) 数组记录起点到每个点的最短路径,实现流程如下:

  1. 初始化起点 \(dist[s]\)\(0\),其他节点 \(dist[i]\) 为无穷大;
  2. 找出未被遍历的,\(dist\) 值最小的 \(dist[u]\),标记 \(u\)
  3. 扫描 \(dist[u]\) 所有出边 \((w,v)\),对于 \(dist[v]\),若 \(dist[u] + w_i < dist[v]\) 则更新 \(dist[v]\)
  4. 重复 \(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]);
} 

任意点对最短路: Floyd 算法

任意点对最短路:Johnson 算法

posted @ 2024-10-09 15:56  Ydoc770  阅读(6)  评论(0编辑  收藏  举报