今天的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 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
第一种方法是我最先想到的,利用回溯的思想枚举出所有的可能,然后选出最小的。
这种方法可行,但是会超时,也不是我这次记录的重点,就不放代码了。
我们可以将这个问题转化成一个「判定性」问题,即:
是否存在一条从左上角到右下角的路径,其经过的所有边权的最大值不超过 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;     } }