Skip to content

130.被围绕的区域

一、问题描述

被围绕的区域:给定一个二维数组 board,其中包含 'X''O'。我们需要将所有被 'X' 围绕的区域中的 'O' 替换为 'X'。被围绕的区域的定义是,这个区域的所有 'O' 都被 'X' 包围,也就是说,这个区域不与边界上的 'O' 相连。边界上的 'O' 不被看作被围绕的。

二、方案一:使用深度优先搜索

1、思路

  • 遍历边界上的所有单元格。如果遇到 'O',则使用 DFS 将所有与边界相连的 'O' 标记为 'A'
  • 再次遍历整个矩阵,将所有未被标记的 'O'(即被 'X' 围绕的 'O')替换为 'X'
  • 最后,将所有 'A' 恢复为 'O'

2、代码实现

go
func solve(board [][]byte) {
    if len(board) == 0 {
        return // 如果矩阵为空,直接返回
    }

    n, m := len(board), len(board[0]) // n 和 m 分别是矩阵的行数和列数

    // 遍历矩阵的边界,将边界上的 'O' 以及与边界 'O' 相连的 'O' 标记为 'A'
    // 如果我们只从 `(0, 0)` 开始 DFS,那么只有那些与 `(0, 0)` 直接或间接相连的 `'O'` 会被标记,而位于矩阵其他区域的 `'O'` 可能不会被访问到
    // 从矩阵的每个边界点开始 DFS,这样才能正确地标记所有与边界相连的 `'O'`
    for i := 0; i < n; i++ {
        dfs(board, i, 0)     // 遍历左边界
        dfs(board, i, m-1)   // 遍历右边界
    }
    for j := 1; j < m-1; j++ {
        dfs(board, 0, j)     // 遍历上边界
        dfs(board, n-1, j)   // 遍历下边界
    }

    // 再次遍历矩阵,将未被标记的 'O' 替换为 'X'
    for i := 0; i < n; i++ {
        for j := 0; j < m; j++ {
            if board[i][j] == 'O' {
                board[i][j] = 'X' // 将被围绕的 'O' 替换为 'X'
            } else if board[i][j] == 'A' {
                board[i][j] = 'O' // 将标记的 'A' 恢复为 'O'
            }
        }
    }
}

// 深度优先搜索函数,用于将边界上的 'O' 以及与边界 'O' 相连的 'O' 标记为 'A'
func dfs(board [][]byte, i, j int) {
    n, m := len(board), len(board[0])
    if i < 0 || i >= n || j < 0 || j >= m || board[i][j] != 'O' {
        return // 如果索引越界或当前位置不是 'O',则返回
    }
    board[i][j] = 'A' // 将当前位置标记为 'A'
    // 递归搜索上下左右四个方向
    dfs(board, i+1, j)
    dfs(board, i-1, j)
    dfs(board, i, j+1)
    dfs(board, i, j-1)
}

3、复杂度分析

  • 时间复杂度:O(n*m),其中 n 和 m 分别是矩阵的行数和列数。在最坏的情况下,我们可能需要遍历整个矩阵多次。
  • 空间复杂度:O(n*m),这是递归调用栈的空间复杂度。

三、方案二:使用并查集

1、思路

  • 创建一个并查集,其大小为 n * m,用于处理所有单元格。
  • 遍历矩阵的边界,将所有边界上的 'O' 以及与边界 'O' 相连的 'O' 合并到同一个集合中。
  • 再次遍历整个矩阵,将所有不属于这些集合的 'O' 替换为 'X'

2、代码实现

go
type UnionFind struct {
	parent []int // 用于存储每个元素的父节点,即它所在集合的代表元素或根节点
	rank   []int // 树的高度
	count  int   // 并查集中剩余的集合数量
}

// 创建并查集的构造函数
func NewUnionFind(n int) *UnionFind {
	parent := make([]int, n)
	rank := make([]int, n)
	for i := range parent {
		parent[i] = i
		rank[i] = 1
	}
	return &UnionFind{parent, rank, n}
}

// 查找元素的根节点,同时进行路径压缩
func (uf *UnionFind) Find(x int) int {
	if uf.parent[x] != x {
		uf.parent[x] = uf.Find(uf.parent[x])
	}
	return uf.parent[x]
}

// 合并两个元素所在的集合
func (uf *UnionFind) Union(x, y int) {
	rootX, rootY := uf.Find(x), uf.Find(y)
	if rootX != rootY {
		if uf.rank[rootX] > uf.rank[rootY] {
			uf.parent[rootY] = rootX
		} else if uf.rank[rootX] < uf.rank[rootY] {
			uf.parent[rootX] = rootY
		} else {
			uf.parent[rootY] = rootX
			uf.rank[rootX]++
		}
		// 每次执行“合并”操作时,集合的数量就会减少
		uf.count--
	}
}

func solve(board [][]byte) {
	if len(board) == 0 {
		return
	}

	n, m := len(board), len(board[0])
	uf := NewUnionFind(n*m + 1)
	// 虚拟节点,用于连接所有边界上的 'O'
	dummy := n * m

	// 遍历矩阵,将边界上的 'O' 和与之相连的 'O' 合并到虚拟节点
	for i := 0; i < n; i++ {
		for j := 0; j < m; j++ {
			if board[i][j] == 'O' {
				// 边界上的 'O' 不会被包围,都合并到同一个集合中
				if i == 0 || i == n-1 || j == 0 || j == m-1 {
					uf.Union(i*m+j, dummy)
				} else {
					// 将当前 'O' 与其上下左右的 'O' 合并
					if i > 0 && board[i-1][j] == 'O' {
						uf.Union(i*m+j, (i-1)*m+j)
					}
					if i < n-1 && board[i+1][j] == 'O' {
						uf.Union(i*m+j, (i+1)*m+j)
					}
					if j > 0 && board[i][j-1] == 'O' {
						uf.Union(i*m+j, i*m+(j-1))
					}
					if j < m-1 && board[i][j+1] == 'O' {
						uf.Union(i*m+j, i*m+(j+1))
					}
				}
			}
		}
	}

	for i := 0; i < n; i++ {
		for j := 0; j < m; j++ {
			if board[i][j] == 'O' && uf.Find(i*m+j) != uf.Find(dummy) {
				board[i][j] = 'X'
			}
		}
	}
}

3、复杂度分析

  • 时间复杂度:O(nmα(nm)),其中 α 是阿克曼函数的反函数,用于表示并查集操作的平均时间复杂度。在最坏的情况下,α(nm) 接近于 O(1),所以整个算法的时间复杂度接近于 O(n*m)。
  • 空间复杂度:O(n*m),用于存储并查集的数据结构。

四、总结

方案时间复杂度空间复杂度优点缺点
DFSO(n*m)O(n*m)实现简单,易于理解可能需要多次遍历整个矩阵
Union-FindO(nmα(nm)) ≈ O(nm)O(n*m)高效,特别是对于大数据集实现复杂,需要理解并查集

在实际应用中,选择哪种方案取决于具体的需求和数据规模。对于小规模或中等规模的问题,方案一通常更易于实现和理解。而对于大规模问题,方案二的高效率可能更受欢迎。

木川工作室 (微信:mcmc2024)