最短路径总结
最佳在写每日一题的时候发现了连续两天所出的题目都是和图最短路径有关的,借此机会,总结一下图最短路径的常见算法以及对应的适用情况。
相关力扣题目:
-
2959 关闭分部的可行集合数目 - 第119场双周赛 Q4
-
3112 访问消失节点的最少时间 - 第128场双周赛 Q3
-
743 网络延迟 - 算数评级:5
算法特点
面对需要计算图最短路径的题目,要选着对应算法的时候,需要观察题目的一些条件,比如:
- 是否有负边。【要是有负边,那就不能使用Dijkstra算法】
- 是否有负环。【要是有负环,那就不能使用Floyd算法】
- 图中节点个数的数据量。
- 是需要计算单源最短路径还是计算多元最短路径。【如果是计算单源最短路径,那么就可以使用Dijkstra算法,要是是计算多源最短路径,就需要使用Floyd算法】。
上面提到了
负环
这个词,那么什么是负环呢,并不是说这个环里面存在负数就是负环。负环:是这个环里面,所有边+起来的值<0,这就导致了负环的情况,这个情况Floyd算法是不能解决问题的。
算法特性
算法名称 | 时间复杂度 | 场景计算 | 单源/多源 |
---|---|---|---|
Dijkstra算法 | O(n^2) | 有负边的时候不能进行使用 | 单 |
Floyd算法 | O(n^3) | 有负环的时候不能进行使用 | 多 |
BellmanFord算法 | O(mn) | 可以用来检查是否有负环 | 单 |
题目一:关闭分部的可行集合数目
- 2959 关闭分部的可行集合数目 - 第119场双周赛 Q4
通过观察这个题目,首先我们看见他最多只会有10个节点,而且不存在负边,我们可以使用【Floyd】算法进行解题。
这个题目其实就是计算图中任意两个节点之间的路径,是否会大于maxDistance
,要是给定的这个图,任意两个节点之间的距离都小于题目所规定的maxDistance
就可以了,但是在这个基础上,还有一个条件,那就是,他会随机的关闭某个分部,那么关闭了这个分部就表示这个节点在这个图上已经是消失了,所以,我们可以枚举所有的图中可能存在的条件:
- 关闭1节点
- 关闭2节点
- …
然后通过枚举所有的情况,来判断每一种情况里面的任意两个节点之间的距离是否有大于maxDistance,要是没有,说明是正确答案中的其中一个部分。
技巧
在这里有一个枚举所有情况的一个技巧,就是采用二进制压缩数组
,那么什么是二进制压缩数组
。
举个例子:
我们图中有四个节点,那么,我们可以用四位二进制数来代替它,其中0表示这个分部是关闭的,1表示这个分部是开着的,举个例子,对于1010
来说,从右到左分别表示第0-3个节点是否是处于开着或者关着的状态。如果后面需要判断,我第3个节点是否是处于开着的状态,我可以直接用(1010 & (1<<3))是否大于0的这个情况,要是说大于0,就说明这个3号节点是开着的,要是小于0,说明是关闭的,我们就使用这种二进制压缩法来记录每个节点的状态。
所以我们一开始会枚举所有的情况,枚举所有情况的方式如下:
for (int i = 0; i < (1 << n); i++) {
if (check(i, n, maxDistance, roads)) {
res++;
}
}
这里可以看见有(1 << n)
,在n=4的情况下,(1 << n)其实这个二进制数字就是10000,对于这个for循环来说,他会从二进制0
开始一直枚举到二进制1111
,一共15种情况。
本体答案:
public int numberOfSets(int n, int maxDistance, int[][] roads) {
// 这个题是无向图,两个节点之间可能有多条边
// 用二进制数组来判断所有的情况
int res = 0;
for (int i = 0; i < (1 << n) ; i++) {
if (check(i, n, maxDistance, roads)) {
res++;
}
}
return res;
}
private boolean check(int u, int n, int maxDistance, int[][] roads) {
int[][] dis = floyd(roads, n, u);
for (int i = 0; i < n; i++) {
// 找个两个可用的点作为起点
if ((u & (1 << i)) == 0) { // 不可用
continue;
}
for (int j = 0; j < n; j++) {
if ((u & (1 << j)) > 0 && dis[i][j] > maxDistance) { // 不可用
return false;
}
}
}
return true;
}
/**
* @param edge 边
* @param n 点的个数
* @param enable 可用情况
* @return
*/
private int[][] floyd(int[][] edge, int n, int enable) {
int[][] res = new int[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(res[i], Integer.MAX_VALUE / 2);
}
for (int i = 0; i < n; i++) {
res[i][i] = 0;
}
for (int i = 0; i < edge.length; i++) {
if ((enable & (1 << edge[i][0]))==0 || (enable & (1 << edge[i][1]))==0) continue;
res[edge[i][0]][edge[i][1]] = Math.min(res[edge[i][0]][edge[i][1]], edge[i][2]);
res[edge[i][1]][edge[i][0]] = Math.min(res[edge[i][1]][edge[i][0]], edge[i][2]);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
res[j][k] = Math.min(res[j][k], res[j][i] + res[i][k]);
}
}
}
return res;
}
题目二:访问消失节点的最少时间
- 3112 访问消失节点的最少时间 - 第128场双周赛 Q3
可以看见这个题不存在负边,但是节点个数较多,所以这里考虑采用Dijkstra算法。使用了优先队列的形式
答案:
public int[] minimumTime(int n, int[][] edges, int[] disappear) {
List<int[]>[] adj = new List[n];
for (int i = 0; i < n; i++) {
adj[i] = new ArrayList<int[]>();
}
for (int[] edge : edges) {
int u = edge[0], v = edge[1], length = edge[2];
adj[u].add(new int[]{v, length});
adj[v].add(new int[]{u, length});
}
PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, 0});
// 先用迪杰斯特拉
boolean[] visited = new boolean[n];
Arrays.fill(visited, false);
// 这里是初始化dis数组
int[] dis = new int[n];
for (int i = 0; i < n; i++) {
dis[i] = Integer.MAX_VALUE/2;
}
dis[0] = 0;
while (!pq.isEmpty()) {
int[] poll = pq.poll();
int choseNodeId = poll[0];
if (visited[choseNodeId]) continue;;
visited[choseNodeId] = true;
List<int[]> adjs = adj[choseNodeId];
for (int[] ints : adjs) {
int adNodeId = ints[0];
int adTime = ints[1];
if (!visited[adNodeId]){
if (Math.min(dis[adNodeId], dis[choseNodeId]+adTime) >= disappear[adNodeId]) continue;
dis[adNodeId] = Math.min(dis[adNodeId], dis[choseNodeId]+adTime);
pq.offer(new int[]{adNodeId, dis[adNodeId]});
}
}
}
for (int i = 0; i < disappear.length; i++) {
if (disappear[i]<dis[i]) {
dis[i] = -1;
}
}
return dis;
}