leetcode-1631

今天的leetcode每日一题是一道中等难度的题目,但是题解的各种思路确实是很有新意,在这里记录一下。

每次遇到这种很有想象力的题解,都会让我感到非常快乐,它们只是使用了一些你早已熟练的方法,却能用的非常巧妙。

题目

https://leetcode-cn.com/problems/path-with-minimum-effort/

你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。

一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。

请你返回从左上角走到右下角的最小 体力消耗值 。

题解

方法一:回溯

第一种方法是我最先想到的,利用回溯的思想枚举出所有的可能,然后选出最小的。

这种方法可行,但是会超时,也不是我这次记录的重点,就不放代码了。

方法二:二分法+BFS

我们可以将这个问题转化成一个「判定性」问题,即:

是否存在一条从左上角到右下角的路径,其经过的所有边权的最大值不超过 xx

这个判定性问题解决起来并不复杂,我们只要从左上角开始进行深度优先搜索或者广度优先搜索,在搜索的过程中只允许经过边权不超过 xx 的边,搜索结束后判断是否能到达右下角即可。

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
var minimumEffortPath = function(heights) {
const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];

const m = heights.length, n = heights[0].length;
let left = 0, right = 999999, ans = 0;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const queue = [[0, 0]];
const seen = new Array(m * n).fill(0);
seen[0] = 1;
while (queue.length) {
const [x, y] = queue.shift();
for (let i = 0; i < 4; i++) {
const nx = x + dirs[i][0];
const ny = y + dirs[i][1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !seen[nx * n + ny] && Math.abs(heights[x][y] - heights[nx][ny]) <= mid) {
queue.push([nx, ny]);
seen[nx * n + ny] = 1;
}
}
}
if (seen[m * n - 1]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
};

这段代码核心思路:

  • 通过二分法不断取得mid值
  • 从(0,0)开始对图进行广度优先遍历,但不同于普通的BFS,如果父子之间的权重超过mid,那么就视为父子之间的关系被切断
  • 根据上一步的遍历结果看一下是否能够遍历到右下角的节点,如果可以, 说明当我们的体力消耗为mid时有一条让我们走到右下角的路,则继续二分

并查集

并查集的介绍可以看这里:https://sunra.top/posts/c2448c65/

我们将这 mn 个节点放入并查集中,实时维护它们的连通性。

由于我们需要找到从左上角到右下角的最短路径,因此我们可以将图中的所有边按照权值从小到大进行排序,并依次加入并查集中。当我们加入一条权值为 xx 的边之后,如果左上角和右下角从非连通状态变为连通状态,那么 xx 即为答案。

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
var minimumEffortPath = function(heights) {
const m = heights.length;
const n = heights[0].length;
const edges = [];
for (let i = 0; i < m; ++i) {
for (let j = 0; j < n; ++j) {
const id = i * n + j;
if (i > 0) {
edges.push([id - n, id, Math.abs(heights[i][j] - heights[i - 1][j])]);
}
if (j > 0) {
edges.push([id - 1, id, Math.abs(heights[i][j] - heights[i][j - 1])]);
}
}
}
edges.sort((a, b) => a[2] - b[2]);

const uf = new UnionFind(m * n);
let ans = 0;
for (const edge of edges) {
const x = edge[0], y = edge[1], v = edge[2];
uf.unite(x, y);
if (uf.connected(0, m * n - 1)) {
ans = v;
break;
}
}
return ans;
};

// 并查集模板
class UnionFind {
constructor (n) {
this.parent = new Array(n).fill(0).map((element, index) => index);
this.size = new Array(n).fill(1);
// 当前连通分量数目
this.setCount = n;
}

findset (x) {
if (this.parent[x] === x) {
return x;
}
this.parent[x] = this.findset(this.parent[x]);
return this.parent[x];
}

unite (a, b) {
let x = this.findset(a), y = this.findset(b);
if (x === y) {
return false;
}
if (this.size[x] < this.size[y]) {
[x, y] = [y, x];
}
this.parent[y] = x;
this.size[x] += this.size[y];
this.setCount -= 1;
return true;
}

connected (a, b) {
const x = this.findset(a), y = this.findset(b);
return x === y;
}
}