Python Flood Fill 算法
困难2用Python实现Flood Fill:像魔法油漆桶一样填充相连区域
你有没有在画图软件里点过“油漆桶”工具?一点下去,整个同颜色的区域就“唰”一下变成了新颜色。这个超好用的功能,背后就用到了Flood Fill(洪水填充)算法。今天我们就用Python把它写出来,像变魔术一样给格子板涂色!
什么是Flood Fill?——从一滴水到一片海洋
想象你有一个方格本,每个格子要么是白色(0),要么是黄色(1)。现在你用手指按住一个黄色格子,然后说“变蓝!”,那么所有和这个格子“手拉手”连在一起的黄色格子都会变成蓝色。这里的关键是“连在一起”——只有上下左右相邻(不能斜着)的黄色格子才算。Flood Fill就是从这个起点出发,像水漫金山一样向四周蔓延,把所有能到达的同颜色格子都染上新颜色。
生活中还有哪里用到它?比如:
- 地图涂色:给地图上某个国家的所有区域填充同一种颜色。
- 游戏关卡:很多消除游戏里,点击一块相同颜色的宝石,周围相同颜色的宝石就会一起消失(也是Flood Fill)。
- 图像处理:抠图时选中某个区域,自动标记出连在一起的像素。
第一步:用数字表示我们的格子板
我们用一个二维列表(列表里套列表)来表示格子板。每个数字代表一种颜色:
0= 白色1= 黄色2= 蓝色
举个简单的例子,这是一个 3 行 4 列的格子板:
# 格子板:白色(0),黄色(1)
grid = [
[0, 1, 0, 0], # 第0行
[1, 1, 1, 0], # 第1行
[0, 1, 0, 0], # 第2行
[0, 0, 0, 0], # 第3行
]
从坐标 (1,1)(第1行第1列,最左上角是第0行第0列)的黄色格子出发,想把这片黄色区域全部变成蓝色。
第二步:递归——“水”是怎么漫开的?
Flood Fill最直观的做法是递归,就像一个人站在起点,然后模仿水流:
- 先看看自己有没有越界(跑去格子外面了?),如果越界就停止。
- 检查自己是不是我们要找的颜色(黄色),如果不是(比如已经是蓝色或白色),就不处理。
- 把自己染成新颜色(蓝色)。
- 分别向上、下、左、右四个方向派去“分身”,让它们重复做同样的事。
这个过程也叫深度优先搜索(DFS)——一条路走到黑,再回头走另一条。就像用一根水管,先往一个方向拼命冲,直到碰到墙再拐回来。
第三步:写代码——完整的Flood Fill函数
下面是一个完整的Python实现,每个变量都加了中文注释,方便理解:
def flood_fill(grid, x, y, new_color):
"""
从格子板 grid 的 (x, y) 位置开始,
把所有与 (x, y) 颜色相同且连通的格子都变成 new_color。
grid: 二维列表,表示格子板
x, y: 起始行列(行号x,列号y)
new_color: 要填充的新颜色数字
"""
old_color = grid[x][y] # 记录起始格子的旧颜色
# 如果新颜色和旧颜色一样,就不用改了,否则会无限循环!
if old_color == new_color:
return
# 定义内部的递归函数fill
def fill(x, y):
# 检查是否越界(行号或列号超出范围)
if x < 0 or x >= len(grid) or y < 0 or y >= len(grid[0]):
return
# 检查当前格子颜色是否等于旧颜色(不是要找的颜色就跳过)
if grid[x][y] != old_color:
return
# 染色:把当前格子变成新颜色
grid[x][y] = new_color
# 向四个方向递归
fill(x-1, y) # 上
fill(x+1, y) # 下
fill(x, y-1) # 左
fill(x, y+1) # 右
# 从起点开始填充
fill(x, y)
# ========== 测试代码 ==========
# 定义格子板(白色=0,黄色=1)
game_board = [
[0, 1, 0, 0], # 第0行
[1, 1, 1, 0], # 第1行
[0, 1, 0, 0], # 第2行
[0, 0, 0, 0], # 第3行
]
print("填充前的格子板:")
for row in game_board:
print(row)
# 从 (1,1) 开始填充,新颜色为2(蓝色)
flood_fill(game_board, 1, 1, 2)
print("\n填充后的格子板(黄色→蓝色):")
for row in game_board:
print(row)
运行结果:
填充前的格子板:
[0, 1, 0, 0]
[1, 1, 1, 0]
[0, 1, 0, 0]
[0, 0, 0, 0]
填充后的格子板(黄色→蓝色):
[0, 2, 0, 0]
[2, 2, 2, 0]
[0, 2, 0, 0]
[0, 0, 0, 0]
所有连在一起的黄色(1)都变成了蓝色(2)!那些白色格子因为颜色不匹配,没有被“感染”。
常见错误与避坑指南
新手写Flood Fill时,容易踩下面几个坑:
错误1:忘记检查旧颜色和新颜色是否相同
如果你从黄色格子开始,偏偏新颜色也是黄色,那么old_color == new_color就会成立,如果不提前return,递归会反复染同一个格子,导致无限循环,程序崩溃。所以第一件事就要判断并退出。
错误2:忘记检查越界
递归时如果x或y变成负数或者超过格子板大小,就会报IndexError。一定要在函数开头先检查坐标是否合法。
错误3:把斜对角也算成连通
Flood Fill只考虑上下左右四个方向,不包括左上、右上、左下、右下。如果你不小心把八个方向都写了,就会把不是同色的对角格子也染上,结果就不是你想要的区域了。
错误4:递归太深导致栈溢出
如果格子板非常大(比如1000×1000),递归层数可能超过Python默认的递归深度(约1000层),程序会报RecursionError。这时候就需要改用**栈(Stack)来模拟递归,或者用队列(Queue)**实现广度优先搜索(BFS)。下面就来介绍用栈实现的方法。
进阶:用栈(非递归)实现Flood Fill——告别栈溢出
既然递归容易爆炸,我们可以用自己管理的“栈”来模拟递归的过程。思路还是一样的:从起点出发,把待处理的坐标放进栈里,然后循环处理,直到栈为空。
def flood_fill_stack(grid, x, y, new_color):
"""
用栈实现Flood Fill,避免递归深度限制
"""
old_color = grid[x][y] # 旧颜色
if old_color == new_color:
return
rows = len(grid) # 格子板行数
cols = len(grid[0]) # 格子板列数
stack = [(x, y)] # 栈:存放待处理的坐标,初始放入起点
while stack: # 只要栈不为空,就继续处理
cur_x, cur_y = stack.pop() # 取出一个坐标(后进先出)
# 检查越界和颜色是否匹配
if cur_x < 0 or cur_x >= rows or cur_y < 0 or cur_y >= cols:
continue
if grid[cur_x][cur_y] != old_color:
continue
# 染色
grid[cur_x][cur_y] = new_color
# 把上下左右四个邻居加入栈
stack.append((cur_x - 1, cur_y)) # 上
stack.append((cur_x + 1, cur_y)) # 下
stack.append((cur_x, cur_y - 1)) # 左
stack.append((cur_x, cur_y + 1)) # 右
这个方法用栈代替了递归调用,原理一模一样,但不会受递归深度限制。另外,如果我们把栈换成队列(用collections.deque),就变成了广度优先搜索(BFS),会一层一层地向外扩散,就像是同心圆一样展开。BFS的代码也很相似,只是把pop()换成popleft()(左端出队)。想挑战的同学可以自己试试。
生活中的扩展:用Flood Fill做“填色游戏”
你可以把Flood Fill和画图结合起来,做一个简单的“填色小游戏”。比如:
- 先用数字数组画一幅黑白线稿(0=白色,1=黑色边框)。
- 允许用户输入点击坐标,然后用Flood Fill把白色区域染成喜欢的颜色。
- 连续点不同区域,就能创作一幅彩色画啦!
如果想做更高级的,还可以添加“容差”(比如相近颜色都算),或者结合鼠标事件实时交互。
相关知识点指引
- 深度优先搜索(DFS):Flood Fill的递归实现就是DFS的一种应用。学会了DFS,你还能解决迷宫寻路、图的连通分量等更多问题。
- 广度优先搜索(BFS):用队列实现的Flood Fill就是BFS,它常用于求最短路径,比如计算从起点到某个点的最少步数。
- 栈和队列:理解这两种数据结构后,你可以轻松写出非递归版本的算法。
- 递归与递归深度:知道了递归的优缺点,以后遇到类似“分形、树形结构”的问题时,你会更好选择是用递归还是用迭代。
Flood Fill是图遍历算法中最简单、最可爱的例子。下次当你打开画图软件点下油漆桶时,就可以自豪地说:“我知道它是怎么工作的!”
例题精讲
若使用递归方式实现Flood Fill算法,在处理大尺寸图像时最可能遇到什么问题?
在Flood Fill算法中,必须执行哪一步才能避免死循环(重复访问同一像素)?
Flood Fill算法的时间复杂度总是与整个图像的像素总数成正比。
以下是用队列实现的Flood Fill函数,请填写缺少的条件,使其正确运行:
def flood_fill(image, sr, sc, new_color):
old_color = image[sr][sc]
if old_color == new_color:
return image
rows, cols = len(image), len(image[0])
from collections import deque
q = deque()
q.append((sr, sc))
while q:
r, c = q.popleft()
if 0 <= r < rows and 0 <= c < cols and image[r][c] == ___:
image[r][c] = new_color
for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
q.append((r+dr, c+dc))
return image以下是用递归实现的Flood Fill函数,请填写缺少的条件,使其正确运行:
def flood_fill(image, x, y, new_color, old_color):
rows, cols = len(image), len(image[0])
if x < 0 or x >= rows or y < 0 or y >= cols or image[x][y] != ___:
return
image[x][y] = new_color
flood_fill(image, x+1, y, new_color, old_color)
flood_fill(image, x-1, y, new_color, old_color)
flood_fill(image, x, y+1, new_color, old_color)
flood_fill(image, x, y-1, new_color, old_color)