[LeetCode 130] - 围绕区域(Surrounded Regions)
问题
给出一个包含'X'和'O'的2D板,捕获所有'X'围绕的区域。
一个区域被捕获是指翻转被围绕区域中的所有'O'为'X'。
例如,
X X X X
X O O X
X X O X
X O X X
在运行你的函数后,板子将变为:
X X X X
X X X X
X X X X
X O X X
初始思路
判断一个O点是否被X围绕,其实就是看从该点出发有没有一条路径能走到二维数组的区域外。在程序语言中,向上下左右移动分别为:[i-1, j], [i+1, j], [i, j-1], [i, j+1]。而找路径的步骤就是从O点出发向附近的O点不断移动的过程。要实现“不断移动”,一种方案是使用递归调用。由于极端情况下四个方向都能移动,在矩阵较大的时候可以想象递归调用栈的层数将会很多。而另一种比较直观的方案就是不断重复扫描二维数组,做出移动的动作,直到没有点可以移动为止。这种方案看起来比较直观,但是还需要解决几个问题:
1. 如果在移动过程中有走到边界的情况,即表示改点没有被围绕。要怎么要判断边界点?
可以通过i-1 < 0, i + 1 >= board.size(), j - 1 < 0, j + 1 >= board[0].size()这四个条件,只要某点坐标符合其中一个条件,它就是在边界上的点。
2. 由于四个方向移动,如何判断已走过的点?
由于我们只向O点移动,可以在移动到某点后将该点的值修改,如改为'P'。每次扫描都以P点作为移动的起点。最后根据是否找到路径的情况将P改为X或O。
3. 如何判断没有点可以移动?
很简单,我们可以在每次扫描前将一个标志设为false。在该次扫描中只要有过移动,将其置为true。扫描完后根据该变量值决定是否继续即可。
4. 如何找P点?
一次二重循环肯定是不够的,因为二重循环是不断地向右下移动。而P点可以向左或向上移动。这里我们先采用一个最简单的方案,不断地从最左上角即[0,0]开始找P,直到没有P点可以移动。这样一定可以保证不会有漏掉的P点。但可以看到将会有一个三重循环。
好了,看起来解决方案已经有了,让我们模拟一下:
遍历至[1,1],发现O点,将其置为P。开始扫描
[1,1]发现P [1,2]发现P [2,2]发现P 没有路径,将P替换成X
只能向右 只能向下 无路可走,没有路径,返回
X X X X X X X X X X X X X X X X X X X X
X P O X X P P X X P P X X P P X X X X X
X X O X X X O X X X P X X X P X X X X X
X O X X X O X X X O X X X O X X X O X X
遍历至[3,1],发现O点,将其置为P。开始扫描
[3, 1]发现P 有路径,将P替换回O
无路可走,有路径
X X X X X X X X X X X X
X X X X X X X X X X X X
X X X X X X X X X X X X
X P X X X P X X X O X X
看来的确能得出例子中的答案。好了,那么开始写代码吧:
1 class Solution 2 { 3 public: 4 void solve(std::vector<std::vector<char>> &board) 5 { 6 for(int i = 0; i < board.size(); ++i) 7 { 8 for(int j = 0; j < board[i].size(); ++j) 9 { 10 if(board[i][j] == 'O') 11 { 12 if(!FindPath(board, i, j)) 13 { 14 ReplaceBack(board, SURROUNDED); 15 } 16 else 17 { 18 ReplaceBack(board, NOT_SURROUNDED); 19 } 20 } 21 } 22 } 23 } 24 private: 25 enum ReplaceMethod 26 { 27 SURROUNDED, 28 NOT_SURROUNDED 29 }; 30 31 void ReplaceBack(std::vector<std::vector<char>> &board, ReplaceMethod replaceMethod) 32 { 33 for(int i = 0; i < board.size(); ++i) 34 { 35 for(int j = 0; j < board[i].size(); ++j) 36 { 37 if(board[i][j] == 'P') 38 { 39 if(replaceMethod == SURROUNDED) 40 { 41 board[i][j] = 'X'; 42 } 43 else 44 { 45 board[i][j] = 'O'; 46 } 47 } 48 } 49 } 50 } 51 52 bool FindPath(std::vector<std::vector<char>> &board, int X, int Y) 53 { 54 board[X][Y] = 'P'; 55 56 bool hasPath = false; 57 bool canMove = false; 58 59 while(true) 60 { 61 canMove = false; 62 for(int i = 0; i < board.size(); ++i) 63 { 64 for(int j = 0; j < board[i].size(); ++j) 65 { 66 if(board[i][j] == 'P') 67 { 68 if(i - 1 < 0) 69 { 70 hasPath = true; 71 } 72 else 73 { 74 if(board[i-1][j] == 'O') 75 { 76 board[i-1][j] = 'P'; 77 canMove = true; 78 79 } 80 } 81 82 if(j - 1 < 0) 83 { 84 hasPath = true; 85 } 86 else 87 { 88 if(board[i][j - 1] == 'O') 89 { 90 board[i][j - 1] = 'P'; 91 canMove = true; 92 93 94 } 95 } 96 97 if(i + 1 >= board.size()) 98 { 99 hasPath = true; 100 } 101 else 102 { 103 if(board[i+1][j] == 'O') 104 { 105 board[i+1][j] = 'P'; 106 canMove = true; 107 } 108 } 109 110 if(j + 1 >= board[0].size()) 111 { 112 hasPath = true; 113 } 114 else 115 { 116 if(board[i][j + 1] == 'O') 117 { 118 board[i][j + 1] = 'P'; 119 canMove = true; 120 } 121 } 122 } 123 } 124 } 125 if(!canMove) 126 { 127 return hasPath; 128 } 129 } 130 return hasPath; 131 } 132 133 };
提交后Judge Small顺利通过。然后…… Judge Large果然失败了,处理一个大概250*250的输入时超时。
优化
存在这么多n重循环,失败还是意料之中的。分析代码可以发现,循环最多的地方就在发现O点之后调用的FindPath函数里,对这个函数的调用和函数自己的实现肯定是优化的地方。
先来看对它的调用。每次FindPath返回后,如果找到了路径,我们会将所有P点替换回O点。然而后面再发现这些已经被验证有路径的O点时,又会进入FindPath做一遍查找。为了避免这些不必要的查找,可以改为不将P点换回O点,而是换为另一个标志,如'D'。这样做以后,在最后要多做一次对二维数组的遍历来将D点替换回O点。但是在有路径而且路径很长时这1次遍历相比至少重复O点个数次遍历还是划算的。
再来看FindPath的实现。为了实现简便,我们会不断的对二维数组从头开始遍历直到没有P点可以移动。由于这是一个三重循环,效率可想而知。前面提到过因为遍历二维数组下标是不断增加的,所以遍历只向右下移动。为了处理P点向左或向上移动的情况我们才采取了这种方案。那我们是不是可以通过修改下标的方式来实现:
如果向上移动,我们期望从本次横坐标-1的横坐标开始继续遍历。由于对横坐标的遍历在外层循环中,我们还需要终止内层循环:
1 if(board[i-1][j] == 'O') 2 { 3 board[i-1][j] = 'P'; 4 5 //注意因为for循环本身会对i进行递增操作,需要减2才能达到 6 //下次循环i从当前i-1开始的效果 7 i -= 2; 8 break; 9 }
注意因为for循环本身会对i进行递增操作,需要减2才能达到下次循环i从当前i-1开始的效果。
如果向左移动,我们期望从本次纵坐标-1的纵坐标开始继续遍历。由于对纵坐标的遍历就在当前内层循环中,直接重新执行循环即可:
1 if(board[i][j - 1] == 'O') 2 { 3 board[i][j - 1] = 'P'; 4 5 j -= 2; 6 continue; 7 }
好了,经过这两个改动,循环的次数应该大大减少了。把所有改动集成进来:
1 class Solution 2 { 3 public: 4 void solve(std::vector<std::vector<char>> &board) 5 { 6 7 for(int i = 0; i < board.size(); ++i) 8 { 9 for(int j = 0; j < board[i].size(); ++j) 10 { 11 if(board[i][j] == 'O') 12 { 13 if(!FindPath(board, i, j)) 14 { 15 ReplaceBack(board, SURROUNDED); 16 } 17 else 18 { 19 ReplaceBack(board, NOT_SURROUNDED); 20 } 21 } 22 } 23 } 24 25 26 for(int i = 0; i < board.size(); ++i) 27 { 28 for(int j = 0; j < board[i].size(); ++j) 29 { 30 if(board[i][j] == 'D') 31 { 32 board[i][j] = 'O'; 33 } 34 } 35 } 36 37 38 } 39 private: 40 enum ReplaceMethod 41 { 42 SURROUNDED, 43 NOT_SURROUNDED 44 }; 45 46 void ReplaceBack(std::vector<std::vector<char>> &board, ReplaceMethod replaceMethod) 47 { 48 for(int i = 0; i < board.size(); ++i) 49 { 50 for(int j = 0; j < board[i].size(); ++j) 51 { 52 if(board[i][j] == 'P') 53 { 54 if(replaceMethod == SURROUNDED) 55 { 56 board[i][j] = 'X'; 57 } 58 else 59 { 60 board[i][j] = 'D'; 61 } 62 } 63 } 64 } 65 } 66 67 bool FindPath(std::vector<std::vector<char>> &board, int X, int Y) 68 { 69 board[X][Y] = 'P'; 70 71 bool hasPath = false; 72 73 for(int i = 0; i < board.size(); ++i) 74 { 75 for(int j = 0; j < board[i].size(); ++j) 76 { 77 if(board[i][j] == 'P') 78 { 79 if(i - 1 < 0) 80 { 81 hasPath = true; 82 } 83 else 84 { 85 if(board[i-1][j] == 'O') 86 { 87 board[i-1][j] = 'P'; 88 89 i -= 2; 90 break; 91 } 92 } 93 94 if(j - 1 < 0) 95 { 96 hasPath = true; 97 } 98 else 99 { 100 if(board[i][j - 1] == 'O') 101 { 102 board[i][j - 1] = 'P'; 103 104 j -= 2; 105 continue; 106 } 107 } 108 109 if(i + 1 >= board.size()) 110 { 111 hasPath = true; 112 } 113 else 114 { 115 if(board[i+1][j] == 'O') 116 { 117 board[i+1][j] = 'P'; 118 } 119 } 120 121 if(j + 1 >= board[0].size()) 122 { 123 hasPath = true; 124 } 125 else 126 { 127 if(board[i][j + 1] == 'O') 128 { 129 board[i][j + 1] = 'P'; 130 } 131 } 132 } 133 } 134 } 135 return hasPath; 136 } 137 };
提交再试:
顺利通过!