LeetCode 补拙笔记
0. 前言
- 日期:2026.05.29
- 题目:1559. 二维网格图中探测环
- 难度:中等
- 标签:并查集、图论、DFS
1. 题目理解
问题描述 :
给定一个二维字符网格 grid,判断网格中是否存在由相同字符 构成的环。
环的定义是:一条长度 ≥ 4 的路径,起点和终点为同一个格子,路径中不能直接回到上一步所在的格子。
示例:
输入:grid =
[["a","a","a","a"],["a","b","b","a"],["a","b","b","a"],["a","a","a","a"]]输出:
true解释:外层的
a和内层的b都各自形成了环。
2. 解题思路
核心观察
- 环的存在等价于在无向图中,两个连通的节点被再次合并时,会形成环。
- 可以用并查集(Union-Find) 解决:遍历相邻格子,若字符相同则尝试合并,合并前发现两节点已连通,说明存在环。
算法步骤
- 初始化并查集,每个格子为独立集合。
- 遍历网格,对每个格子只检查右侧和下侧的邻居(避免重复检查)。
- 若当前格子与邻居字符相同,执行合并操作。
- 合并时若发现两节点已连通,说明形成了环,直接返回
true。 - 遍历结束未发现环,返回
false。
3. 代码实现
java
package lc1559;
class Solution {
public boolean containsCycle(char[][] grid) {
int n = grid.length;
if (n == 0) return false;
int m = grid[0].length;
int[] parent = new int[n * m];
for (int i = 0; i < n * m; i++) {
parent[i] = i;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
char c = grid[i][j];
// 向右看
if (j + 1 < m && grid[i][j + 1] == c) {
int u = i * m + j;
int v = i * m + (j + 1);
if (union(parent, u, v)) {
return true;
}
}
if (i + 1 < n && grid[i + 1][j] == c) {
int u = i * m + j;
int v = (i + 1) * m + j;
if (union(parent, u, v)) {
return true;
}
}
}
}
return false;
}
private int find(int[] parent, int x) {
int root = x;
while (parent[root] != root) {
root = parent[root];
}
while (parent[x] != root) {
int next = parent[x];
parent[x] = root;
x = next;
}
return root;
}
private boolean union(int[] parent, int x, int y) {
int rootX = find(parent, x);
int rootY = find(parent, y);
if (rootX == rootY) {
return true;
}
parent[rootY] = rootX;
return false;
}
}
4. 代码优化说明
(代码未做任何修改,仅添加注释讲解)
java
class Solution {
public boolean containsCycle(char[][] grid) {
int len1=grid.length;
int len2=grid[0].length;
// 并查集数组,索引表示格子编号,值为父节点
int[] a=new int[len1*len2];
a[0]=0;
// 初始化第一行的父节点
for (int j=1; j<len2; j++){
// 若与左侧字符相同,继承左侧的父节点;否则父节点为自身
if (grid[0][j]==grid[0][j-1]){
a[j]=a[j-1];
}else{
a[j]=j;
}
}
int k=len2;
// 遍历后续每一行
for (int i=1; i<len1; i++){
// 初始化每行第一个格子的父节点
if (grid[i][0]==grid[i-1][0]){
a[k]=a[i*len2-len2];
}else{
a[k]=k;
}
k++;
// 遍历每行后续的格子
for (int j=1; j<len2; j++, k++){
// 先处理左侧邻居
if (grid[i][j]==grid[i][j-1]){
a[k]=a[k-1];
}else{
a[k]=k;
}
// 再处理上方邻居,判断是否形成环
if (grid[i][j]==grid[i-1][j]){
// 若当前格子与上方格子已连通,则存在环
if (fn(a, k)==fn(a, k-len2)){
return true;
}else{
// 合并两个集合
a[a[k-len2]]=a[k];
}
}
}
}
return false;
}
// 路径压缩的查找函数
public int fn(int[] a, int k){
if (a[k]!=k){
a[k]=fn(a, a[k]);
}
return a[k];
}
}
5. 复杂度分析
- 时间复杂度 :O(n×m⋅α(n×m))O(n \times m \cdot \alpha(n \times m))O(n×m⋅α(n×m))
其中 nnn 和 mmm 为网格的行列数,α\alphaα 为阿克曼函数的反函数,近似为常数。并查集的查找和合并操作几乎是常数时间。 - 空间复杂度 :O(n×m)O(n \times m)O(n×m)
主要为并查集数组的开销。
6. 总结
- 核心思路:并查集检测环,通过合并相同字符的相邻节点,利用连通性判断环的存在。
- 优化后代码在遍历顺序和并查集初始化上做了简化,逻辑更紧凑。
- 关键技巧:只检查右侧和下侧邻居,避免重复合并;路径压缩优化查找效率。