issue 140 translation
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
pictures/floodfill/leetcode_en.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
@ -1,221 +0,0 @@
|
||||
# FloodFill算法详解及应用
|
||||
|
||||
啥是 FloodFill 算法呢,最直接的一个应用就是「颜色填充」,就是 Windows 绘画本中那个小油漆桶的标志,可以把一块被圈起来的区域全部染色。
|
||||
|
||||

|
||||
|
||||
这种算法思想还在许多其他地方有应用。比如说扫雷游戏,有时候你点一个方格,会一下子展开一片区域,这个展开过程,就是 FloodFill 算法实现的。
|
||||
|
||||

|
||||
|
||||
类似的,像消消乐这类游戏,相同方块积累到一定数量,就全部消除,也是 FloodFill 算法的功劳。
|
||||
|
||||

|
||||
|
||||
通过以上的几个例子,你应该对 FloodFill 算法有个概念了,现在我们要抽象问题,提取共同点。
|
||||
|
||||
### 一、构建框架
|
||||
|
||||
以上几个例子,都可以抽象成一个二维矩阵(图片其实就是像素点矩阵),然后从某个点开始向四周扩展,直到无法再扩展为止。
|
||||
|
||||
矩阵,可以抽象为一幅「图」,这就是一个图的遍历问题,也就类似一个 N 叉树遍历的问题。几行代码就能解决,直接上框架吧:
|
||||
|
||||
```java
|
||||
// (x, y) 为坐标位置
|
||||
void fill(int x, int y) {
|
||||
fill(x - 1, y); // 上
|
||||
fill(x + 1, y); // 下
|
||||
fill(x, y - 1); // 左
|
||||
fill(x, y + 1); // 右
|
||||
}
|
||||
```
|
||||
|
||||
这个框架可以解决所有在二维矩阵中遍历的问题,说得高端一点,这就叫深度优先搜索(Depth First Search,简称 DFS),说得简单一点,这就叫四叉树遍历框架。坐标 (x, y) 就是 root,四个方向就是 root 的四个子节点。
|
||||
|
||||
下面看一道 LeetCode 题目,其实就是让我们来实现一个「颜色填充」功能。
|
||||
|
||||

|
||||
|
||||
根据上篇文章,我们讲了「树」算法设计的一个总路线,今天就可以用到:
|
||||
|
||||
```java
|
||||
int[][] floodFill(int[][] image,
|
||||
int sr, int sc, int newColor) {
|
||||
|
||||
int origColor = image[sr][sc];
|
||||
fill(image, sr, sc, origColor, newColor);
|
||||
return image;
|
||||
}
|
||||
|
||||
void fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// 出界:超出边界索引
|
||||
if (!inArea(image, x, y)) return;
|
||||
// 碰壁:遇到其他颜色,超出 origColor 区域
|
||||
if (image[x][y] != origColor) return;
|
||||
image[x][y] = newColor;
|
||||
|
||||
fill(image, x, y + 1, origColor, newColor);
|
||||
fill(image, x, y - 1, origColor, newColor);
|
||||
fill(image, x - 1, y, origColor, newColor);
|
||||
fill(image, x + 1, y, origColor, newColor);
|
||||
}
|
||||
|
||||
boolean inArea(int[][] image, int x, int y) {
|
||||
return x >= 0 && x < image.length
|
||||
&& y >= 0 && y < image[0].length;
|
||||
}
|
||||
```
|
||||
|
||||
只要你能够理解这段代码,一定要给你鼓掌,给你 99 分,因为你对「框架思维」的掌控已经炉火纯青,此算法已经 cover 了 99% 的情况,仅有一个细节问题没有解决,就是当 origColor 和 newColor 相同时,会陷入无限递归。
|
||||
|
||||
### 二、研究细节
|
||||
|
||||
为什么会陷入无限递归呢,很好理解,因为每个坐标都要搜索上下左右,那么对于一个坐标,一定会被上下左右的坐标搜索。**被重复搜索时,必须保证递归函数能够能正确地退出,否则就会陷入死循环。**
|
||||
|
||||
为什么 newColor 和 origColor 不同时可以正常退出呢?把算法流程画个图理解一下:
|
||||
|
||||

|
||||
|
||||
可以看到,fill(1, 1) 被重复搜索了,我们用 fill(1, 1)* 表示这次重复搜索。fill(1, 1)* 执行时,(1, 1) 已经被换成了 newColor,所以 fill(1, 1)* 会在这个 if 语句被怼回去,正确退出了。
|
||||
|
||||
```java
|
||||
// 碰壁:遇到其他颜色,超出 origColor 区域
|
||||
if (image[x][y] != origColor) return;
|
||||
```
|
||||

|
||||
|
||||
但是,如果说 origColor 和 newColor 一样,这个 if 语句就无法让 fill(1, 1)* 正确退出,而是开启了下面的重复递归,形成了死循环。
|
||||
|
||||

|
||||
|
||||
### 三、处理细节
|
||||
|
||||
如何避免上述问题的发生,最容易想到的就是用一个和 image 一样大小的二维 bool 数组记录走过的地方,一旦发现重复立即 return。
|
||||
|
||||
```java
|
||||
// 出界:超出边界索引
|
||||
if (!inArea(image, x, y)) return;
|
||||
// 碰壁:遇到其他颜色,超出 origColor 区域
|
||||
if (image[x][y] != origColor) return;
|
||||
// 不走回头路
|
||||
if (visited[x][y]) return;
|
||||
visited[x][y] = true;
|
||||
image[x][y] = newColor;
|
||||
```
|
||||
|
||||
完全 OK,这也是处理「图」的一种常用手段。不过对于此题,不用开数组,我们有一种更好的方法,那就是回溯算法。
|
||||
|
||||
前文「回溯算法详解」讲过,这里不再赘述,直接套回溯算法框架:
|
||||
|
||||
```java
|
||||
void fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// 出界:超出数组边界
|
||||
if (!inArea(image, x, y)) return;
|
||||
// 碰壁:遇到其他颜色,超出 origColor 区域
|
||||
if (image[x][y] != origColor) return;
|
||||
// 已探索过的 origColor 区域
|
||||
if (image[x][y] == -1) return;
|
||||
|
||||
// choose:打标记,以免重复
|
||||
image[x][y] = -1;
|
||||
fill(image, x, y + 1, origColor, newColor);
|
||||
fill(image, x, y - 1, origColor, newColor);
|
||||
fill(image, x - 1, y, origColor, newColor);
|
||||
fill(image, x + 1, y, origColor, newColor);
|
||||
// unchoose:将标记替换为 newColor
|
||||
image[x][y] = newColor;
|
||||
}
|
||||
```
|
||||
|
||||
这种解决方法是最常用的,相当于使用一个特殊值 -1 代替 visited 数组的作用,达到不走回头路的效果。为什么是 -1,因为题目中说了颜色取值在 0 - 65535 之间,所以 -1 足够特殊,能和颜色区分开。
|
||||
|
||||
|
||||
### 四、拓展延伸:自动魔棒工具和扫雷
|
||||
|
||||
大部分图片编辑软件一定有「自动魔棒工具」这个功能:点击一个地方,帮你自动选中相近颜色的部分。如下图,我想选中老鹰,可以先用自动魔棒选中蓝天背景,然后反向选择,就选中了老鹰。我们来分析一下自动魔棒工具的原理。
|
||||
|
||||

|
||||
|
||||
显然,这个算法肯定是基于 FloodFill 算法的,但有两点不同:首先,背景色是蓝色,但不能保证都是相同的蓝色,毕竟是像素点,可能存在肉眼无法分辨的深浅差异,而我们希望能够忽略这种细微差异。第二,FloodFill 算法是「区域填充」,这里更像「边界填充」。
|
||||
|
||||
对于第一个问题,很好解决,可以设置一个阈值 threshold,在阈值范围内波动的颜色都视为 origColor:
|
||||
|
||||
```java
|
||||
if (Math.abs(image[x][y] - origColor) > threshold)
|
||||
return;
|
||||
```
|
||||
|
||||
对于第二个问题,我们首先明确问题:不要把区域内所有 origColor 的都染色,而是只给区域最外圈染色。然后,我们分析,如何才能仅给外围染色,即如何才能找到最外围坐标,最外围坐标有什么特点?
|
||||
|
||||

|
||||
|
||||
可以发现,区域边界上的坐标,至少有一个方向不是 origColor,而区域内部的坐标,四面都是 origColor,这就是解决问题的关键。保持框架不变,使用 visited 数组记录已搜索坐标,主要代码如下:
|
||||
|
||||
```java
|
||||
int fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// 出界:超出数组边界
|
||||
if (!inArea(image, x, y)) return 0;
|
||||
// 已探索过的 origColor 区域
|
||||
if (visited[x][y]) return 1;
|
||||
// 碰壁:遇到其他颜色,超出 origColor 区域
|
||||
if (image[x][y] != origColor) return 0;
|
||||
|
||||
visited[x][y] = true;
|
||||
|
||||
int surround =
|
||||
fill(image, x - 1, y, origColor, newColor)
|
||||
+ fill(image, x + 1, y, origColor, newColor)
|
||||
+ fill(image, x, y - 1, origColor, newColor)
|
||||
+ fill(image, x, y + 1, origColor, newColor);
|
||||
|
||||
if (surround < 4)
|
||||
image[x][y] = newColor;
|
||||
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
这样,区域内部的坐标探索四周后得到的 surround 是 4,而边界的坐标会遇到其他颜色,或超出边界索引,surround 会小于 4。如果你对这句话不理解,我们把逻辑框架抽象出来看:
|
||||
|
||||
```java
|
||||
int fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// 出界:超出数组边界
|
||||
if (!inArea(image, x, y)) return 0;
|
||||
// 已探索过的 origColor 区域
|
||||
if (visited[x][y]) return 1;
|
||||
// 碰壁:遇到其他颜色,超出 origColor 区域
|
||||
if (image[x][y] != origColor) return 0;
|
||||
// 未探索且属于 origColor 区域
|
||||
if (image[x][y] == origColor) {
|
||||
// ...
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这 4 个 if 判断涵盖了 (x, y) 的所有可能情况,surround 的值由四个递归函数相加得到,而每个递归函数的返回值就这四种情况的一种。借助这个逻辑框架,你一定能理解上面那句话了。
|
||||
|
||||
这样就实现了仅对 origColor 区域边界坐标染色的目的,等同于完成了魔棒工具选定区域边界的功能。
|
||||
|
||||
这个算法有两个细节问题,一是必须借助 visited 来记录已探索的坐标,而无法使用回溯算法;二是开头几个 if 顺序不可打乱。读者可以思考一下原因。
|
||||
|
||||
同理,思考扫雷游戏,应用 FloodFill 算法展开空白区域的同时,也需要计算并显示边界上雷的个数,如何实现的?其实也是相同的思路,遇到雷就返回 true,这样 surround 变量存储的就是雷的个数。当然,扫雷的 FloodFill 算法不能只检查上下左右,还得加上四个斜向。
|
||||
|
||||

|
||||
|
||||
以上详细讲解了 FloodFill 算法的框架设计,**二维矩阵中的搜索问题,都逃不出这个算法框架**。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:
|
||||
|
||||

|
217
think_like_computer/flood_fill.md
Normal file
@ -0,0 +1,217 @@
|
||||
# Analysis and Application of FloodFill Algorithm
|
||||
|
||||
**Translator: [youyun](https://github.com/youyun)**
|
||||
|
||||
**Author: [labuladong](https://github.com/labuladong)**
|
||||
|
||||
What is the FloodFill algorithm? A real-life example is color filling. In the default Windows application _Paint_, using the bucket icon, we can fill the selected area with a color.
|
||||
|
||||

|
||||
|
||||
There are other applications of the FloodFill algorithm. Another example would be Minesweeper. Sometimes when you click on a tile, an area will expand out. The process of expansion is implemented through the FloodFill algorithm.
|
||||
|
||||

|
||||
|
||||
Similarly, those puzzle-matching games such as Candy Crush also use the FloodFill algorithm to remove blocks of the same color.
|
||||
|
||||

|
||||
|
||||
Now you should have some idea about the FloodFill algorithm. Let's abstract out the problems and find out what is common.
|
||||
|
||||
### 1. Build Framework
|
||||
|
||||
All above examples can be abstract as a 2D array. In fact, a picture is an array of pixels. We take an element as the starting point and expand till the end.
|
||||
|
||||
An array can be further abstracted as a graph. Hence, the problem becomes about traversing a graph, similar to traversing an N-ary tree. A few lines of code are enough to resolve the problem. Here is the framework:
|
||||
|
||||
```java
|
||||
// (x, y) represents the coordinate
|
||||
void fill(int x, int y) {
|
||||
fill(x - 1, y); // up
|
||||
fill(x + 1, y); // down
|
||||
fill(x, y - 1); // left
|
||||
fill(x, y + 1); // right
|
||||
}
|
||||
```
|
||||
|
||||
Using this framework, we can resolve all problems about traversing a 2D array. The concept is also called Depth First Search (DFS), or quaternary (4-ary) tree traversal. The root node is coordinate (x, y). Its four child nodes are at root's four directions.
|
||||
|
||||
Let's take a look at [a LeetCode problem](https://leetcode.com/problems/flood-fill/). It's actually just a color fill function.
|
||||
|
||||

|
||||
|
||||
In [another article](), we discussed a generic design of tree related algorithms. We can apply the concept here:
|
||||
|
||||
```java
|
||||
int[][] floodFill(int[][] image,
|
||||
int sr, int sc, int newColor) {
|
||||
|
||||
int origColor = image[sr][sc];
|
||||
fill(image, sr, sc, origColor, newColor);
|
||||
return image;
|
||||
}
|
||||
|
||||
void fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// OUT: out of index
|
||||
if (!inArea(image, x, y)) return;
|
||||
// CLASH: meet other colors, beyond the area of origColor
|
||||
if (image[x][y] != origColor) return;
|
||||
image[x][y] = newColor;
|
||||
|
||||
fill(image, x, y + 1, origColor, newColor);
|
||||
fill(image, x, y - 1, origColor, newColor);
|
||||
fill(image, x - 1, y, origColor, newColor);
|
||||
fill(image, x + 1, y, origColor, newColor);
|
||||
}
|
||||
|
||||
boolean inArea(int[][] image, int x, int y) {
|
||||
return x >= 0 && x < image.length
|
||||
&& y >= 0 && y < image[0].length;
|
||||
}
|
||||
```
|
||||
|
||||
If you can understand this block of code, you are almost there! It means that you have honed the mindset of framework. This block of code can cover 99% of cases. There is only one tiny problem to be resolved: an infinite loop will happen if `origColor` is the same as `newColor`.
|
||||
|
||||
### 2. Pay Attention to Details
|
||||
|
||||
Why is there infinite loop? Each coordinate needs to go through its 4 neighbors. Consequently, each coordinate will also be traversed 4 times by its 4 neighbors. __When we visit an visited coordinate, we must guarantee to identify the situation and exit. If not, we'll go into infinite loop.__
|
||||
|
||||
Why can the code exit properly when `newColr` and `origColor` are different? Let's draw an diagram of the algorithm execution:
|
||||
|
||||

|
||||
|
||||
As we can see from the diagram, `fill(1, 1)` is visited twice. Let's use `fill(1, 1)*` to represent this duplicated visit. When `fill(1, 1)*` is executed, `(1, 1)` has already been replaced with `newColor`. So `fill(1, 1)*` will return the control directly at the _CLASH_, i.e. exit as expected.
|
||||
|
||||
```java
|
||||
// CLASH: meet other colors, beyond the area of origColor
|
||||
if (image[x][y] != origColor) return;
|
||||
```
|
||||

|
||||
|
||||
However, if `origColor` is the same as `newCOlor`, `fill(1, 1)*` will not exit at the _CLASH_. Instead, an infinite loop will start as shown below.
|
||||
|
||||

|
||||
|
||||
### 3. Handling Details
|
||||
|
||||
How to avoid the case of infinite loop? The most intuitive answer is to use a boolean 2D array of the same size as image, to record whether a coordinate has been traversed or not. If visited, return immediately.
|
||||
|
||||
```java
|
||||
// OUT: out of index
|
||||
if (!inArea(image, x, y)) return;
|
||||
// CLASH: meet other colors, beyond the area of origColor
|
||||
if (image[x][y] != origColor) return;
|
||||
// VISITED: don't visit a coordinate twice
|
||||
if (visited[x][y]) return;
|
||||
visited[x][y] = true;
|
||||
image[x][y] = newColor;
|
||||
```
|
||||
|
||||
This is a common technique to handle graph related problems. For this particular problem, there is actually a better way: backtracking algorithm.
|
||||
|
||||
Refer to the article [Backtracking Algorithm in Depth]() for details. We directly apply the backtracking algorithm framework here:
|
||||
|
||||
```java
|
||||
void fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// OUT: out of index
|
||||
if (!inArea(image, x, y)) return;
|
||||
// CLASH: meet other colors, beyond the area of origColor
|
||||
if (image[x][y] != origColor) return;
|
||||
// VISITED: visited origColor
|
||||
if (image[x][y] == -1) return;
|
||||
|
||||
// choose: mark a flag as visited
|
||||
image[x][y] = -1;
|
||||
fill(image, x, y + 1, origColor, newColor);
|
||||
fill(image, x, y - 1, origColor, newColor);
|
||||
fill(image, x - 1, y, origColor, newColor);
|
||||
fill(image, x + 1, y, origColor, newColor);
|
||||
// unchoose: replace the mark with newColor
|
||||
image[x][y] = newColor;
|
||||
}
|
||||
```
|
||||
|
||||
This is a typical way, using a special value -1 to replace the visited 2D array, to achieve the same purpose. Because the range of color is `[0, 65535]`, -1 is special enough to differentiate with actual colors.
|
||||
|
||||
### 4. Extension: Magic Wand Tool and Minesweeper
|
||||
|
||||
Most picture editing softwares have the function "Magic Wand Tool". When you click a point, the application will help you choose a region of similar colors automatically. Refer to the picture below, if we want to select the eagle, we can use the Magic Wand Tool to select the blue sky, and perform inverse selection. Let's analyze the mechanism of the Magic Wand Tool.
|
||||
|
||||

|
||||
|
||||
Obviously, the algorithm must be based on the FloodFill algorithm. However, there are two differences:
|
||||
1. Though the background color is blue, we can't guarantee all the blue pixels are exactly the same. There could be minor differences that can be told by our eyes. But we still want to ignore these minor differences.
|
||||
2. FloodFill is to fill regions. Magic Wand Tool is more about filling the edges.
|
||||
|
||||
It's easy to resolve the first problem by setting a `threshold`. All colors within the threshold from the `origColor` can be recognized as `origColor`.
|
||||
|
||||
```java
|
||||
if (Math.abs(image[x][y] - origColor) > threshold)
|
||||
return;
|
||||
```
|
||||
|
||||
As for the second problem, let's first define the problem clearly: _"do not color all `origColor` coordinates in the region; only care about the edges."_. Next, let's analyze how to only color edges. i.e. How to find out the coordinates at the edges? What special properties do coordinates at the edges hold?
|
||||
|
||||

|
||||
|
||||
From the diagram above, we can see that for all coordinates at the edges, there is at least one direction that is not `origColor`. For all inner coordinates, all 4 directions are `origColor`. This is the key to the solution. Using the same framework, using `visited` array to represent traversed coordinates:
|
||||
|
||||
```java
|
||||
int fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// OUT: out of index
|
||||
if (!inArea(image, x, y)) return 0;
|
||||
// VISITED: visited origColor
|
||||
if (visited[x][y]) return 1;
|
||||
// CLASH: meet other colors, beyond the area of origColor
|
||||
if (image[x][y] != origColor) return 0;
|
||||
|
||||
visited[x][y] = true;
|
||||
|
||||
int surround =
|
||||
fill(image, x - 1, y, origColor, newColor)
|
||||
+ fill(image, x + 1, y, origColor, newColor)
|
||||
+ fill(image, x, y - 1, origColor, newColor)
|
||||
+ fill(image, x, y + 1, origColor, newColor);
|
||||
|
||||
if (surround < 4)
|
||||
image[x][y] = newColor;
|
||||
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
In this way, all inner coordinates will have `surround` equal to 4 after traversing the four directions; all edge coordinates will be either OUT or CLASH, resulting `surround` less than 4. If you are still not clear, let's only look at the framework's logic flow:
|
||||
|
||||
```java
|
||||
int fill(int[][] image, int x, int y,
|
||||
int origColor, int newColor) {
|
||||
// OUT: out of index
|
||||
if (!inArea(image, x, y)) return 0;
|
||||
// VISITED: visited origColor
|
||||
if (visited[x][y]) return 1;
|
||||
// CLASH: meet other colors, beyond the area of origColor
|
||||
if (image[x][y] != origColor) return 0;
|
||||
// UNKNOWN: unvisited area that is origColor
|
||||
if (image[x][y] == origColor) {
|
||||
// ...
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These 4 `if`s cover all possible scenarios of (x, y). The value of `surround` is the sum of the return values of the 4 recursive functions. And each recursive function will fall into one of the 4 scenarios. You should be much clearer now after looking at this framework.
|
||||
|
||||
This implementation colors all edge coordinates only for the `origColor` region, which is what the Magic Wand TOol does.
|
||||
|
||||
Pay attention to 2 details in this algorithm:
|
||||
1. We must use `visited` to record traversed coordinates instead of backtracking algorithm.
|
||||
2. The order of the `if` clauses can't be modified. (Why?)
|
||||
|
||||
Similarly, for Minesweeper, when we use the FloodFill algorithm to expand empty areas, we also need to show the number of mines nearby. How to implement it? Following the same idea, return `true` when we meet mine. Thus, `surround` will store the number of mines nearby. Of course, in Minesweeper, there are 8 directions instead of 4, including diagonals.
|
||||
|
||||

|
||||
|
||||
We've discussed the design and framework of the FloodFill algorithm. __All searching problems in a 2D array can be fit into this framework.__
|