赞
踩
最小阶梯的远足活动
你参加了一次远足活动,并且有一张地图。
地图是一个矩阵,
height[i][j]
表示格子(i, j)
的高度。你有一个习惯,那就是在整段旅途中你不想走落差较大的阶梯,也就是说,一整条路径耗费的体力值是旅途中高度差绝对值的最大值决定的。
请你返回从左上角走到右下角的最小体力消耗值。
注:1 <= rows, columns <= 100,1 <= heights[i][j] <= 10^6
方法一——并查集
这是个很巧妙的「连通」思想。首先将点的矩阵转化为边的集合(这一步可以当作模板记住),然后将这些边按所谓高度差绝对值由小到大排序(排序很关键);这些点看作一个个孤立的点(并查集初始化),然后用这些已排序的边一条条连通它们(并查集union操作),当左上角与右下角连通时即找到了最终答案。
class Solution { public int minimumEffortPath(int[][] heights) { int R = heights.length; // 几行 int C = heights[0].length; // 几列 int LEN = R * C; // 将点矩阵转换为边的集合,边的格式为(点1,点2,边的权值) List<int[]> edges = new ArrayList<>(); for(int i = 0; i < R; i++) { for(int j = 0; j < C; j++) { int id = i * C + j; if(i > 0) { edges.add(new int[]{id, id - C, Math.abs(heights[i][j] - heights[i - 1][j])}); } if(j > 0) { edges.add(new int[]{id, id - 1, Math.abs(heights[i][j] - heights[i][j - 1])}); } } } // 按权重由小到大进行排序 Collections.sort(edges, (Comparator.comparingInt(o -> o[2]))); // 一个一个边(已排序)连接上,即并查集合并操作,当左上角与右下角连通时即找到了整个所谓"最小值" UnionSet unionSet = new UnionSet(LEN); for (int[] edge : edges) { unionSet.union(edge[0], edge[1]); if(unionSet.isUnion(0, LEN - 1)) { return edge[2]; } } return 0; } } /** * 并查集模板 */ class UnionSet { int[] roots; public UnionSet(int len) { roots = new int[len]; for(int i = 0; i < len; i++) { roots[i] = i; } } public int findRoot(int node) { if(roots[node] == node) { return node; } roots[node] = findRoot(roots[node]); return roots[node]; } public void union(int node1, int node2) { roots[findRoot(node1)] = findRoot(node2); } public boolean isUnion(int node1, int node2) { return findRoot(node1) == findRoot(node2); } }
>>> 时间复杂度:点阵转化为边集是"O(mn)",边的排序是"O(mnlog(mn))",并查集是"O(mnα(mn))"其中α函数是阿克曼函数的反函数,其渐进意义小于log。综上,时间复杂度是"O(mnlog(mn))"
>>> 空间复杂度:显然是"O(mn)"
方法二——二分法+BFS/DFS
我们已知这个高度差最大值的最小值(0)和最大值(999999),二分法呼之欲出。每次取的mid值作为高度差最大值(即本次图的遍历的限制条件),看看能否从左上角到达右下角(DFS/BFS)。并以此为依据进行二分。
class Solution { public int minimumEffortPath(int[][] heights) { int R = heights.length; // 几行 int C = heights[0].length; // 几列 int LEN = R * C; int left = 0; int right = 999999; int res = 0; int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; while (left <= right) { int mid = (left + right) / 2; // 每次取到一次mid后,开始BFS >_< Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{0, 0}); boolean[] visited = new boolean[LEN]; visited[0] = true; while (!queue.isEmpty()) { int[] point = queue.poll(); int row = point[0]; int col = point[1]; for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; int newId = newRow * C + newCol; if(newRow >= 0 && newRow < R && newCol >= 0 && newCol < C && !visited[newId] && Math.abs(heights[newRow][newCol] - heights[row][col]) <= mid) { queue.offer(new int[]{newRow, newCol}); visited[newId] = true; } } } // 二分 if(visited[LEN - 1]) { res = mid; right = mid - 1; } else { left = mid + 1; } } return res; } }
class Solution { public int minimumEffortPath(int[][] heights) { int R = heights.length; // 几行 int C = heights[0].length; // 几列 int LEN = R * C; int left = 0; int right = 999999; int res = 0; while (left <= right) { int mid = (left + right) / 2; // 每次取到一次mid后,开始BFS >_< boolean[] visited = new boolean[LEN]; visited[0] = true; // 二分 if(DFS(0, 0, heights, R, C, mid, visited)) { res = mid; right = mid - 1; } else { left = mid + 1; } } return res; } private int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; private boolean DFS(int row, int col, int[][] heights, int R, int C, int mid, boolean[] visited) { for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; int newId = newRow * C + newCol; if(newRow >= 0 && newRow < R && newCol >= 0 && newCol < C && !visited[newId] && Math.abs(heights[newRow][newCol] - heights[row][col]) <= mid) { visited[newId] = true; DFS(newRow, newCol, heights, R, C, mid, visited); } } return visited[R * C - 1]; } }
>>> 时间复杂度:"O(mnlogC)",其中C为二分区间的上界值10^6
>>> 空间复杂度:"O(mn)"
方法三——Dijkstra
Dijkstra本质是一种启发式搜索算法。归根结底,本题还是一个最短路径问题,只是整条路径的权值不再是各边权值之和,而是各边权值的最大值。另外,本题也没有负数权值的边,完全符合Dijkstra的适用范围。
class Solution { public int minimumEffortPath(int[][] heights) { int R = heights.length; int C = heights[0].length; int LEN = R * C; int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; int[] dist = new int[LEN]; Arrays.fill(dist, Integer.MAX_VALUE); dist[0] = 0; boolean[] visited = new boolean[LEN]; PriorityQueue<int[]> queue = new PriorityQueue<>(Comparator.comparingInt(o -> o[2])); queue.offer(new int[]{0, 0, 0}); while (!queue.isEmpty()) { // 找最短的边 int[] edge = queue.poll(); int row = edge[0]; int col = edge[1]; int id = row * C + col; if(visited[id]) { continue; } if(id == LEN - 1) { return dist[LEN - 1]; } visited[id] = true; // 更新 for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; int newId = newRow * C + newCol; if(newRow >= 0 && newRow < R && newCol >= 0 && newCol < C && Math.max(edge[2], Math.abs(heights[newRow][newCol] - heights[row][col])) < dist[newId]) { dist[newId] = Math.max(edge[2], Math.abs(heights[newRow][newCol] - heights[row][col])); queue.offer(new int[]{newRow, newCol, dist[newId]}); } } } return dist[LEN - 1]; } }
时间复杂度:Dijkstra算法仅与图的边数有关,本题近似看成边数为"mn",则复杂度为"O(mnlogmn)"
空间复杂度:"O(mn)"
水位上升的泳池
在一个 N x N 的坐标方格 grid 中,每一个方格的值
grid[i][j]
表示在位置(i, j)
的平台高度。
现在开始下雨了。当时间为 t 时,此时雨水导致水池中任意位置的水位为 t 。你只能在被水淹没的地方游泳。
你从坐标方格的左上平台 (0,0) 出发。最少耗时多久你才能到达坐标方格的右下平台 (N-1, N-1)?
注:2 <= N <= 50,grid[i][j] 是 [0, …, N×N - 1] 的排列。
方法一——并查集
假如此时的时间为t,则能知道水刚刚淹没的位置p,p向四周比它低的平台进行连通。时间t递增,当到达某个时间时,左上角与右下角连通,此时的t即为正确答案。
class Solution { public int swimInWater(int[][] grid) { int R = grid.length; int C = grid[0].length; int LEN = R * C; // 实现时间到位置的直接映射(知道时间t,立即知道此时水刚刚淹没的位置) int[] positions = new int[LEN]; for(int i = 0; i < R; i++) { for(int j = 0; j < C; j++) { positions[grid[i][j]] = i * C + j; } } // 递增t,进行连通 UnionSet unionSet = new UnionSet(LEN); int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; for(int t = 0; t < LEN; t++) { int position = positions[t]; int row = position / C; int col = position % C; for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; int newPosition = newRow * C + newCol; if(newRow >= 0 && newRow < R && newCol >= 0 && newCol < C && grid[newRow][newCol] < grid[row][col]) { unionSet.union(position, newPosition); } } if(unionSet.isUnion(0, LEN - 1)) { return t; } } return 0; } } /* * UnionSet模板略 */
>>> 时间复杂度:"O(n2logn2)"
>>> 空间复杂度:"O(n2)"
方法二——二分法+BFS/DFS
我们已知正确答案区间最大值的最小值(0)和最大值(N * N - 1),二分法呼之欲出。每次取的mid值作为限制条件(小于等于mid的平台可游,大于mid的平台不能游),看看能否从左上角到达右下角(DFS/BFS)。并以此为依据进行二分。
class Solution { public int swimInWater(int[][] grid) { int R = grid.length; int C = grid[0].length; int LEN = R * C; int left = 0; int right = LEN - 1; int res = 0; int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; while (left <= right) { int mid = (left + right) / 2; // 每次取一个mid的值,开始BFS if(mid < grid[0][0]) { left = mid + 1; continue; } boolean[][] visited = new boolean[R][C]; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{0, 0}); visited[0][0] = true; while (!queue.isEmpty()) { int[] point = queue.poll(); int row = point[0]; int col = point[1]; for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; if(newRow >= 0 && newRow < R && newCol >= 0 && newCol < C && !visited[newRow][newCol] && grid[newRow][newCol] <= mid) { queue.offer(new int[]{newRow, newCol}); visited[newRow][newCol] = true; } } } // 二分 if(visited[R - 1][C - 1]) { res = mid; right = mid - 1; } else { left = mid + 1; } } return res; } }
class Solution { public int swimInWater(int[][] grid) { int N = grid.length; int left = 0; int right = N * N - 1; int res = 0; while (left <= right) { int mid = (left + right) / 2; // 每次取一个mid值,开始DFS if(mid < grid[0][0]) { left = mid + 1; continue; } boolean[][] visited = new boolean[N][N]; visited[0][0] = true; // 二分 if(DFS(0, 0, grid, visited, mid)) { res = mid; right = mid - 1; } else { left = mid + 1; } } return res; } private int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; private boolean DFS(int row, int col, int[][] grid, boolean[][] visited, int mid) { int N = grid.length; for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; if(newRow >= 0 && newRow < N && newCol >= 0 && newCol < N && !visited[newRow][newCol] && grid[newRow][newCol] <= mid) { visited[newRow][newCol] = true; DFS(newRow, newCol, grid, visited, mid); } } return visited[N - 1][N - 1]; } }
>>> 时间复杂度:"O(n2logn2)"
>>> 空间复杂度:"O(n2)"
方法三——Dijkstra
Dijkstra本质是一种启发式搜索算法。归根结底,本题还是一个最短路径问题,只是整条路径的权值不再是各边权值之和,而是经过的点的最大值(边的全值,置为其所指向的点的值)。另外,本题也没有负数权值的边,完全符合Dijkstra的适用范围。
class Solution { public int swimInWater(int[][] grid) { int N = grid.length; int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; boolean[][] visited = new boolean[N][N]; int[][] dist = new int[N][N]; for(int[] arr : dist) { Arrays.fill(arr, N * N); } dist[0][0] = grid[0][0]; Queue<int[]> queue = new PriorityQueue<>(Comparator.comparingInt(o -> grid[o[0]][o[1]])); queue.offer(new int[]{0, 0}); while (!queue.isEmpty()) { // 找最短的边 int[] point = queue.poll(); int row = point[0]; int col = point[1]; if(visited[row][col]) { continue; } if(row == N - 1 && col == N - 1) { return dist[row][col]; } visited[row][col] = true; // 更新 for(int[] direction : directions) { int newRow = row + direction[0]; int newCol = col + direction[1]; if(newRow >= 0 && newRow < N && newCol >= 0 && newCol < N && Math.max(dist[row][col], grid[newRow][newCol]) < dist[newRow][newCol]) { dist[newRow][newCol] = Math.max(dist[row][col], grid[newRow][newCol]); queue.offer(new int[]{newRow, newCol}); } } } return dist[N - 1][N - 1]; } }
>>> 时间复杂度:"O(n2logn2)"
>>> 空间复杂度:"O(n2)"
int[i][j]
和int[id](其中id=i*C+j)
都可以唯一表示其位置。虽然表面是一维和二维,但其实空间复杂度都是
O
(
m
n
)
O(mn)
O(mn)
E N D END END
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。