733、难度简单:本题和200、岛屿数量类似
题意:题目会传给我们 1 个二维数组(作为图画)、sr 和 sc 作为该二维数组某元素(初始坐标)的坐标、newColor作为要替换的值。
观察示例1:二维数组如下(阶段一),初始坐标为(1,1)也就是二维数组中第二行、第二列的值为 1 的元素。
然后根据题目要求,将该元素替换为 2,同时将该元素的上下左右四个方向的与该元素同值的元素也都替换为 2,变为阶段二。
同时阶段二中所有值为 2 的元素其四个方向元素只要值为 1 就也替换为 2。一直到无法再扩散出相同值的四周后。
/*阶段一 阶段二 阶段三
[1、1、1] [1、2、1] [2、2、2]
[1、1、0] -> [2、2、0] -> [2、2、0]
[1、0、1] [1、0、1] [2、0、1]
*/
方法一:原创:暴力解法?深度搜索:
原理:从初始坐标开始向其四周递归扩散,判断四周元素值是否相同,若相同那么继续递归扩散。
深度优先搜索的体现:
先对原点的正下方开始扩散,当向下到不能再向下了就回到原点,开始对原点的正上方操作,向上到极限又回到原点,开始向左…。
也就是一个方向到最深再开始另一个方向。
// 以下代码是最初的思路,存在栈溢出的错误,修正可通过版本在后面。
class Solution {
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
// 调用递归方法
return dfs(image,sr,sc,newColor,image[sr][sc]);
}
// 返回一个二维数组
public int[][] dfs(int[][] image, int sr, int sc, int newColor, int target){
int hang = image.length;
int lie = image[0].length;
// 若正下方元素坐标没有超出二维数组的范围
if(sr+1 < hang ){
// 正下方元素值和target相同,说明可以继续向正下方元素的四周递归扩散
if(image[sr+1][sc] == target){
image[sr+1][sc] = newColor;
// 将递归结果重新赋给image,此时正下方以及正下方的扩展区域都已经修改完了。
image = dfs(image,sr+1,sc,newColor,target);
}
}
// 用正下方修改完的image继续正上方的开拓
if(sr-1 >= 0){
if(image[sr-1][sc] == target){
image[sr-1][sc] = newColor;
image = dfs(image,sr-1,sc,newColor,target);
}
}
// 用正上、下方都开拓完的image开拓正右方的区域
if(sc+1 < lie){
if(image[sr][sc+1] == target){
image[sr][sc+1] = newColor;
image = dfs(image,sr,sc+1,newColor,target);
}
}
// 用正上、下、右方都开拓完的image开拓正左方的区域
if(sc-1 >= 0){
if(image[sr][sc-1] == target){
image[sr][sc-1] = newColor;
image = dfs(image,sr,sc-1,newColor,target);
}
}
return image;
}
}
注意点:如果只是单纯的递归遍历初始坐标的四周以及相同值的四周,可能会出现情况 target 和 newColor 相同的情况如
/* 初始坐标target为坐标(1,1)的元素值为 1,替换值也为 1
[0,0,0]
[0,1,1]
*/
这时,在将(1,1)点替换为 1 后,开始向其四周扩散,当遍历到(1,1+1)时由于该坐标元素值与target相同,所以会再遍历其四周,也就是会回到(1,1),而遍历到(1,1)又会遍历到(1,1+1)这就形成了死循环。会导致栈溢出(StackOverflowError)报错。
所以我们需要记录哪些坐标被遍历到了,当得知被遍历到了就将不再往其四周递归。
不能采用哈希表的key对应行value对应列来保存哪些坐标元素被遍历过了:因为一个行可以对应多个列,一个列也可以对应多个行。
解决措施:我们只需在调用递归方法前加上判断条件:if(target == newColor){ return image; }即可:
因为当 target 和 newColor 相同时我们就不需要递归扩散了:递归扩散是为了把和 target 值相同的四周元素全替换为 newColor,而二者相当说明就不用递归扩散也达到值替换的目的了(这点需要仔细想)
// 修正可通过版本
class Solution {
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int target = image[sr][sc];
// 若二者相同说明不用递归扩散了
if(target == newColor){
return image;
}
image[sr][sc] = newColor;
// 调用递归
return dfs(image,sr,sc,newColor,target);
}
// 该方法与前一个版本相同
public int[][] dfs(int[][] image, int sr, int sc, int newColor, int target){
int hang = image.length;
int lie = image[0].length;
if(sr+1 < hang ){
if(image[sr+1][sc] == target){
image[sr+1][sc] = newColor;
image = dfs(image,sr+1,sc,newColor,target);
}
}
if(sr-1 >= 0){
if(image[sr-1][sc] == target){
image[sr-1][sc] = newColor;
image = dfs(image,sr-1,sc,newColor,target);
}
}
if(sc+1 < lie){
if(image[sr][sc+1] == target){
image[sr][sc+1] = newColor;
image = dfs(image,sr,sc+1,newColor,target);
}
}
if(sc-1 >= 0){
if(image[sr][sc-1] == target){
image[sr][sc-1] = newColor;
image = dfs(image,sr,sc-1,newColor,target);
}
}
return image;
}
}
方法二:广度优先搜索:时空复杂度O(n*m)
原理:本题要求将给定的二维数组中指定的「色块」染成另一种颜色。「色块」的定义是:直接或间接相邻的同色方格构成的整体。
可以发现,「色块」就是被不同颜色的方格包围的一个同色岛屿。我们从色块中任意一个地方开始,利用广度优先搜索或深度优先搜索即可遍历整个岛屿。
注意:当目标颜色和初始颜色相同时,我们无需对原数组进行修改。
我们从给定的起点开始,进行广度优先搜索。每次搜索到一个方格时,如果其与初始位置的方格颜色相同,就将该方格加入队列,并将该方格的颜色更新,以防止重复入队。
注意:因为初始位置的颜色会被修改,所以我们需要保存初始位置的颜色,以便于之后的更新操作。
class Solution {
// 由于递归扩散是向四周扩散的,假设初始坐标为(x,y)
// 四周递归分为(x+1,y+0)、(x+0,y+1)...坐标变化正好和dx、dy的对应项相符合
// 用这种写法可以省去方法一中大量的 if 判断语句
int[] dx = {1, 0, 0, -1};
int[] dy = {0, 1, -1, 0};
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int currColor = image[sr][sc];
// 当目标颜色和初始颜色相同时,我们无需对原数组进行修改
if (currColor == newColor) {
return image;
}
int n = image.length, m = image[0].length;
// 用于存储要进行操作的元素坐标,比如一个坐标引发的四周四个坐标
Queue<int[]> queue = new LinkedList<int[]>();
// 先纳入初始坐标
queue.offer(new int[]{sr, sc});
// 更新值防止当前元素再次被纳入队列中
image[sr][sc] = newColor;
while (!queue.isEmpty()) {
// 取出队列里符合扩散条件的坐标,开始向四周遍历更替值
int[] cell = queue.poll();
// 队列里元素的行列下标
int x = cell[0], y = cell[1];
// 遍历当前元素的四周,也就是四种可能性
for (int i = 0; i < 4; i++) {
// 四周元素坐标
int mx = x + dx[i], my = y + dy[i];
// 在不溢出二维数组范围情况下且符合被更替条件
if (mx >= 0 && mx < n && my >= 0 && my < m && image[mx][my] == currColor) {
queue.offer(new int[]{mx, my});
// 更新值防止当前元素再次被纳入队列中
image[mx][my] = newColor;
}
}
}
return image;
}
}
方法二:深度优先搜索:
原理:我们从给定的起点开始,进行深度优先搜索。每次搜索到一个方格时,如果其与初始位置的方格颜色相同,就将该方格的颜色更新,以防止重复搜索;如果不相同,则进行回溯。
class Solution {
int[] dx = {1, 0, 0, -1};
int[] dy = {0, 1, -1, 0};
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int currColor = image[sr][sc];
if (currColor != newColor) {
// 将image传入dfs方法中,dfs无return但直接对传入的image进行修改操作
dfs(image, sr, sc, currColor, newColor);
}
return image;
}
public void dfs(int[][] image, int x, int y, int color, int newColor) {
if (image[x][y] == color) {
image[x][y] = newColor;
for (int i = 0; i < 4; i++) {
int mx = x + dx[i], my = y + dy[i];
if (mx >= 0 && mx < image.length && my >= 0 && my < image[0].length) {
dfs(image, mx, my, color, newColor);
}
}
}
}
}
542、难度中等:
方法一:广度优先搜索:时空复杂度:O(rc) r 为矩阵行数,c 为矩阵列数
思路:直接看下方链接,描述很详细。如果没看懂链接里的可以看这里我添加的注释和代码解析
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/01-matrix/solution/01ju-zhen-by-leetcode-solution/
class Solution {
// 假设当前坐标为(x,y)那么该二维数组里的元素代表该坐标上下左右四个位置的坐标
// (x-1,y+0)、(x+1,y+0)、(x+0,y-1)、(x+0,y+1)
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int[][] updateMatrix(int[][] matrix) {
// 题传二维数组的行列长度
int m = matrix.length, n = matrix[0].length;
// 结果数组
int[][] dist = new int[m][n];
// 一个二维数组,每个元素都是bool值。用来存储被遍历到的元素坐标
boolean[][] seen = new boolean[m][n];
// 用于存储所有值为 0 元素的坐标(在二维数组中的行列),该队列的元素为一维数组
Queue<int[]> queue = new LinkedList<int[]>();
// 将所有的 0 添加进初始队列中
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j] == 0) {
// 给队列添加一个元素(一维数组),数组里有两元素,行和列
queue.offer(new int[]{i, j});
seen[i][j] = true;
}
}
}
// 广度优先搜索
while (!queue.isEmpty()) {
// 取出队列 0 元素的坐标
int[] cell = queue.poll();
// 取出行下表和列下表
int i = cell[0], j = cell[1];
// 从 0 元素其四周延伸
for (int d = 0; d < 4; ++d) {
int ni = i + dirs[d][0];
int nj = j + dirs[d][1];
// (ni,nj)没有溢出,并且之前没有被遍历到过
if (ni >= 0 && ni < m && nj >= 0 && nj < n && !seen[ni][nj]) {
// 这里看下面的代码解析
dist[ni][nj] = dist[i][j] + 1;
queue.offer(new int[]{ni, nj});
seen[ni][nj] = true;
}
}
}
return dist;
}
}
代码解析:
1、while循环、for循环的运转原理:
开始时队列queue里只有 0 元素,因为我们向 0 元素四周扩散,然后进入到 for 循环中,由于当前元素是 0 元素的四周元素,所以和 0 的最短距离为 dist[i] [j] + 1 = 0 + 1 = 1;然后我们将当前元素纳入队列中,并将当前元素的坐标纳入已被遍历元素中让其不再被遍历到。
为什么如此确信这么一次遍历就能得到当前元素距离 0 的最短距离:因为我们首先将所有 0 坐标记录了下来让其不被遍历到,并且开始时从 0 向其四周遍历,这样最初的四周元素必为最短距离,以这些元素的最短距离为基础继续向四周蔓延,就能依次得到所有元素的最短距离。
2、由于队列是先进先出,所以所有的 0 元素会最先被选出作为根基元素向四周遍历,在得到所有 0 元素四周一圈元素的最短距离后,我们就可以把这些一圈元素当做新的 0 元素:因为它们自身已经具备最短距离,而且它们将 0 包围,除它们以外的元素想要到达 0 元素必须先经过它们才行。
方法二:动态规划:时间复杂度O(RC) 空间复杂度O(1)
思路:看下方链接,描述很详细,看完方法二的描述直接看方法三的描述和代码。
如果没看懂链接里的可以看这里我添加的代码解析
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/01-matrix/solution/01ju-zhen-by-leetcode-solution/
class Solution {
static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int[][] updateMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
// 初始化动态规划的数组,所有的距离值都设置为一个很大的数
int[][] dist = new int[m][n];
for (int i = 0; i < m; ++i) {
Arrays.fill(dist[i], Integer.MAX_VALUE / 2);
}
// 如果 (i, j) 的元素为 0,那么距离为 0
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j] == 0) {
dist[i][j] = 0;
}
}
}
// 只有 水平向左移动 和 竖直向上移动,注意动态规划的计算顺序
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (i - 1 >= 0) {
dist[i][j] = Math.min(dist[i][j], dist[i - 1][j] + 1);
}
if (j - 1 >= 0) {
dist[i][j] = Math.min(dist[i][j], dist[i][j - 1] + 1);
}
}
}
// 只有 水平向右移动 和 竖直向下移动,注意动态规划的计算顺序
for (int i = m - 1; i >= 0; --i) {
for (int j = n - 1; j >= 0; --j) {
if (i + 1 < m) {
dist[i][j] = Math.min(dist[i][j], dist[i + 1][j] + 1);
}
if (j + 1 < n) {
dist[i][j] = Math.min(dist[i][j], dist[i][j + 1] + 1);
}
}
}
return dist;
}
}
代码解析:假设二维数组如下(阶段一):
[1,1,1,1,1,1] [1,1,1,1,1,1]
[1,1,1,1,1,1] -> [1,1,1,1,1,1] -> 最终结果
[1,1,1,0,1,1] -> [1,1,1,0,1,2] ->
[1,1,1,1,1,1] [1,1,1,1,2,3]
开始时的结果二维数组只有那个 0 被记录了下来,其余位置元素值均为 Integer.MAX_VALUE / 2
然后水平向左移动 和 竖直向上移动(从坐标(0,0)元素,也就是第一个元素开始遍历)让以 0 元素为左上角点的矩形区域内全部得到了最短距离,原理看代码就能理解。
随后水平向右移动 和 竖直向下移动(从坐标(3,4)元素,也就是最后一个元素开始遍历)这样就得到了以矩形区域左边往左和上边往上所有区域、二者夹角区域所有元素的最短距离。