最短路问题

目录

一,单源最短路径

1,权值

2,环路、简单路径

3,有向图

二,Dijskra

1,原理

2,性能分析

3,实现

4,OJ实战

HDU 1874 畅通工程续(无向图)

力扣 743. 网络延迟时间(有向图)

力扣 505. 迷宫 II

力扣 499. 迷宫 III

力扣 1976. 到达目的地的方案数

力扣 2385. 感染二叉树需要的总时间

力扣 2642. 设计可以求最短路径的图类

5,其他应用

三,Bellman-Ford

1,原理

2,实现

3,OJ实战

力扣 787. K 站中转内最便宜的航班

四,SPFA

1,原理

2,实现

3,实战

力扣 743. 网络延迟时间

五,多源最短路

1,转化成单源最短路问题

2,Floyd-Warshall算法


一,单源最短路径

求一点到其他所有点的最短路径的问题,叫单源最短路径问题。

1,权值

按照是否存在负权值的边,可以分为三种情况的图:

(1)不存在负权值的边

(2)存在负权值的边,但不存在权值为负的环

(3)存在权值为负的环

对于(1)(2)最短路都是存在且明确的,对于(3)不存在最短路。

2,环路、简单路径

最短路不可能包含权值为负的环,也不可能包含权值为正的环。

对于权值为0的环,可以把环删掉。所以一般我们默认最短路都是指无环路径,即简单路径。

3,有向图


template<typename T = int>
struct DirectedEdge
{
	DirectedEdge() {
		a = b = dist = 0;
	};
	DirectedEdge(vector<int>v) {
		a = v[0], b = v[1], dist = v[2];
	}
	int a;//端点a的id
	int b;//端点b的id
	int dist;//从a到b的距离
};
template<typename T = int>
struct DirectedGraphData
{
public:
	vector<DirectedEdge<T>>edges; //边集,无法表示孤立点
	map<pair<int, int>, T>edgeMap; //边集,无法表示孤立点
	map<int, vector<int>>adjaList;//邻接表,孤立点是否出现取决于allPointFlag
	bool allPointFlag = false;//默认邻接表中不含孤立点
	int startId = 0;
public:
	DirectedGraphData(const vector<DirectedEdge<T>>& edges) {
		this->edges = edges;
		adjaList = directedEdgeToAdjaList(edges);
		edgeMap = directedEdgeToValueMap(edges);
	};
	DirectedGraphData(const vector<vector<int>>& edges) { //仅限权值为整数的图
		transform(edges.begin(), edges.end(), std::back_inserter(this->edges), [](const vector<int>& v) {return DirectedEdge<int>{v}; });
		adjaList = directedEdgeToAdjaList(this->edges);
		edgeMap = directedEdgeToValueMap(this->edges);
	}
	DirectedGraphData(map<int, vector<int>>& adjaList) { //仅限没有权值的图
		this->adjaList = adjaList;
		for (auto& v : adjaList) {
			for (auto vi : v.second)edges.push_back(DirectedEdge<T>({ v.first, vi, 0 }));
		}
		edgeMap = directedEdgeToValueMap(edges);
	}
	void setNumV(int n, int startId = 0) { //startId一般是0或1,可以大于1
		allPointFlag = true;
		for (int i = startId; i < startId + n; i++)adjaList[i];
		this->startId = startId;
	}
	int getNumV() const {
		return adjaList.size();
	}
	int getNumE() const {
		return edges.size();
	}
private:
	//输入有向边集{{1,2}{1,3}{2,3}},输出邻接表{1:{2,3},2:{3}}
	static map<int, vector<int>> directedEdgeToAdjaList(const vector<DirectedEdge<T>>& v)
	{
		map<int, vector<int>> ans;
		for (auto& vi : v) {
			ans[vi.a].push_back(vi.b);
			ans[vi.b];
		}
		return ans;
	}
	//输入有向带权边集,输出边和权的映射
	static map<pair<int, int>, int> directedEdgeToValueMap(const vector<DirectedEdge<T>>& v)
	{
		map<pair<int, int>, int>m;
		for (auto& vi : v) {
			m[{vi.a, vi.b}] = vi.dist;
		}
		return m;
	}
};

二,Dijskra

1,原理

Dijskra适用于不存在负权值的边的图。

把点分为已经确定最短路的点和还没确定的点,每次新增一个确定最短路的点,确定之后根据它来对剩下的点刷新从起点到该点的可能的最短路,

每次都在剩下的点中,选取离起点最近的点加入到确定最短路的集合中,直到所有的点都确定最短路。

 

2,性能分析

根据优先队列实现方式的不同,有不同的性能。

如果用数组实现,则时间复杂度是O(V^2)

如果是priority_queue,则时间复杂度是O((V+E)* lgV)

如果是斐波那契堆,则时间复杂度是O(V * lgV + E),这是三者中时间复杂度最低的。

3,实现

class Dijskra//求最短路,适用于不存在负权值的边的图
{
public:
	static map<int, int> shortestPath(map<int, vector<int>>& m, map<pair<int, int>, int>& value, int n, int src)
	{
		map<int, int>dis;
		priority_queue< Node, vector< Node>, cmp>que;
		map<int, int>visit;
		for (int i = 0; i < n; i++)dis[i] = INT_MAX;
		que.push({ src,0 });
		dis[src] = 0;
		while (!que.empty())
		{
			Node nod = que.top();
			que.pop();
			if (visit[nod.id])continue;
			visit[nod.id] = 1;
			for (auto& vi : m[nod.id]) {
				if (nod.len + value[{nod.id, vi}] < dis[vi]) {
					que.push({ vi, dis[vi] = nod.len + value[{nod.id, vi}] });
				}
			}
		}
		return dis;
	}
private:
	struct Node
	{
		int id;
		int len;
	};
	class cmp
	{
	public:
		bool operator()(Node a, Node b)
		{
			return a.len > b.len;
		}
	};
};

4,OJ实战

HDU 1874 畅通工程续(无向图)

题目:


Description

某省自从实行了很多年的畅通工程计划后,终于修建了很多路。不过路多了也不好,每次要从一个城镇到另一个城镇时,都有许多种道路方案可以选择,而某些方案要比另一些方案行走的距离要短很多。这让行人很困扰。 

现在,已知起点和终点,请你计算出要从起点到终点,最短需要行走多少距离。
Input

本题目包含多组数据,请处理到文件结束。 
每组数据第一行包含两个正整数N和M(0<N<200,0<M<1000),分别代表现有城镇的数目和已修建的道路的数目。城镇分别以0~N-1编号。 
接下来是M行道路信息。每一行有三个整数A,B,X(0<=A,B<N,A!=B,0<X<10000),表示城镇A和城镇B之间有一条长度为X的双向道路。 
再接下一行有两个整数S,T(0<=S,T<N),分别代表起点和终点。
Output

对于每组数据,请在一行里输出最短需要行走的距离。如果不存在从S到T的路线,就输出-1. 
Sample Input

3 3
0 1 1
0 2 3
1 2 1
0 2
3 1
0 1 1
1 2
Sample Output

2
-1

代码:
 

#include<iostream>
#include<string.h>
#include<map>
#include<vector>
#include <queue>
using namespace std;
class Dijskra
{
};

int main()
{
	int a, b, x;
	int n, m, s, t;
	while (scanf("%d%d", &n, &m) != -1)
	{
		map<int, vector<int>> g;
		map<pair<int, int>, int>value;
		while (m--)
		{
			scanf("%d%d%d", &a, &b, &x);
			g[a].push_back(b), g[b].push_back(a);
			if (value[{a, b}]==0|| value[{a, b}] > x)value[{a, b}] = value[{b, a}] = x;
		}
		scanf("%d%d", &s, &t);
		auto dis = Dijskra(g, value, n, s).dis;
		if (dis[t]!= INT_MAX)printf("%d\n", dis[t]);
		else printf("-1\n");
	}
	return 0;
}

还是挺快的,31ms  AC

这个题目有一个略坑的地方,2个城市之间不一定只有1条路,建图的时候需要判断。

力扣 743. 网络延迟时间(有向图)

有 n 个网络节点,标记为 1 到 n。

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例 1:

输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2
示例 2:

输入:times = [[1,2,1]], n = 2, k = 1
输出:1
示例 3:

输入:times = [[1,2,1]], n = 2, k = 2
输出:-1
 

提示:

1 <= k <= n <= 100
1 <= times.length <= 6000
times[i].length == 3
1 <= ui, vi <= n
ui != vi
0 <= wi <= 100
所有 (ui, vi) 对都 互不相同(即,不含重复边)

//输入有向边集{{1,2}{1,3}{2,3}},输出邻接表{1:{2,3},2:{3}}
map<int, vector<int>> edgeToAdjaList(vector<vector<int>>& v)
{
	map<int, vector<int>> ans;
	for (auto& vi : v) {
		ans[vi[0]].push_back(vi[1]);
	}
	return ans;
}
//输入有向带权边集,输出边和权的映射
map<pair<int, int>, int> edgeToValueMap(vector<vector<int>>& v)
{
	map<pair<int, int>, int>m;
	for (auto& vi : v) {
		m[{vi[0], vi[1]}] = vi[2];
	}
	return m;
}

class Dijskra
{
......
	Dijskra(map<int, vector<int>>&m, map<pair<int, int>, int> &value, int n, int start) 
	{
		for (int i = 1; i <= n; i++)dis[i] = INT_MAX;
......
};


class Solution {
public:
	int networkDelayTime(vector<vector<int>>& times, int n, int k) {
		map<int, vector<int>> adja = edgeToAdjaList(times);
		map<pair<int, int>, int> value = edgeToValueMap(times);
		int ans = 0;
		auto dis = Dijskra(adja, value, n, k).dis;
		for (int i = 1; i <= n; i++) {
			if (dis[i] == INT_MAX)return -1;
			ans = max(ans, dis[i]);
		}
		return ans;
	}
};

力扣 505. 迷宫 II

由空地和墙组成的迷宫中有一个球。球可以向上下左右四个方向滚动,但在遇到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。

给定球的起始位置,目的地和迷宫,找出让球停在目的地的最短距离。距离的定义是球从起始位置(不包括)到目的地(包括)经过的空地个数。如果球无法停在目的地,返回 -1。

迷宫由一个0和1的二维数组表示。 1表示墙壁,0表示空地。你可以假定迷宫的边缘都是墙壁。起始位置和目的地的坐标通过行号和列号给出。

示例 1:

输入 1: 迷宫由以下二维数组表示

0 0 1 0 0
0 0 0 0 0
0 0 0 1 0
1 1 0 1 1
0 0 0 0 0

输入 2: 起始位置坐标 (rowStart, colStart) = (0, 4)
输入 3: 目的地坐标 (rowDest, colDest) = (4, 4)

输出: 12

解析: 一条最短路径 : left -> down -> left -> down -> right -> down -> right。
             总距离为 1 + 1 + 3 + 1 + 2 + 2 + 2 = 12。

示例 2:

输入 1: 迷宫由以下二维数组表示

0 0 1 0 0
0 0 0 0 0
0 0 0 1 0
1 1 0 1 1
0 0 0 0 0

输入 2: 起始位置坐标 (rowStart, colStart) = (0, 4)
输入 3: 目的地坐标 (rowDest, colDest) = (3, 2)

输出: -1

解析: 没有能够使球停在目的地的路径。

注意:

迷宫中只有一个球和一个目的地。
球和目的地都在空地上,且初始时它们不在同一位置。
给定的迷宫不包括边界 (如图中的红色矩形), 但你可以假设迷宫的边缘都是墙壁。
迷宫至少包括2块空地,行数和列数均不超过100。

class Solution {
public:
	int id(int x, int y)
	{
		return x * col + y;
	}
	void make(map<int, vector<int>>& m, map<pair<int, int>, int>& value, int s,int e)
	{
		int d = (s / col == e / col) ? 1 : col;
		m[s].push_back(e), m[e].push_back(s);
		value[{s, e}] = value[{e, s}] = (e - s) / d;
		for (int i = s + d; i < e; i += d) {
			m[i].push_back(s), m[i].push_back(e);
			value[{i, s}] = (i - s) / d, value[{i, e}] = (e - i) / d;
		}
	}
	int shortestDistance(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
		row = maze.size(),col = maze[0].size();
		int s = id(start[0], start[1]), e = id(destination[0], destination[1]);
		int dx4[] = { 0,0,1,-1 };
		int dy4[] = { 1,-1,0,0 };
		map<int, vector<int>> m;
		map<pair<int, int>, int> value;
		for (int i = 0; i < row; i++) {
			int s = 0, e = 0;
			while (s < col) {
				while (s < col && maze[i][s])s++;
				if (s == col)break;
				e = s;
				while (e < col && maze[i][e]==0)e++;
				make(m, value, id(i, s), id(i, e-1));
				s = e;
			}
		}
		for (int i = 0; i < col; i++) {
			int s = 0, e = 0;
			while (s < row) {
				while (s < row && maze[s][i])s++;
				if (s == row)break;
				e = s;
				while (e < row && maze[e][i] == 0)e++;
				make(m, value, id(s, i), id(e - 1, i));
				s = e;
			}
		}
		auto dis = Dijskra().shortestPath(m, value, row * col, s);
		if (dis[e] == INT_MAX)return -1;
		return dis[e];
	}
	int row;
	int col;
};

力扣 499. 迷宫 III

由空地和墙组成的迷宫中有一个。球可以向上(u)下(d)左(l)右(r)四个方向滚动,但在遇到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。迷宫中还有一个,当球运动经过洞时,就会掉进洞里。

给定球的起始位置,目的地迷宫,找出让球以最短距离掉进洞里的路径。 距离的定义是球从起始位置(不包括)到目的地(包括)经过的空地个数。通过'u', 'd', 'l' 和 'r'输出球的移动方向。 由于可能有多条最短路径, 请输出字典序最小的路径如果球无法进入洞,输出"impossible"。

迷宫由一个0和1的二维数组表示。 1表示墙壁,0表示空地。你可以假定迷宫的边缘都是墙壁。起始位置和目的地的坐标通过行号和列号给出。

示例1:

输入 1: 迷宫由以下二维数组表示

0 0 0 0 0
1 1 0 0 1
0 0 0 0 0
0 1 0 0 1
0 1 0 0 0

输入 2: 球的初始位置 (rowBall, colBall) = (4, 3)
输入 3: 洞的位置 (rowHole, colHole) = (0, 1)

输出: "lul"

解析: 有两条让球进洞的最短路径。
第一条路径是 左 -> 上 -> 左, 记为 "lul".
第二条路径是 上 -> 左, 记为 'ul'.
两条路径都具有最短距离6, 但'l' < 'u',故第一条路径字典序更小。因此输出"lul"。

示例 2:

输入 1: 迷宫由以下二维数组表示

0 0 0 0 0
1 1 0 0 1
0 0 0 0 0
0 1 0 0 1
0 1 0 0 0

输入 2: 球的初始位置 (rowBall, colBall) = (4, 3)
输入 3: 洞的位置 (rowHole, colHole) = (3, 0)

输出: "impossible"

示例: 球无法到达洞。

注意:

  1. 迷宫中只有一个球和一个目的地。
  2. 球和洞都在空地上,且初始时它们不在同一位置。
  3. 给定的迷宫不包括边界 (如图中的红色矩形), 但你可以假设迷宫的边缘都是墙壁。
  4. 迷宫至少包括2块空地,行数和列数均不超过30。

思路:

相对于迷宫II,有2个差别:

(1)需要输出路径,所以我新增了ReBuild类,进行二次搜索重建路径。

(2)多条路径需要排序,输出最小字典序的路径,只需要建图时按照顺序构建邻接表即可。

class ReBuild
{
public:
	stack<int> ans;
	ReBuild(map<int, int>& dis, map<int, vector<int>>& m, int col, int s, int e)
	{
		this->e = e;
		this->col = col;
		ans.push(e);
		dfs(dis, m, s);
	}
private:
	bool dfs(map<int, int>& dis, map<int, vector<int>>& m, int k)
	{
		if (k == e)return true;
		for (int nex : m[k]) {
			if (dis[nex]==dis[k]+len(k,nex) && dfs(dis, m, nex)) {
				ans.push(k);
				return true;
			}
		}
		return false;
	}
	int len(int s, int e)
	{
		if (s / col == e / col)return abs(s - e);
		return abs(s - e) / col;
	}
	int col;
	int e;
};
 

class Solution {
public:
	int id(int x, int y)
	{
		return x * col + y;
	}
	bool inBoard(int r, int c)
	{
		return r >= 0 && r < row&& c >= 0 && c < col;
	}
	void make(map<int, vector<int>>& m, map<pair<int, int>, int>& value, int s,int e)
	{
		if (s == e)return;
		int d = (s / col == e / col) ? 1 : col;
		m[s].insert(m[s].begin(),e), m[e].push_back(s);
		value[{s, e}] = value[{e, s}] = (e - s) / d;
		for (int i = s + d; i < e; i += d) {
			m[i].push_back(s), m[i].insert(m[i].begin(),e);
			value[{i, s}] = (i - s) / d, value[{i, e}] = (e - i) / d;
		}
	}
	string findShortestWay(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
		row = maze.size(),col = maze[0].size();
		int s = id(start[0], start[1]), e = id(destination[0], destination[1]);
		map<int, vector<int>> m;
		map<pair<int, int>, int> value;
		for (int i = 0; i < row; i++) {
			int s = 0, e = 0;
			while (s < col) {
				while (s < col && maze[i][s])s++;
				if (s == col)break;
				e = s;
				while (e < col && maze[i][e]==0)e++;
				if (i == destination[0] && s < destination[1] && destination[1] < e) {
					make(m, value, id(i, s), id(i, destination[1]));
					make(m, value, id(i, destination[1]), id(i, e - 1));
				} else make(m, value, id(i, s), id(i, e - 1));
				s = e;
			}
		}
		for (int i = 0; i < col; i++) {
			int s = 0, e = 0;
			while (s < row) {
				while (s < row && maze[s][i])s++;
				if (s == row)break;
				e = s;
				while (e < row && maze[e][i] == 0)e++;
				if (i == destination[1] && s < destination[0] && destination[0] < e) {
					make(m, value, id(s, i), id(destination[0], i));
					make(m, value, id(destination[0], i), id(e - 1, i));
				} else make(m, value, id(s, i), id(e - 1, i));
				s = e;
			}
		}
		auto dis = Dijskra().shortestPath(m, value, row * col, s);
		if (dis[e] == INT_MAX)return "impossible";
		stack<int> ans = ReBuild(dis, m, col, s, e).ans;
		int x = ans.top(), y = 0;
		ans.pop();
		string ret;
		while (!ans.empty())
		{
			y = ans.top();
			ans.pop();
			if (x / col == y / col)ret += x < y ? 'r' : 'l';
			else ret += x < y ? 'd' : 'u';
			x = y;
		}
		return ret;
	}
	int row;
	int col;
	map<int, int>m;
};

力扣 1976. 到达目的地的方案数

你在一个城市里,城市由 n 个路口组成,路口编号为 0 到 n - 1 ,某些路口之间有 双向 道路。输入保证你可以从任意路口出发到达其他任意路口,且任意两个路口之间最多有一条路。

给你一个整数 n 和二维整数数组 roads ,其中 roads[i] = [ui, vi, timei] 表示在路口 ui 和 vi 之间有一条需要花费 timei 时间才能通过的道路。你想知道花费 最少时间 从路口 0 出发到达路口 n - 1 的方案数。

请返回花费 最少时间 到达目的地的 路径数目 。由于答案可能很大,将结果对 109 + 7 取余 后返回。

示例 1:

输入:n = 7, roads = [[0,6,7],[0,1,2],[1,2,3],[1,3,3],[6,3,3],[3,5,1],[6,5,1],[2,5,1],[0,4,5],[4,6,2]]
输出:4
解释:从路口 0 出发到路口 6 花费的最少时间是 7 分钟。
四条花费 7 分钟的路径分别为:
- 0 ➝ 6
- 0 ➝ 4 ➝ 6
- 0 ➝ 1 ➝ 2 ➝ 5 ➝ 6
- 0 ➝ 1 ➝ 3 ➝ 5 ➝ 6

示例 2:

输入:n = 2, roads = [[1,0,10]]
输出:1
解释:只有一条从路口 0 到路口 1 的路,花费 10 分钟。

提示:

  • 1 <= n <= 200
  • n - 1 <= roads.length <= n * (n - 1) / 2
  • roads[i].length == 3
  • 0 <= ui, vi <= n - 1
  • 1 <= timei <= 109
  • ui != vi
  • 任意两个路口之间至多有一条路。
  • 从任意路口出发,你能够到达其他任意路口。

思路:

把Dijskra略改一下,加上计数功能就行了。

int p = 1000000007;
class DijskraPlus//求最短路,适用于不存在负权值的边的图
{
public:
	static map<int, long long> shortestPath(map<int, vector<int>>& m, map<pair<int, int>, int>& value, int n, int src, map<int, int>&num)
	{
		map<int, long long>dis;
		priority_queue< Node, vector< Node>, cmp>que;
		map<int, int>visit;
		for (int i = 0; i < n; i++)dis[i] = 123456789876543;
		que.push({ src,0});
		dis[src] = 0, num[src] = 1;
		while (!que.empty())
		{
			Node nod = que.top();
			que.pop();
			if (visit[nod.id])continue;
			visit[nod.id] = 1;
			for (auto& vi : m[nod.id]) {
				if (nod.len + value[{nod.id, vi}] < dis[vi]) {
					que.push({ vi, dis[vi] = nod.len + value[{nod.id, vi}] });
					num[vi] = num[nod.id];
				}
				else if (nod.len + value[{nod.id, vi}] == dis[vi]) {
					que.push({ vi, dis[vi] = nod.len + value[{nod.id, vi}] });
					num[vi] += num[nod.id];
					num[vi] %= p;
				}
				
			}
		}
		return dis;
	}
private:
	struct Node
	{
		int id;
		long long len;
	};
	class cmp
	{
	public:
		bool operator()(Node a, Node b)
		{
			return a.len > b.len;
		}
	};
};

class Solution {
public:
	int countPaths(int n, vector<vector<int>>& roads) {
		auto g = UndirectedGraphData<>(roads);
		map<int, int>num;
		DijskraPlus::shortestPath(g.adjaList, g.edgeMap, n, 0, num);
		return num[n - 1];
	}
};

力扣 2385. 感染二叉树需要的总时间

给你一棵二叉树的根节点 root ,二叉树中节点的值 互不相同 。另给你一个整数 start 。在第 0 分钟,感染 将会从值为 start 的节点开始爆发。

每分钟,如果节点满足以下全部条件,就会被感染:

  • 节点此前还没有感染。
  • 节点与一个已感染节点相邻。

返回感染整棵树需要的分钟数

示例 1:

输入:root = [1,5,3,null,4,10,6,9,2], start = 3
输出:4
解释:节点按以下过程被感染:
- 第 0 分钟:节点 3
- 第 1 分钟:节点 1、10、6
- 第 2 分钟:节点5
- 第 3 分钟:节点 4
- 第 4 分钟:节点 9 和 2
感染整棵树需要 4 分钟,所以返回 4 。

示例 2:

输入:root = [1], start = 1
输出:0
解释:第 0 分钟,树中唯一一个节点处于感染状态,返回 0 。

提示:

  • 树中节点的数目在范围 [1, 105] 内
  • 1 <= Node.val <= 105
  • 每个节点的值 互不相同
  • 树中必定存在值为 start 的节点
class Solution {
public:
	int amountOfTime(TreeNode* root, int start) {
		auto m = GetAdjaListFromTree(root);
		map<pair<int, int>, int> value;
		int n = 0;
		for (auto mi : m) {
			n = max(n, mi.first);
			for (auto x : mi.second)value[make_pair(mi.first, x)] = 1;
		}
		auto len = DijskraShortestPath(m, value, n + 1, start);
		int ans = 0;
		for (auto mi : m)ans = max(ans, len[mi.first]);
		return ans;
	}
};

力扣 2642. 设计可以求最短路径的图类

给你一个有 n 个节点的 有向带权 图,节点编号为 0 到 n - 1 。图中的初始边用数组 edges 表示,其中 edges[i] = [fromi, toi, edgeCosti] 表示从 fromi 到 toi 有一条代价为 edgeCosti 的边。

请你实现一个 Graph 类:

  • Graph(int n, int[][] edges) 初始化图有 n 个节点,并输入初始边。
  • addEdge(int[] edge) 向边集中添加一条边,其中 edge = [from, to, edgeCost] 。数据保证添加这条边之前对应的两个节点之间没有有向边。
  • int shortestPath(int node1, int node2) 返回从节点 node1 到 node2 的路径 最小 代价。如果路径不存在,返回 -1 。一条路径的代价是路径中所有边代价之和。

示例 1:

输入:
["Graph", "shortestPath", "shortestPath", "addEdge", "shortestPath"]
[[4, [[0, 2, 5], [0, 1, 2], [1, 2, 1], [3, 0, 3]]], [3, 2], [0, 3], [[1, 3, 4]], [0, 3]]
输出:
[null, 6, -1, null, 6]

解释:
Graph g = new Graph(4, [[0, 2, 5], [0, 1, 2], [1, 2, 1], [3, 0, 3]]);
g.shortestPath(3, 2); // 返回 6 。从 3 到 2 的最短路径如第一幅图所示:3 -> 0 -> 1 -> 2 ,总代价为 3 + 2 + 1 = 6 。
g.shortestPath(0, 3); // 返回 -1 。没有从 0 到 3 的路径。
g.addEdge([1, 3, 4]); // 添加一条节点 1 到节点 3 的边,得到第二幅图。
g.shortestPath(0, 3); // 返回 6 。从 0 到 3 的最短路径为 0 -> 1 -> 3 ,总代价为 2 + 4 = 6 。

提示:

  • 1 <= n <= 100
  • 0 <= edges.length <= n * (n - 1)
  • edges[i].length == edge.length == 3
  • 0 <= fromi, toi, from, to, node1, node2 <= n - 1
  • 1 <= edgeCosti, edgeCost <= 106
  • 图中任何时候都不会有重边和自环。
  • 调用 addEdge 至多 100 次。
  • 调用 shortestPath 至多 100 次。
class Graph {
public:
    Graph(int n, vector<vector<int>>& edges) {
		this->n = n;
		DirectedGraphData<> g(edges);
		m = g.adjaList;
		value = g.edgeMap;
    }

    void addEdge(vector<int> edge) {
		m[edge[0]].push_back(edge[1]);
		value[make_pair(edge[0], edge[1])] = edge[2];
    }

    int shortestPath(int node1, int node2) {
        int ans = DijskraShortestPath(m, value, n, node1)[node2];
        return ans == INT_MAX ? -1 : ans;
    }
	int n;
	map<int, vector<int>> m;
	map<pair<int, int>, int> value;
};

5,其他应用

图像边缘提取

三,Bellman-Ford

1,原理

Bellman-Ford适用于不存在负权值的环的图,比Dijskra适用范围更广,但时间复杂度要高。

因为存在负权值,所以并不能一眼看出是否存在负环。

Bellman-Ford的计算流程就是从起点出发,做V次松弛(每次松弛需要更新所有点的最短路的上界),第V次如果所有点都没有变化,则此时所有点的最短路的上界就是最短路长度,第V次如果有变化,说说明有负环。

时间复杂度:O(V * E)

2,实现

class BellmanFord //求最短路,适用于不存在负权值的环的图
{
public:
	static map<int, int> shortestPath(const DirectedGraphData<int>& g, int src)
	{
		map<int, int>dis;
		int n = g.getNumV();
		for (int i = 0; i < n; i++)dis[i] = INT_MAX;
		dis[src] = 0;
		for (int i = 0; i < n; i++) {
			if (!refresh(g.edgeMap, dis))break;
			if (i == n - 1)return map<int, int>{}; //有负环
		}
		return dis;
	}
private:
	static inline bool refresh(const map<pair<int, int>, int>& value, map<int, int>&dis)
	{
		bool flag = false;
		auto dis2 = dis;
		for (auto& e : value) {
			if (dis2[e.first.second] > ((long long)dis[e.first.first]) + e.second) {
				dis2[e.first.second] = ((long long)dis[e.first.first]) + e.second, flag = true;
			}
		}
		dis = dis2;
		return flag;
	}
};

3,OJ实战

力扣 787. K 站中转内最便宜的航班

有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi

现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1

示例 1:

输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 1
输出: 200
解释: 
城市航班图如下

从城市 0 到城市 2 在 1 站中转以内的最便宜价格是 200,如图中红色所示。

示例 2:

输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 0
输出: 500
解释: 
城市航班图如下

从城市 0 到城市 2 在 0 站中转以内的最便宜价格是 500,如图中蓝色所示。

提示:

  • 1 <= n <= 100
  • 0 <= flights.length <= (n * (n - 1) / 2)
  • flights[i].length == 3
  • 0 <= fromi, toi < n
  • fromi != toi
  • 1 <= pricei <= 104
  • 航班没有重复,且不存在自环
  • 0 <= src, dst, k < n
  • src != dst
class BellmanFord
{
public:
	static map<int, int> shortestPath(const DirectedGraphData<int>& g, int src, int k)
	{
		map<int, int>dis;
		int n = g.getNumV();
		cout<<n;
		for (int i = 0; i < n; i++)dis[i] = INT_MAX;
		dis[src] = 0;
		for (int i = 0; i < k; i++) {
			if (!refresh(g.edgeMap, dis))break;
			if (i == n - 1)return map<int, int>{}; //有负环
		}
		return dis;
	}
private:
	static inline bool refresh(const map<pair<int, int>, int>& value, map<int, int>&dis)
	{
		bool flag = false;
		map<int, int> dis2 = dis;
		for (auto& e : value) {
			if (dis2[e.first.second] > ((long long)dis[e.first.first]) + e.second) {
				dis2[e.first.second] = ((long long)dis[e.first.first]) + e.second, flag = true;
			}
		}
		dis = dis2;
		return flag;
	}
};
class Solution {
public:
	int findCheapestPrice(int n, const vector<vector<int>>& flights, int src, int dst, int k) {
		DirectedGraphData<int>g{flights};
		g.setNumV(n);
		auto ans = BellmanFord::shortestPath(g, src, k+1);
		if (ans[dst] == INT_MAX)return -1;
		return ans[dst];
	}
};

四,SPFA

1,原理

SPFA是BellmanFord的基于队列的优化,平均效率更高,但是最坏时间复杂度仍然是O(V * E)

在BellmanFord的V次迭代过程中,有些节点已经找到了最短路,就不再需要反复访问这个节点了,于是可以把这一部分运算量优化掉。

把迭代方式改成基于队列,类似Dijskra,但是SPFA用的是普通队列,类似BFS,每次从队列取出一个元素,根据他的邻接表判断哪些节点需要松弛,并把不在队列中的那些节点放入队列尾部,直到队列清空则得到所有节点的最短路。

如果中途发现某个节点入队次数达到V,那说明有负环。

2,实现

class SPFA //求最短路,适用于不存在负权值的环的图
{
public:
	static map<int, int> shortestPath(const DirectedGraphData<int>& g, int src)
	{
		map<int, int>dis;
		map<int, bool>inQueue;
		map<int, int>visit;
		int n = g.getNumV();
		for (int i = g.startId; i < g.startId + n; i++)dis[i] = INT_MAX;
		dis[src] = 0;
		queue<int>q;
		q.push(src);
		visit[src]++;
		inQueue[src] = true;
		while (!q.empty()) {
			int t = q.front();
			q.pop();
			inQueue[t] = false;
			auto v = refresh(dis, t, g);
			for (auto vi : v) {
				if (inQueue[vi])continue;
				q.push(vi);
				inQueue[vi] = true;
				if (++visit[vi] >= n)return map<int, int>{};//存在负环
			}
		}
		return dis;
	}
private:
	static inline vector<int> refresh(map<int, int>&dis, int t, const DirectedGraphData<int>& g)
	{
		vector<int>ans;
		auto it = g.adjaList.find(t);
		if (it == g.adjaList.end())return ans;
		long long d = dis[t];
		for (auto vi : it->second) {
			if (dis[vi] > d+ g.edgeMap.at(make_pair(t, vi))) {
				dis[vi] = d + g.edgeMap.at(make_pair(t,vi));
				ans.push_back(vi);
			}
		}
		return ans;
	}
};

3,实战

力扣 743. 网络延迟时间

题目在上文,用Dijskra求解了,这里换SPFA再次求解。

#define SPFAShortestPath SPFA::shortestPath//求最短路,适用于不存在负权值的环的图
class Solution {
public:
	int networkDelayTime(vector<vector<int>>& times, int n, int k) {
		int ans = 0;
		DirectedGraphData<int>g{ times };
		g.setNumV(n, 1);
		auto dis = SPFAShortestPath(g, k);
		for (int i = 1; i <= n; i++) {
			if (dis[i] == INT_MAX)return -1;
			ans = max(ans, dis[i]);
		}
		return ans;
	}
};

五,多源最短路

单源最短路是求出某个固定的起点到所有节点的最短路,多源最短路是一次性求出所有的任意一点到任意一点的距离。

1,转化成单源最短路问题

假设有n个节点,把单源最短路算法调用n次即可。

2,Floyd-Warshall算法

动态规划。

假设节点编号为0到n-1,取出0到k的所有节点和i和j构成的子图中,i到j的最短路记为dp[k][i][j],

递推式:

dp[k][i][j]=min\{dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]\}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用Matlab解决最短路问题的示例代码: ```matlab % 首先定义图的邻接矩阵 % 例如下面的邻接矩阵表示一个6个节点的有向图 % 从1到2的边权重为2,从1到3的边权重为4,以此类推 % 如果两个节点之间没有边相连,则边权重为inf G = [0 2 4 inf inf inf; inf 0 1 5 inf inf; inf inf 0 1 inf inf; inf inf inf 0 3 inf; inf inf inf inf 0 2; inf inf inf inf inf 0]; % 使用Dijkstra算法计算从节点1到其他节点的最短路径和距离 [start_node, dist] = dijkstra(G, 1); % 打印结果 fprintf('从节点1到其他节点的最短路径和距离如下:\n'); for i = 1:length(dist) fprintf('从节点1到节点%d的最短路径为:', i); print_path(start_node, i); fprintf(',距离为:%d\n', dist(i)); end % Dijkstra算法实现 function [start_node, dist] = dijkstra(G, s) n = size(G, 1); start_node = zeros(1, n); dist = inf(1, n); visited = false(1, n); dist(s) = 0; for i = 1:n-1 u = find_min_dist(dist, visited); visited(u) = true; for v = 1:n if ~visited(v) && G(u,v) ~= inf && dist(u) + G(u,v) < dist(v) dist(v) = dist(u) + G(u,v); start_node(v) = u; end end end end % 辅助函数:找到距离源节点最近的未访问节点 function u = find_min_dist(dist, visited) dist(visited) = inf; [~, u] = min(dist); end % 辅助函数:打印路径 function print_path(start_node, v) if start_node(v) == 0 fprintf('%d', v); else print_path(start_node, start_node(v)); fprintf(' -> %d', v); end end ``` 希望这个示例代码能够帮助您解决最短路问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值