#c

泛洪算法专题

从基础到竞赛的泛洪填充技巧

#c
@鞠大龙
魅客科创中心

CONTENTS

目录

1. 泛洪算法基础知识

1.1 泛洪算法的定义和特点

  • 泛洪算法(Flood Fill)的定义
    • 泛洪算法是一种用于确定连通区域的算法
    • 从一个起始点开始,向四周扩散,直到无法继续扩散为止
    • 类似于水从一个点开始向四周扩散的过程
    • 常用于图像处理、游戏开发和图论问题
  • 泛洪算法的特点
    • 连通性:只填充与起始点连通的区域
    • 边界限制:遇到边界或不符合条件的点停止扩散
    • 完整性:保证访问所有符合条件的连通点
    • 确定性:对于相同的输入,总是产生相同的结果
  • 泛洪算法的基本思想
    • 从起始点开始,检查其相邻点
    • 如果相邻点满足特定条件,则将其标记为已访问
    • 继续从新标记的点扩散,直到没有新的点可以标记
  • 泛洪算法的应用场景
    • 图像处理中的区域填充
    • 游戏中的区域探索和迷宫生成
    • 连通分量的计算
    • 图像分割和边界提取
返回目录 信息学 泛洪算法专题 魅客科创中心

1.2 泛洪算法的基本原理

泛洪算法的核心步骤

  1. 选择一个起始点
  2. 检查该点是否满足填充条件
  3. 如果满足,则标记该点为已访问
  4. 递归或迭代地处理相邻点
  5. 重复步骤2-4,直到没有新的点可以访问

泛洪过程示例
初始网格:

0 0 0 0 0
0 1 1 1 0
0 1 1 1 0
0 1 1 1 0
0 0 0 0 0
从(2,2)开始泛洪填充为2:
0 0 0 0 0
0 1 1 1 0
0 1 2 1 0
0 1 1 1 0
0 0 0 0 0
继续扩散:
0 0 0 0 0
0 1 2 1 0
0 2 2 2 0
0 1 2 1 0
0 0 0 0 0
最终结果:
0 0 0 0 0
0 1 2 1 0
0 2 2 2 0
0 1 2 1 0
0 0 0 0 0
返回目录 信息学 泛洪算法专题 魅客科创中心

1.3 泛洪算法的实现方法

泛洪算法的伪代码

function floodFill(grid, x, y, oldColor, newColor):
    if x < 0 or x >= grid.width or y < 0 or y >= grid.height:
        return  // 超出边界
    
    if grid[x][y] != oldColor:
        return  // 不是目标颜色
    
    grid[x][y] = newColor  // 填充新颜色
    
    // 递归处理四个方向
    floodFill(grid, x+1, y, oldColor, newColor)  // 右
    floodFill(grid, x-1, y, oldColor, newColor)  // 左
    floodFill(grid, x, y+1, oldColor, newColor)  // 下
    floodFill(grid, x, y-1, oldColor, newColor)  // 上

泛洪算法的关键点

  • 起始条件:选择合适的起始点
  • 扩散条件:定义何时继续扩散
  • 终止条件:确定何时停止扩散
  • 访问标记:避免重复访问同一点
  • 边界检查:防止越界访问

泛洪算法的变种

  • 4-连通泛洪:只考虑上下左右四个方向
  • 8-连通泛洪:考虑包括对角线在内的八个方向
  • 条件泛洪:根据特定条件决定是否扩散
  • 多源泛洪:从多个起始点同时开始扩散
返回目录 信息学 泛洪算法专题 魅客科创中心

1.4 与DFS的关系

泛洪算法与DFS的关系

  • 泛洪算法可以使用DFS实现
  • DFS实现的泛洪算法特点:
    • 深度优先,尽可能深入探索
    • 使用递归或栈实现
    • 空间复杂度较低(递归调用栈)
    • 不保证按距离顺序访问

DFS实现泛洪的代码框架

void floodFillDFS(vector<vector<int>>& grid, 
                 int x, int y, 
                 int oldColor, int newColor) {
    // 边界检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size()) {
        return;
    }
    
    // 检查是否是目标颜色
    if (grid[x][y] != oldColor) {
        return;
    }
    
    // 填充新颜色
    grid[x][y] = newColor;
    
    // 递归处理四个方向
    floodFillDFS(grid, x+1, y, oldColor, newColor);
    floodFillDFS(grid, x-1, y, oldColor, newColor);
    floodFillDFS(grid, x, y+1, oldColor, newColor);
    floodFillDFS(grid, x, y-1, oldColor, newColor);
}
返回目录 信息学 泛洪算法专题 魅客科创中心

1.5 与BFS的关系

泛洪算法与BFS的关系

  • 泛洪算法也可以使用BFS实现
  • BFS实现的泛洪算法特点:
    • 按层次扩展,先处理近的点
    • 使用队列实现
    • 空间复杂度较高(需要存储队列)
    • 按距离顺序访问点

BFS实现泛洪的代码框架

void floodFillBFS(vector<vector<int>>& grid, 
                 int x, int y, 
                 int oldColor, int newColor) {
    // 边界检查和初始颜色检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size() || 
        grid[x][y] != oldColor) {
        return;
    }
    
    // 方向数组
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    queue<pair<int, int>> q;
    q.push({x, y});
    grid[x][y] = newColor;  // 标记起始点
    
    while (!q.empty()) {
        auto [cx, cy] = q.front();
        q.pop();
        
        // 检查四个方向
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            // 边界检查和颜色检查
            if (nx >= 0 && nx < grid.size() && 
                ny >= 0 && ny < grid[0].size() && 
                grid[nx][ny] == oldColor) {
                grid[nx][ny] = newColor;
                q.push({nx, ny});
            }
        }
    }
}
返回目录 信息学 泛洪算法专题 魅客科创中心

1.6 DFS vs BFS

DFS vs BFS实现泛洪的比较

特性 DFS实现 BFS实现
实现方式 递归/栈 队列
扩散顺序 深度优先 层次扩展
空间复杂度
适用场景 路径查找 最短路径
特点 代码简洁 按距离扩展
返回目录 信息学 泛洪算法专题 魅客科创中心

1.7 时间和空间复杂度分析

  • 时间复杂度分析
    • 基本时间复杂度:,其中是需要填充的点的数量
    • 在二维网格中:分别是行数和列数
    • 最坏情况:需要访问所有点,复杂度为
    • 最好情况:只需访问少量点,复杂度为
  • 空间复杂度分析
    • DFS实现

      • 递归调用栈:,最坏情况下需要递归访问所有点
      • 实际上通常为,因为递归深度受网格大小限制
    • BFS实现

      • 队列空间:,最坏情况下队列中存储一层的所有点
      • 访问标记数组:
返回目录 信息学 泛洪算法专题 魅客科创中心

1.8 影响泛洪算法性能的因素

  • 影响泛洪算法性能的因素
    1. 网格大小
      • 网格越大,需要处理的点越多
      • 大型网格可能导致栈溢出
    2. 连通性
      • 高连通性会导致更多点被访问
      • 分散的小连通区域可能更高效
    3. 实现方式
      • 递归实现可能导致栈溢出
      • 迭代实现需要额外的数据结构(栈或队列)
    4. 边界条件
      • 复杂的边界条件会增加检查开销
      • 简单的边界条件可以优化性能
  • 常见应用场景的复杂度
    1. 图像填充
      • 时间:是图像的宽和高
      • 空间:,需要标记已访问的像素
    2. 连通区域标记
      • 时间:是区域内的点数
      • 空间:,最坏情况下需要存储所有点
    3. 迷宫探索
      • 时间:是迷宫的行数和列数
      • 空间:,需要标记已访问的格子
返回目录 信息学 泛洪算法专题 魅客科创中心

2. 实现方法

2.1 基础代码模板

递归实现(DFS)

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    // 边界检查
    if (x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size()) {
        return;
    }
    
    // 检查当前点是否符合填充条件
    if (grid[x][y] != oldColor) {
        return;
    }
    
    // 填充当前点
    grid[x][y] = newColor;
    
    // 递归处理四个方向的相邻点
    floodFill(grid, x+1, y, oldColor, newColor); // 右
    floodFill(grid, x-1, y, oldColor, newColor); // 左
    floodFill(grid, x, y+1, oldColor, newColor); // 下
    floodFill(grid, x, y-1, oldColor, newColor); // 上
}

迭代实现(BFS)

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    // 边界检查和初始颜色检查
    if (x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size() || 
        grid[x][y] != oldColor) {
        return;
    }
    
    // 方向数组
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    // 使用队列进行BFS
    queue<pair<int, int>> q;
    q.push({x, y});
    grid[x][y] = newColor;  // 标记起始点
    
    while (!q.empty()) {
        auto [cx, cy] = q.front();
        q.pop();
        
        // 检查四个方向
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            // 边界检查和颜色检查
            if (nx >= 0 && nx < grid.size() && 
                ny >= 0 && ny < grid[0].size() && 
                grid[nx][ny] == oldColor) {
                grid[nx][ny] = newColor;
                q.push({nx, ny});
            }
        }
    }
}

常用优化技巧

  • 使用访问标记数组避免重复访问
  • 预先检查起始点的颜色
  • 使用方向数组简化代码
  • 使用栈代替递归避免栈溢出
返回目录 信息学 泛洪算法专题 魅客科创中心

2.2 方向数组的使用

4方向移动

// 上、右、下、左四个方向
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};

// 使用方向数组
for (int i = 0; i < 4; i++) {
    int nx = x + dx[i];
    int ny = y + dy[i];
    // 处理新位置(nx, ny)
}

8方向移动

// 8个方向(包括对角线)
int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1};
int dy[] = {0, 1, 1, 1, 0, -1, -1, -1};

// 使用方向数组
for (int i = 0; i < 8; i++) {
    int nx = x + dx[i];
    int ny = y + dy[i];
    // 处理新位置(nx, ny)
}

自定义方向

// 国际象棋中马的移动方向
int dx[] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[] = {1, 2, 2, 1, -1, -2, -2, -1};

// 使用方向数组
for (int i = 0; i < 8; i++) {
    int nx = x + dx[i];
    int ny = y + dy[i];
    // 处理新位置(nx, ny)
}

方向数组的优势

  • 代码更加简洁
  • 避免重复编写相似的代码
  • 易于扩展和修改移动方向
  • 可以根据需要自定义不同的移动模式
  • 结合循环使用,提高代码可读性

注意事项

  • 确保方向数组的长度与使用时的循环范围一致
  • 在使用新位置前进行边界检查
  • 根据问题特点选择合适的方向集合
返回目录 信息学 泛洪算法专题 魅客科创中心

3. 典型应用场景

3.1 图像填充

MS Paint中的油漆桶工具

  • 油漆桶工具是泛洪算法的典型应用
  • 用户点击图像中的一个像素
  • 算法找出所有与该像素颜色相同且相连的像素
  • 将这些像素全部填充为新的颜色

实现原理

void paintBucketTool(Image& img, int x, int y, Color newColor) {
    Color oldColor = img.getPixel(x, y);
    if (oldColor == newColor) return; // 颜色相同,无需填充
    
    queue<pair<int, int>> q;
    q.push({x, y});
    img.setPixel(x, y, newColor);
    
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    while (!q.empty()) {
        auto [cx, cy] = q.front();
        q.pop();
        
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            if (nx >= 0 && nx < img.width && 
                ny >= 0 && ny < img.height && 
                img.getPixel(nx, ny) == oldColor) {
                img.setPixel(nx, ny, newColor);
                q.push({nx, ny});
            }
        }
    }
}

游戏地图探索

  • 在游戏中用于发现可探索区域
  • 玩家进入新区域时,使用泛洪算法计算可见区域
  • 用于迷雾战争(Fog of War)的视野计算
  • 地图生成和区域划分

实现示例

void exploreMap(GameMap& map, int x, int y, int visibility) {
    queue<tuple<int, int, int>> q; // 坐标和剩余可见距离
    q.push({x, y, visibility});
    map.setVisible(x, y);
    
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    while (!q.empty()) {
        auto [cx, cy, dist] = q.front();
        q.pop();
        
        if (dist <= 0) continue; // 可见距离用尽
        
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            if (nx >= 0 && nx < map.width && 
                ny >= 0 && ny < map.height && 
                !map.isWall(nx, ny) && 
                !map.isVisible(nx, ny)) {
                map.setVisible(nx, ny);
                q.push({nx, ny, dist - 1});
            }
        }
    }
}

应用特点

  • 通常使用BFS实现,以获得更自然的扩散效果
  • 可能需要考虑障碍物和视线阻挡
  • 在大型地图中可能需要优化算法效率
返回目录 信息学 泛洪算法专题 魅客科创中心

3.2 连通区域标记

岛屿计数

  • 在二维网格中计算连通区域的数量
  • 典型问题:计算地图中岛屿的数量
  • 每个岛屿是由相邻的陆地单元格组成的区域
  • 使用泛洪算法标记每个岛屿

实现示例

int countIslands(vector<vector<int>>& grid) {
    if (grid.empty()) return 0;
    
    int rows = grid.size();
    int cols = grid[0].size();
    int count = 0;
    
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (grid[i][j] == 1) { // 陆地
                count++; // 发现新岛屿
                // 使用泛洪算法将整个岛屿标记为已访问
                floodFill(grid, i, j, 1, 0);
            }
        }
    }
    
    return count;
}

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int land, int visited) {
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size() || 
        grid[x][y] != land) {
        return;
    }
    
    grid[x][y] = visited; // 标记为已访问
    
    // 递归处理四个方向
    floodFill(grid, x+1, y, land, visited);
    floodFill(grid, x-1, y, land, visited);
    floodFill(grid, x, y+1, land, visited);
    floodFill(grid, x, y-1, land, visited);
}

区域分割

  • 将图像或网格分割成不同的连通区域
  • 每个区域具有相似的特性(如颜色、纹理)
  • 用于图像分析、计算机视觉和模式识别
  • 可以为每个区域分配唯一标识符

实现示例

void segmentRegions(vector<vector<int>>& image) {
    int rows = image.size();
    int cols = image[0].size();
    int regionId = 2; // 从2开始标记区域(假设0和1有特殊含义)
    
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (image[i][j] == 1) { // 未标记的区域
                // 使用泛洪算法标记整个区域
                labelRegion(image, i, j, 1, regionId);
                regionId++; // 下一个区域使用新的标识符
            }
        }
    }
}

void labelRegion(vector<vector<int>>& image, int x, int y, 
                int oldLabel, int newLabel) {
    if (x < 0 || x >= image.size() || 
        y < 0 || y >= image[0].size() || 
        image[x][y] != oldLabel) {
        return;
    }
    
    image[x][y] = newLabel; // 标记为新区域
    
    // 递归处理四个方向
    labelRegion(image, x+1, y, oldLabel, newLabel);
    labelRegion(image, x-1, y, oldLabel, newLabel);
    labelRegion(image, x, y+1, oldLabel, newLabel);
    labelRegion(image, x, y-1, oldLabel, newLabel);
}

应用特点

  • 可以结合其他算法进行区域特征提取
  • 在图像处理中常与边缘检测结合使用
  • 可以用于对象识别和场景理解
返回目录 信息学 泛洪算法专题 魅客科创中心

3.3 迷宫问题

路径寻找

  • 在迷宫中找到从起点到终点的路径
  • 可以使用泛洪算法探索所有可能路径
  • 通常结合回溯来记录实际路径
  • 可以找到最短路径(使用BFS)或任意路径(使用DFS)

实现示例

bool findPath(vector<vector<char>>& maze, 
             pair<int, int> start, 
             pair<int, int> end,
             vector<vector<bool>>& visited,
             vector<pair<int, int>>& path) {
    
    int x = start.first, y = start.second;
    
    // 检查边界和障碍物
    if (x < 0 || x >= maze.size() || 
        y < 0 || y >= maze[0].size() || 
        maze[x][y] == '#' || visited[x][y]) {
        return false;
    }
    
    // 标记当前位置为已访问
    visited[x][y] = true;
    path.push_back({x, y});
    
    // 到达终点
    if (x == end.first && y == end.second) {
        return true;
    }
    
    // 四个方向的移动
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        
        if (findPath(maze, {nx, ny}, end, visited, path)) {
            return true;
        }
    }
    
    // 回溯
    path.pop_back();
    return false;
}

区域探索

  • 计算迷宫中可到达的区域
  • 统计不同区域的大小
  • 找出最大的连通区域
  • 检测迷宫中的死胡同和循环

实现示例

int exploreRegion(vector<vector<char>>& maze, int x, int y) {
    if (x < 0 || x >= maze.size() || 
        y < 0 || y >= maze[0].size() || 
        maze[x][y] == '#') {
        return 0;
    }
    
    // 标记为已访问
    maze[x][y] = '#';
    int size = 1; // 当前格子计入大小
    
    // 探索四个方向
    size += exploreRegion(maze, x+1, y);
    size += exploreRegion(maze, x-1, y);
    size += exploreRegion(maze, x, y+1);
    size += exploreRegion(maze, x, y-1);
    
    return size;
}

int findLargestRegion(vector<vector<char>>& maze) {
    int rows = maze.size();
    int cols = maze[0].size();
    int maxSize = 0;
    
    // 创建迷宫的副本,避免修改原始数据
    vector<vector<char>> mazeCopy = maze;
    
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (mazeCopy[i][j] == '.') { // 空地
                int size = exploreRegion(mazeCopy, i, j);
                maxSize = max(maxSize, size);
            }
        }
    }
    
    return maxSize;
}

应用特点

  • 在游戏开发中广泛应用
  • 可用于自动生成迷宫和关卡设计
  • 结合A*等算法可以实现更高效的路径寻找
  • 可以扩展到三维空间的迷宫问题
返回目录 信息学 泛洪算法专题 魅客科创中心

4. 经典问题解析

4.1 岛屿数量(POJ-2386)

问题描述

  • 题目:POJ-2386 Lake Counting
  • 有一个N×M的网格,表示一片区域
  • 'W'表示积水,'.'表示干燥的地面
  • 相邻的积水(8个方向)形成一个湖泊
  • 求整个区域中湖泊的数量

问题分析

  • 这是一个典型的连通区域计数问题
  • 每个湖泊是由相邻的'W'组成的连通区域
  • 需要使用泛洪算法标记每个湖泊
  • 每次发现一个未访问的'W',就表示找到一个新湖泊
  • 使用泛洪算法将整个湖泊标记为已访问

解题思路

  1. 遍历整个网格
  2. 当找到一个未访问的'W'时,计数器加1
  3. 使用泛洪算法将与该点相连的所有'W'标记为已访问
  4. 重复步骤1-3,直到遍历完整个网格
  5. 返回计数器的值,即为湖泊数量

代码实现

#include <iostream>
#include <vector>
using namespace std;

int n, m; // 网格的行数和列数
vector<vector<char>> grid; // 存储网格

// 8个方向的移动
int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1};
int dy[] = {0, 1, 1, 1, 0, -1, -1, -1};

// 泛洪算法,标记与(x,y)相连的所有'W'
void dfs(int x, int y) {
    // 将当前点标记为已访问
    grid[x][y] = '.';
    
    // 检查8个方向
    for (int i = 0; i < 8; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        
        // 检查边界和是否是积水
        if (nx >= 0 && nx < n && ny >= 0 && ny < m && 
            grid[nx][ny] == 'W') {
            dfs(nx, ny);
        }
    }
}

int countLakes() {
    int count = 0;
    
    // 遍历整个网格
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (grid[i][j] == 'W') {
                // 发现一个新湖泊
                count++;
                // 标记整个湖泊
                dfs(i, j);
            }
        }
    }
    
    return count;
}

int main() {
    cin >> n >> m;
    
    // 初始化网格
    grid.resize(n, vector<char>(m));
    
    // 读入网格数据
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> grid[i][j];
        }
    }
    
    // 计算湖泊数量
    int result = countLakes();
    cout << result << endl;
    
    return 0;
}

复杂度分析

  • 时间复杂度:O(N×M),其中N和M是网格的行数和列数
  • 空间复杂度:O(N×M),用于存储网格和递归调用栈

注意事项

  • 本题使用了8方向的泛洪算法
  • 直接修改原网格作为访问标记,节省空间
  • 可以使用BFS代替DFS实现泛洪算法
返回目录 信息学 泛洪算法专题 魅客科创中心

4.2 围棋吃子(CSP-J 2019)

问题描述

  • 题目:CSP-J 2019 围棋(简化版)
  • 有一个N×N的棋盘,'0'表示空位,'1'表示黑子,'2'表示白子
  • 如果一组相同颜色的棋子被对方棋子或棋盘边界完全包围,则这组棋子被"吃掉"
  • 给定一个棋盘状态和一个新落子的位置,判断是否有棋子被吃掉,并输出被吃掉的棋子数量

问题分析

  • 这是一个判断连通区域是否被包围的问题
  • 需要使用泛洪算法检查每个连通区域是否有"气"(与空位相邻)
  • 如果一个连通区域没有"气",则这个区域被吃掉
  • 需要分别检查新落子周围的对方棋子连通区域

解题思路

  1. 模拟落子,更新棋盘状态
  2. 检查新落子周围的对方棋子连通区域
  3. 对每个连通区域使用泛洪算法,判断是否有"气"
  4. 如果没有"气",则这个连通区域被吃掉
  5. 统计被吃掉的棋子数量

代码实现

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int n; // 棋盘大小
vector<vector<int>> board; // 棋盘状态

// 4个方向的移动
int dx[] = {1, -1, 0, 0};
int dy[] = {0, 0, 1, -1};

// 检查一个连通区域是否有"气"
bool hasLiberty(int x, int y, vector<vector<bool>>& visited, 
                vector<pair<int, int>>& stones) {
    int color = board[x][y];
    queue<pair<int, int>> q;
    q.push({x, y});
    visited[x][y] = true;
    stones.push_back({x, y});
    
    bool hasAir = false;
    
    while (!q.empty()) {
        auto [cx, cy] = q.front();
        q.pop();
        
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            // 检查边界
            if (nx < 0 || nx >= n || ny < 0 || ny >= n) {
                continue;
            }
            
            // 如果是空位,则有"气"
            if (board[nx][ny] == 0) {
                hasAir = true;
            }
            // 如果是同色且未访问,则加入队列
            else if (board[nx][ny] == color && !visited[nx][ny]) {
                visited[nx][ny] = true;
                q.push({nx, ny});
                stones.push_back({nx, ny});
            }
        }
    }
    
    return hasAir;
}

// 计算被吃掉的棋子数量
int countCaptured(int x, int y, int color) {
    int oppositeColor = (color == 1) ? 2 : 1;
    vector<vector<bool>> visited(n, vector<bool>(n, false));
    int capturedCount = 0;
    
    // 检查四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        
        // 检查边界和是否是对方棋子
        if (nx >= 0 && nx < n && ny >= 0 && ny < n && 
            board[nx][ny] == oppositeColor && !visited[nx][ny]) {
            
            vector<pair<int, int>> stones;
            // 如果这个连通区域没有"气",则被吃掉
            if (!hasLiberty(nx, ny, visited, stones)) {
                capturedCount += stones.size();
                // 移除被吃掉的棋子
                for (auto& [sx, sy] : stones) {
                    board[sx][sy] = 0;
                }
            }
        }
    }
    
    return capturedCount;
}

int main() {
    cin >> n;
    
    // 初始化棋盘
    board.resize(n, vector<int>(n));
    
    // 读入棋盘状态
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            cin >> board[i][j];
        }
    }
    
    // 读入新落子的位置和颜色
    int x, y, color;
    cin >> x >> y >> color;
    
    // 模拟落子
    board[x-1][y-1] = color; // 注意:题目中的坐标从1开始
    
    // 计算被吃掉的棋子数量
    int result = countCaptured(x-1, y-1, color);
    cout << result << endl;
    
    return 0;
}
返回目录 信息学 泛洪算法专题 魅客科创中心

4.3 农田灌溉(USACO Bronze)

问题描述

  • 题目:USACO Bronze - Flood Fill
  • 有一个N×M的网格,表示农田
  • '.'表示空地,'#'表示障碍物
  • 在位置(r,c)安装一个灌溉系统
  • 水可以向四个方向流动,但不能穿过障碍物
  • 求能被灌溉的农田面积(格子数量)

问题分析

  • 这是一个典型的泛洪填充问题
  • 从起点(r,c)开始,水可以流向四个方向的空地
  • 障碍物会阻止水的流动
  • 需要计算与起点连通的所有空地的数量

解题思路

  1. 从起点(r,c)开始进行泛洪填充
  2. 使用DFS或BFS遍历所有可达的空地
  3. 统计访问过的格子数量
  4. 返回统计结果,即为可灌溉的农田面积

关键点

  • 使用访问标记数组避免重复计数
  • 注意边界条件和障碍物的处理
  • 可以选择DFS或BFS实现,本题两种方法都可以
  • 需要验证起始位置的合法性

代码实现

#include <iostream>
#include <vector>
using namespace std;

int n, m; // 网格的行数和列数
vector<vector<char>> grid; // 存储网格
vector<vector<bool>> visited; // 标记已访问的格子

// 4个方向的移动
int dx[] = {1, -1, 0, 0};
int dy[] = {0, 0, 1, -1};

// 使用DFS进行泛洪填充
int dfs(int x, int y) {
    // 检查边界和是否可访问
    if (x < 0 || x >= n || y < 0 || y >= m || 
        grid[x][y] == '#' || visited[x][y]) {
        return 0;
    }
    
    // 标记为已访问
    visited[x][y] = true;
    int count = 1; // 当前格子计入面积
    
    // 递归处理四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        count += dfs(nx, ny);
    }
    
    return count;
}

int main() {
    cin >> n >> m;
    
    // 初始化网格和访问标记
    grid.resize(n, vector<char>(m));
    visited.resize(n, vector<bool>(m, false));
    
    // 读入网格数据
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> grid[i][j];
        }
    }
    
    // 读入起点坐标
    int r, c;
    cin >> r >> c;
    
    // 检查起点是否合法
    if (r < 1 || r > n || c < 1 || c > m || 
        grid[r-1][c-1] == '#') {
        cout << "0\n";
        return 0;
    }
    
    // 计算可灌溉的农田面积
    int result = dfs(r-1, c-1); // 注意:题目中的坐标从1开始
    cout << result << endl;
    
    return 0;
}

复杂度分析

  • 时间复杂度:O(N×M),每个格子最多访问一次
  • 空间复杂度:O(N×M),需要存储网格和访问标记数组

优化建议

  • 可以使用BFS实现,获得更直观的扩散效果
  • 可以直接修改原网格作为访问标记
  • 可以使用方向数组简化代码
  • 对于大规模数据,可以考虑使用迭代而非递归
返回目录 信息学 泛洪算法专题 魅客科创中心

5. 练习题推荐

5.1 入门级题目

HDU-1312 Red and Black

  • 题目描述

    • 一个房间由黑白瓷砖组成
    • '@'表示起始位置,'.'表示可以走的位置,'#'表示墙
    • 从起始位置出发,可以上下左右移动
    • 求可以到达的瓷砖数量(包括起始位置)
  • 难度分析

    • 基础泛洪算法题目
    • 只需要考虑四个方向
    • 无需特殊处理,直接DFS或BFS即可
  • 解题提示

    1. 使用DFS或BFS遍历可达位置
    2. 用visited数组标记已访问的位置
    3. 注意统计起始位置
    4. 处理多组测试数据

POJ-1979 Red and Black

  • 题目描述

    • 与HDU-1312类似
    • 在黑白瓷砖组成的房间中移动
    • 计算可以到达的瓷砖数量
    • 格式略有不同
  • 难度分析

    • 入门级泛洪算法题目
    • 代码结构简单
    • 适合初学者练习

解题技巧

  1. 处理输入
// 读入网格大小
while (cin >> w >> h && w && h) {
    // 读入网格数据
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            cin >> grid[i][j];
            // 记录起始位置
            if (grid[i][j] == '@') {
                start_x = i;
                start_y = j;
            }
        }
    }
    // 处理当前测试用例
    cout << solve() << endl;
}
  1. DFS实现
int dfs(int x, int y) {
    // 边界检查
    if (x < 0 || x >= h || y < 0 || y >= w) return 0;
    // 检查是否可访问
    if (grid[x][y] == '#' || visited[x][y]) return 0;
    
    visited[x][y] = true;
    int count = 1;
    
    // 遍历四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        count += dfs(nx, ny);
    }
    
    return count;
}
  1. 注意事项
  • 每次新的测试用例要重置访问标记
  • 可以直接修改原网格作为访问标记
  • 使用方向数组简化代码
  • 注意处理输入输出格式
  1. 代码优化
  • 使用全局变量避免参数传递
  • 使用字符数组代替vector节省时间
  • 预处理方向数组
  • 合理使用引用参数
返回目录 信息学 泛洪算法专题 魅客科创中心

5.2 提高级题目

CSES-1192 Counting Rooms

  • 题目描述

    • 给定一个n×m的网格表示建筑物平面图
    • '.'表示空房间,'#'表示墙
    • 相邻的空房间形成一个房间
    • 求建筑物中房间的总数
  • 难度分析

    • 中等难度的连通分量计数问题
    • 需要考虑多个连通区域
    • 代码实现相对简单,但数据规模较大
  • 解题提示

    1. 遍历整个网格,对每个未访问的'.'进行泛洪填充
    2. 每次泛洪填充代表一个新房间
    3. 使用DFS或BFS标记连通区域
    4. 注意处理大规模数据

CSP-J 2022 T2 灌溉

  • 题目描述
    • n×m的农田网格
    • 多个灌溉系统,每个系统有不同的灌溉范围
    • 计算可以被灌溉的农田面积
    • 需要处理多个灌溉源

解题技巧

  1. Counting Rooms实现
int countRooms(vector<vector<char>>& building) {
    int n = building.size();
    int m = building[0].size();
    int rooms = 0;
    
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (building[i][j] == '.') {
                rooms++; // 发现新房间
                dfs(building, i, j); // 标记整个房间
            }
        }
    }
    
    return rooms;
}

void dfs(vector<vector<char>>& building, int x, int y) {
    if (x < 0 || x >= building.size() || 
        y < 0 || y >= building[0].size() || 
        building[x][y] != '.') {
        return;
    }
    
    building[x][y] = '#'; // 标记为已访问
    
    // 访问四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        dfs(building, nx, ny);
    }
}
  1. 灌溉问题实现
int irrigateField(vector<vector<int>>& field, 
                 vector<Source>& sources) {
    int n = field.size();
    int m = field[0].size();
    vector<vector<bool>> irrigated(n, vector<bool>(m));
    
    // 处理每个灌溉源
    for (const auto& source : sources) {
        bfs(field, irrigated, source);
    }
    
    // 统计灌溉面积
    int area = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (irrigated[i][j]) area++;
        }
    }
    
    return area;
}
  1. 优化策略
  • 使用BFS处理多源灌溉
  • 使用位运算优化状态标记
  • 预处理障碍物信息
  • 使用队列优化内存使用
  1. 注意事项
  • 处理大规模数据时注意效率
  • 考虑多个灌溉源的重叠区域
  • 正确处理边界条件
  • 注意内存限制
返回目录 信息学 泛洪算法专题 魅客科创中心

6. 优化技巧

6.1 避免重复访问

访问标记数组

  • 使用单独的布尔数组标记已访问的位置
  • 避免重复处理同一个位置
  • 防止无限递归和栈溢出

实现示例

vector<vector<bool>> visited(n, vector<bool>(m, false));

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    // 边界检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size()) {
        return;
    }
    
    // 检查颜色和是否已访问
    if (grid[x][y] != oldColor || visited[x][y]) {
        return;
    }
    
    // 标记为已访问
    visited[x][y] = true;
    grid[x][y] = newColor;
    
    // 递归处理四个方向
    floodFill(grid, x+1, y, oldColor, newColor);
    floodFill(grid, x-1, y, oldColor, newColor);
    floodFill(grid, x, y+1, oldColor, newColor);
    floodFill(grid, x, y-1, oldColor, newColor);
}

优点

  • 不修改原始数据
  • 可以在多次泛洪填充中重用
  • 适合需要保留原始数据的场景

原地修改

  • 直接修改原始数据作为访问标记
  • 节省额外的空间
  • 适合只需要一次泛洪填充的场景

实现示例

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    // 边界检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size()) {
        return;
    }
    
    // 检查颜色
    if (grid[x][y] != oldColor || grid[x][y] == newColor) {
        return;
    }
    
    // 直接修改原始数据
    grid[x][y] = newColor;
    
    // 递归处理四个方向
    floodFill(grid, x+1, y, oldColor, newColor);
    floodFill(grid, x-1, y, oldColor, newColor);
    floodFill(grid, x, y+1, oldColor, newColor);
    floodFill(grid, x, y-1, oldColor, newColor);
}

优点

  • 节省内存空间
  • 代码更简洁
  • 适合大规模数据

注意事项

  • 确保新颜色与旧颜色不同
  • 如果需要保留原始数据,先创建副本
  • 在某些问题中可能需要恢复原始数据
返回目录 信息学 泛洪算法专题 魅客科创中心

6.2 内存优化

栈空间控制

  • 递归实现的泛洪算法可能导致栈溢出
  • 控制递归深度或使用迭代实现
  • 尾递归优化

递归深度限制

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor, int depth) {
    // 限制递归深度
    if (depth > MAX_DEPTH) return;
    
    // 边界检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size()) {
        return;
    }
    
    // 检查颜色
    if (grid[x][y] != oldColor) {
        return;
    }
    
    grid[x][y] = newColor;
    
    // 递归处理四个方向,增加深度计数
    floodFill(grid, x+1, y, oldColor, newColor, depth+1);
    floodFill(grid, x-1, y, oldColor, newColor, depth+1);
    floodFill(grid, x, y+1, oldColor, newColor, depth+1);
    floodFill(grid, x, y-1, oldColor, newColor, depth+1);
}

迭代实现(使用栈)

void floodFillIterative(vector<vector<int>>& grid, int x, int y, 
                       int oldColor, int newColor) {
    // 边界检查和初始颜色检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size() || 
        grid[x][y] != oldColor) {
        return;
    }
    
    stack<pair<int, int>> s;
    s.push({x, y});
    
    while (!s.empty()) {
        auto [cx, cy] = s.top();
        s.pop();
        
        // 检查是否已处理
        if (grid[cx][cy] != oldColor) {
            continue;
        }
        
        grid[cx][cy] = newColor;
        
        // 检查四个方向
        if (cx+1 < grid.size() && grid[cx+1][cy] == oldColor) {
            s.push({cx+1, cy});
        }
        if (cx-1 >= 0 && grid[cx-1][cy] == oldColor) {
            s.push({cx-1, cy});
        }
        if (cy+1 < grid[0].size() && grid[cx][cy+1] == oldColor) {
            s.push({cx, cy+1});
        }
        if (cy-1 >= 0 && grid[cx][cy-1] == oldColor) {
            s.push({cx, cy-1});
        }
    }
}

队列优化

  • BFS实现中队列可能占用大量内存
  • 优化队列使用,减少内存占用
  • 使用双端队列或循环队列

内存优化的BFS实现

void floodFillBFS(vector<vector<int>>& grid, int x, int y, 
                 int oldColor, int newColor) {
    // 边界检查和初始颜色检查
    if (x < 0 || x >= grid.size() || 
        y < 0 || y >= grid[0].size() || 
        grid[x][y] != oldColor) {
        return;
    }
    
    // 使用队列进行BFS
    queue<pair<int, int>> q;
    q.push({x, y});
    grid[x][y] = newColor;  // 立即标记起始点
    
    // 方向数组
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    while (!q.empty()) {
        auto [cx, cy] = q.front();
        q.pop();
        
        // 检查四个方向
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            // 边界检查和颜色检查
            if (nx >= 0 && nx < grid.size() && 
                ny >= 0 && ny < grid[0].size() && 
                grid[nx][ny] == oldColor) {
                grid[nx][ny] = newColor;  // 立即标记
                q.push({nx, ny});
            }
        }
    }
}

内存优化技巧

  • 使用位运算代替布尔数组
  • 使用数组代替STL容器
  • 重用已分配的内存
  • 使用内存池
  • 避免不必要的对象复制
  • 使用移动语义(C++11及以上)

示例:使用位运算优化

// 使用整数的位表示访问状态
vector<vector<int>> visited(n, vector<int>(m/32 + 1, 0));

// 设置位
void setBit(int x, int y) {
    visited[x][y/32] |= (1 << (y % 32));
}

// 检查位
bool checkBit(int x, int y) {
    return visited[x][y/32] & (1 << (y % 32));
}
返回目录 信息学 泛洪算法专题 魅客科创中心

6.3 特殊情况处理

边界条件处理

  • 网格边界检查
  • 起始点合法性验证
  • 特殊输入处理

边界检查示例

bool isValid(int x, int y, int rows, int cols) {
    return x >= 0 && x < rows && y >= 0 && y < cols;
}

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    int rows = grid.size();
    int cols = grid[0].size();
    
    // 边界检查
    if (!isValid(x, y, rows, cols)) {
        return;
    }
    
    // 颜色检查
    if (grid[x][y] != oldColor || grid[x][y] == newColor) {
        return;
    }
    
    grid[x][y] = newColor;
    
    // 方向数组
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    // 使用方向数组简化代码
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        floodFill(grid, nx, ny, oldColor, newColor);
    }
}

起始点验证

bool floodFillWithValidation(vector<vector<int>>& grid, 
                            int x, int y, 
                            int oldColor, int newColor) {
    int rows = grid.size();
    int cols = rows > 0 ? grid[0].size() : 0;
    
    // 检查网格是否为空
    if (rows == 0 || cols == 0) {
        return false;
    }
    
    // 检查起始点是否在网格内
    if (x < 0 || x >= rows || y < 0 || y >= cols) {
        return false;
    }
    
    // 检查颜色
    if (oldColor == newColor) {
        return true; // 已经是目标颜色,无需填充
    }
    
    // 执行泛洪填充
    floodFill(grid, x, y, oldColor, newColor);
    return true;
}

异常情况处理

  • 处理空网格
  • 处理颜色相同的情况
  • 处理大规模数据
  • 处理特殊形状的连通区域

处理空网格

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    // 检查网格是否为空
    if (grid.empty() || grid[0].empty()) {
        return;
    }
    
    // 其他代码...
}

处理颜色相同的情况

void floodFill(vector<vector<int>>& grid, int x, int y, 
               int oldColor, int newColor) {
    // 如果新旧颜色相同,直接返回
    if (oldColor == newColor) {
        return;
    }
    
    // 其他代码...
}

处理大规模数据

void floodFillLarge(vector<vector<int>>& grid, int x, int y, 
                   int oldColor, int newColor) {
    // 使用迭代而非递归
    queue<pair<int, int>> q;
    q.push({x, y});
    grid[x][y] = newColor;
    
    int dx[] = {1, -1, 0, 0};
    int dy[] = {0, 0, 1, -1};
    
    // 分批处理,避免队列过大
    const int BATCH_SIZE = 1000;
    while (!q.empty()) {
        int size = min((int)q.size(), BATCH_SIZE);
        
        for (int i = 0; i < size; i++) {
            auto [cx, cy] = q.front();
            q.pop();
            
            for (int j = 0; j < 4; j++) {
                int nx = cx + dx[j];
                int ny = cy + dy[j];
                
                if (nx >= 0 && nx < grid.size() && 
                    ny >= 0 && ny < grid[0].size() && 
                    grid[nx][ny] == oldColor) {
                    grid[nx][ny] = newColor;
                    q.push({nx, ny});
                }
            }
        }
    }
}

处理特殊形状

  • 长条形区域可能导致栈溢出
  • 使用迭代方法处理
  • 考虑分块处理
  • 使用双向BFS优化
返回目录 信息学 泛洪算法专题 魅客科创中心

7. 总结

7.1 关键要点

算法核心

  • 从起始点开始,向四周扩散
  • 递归或迭代访问相邻位置
  • 标记已访问的位置
  • 处理满足条件的区域

实现方式

  1. DFS实现

    • 使用递归或栈
    • 深度优先遍历
    • 适合路径查找
  2. BFS实现

    • 使用队列
    • 广度优先遍历
    • 适合最短路径

优化策略

  • 使用访问标记避免重复
  • 控制递归深度
  • 优化内存使用
  • 处理特殊情况

常见应用

  1. 图像处理

    • 区域填充
    • 边界提取
    • 连通区域标记
  2. 游戏开发

    • 地图探索
    • 区域控制
    • 迷雾战争
  3. 图论问题

    • 连通分量
    • 路径寻找
    • 区域划分

注意事项

  • 边界条件检查
  • 内存限制考虑
  • 栈溢出预防
  • 性能优化权衡
返回目录 信息学 泛洪算法专题 魅客科创中心

7.2 进阶建议

算法扩展

  1. 多源泛洪

    • 多个起始点同时扩散
    • 处理重叠区域
    • 优化扩散顺序
  2. 带权泛洪

    • 考虑移动代价
    • 结合Dijkstra算法
    • 处理不同地形
  3. 双向泛洪

    • 同时从起点和终点扩散
    • 提高搜索效率
    • 适用于路径查找

高级应用

  • 图像分割
  • 迷宫生成
  • 地形分析
  • 网络流问题

学习建议

  1. 基础巩固

    • 熟练掌握DFS和BFS
    • 理解递归和迭代的区别
    • 练习基本题目
  2. 实践应用

    • 实现简单画图工具
    • 编写迷宫游戏
    • 尝试图像处理项目
  3. 深入研究

    • 学习相关算法
    • 研究优化技术
    • 探索新应用场景

参考资源

  • 算法书籍
  • 在线题库
  • 开源项目
  • 学术论文
返回目录 信息学 泛洪算法专题 魅客科创中心

这个讲义使用了Awesome Marp主题的蓝色风格。 内容针对竞赛选手设计,注重基础概念和实际应用。