目录
一,单源最短路径
求一点到其他所有点的最短路径的问题,叫单源最短路径问题。
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" 示例: 球无法到达洞。
注意:
- 迷宫中只有一个球和一个目的地。
- 球和洞都在空地上,且初始时它们不在同一位置。
- 给定的迷宫不包括边界 (如图中的红色矩形), 但你可以假设迷宫的边缘都是墙壁。
- 迷宫至少包括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],
递推式: