本题主要和图的遍历求解最短路径相关,可以用 Dijkstra 或者 Bellman-Ford 算法进行解决。
原题
给你一个由 n 个节点(下标从 0 开始)组成的无向加权图,该图由一个描述边的列表组成,其中 edges[i] = [a, b] 表示连接节点 a 和 b 的一条无向边,且该边遍历成功的概率为 succProb[i] 。
指定两个节点分别作为起点 start 和终点 end ,请你找出从起点到终点成功概率最大的路径,并返回其成功概率。
如果不存在从 start 到 end 的路径,请 返回 0 。只要答案与标准答案的误差不超过 1e-5 ,就会被视作正确答案。
示例 1:
1 | 输入:n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.2], start = 0, end = 2 |
示例 2:
1 | 输入:n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.3], start = 0, end = 2 |
示例 3:
1 | 输入:n = 3, edges = [[0,1]], succProb = [0.5], start = 0, end = 2 |
提示:
- 2 <= n <= 10^4
- 0 <= start, end < n
- start != end
- 0 <= a, b < n
- a != b
- 0 <= succProb.length == edges.length <= 2*10^4
- 0 <= succProb[i] <= 1
- 每两个节点之间最多有一条边
解题
首次尝试
原本,我想利用树的深度优先搜索遍历,加上一定程度的剪枝(就是排除已经遍历过的节点),完成这道题目,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67class Solution {
/**
* key为起始点,value为所有相连的点
*/
Map<Integer, Set<Integer>> map;
/**
* key为"点A_点B"(A < B),value为对应的概率
*/
Map<String, Double> probMap;
double maxProb = -1;
int end;
public double maxProbability(int n, int[][] edges, double[] succProb, int start, int end) {
map = new HashMap<>(n * 4 / 3 + 1);
probMap = new HashMap<>(succProb.length * 4 / 3 + 1);
this.end = end;
// 构造每个点的相连关系
for (int i = 0; i < edges.length; i++) {
int[] edge = edges[i];
Set<Integer> set = map.computeIfAbsent(edge[0], k -> new HashSet<>());
set.add(edge[1]);
set = map.computeIfAbsent(edge[1], k -> new HashSet<>());
set.add(edge[0]);
String key = edge[0] < edge[1] ? (edge[0] + "_" + edge[1]) : (edge[1] + "_" + edge[0]);
probMap.put(key, succProb[i]);
}
boolean[] visited = new boolean[n];
dp(start, 1, visited);
return maxProb == -1 ? 0 : maxProb;
}
public void dp(int index, double prob, boolean[] visited) {
// 已到终点
if (index == end) {
maxProb = prob > maxProb ? prob : maxProb;
return;
}
// 获取当前点可以到达的所有点
Set<Integer> set = map.get(index);
// 如果当前点到达不了其余点
if (set == null) {
return;
}
// 标记当前点已访问
visited[index] = true;
// 遍历相邻的点
for (int next : set) {
if (visited[next]) {
continue;
}
String key = index < next ? (index + "_" + next) : (next + "_" + index);
// 访问下一个点
dp(next, prob * probMap.get(key), visited);
}
// 退出,将该点标记为未访问
visited[index] = false;
}
}
但很可惜,超时了。我想了一下,应该是因为没有借用之前已经计算出来的结果,因此比较浪费时间。
其时间复杂度取决于边的数量,假设边的数量是 m ,则时间复杂度为O(m^2)
。
而边 m 与点 n 的关系,m 最小是 0(也就是点之间没有线),最大是 (n - 1) * n / 2
,每个点之间都有连线。
因此可以预见,这样的算法效率确实很差。
Dijkstra 算法
定义概览
Dijkstra (迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
注意该算法要求图中不存在负权边。
算法思想
设 G=(V,E) 是一个带权有向图,把图中顶点集合 V 分成两组:
第一组为已求出最短路径的顶点集合(用 S 表示,初始时 S 中只有一个源点,以后每求得一条最短路径 , 就将加入到集合 S 中,直到全部顶点都加入到 S 中,算法就结束了)。
第二组为其余未确定最短路径的顶点集合(用 U 表示),按最短路径长度的递增次序依次把第二组的顶点加入 S 中。
在加入的过程中,总保持从源点 v 到 S 中各顶点的最短路径长度不大于从源点 v 到 U 中任何顶点的最短路径长度。
此外,每个顶点对应一个距离,S 中的顶点的距离就是从 v 到此顶点的最短路径长度。U 中的顶点的距离,是从 v 到此顶点只包括 S 中的顶点为中间顶点的当前最短路径长度。
算法步骤
- 初始时,S 只包含源点,即 S ={v},v 的距离为0。U 包含除 v 外的其他顶点,即: U ={其余顶点},若 v 与 U 中顶点 u 有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
- 从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
- 以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
- 重复步骤b和c直到所有顶点都包含在S中。
执行动画过程如下图
本题解法
1 | class Solution { |
提交OK,执行用时超过了69%
的 java 提交记录,看来还有值得优化的地方。
假设边的数量为 m ,点的数量为 n ,则时间复杂度为O(n + m + nlogn)
。
Bellman-Ford 算法
之前有说到 Dijkstra 算法要求不能有负权边
,而这个 Bellman-Ford 算法是支持的。
算法步骤
- 创建源顶点 v 到图中所有顶点的距离的集合 distSet,为图中的所有顶点指定一个距离值,初始均为 Infinite,源顶点距离为 0;
- 计算最短路径,执行 V - 1 次遍历;对于图中的每条边:如果起点 u 的距离 d 加上边的权值 w 小于终点 v 的距离 d,则更新终点 v 的距离值 d;
- 检测图中是否有负权边形成了环,遍历图中的所有边,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环;
例如,下面的有向图 G 中包含 5 个顶点和 8 条边。假设源点 为 A。初始化 distSet 所有距离为 INFI,源点 A 为 0。
由于图中有 5 个顶点,按照步骤 1 需要遍历 4 次,第一次遍历的结果如下。
第二次遍历的结果如下。
以此类推可以得出完全遍历的结果。
本题解法
1 | class Solution { |
提交OK,执行用时超过了95%
的 java 提交记录。
其时间假设边的数量为 m ,点的数量为 n ,则时间复杂度为O(mn)
。
总结
以上就是这道题目我的解答过程了,不知道大家是否理解了。本题主要和图的遍历求解最短路径相关,可以用 Dijkstra 或者 Bellman-Ford 算法进行解决。
有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。
公众号:健程之道