今天的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; } }