图论·最短路径问题

文章目录

最短路径问题的定义:搜索,单源和多源

  • 搜索中的最短路
  • 单源最短路问题
  • 多源最短路问题

搜索问题

参考:图论·搜索最短路径

  • BFS:权值为1情况下的最短路径
  • A*:权值不为1的情况下的最短路径,启发式算法

单源最短路径

参考:图论·单源最短路径·Bellman_ford算法

Dijsktra算法

  • 三部曲:贪心选点加入集合更新距离

例题

朴素实现: O ( n 2 ) O(n^2) O(n2)

cpp 复制代码
int n, m, g[509][509], visited[509], dist[509];
int dj() {
	dist[1] = 0;
	for (int i = 1; i <= n; i++) {
		int idx = -1, temp = MAX_VALUE;
		for (int j = 1; j <= n; j++) {
			if (!visited[j] && dist[j] < temp) {
				temp = dist[j];
				idx = j;
			}
		}
		if (idx == -1)break;
		visited[idx] = 1;
		for (int j = 1; j <= n; j++) {
			if (!visited[j] && g[idx][j] != MAX_VALUE) {
				dist[j] = min(dist[j], dist[idx] + g[idx][j]);
			}
		}
	}
	return dist[n] == MAX_VALUE ? -1 : dist[n];
}

堆优化版本: O ( m l o g n ) O(mlogn) O(mlogn)

  • 朴素实现的问题:每次都需要手动遍历寻找disti最小的节点加入数组 ,每一次都要遍历邻接矩阵中所有终点的边(存在无效遍历)。
  • 使用堆来获得disti的数组 ,使用邻接表来优化邻接矩阵的存储(减少无效边的遍历)。
  • 堆中存储当前加入节点和离起点的距离
时间复杂度分析

时间复杂度分析:更新邻接矩阵所有边,时间复杂度为 O ( m ) O(m) O(m),然后不需要遍历所有节点,通过堆来加入节点,堆中元素至多为m个(将所有相邻边的节点加入),因此取出和插入操作复杂度为 O ( l o g m ) = O ( l o g n ) O(logm)=O(logn) O(logm)=O(logn),因此总共复杂度为 O ( m l o g n ) O(mlogn) O(mlogn)

链表前向星
  • 额外定义w数组,wi表示存储与i位置的节点(存储于i != 节点i)的边权值
cpp 复制代码
int n, m;
int h[150009],node[150009],w[150009],nxt[150009], len = 0,dist[150009],visited[150009];
class cmp {
public:
	bool operator()(const pair<int,int> &a ,const pair<int,int>&b) {
		return a.second > b.second;
	}
};
priority_queue<pair<int,int>, vector<pair<int,int>>, cmp>q;
void insert(int x, int y, int z) {
	len++;
	node[len] = y;
	w[len] = z;
	nxt[len] = h[x];
	h[x] = len;
}
int dj() {
	dist[1] = 0;
	q.push(make_pair(1,0));
	while (q.size()) {
		pair<int,int> cur = q.top();
		q.pop();
		if (visited[cur.first])continue;
		visited[cur.first] = 1;
		for (int i = h[cur.first]; i != -1; i = nxt[i]) {
			int v = node[i];// node's number
			int weight = w[i];
			if (!visited[v]&& dist[cur.first] + weight <dist[v]) {
				dist[v] = dist[cur.first] + weight;
				q.push(make_pair(v, dist[v]));
			}
		}
	}
	return dist[n] == MAX_VALUE ? -1 : dist[n];

}
STL链表实现
cpp 复制代码
int n, m;
vector<list<pair<int,int>>>g(1500009);
vector<int>visited(1500009, 0);
vector<int>dist(1500009, MAX_VALUE);
class cmp {
public:
	bool operator()(const pair<int,int>&a, const pair<int,int>&b) {
		return a.second > b.second;//small top heap
	}
};
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp>q;
int dj() {
	dist[1] = 0;
	q.push({ 1,0 });// node, dist
	while (q.size()) {
		auto cur = q.top();
		q.pop();
		if (visited[cur.first]) {
			continue;
		}
		visited[cur.first] = 1;

		for (auto item : g[cur.first]) {
			if (!visited[item.first]&&
				dist[cur.first] + item.second < dist[item.first]
				) {
				dist[item.first] = dist[cur.first] + item.second;
				q.push({ item.first,dist[item.first] });
			}
		}
	}

	return dist[n] == MAX_VALUE ? -1:dist[n];
}
失效情况:边权值为负数

简要证明思路和理解如下:

  • 贪心假设:加入到当前集合中的节点都是离起点的距离最短。
  • 情况分析:假设有一个边的权值为负数,但是它还没有被加入到当前集合中 (例如,离当前集合中所有点在图上的"距离"相对较远)。
  • 失效案例:假设该边的权值为-无穷 ,加入到当前集合中可以使得当前集合中所有点到出发点的距离更新为负无穷 ,贪心假设不成立(这说明当前集合中所有点到出发点的距离不一定最短),矛盾!

Bellman_ford算法

参考:图论·单源最短路径·Bellman_ford算法

  • 松弛操作:dist[j]=dist[i]+graph[i][j]
  • 相当于动态规划,distj的距离减少 ,但是到distj的路径长度增加1,引入中间节点减少距离。

用法:负权重图的最短路问题 和 负环路

  • 可以用于负权重图的单源最短路
  • 可以用于检测是否存在负环路 :如果第n+1次更新有效的话,那么图中一定存在负环路

例题

朴素实现: O ( n m ) O(nm) O(nm)

  • 遍历每一个边进行松弛即可
  • 为了控制最短距离中路径的长度,需要定义一个二维数组,确保使用的是之前的数据
  • 注意dist[i][j]表示起点1 到点i长度<=j的路径所需要的最短距离。
cpp 复制代码
int n, m, k;
typedef struct node {
	int x, y, z;
};
node edges[10009];
int dist[509][509];
void bf() {
	for (int i = 0; i <= k; i++)dist[1][i] = 0;
	for (int i = 1; i <= k; i++) {
		for (int j = 1; j <= m; j++) {
			int x = edges[j].x, y = edges[j].y, z = edges[j].z;
// 避免路径长度超过限制:例如 i=1时, x,y y,z 按顺序加入, 此时dist[z]的长度最短,但是不符合dp数组的定义(路径长度超过1). 
			dist[y][i] = min(dist[y][i], dist[x][i - 1] + z);
		}

	}
	if (dist[n][k] > (MAX_VALUE / 2))cout << "impossible";
	else cout << dist[n][k];
}
void solve() {
	cin >> n >> m >> k;
	memset(dist, 0x3F, sizeof dist);
	for (int i = 1; i <= m; i++) {
		int a, b, c;
		cin >> a >> b >> c;
		edges[i] = { a,b,c };
	}
	bf();
}

SPFA: O ( m ) − O ( n m ) O(m)-O(nm) O(m)−O(nm)

  • 优化思路:动态规划的更新方法中存在一些无效操作:例如如果distxi-1在第i-1轮中没有更新,那么这一步更新操作可以省略
  • 我们可以只记录更新过后的节点 ,将其加入到队列中,确保所有的更新操作都是高效的
  • 注意这里的节点可以重复加入
cpp 复制代码
int n, m;
vector < list<pair<int, int>>>g(100009);// node, weight
queue<int>q;
int visited[100009],dist[100009];
void spfa() {
	dist[1] = 0;
	q.push(1);
	while (q.size()) {
		int cur = q.front();
		q.pop();
		// 允许重复更新
		visited[cur] = 0;
		for (auto item : g[cur]) {
			if (dist[cur] + item.second < dist[item.first]) {
				dist[item.first] = dist[cur] + item.second;
				// 确保不存在多个重复的结点数
				if (!visited[item.first]) {
					q.push(item.first);
				}
			}
		}
	}
	if (dist[n] > (MAX_VALUE / 2)) {
		cout << "impossible";
	}
	else cout << dist[n];
}
相关推荐
江屿风2 天前
C++图论基础最小生成树经典OJ题流食般投喂
开发语言·c++·笔记·算法·深度优先·图论
San813_LDD2 天前
[数据结构]LeetCode学习
数据结构·算法·图论
handler014 天前
【算法】并查集(普通/扩展/带权)模板与例题
数据结构·c++·笔记·算法·c·图论·查并集
Lsk_Smion4 天前
力扣实训 _ [994].腐烂的橘子/图论
算法·leetcode·图论
Lucis__4 天前
图的高阶算法:从构造最小生成树到求解最短路径问题
数据结构·c++·算法·图论
随意起个昵称5 天前
线性dp-LIS题目2(导弹拦截III)
算法·动态规划·图论
05候补工程师5 天前
【408 数据结构】图论核心算法(拓扑/关键路径)与二叉搜索树精髓夺分笔记
数据结构·经验分享·笔记·考研·算法·图论
江屿风7 天前
C++图的两种构建算法流食般投喂-竞赛编
开发语言·c++·笔记·算法·图论
代码中介商7 天前
图论入门:从基础到遍历算法
数据结构·算法·图论
一个爱编程的人8 天前
图的相关概念
c++·算法·图论