岛屿类问题(DFS、BFS、DSU)

目录

1. 深度优先搜索(DFS)

1.1 模板

1.2 避免重复

1.3 图解

1.4 实现

2. 广度优先搜索(BFS)

2.1 实现

3. 并查集(DSU)

3.1 思路

3.1.1 图解

3.1.2 初始化数组 QuickUnion()

3.1.3 查找所属集合 find()

3.1.4 合并 union()

3.2 实现


在 leetCode 中,「岛屿问题」是一个系列系列问题。

我们首先明确一下岛屿问题中的网格结构是如何定义的,以方便我们后面的讨论。

网格问题是由 m \times nm×n 个小方格组成一个网格,每个小方格与其上下左右四个方格认为是相邻的,要在这样的网格上进行某种搜索。

岛屿问题是一类典型的网格问题。每个格子中的数字可能是 0 或者 1。我们把数字为 0 的格子看成海洋格子,数字为 1 的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿。

以 leetcode 200 题为例 https://leetcode-cn.com/problems/number-of-islands/

此类问题一般有三种方法:深度优先搜索(DFS)、广度优先搜索(BFS)和并查集(DSU)。

1. 深度优先搜索(DFS)

1.1 模板

void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    // 如果坐标 (r, c) 超出了网格范围,直接返回
    if (!inArea(grid, r, c)) {
        return;
    }
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

1.2 避免重复

是标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

0 —— 海洋格子
1 —— 陆地格子(未遍历过)
2 —— 陆地格子(已遍历过)

void dfs(int[][] grid, int r, int c) {
    // 判断 base case
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 将格子标记为「已遍历过」
    
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

1.3 图解

我们可以将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。

为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被标记为 2。

最终岛屿的数量就是我们进行深度优先搜索的次数。

1.4 实现

public int numIslands1(char[][] grid) {
    // 判断 base case
    if(grid == null || grid.length == 0) return 0;
    int ri = grid.length;
    int rj = grid[0].length;
    int num = 0;
    for(int i = 0; i < ri; i++) {
        for(int j = 0; j < rj; j++) {
            if(grid[i][j] == '1') {
                num++;
                dfs(grid, i, j);
            }
        }
    }
    return num;
}

void dfs(char[][] grid, int r, int c) {
    // 判断 base case
    if(!inArea(grid, r, c)) {
        return;
    }
    // 如果这个格子不是岛屿,直接返回
    if(grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 将格子标记为「已遍历过」
    // 访问上、下、左、右四个相邻结点
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(char[][] grid, int r, int c) {
    return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length;
}

2. 广度优先搜索(BFS)

思想同DFS,只不过广度优先遍历需要队列帮助实现。

2.1 实现

public int numIslands2(char[][] grid) {
    if(grid == null || grid.length == 0) return 0;
    int ri = grid.length;
    int rj = grid[0].length;
    int num = 0;
    for(int i = 0; i < ri; i++) {
        for(int j = 0; j < rj; j++) {
            if(grid[i][j] == '1') {
                num++;
                bfs(grid, i, j);
            }
        }
    }
    return num;
}

private void bfs(char[][] grid, int r, int c) {
    Queue < int[] > queue = new LinkedList < > (); // 存放x,y的坐标
    queue.offer(new int[] {
        r, c
    });
    while(!queue.isEmpty()) {
        int[] poll = queue.poll();
        r = poll[0]; // x坐标
        c = poll[1]; // y坐标
        // 判断 base case
        if(!inArea(grid, r, c)) {
            continue;
        }
        // 如果这个格子不是岛屿,直接continue,开始下次循环
        if(grid[r][c] != 1) {
            continue;
        }
        grid[r][c] = 2; // 将格子标记为「已遍历过」
        queue.offer(new int[] {
            r - 1, c
        });
        queue.offer(new int[] {
            r + 1, c
        });
        queue.offer(new int[] {
            r, c - 1
        });
        queue.offer(new int[] {
            r, c + 1
        });
    }
}

// 判断坐标 (r, c) 是否在网格中
boolean inArea(char[][] grid, int r, int c) {
    return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length;
}

3. 并查集(DSU)

关于连通性问题,并查集也是常用的数据结构。

并查集是一种数据结构, 常用于描述集合,经常用于解决此类问题:某个元素是否属于某个集合,或者 某个元素 和 另一个元素是否同属于一个集合。

一般需要三个步骤,用三个函数分别表示:

  • QuickUnion()初始化数组
  • union()合并(组队)
  • find() 查找所属集合

3.1 思路

并查集中维护连通分量的个数,在遍历的过程中:

  • 相邻的陆地(只需要向右看和向下看)合并,只要发生过合并,岛屿的数量就减少 11;
  • 在遍历的过程中,同时记录空地的数量;
  • 并查集中连通分量的个数 - 空地的个数,就是岛屿数量。

3.1.1 图解

简单说就是,创建一个数组,该数组里存的数字代表所属的集合。

比如arr[4]==1;代表4是第一组。如果arr[7]==1,代表7也是第一组。既然 arr[4] == arr[7] == 1 ,那么说明4 和 7同属于一个集合。

1、初始化所属集合数组

先初始化一个数组。初始时数组内的值与数组的下角标一致。即每个数字都自成立一个小组。

0属于第0个小组(集合),1属于第1个小组(集合),2属于第2个小组(集合)..........

将这个例子的并查集用树形表示来,如下图所示:

2、合并集合

接下来让几个数字进行合并操作,就是组队的过程(合并集合)。

让 5和6进行组队。5里的值就变为6了。含义就是:5放弃了第5小组,加入到了第6小组。5和6属于第6小组。

接下来 让1 和2 进行组队。1的下角标就变为2了。含义就是:1和2都属于第2小组。

接下来让 2 3进行组队:2想和3进行组队,2就带着原先的所有队友,加入到了3所在的队伍。看下面arr[1] == arr[2]==arr[3]==3,意思就是1 2 3 都属于第3小组。

接下来 1 和 4 进行组队:1就带着原先所有的队友一起加入到4所在的队伍中了。看下面arr[1] == arr[2]==arr[3]==arr[4]==4,意思就是1 2 3 4都属于第4小组。

接下来1 和 5进行组队:1就带着原先所有的队友一起加入到5所在的队伍中。5在哪个队伍呢? 因为arr[5]==6,所以5在第6小组。1就带着所有队友进入了小组6。

看下面arr[1] == arr[2]==arr[3]==arr[4]==arr[5]==arr[6]==6,意思就是1 2 3 4 5 6都属于第6小组。

用树形表示来,如下图所示:

最后就看所属集合数组中有多少个不同的值,即为所求。

3.1.2 初始化数组 QuickUnion()

public class QuickUnion {
    private int[] parents;
    private int count = 0;
    QuickUnion(char[][] grid) {
        int x = grid.length;
        int y = grid[0].length;
        parents = new int[x * y];
        for(int i = 0; i < x; i++) {
            for(int j = 0; j < y; j++) {
                if(grid[i][j] == '1') {
                    // 初始化指向自己
                    parents[i * y + j] = i * y + j;
                    count++;
                }
            }
        }
    }
}

3.1.3 查找所属集合 find()

也就是说求出某个元素所属的集合

public int find(int element) {
    while(parents[element] != element) {
        element = parents[element];
    }
    return element;
}

这样的实现方法效率有点慢,直接进行优化。

并查集优化:路径压缩

路径压缩就是处理并查集中的深的结点。实现方法很简单,就是在find函数里加上一句 parent[element] = parent[parent[element]];就好了,就是让当前结点指向自己父亲的父亲,减少深度,同时还没有改变根结点的weight。

public int find(int element) {
    while(parents[element] != element) {
        parents[element] = parents[parents[element]];
        element = parents[element];
    }
    return element;
}

3.1.4 合并 union()

public void union(int x, int y) {
    int xRoot = find(x);
    int yRoot = find(y);
    if(xRoot == yRoot) return;
    parents[xRoot] = yRoot;
    count--;
}

3.2 实现

public int numIslands3(char[][] grid) {
    if(grid == null || grid.length == 0) return 0;
    int x = grid.length;
    int y = grid[0].length;
    QuickUnion union = new QuickUnion(grid);
    for(int i = 0; i < x; i++) {
        for(int j = 0; j < y; j++) {
            if(grid[i][j] == '1') {
                // 下侧
                if(i + 1 < x && grid[i + 1][j] == '1') 
                    union.union(i * y + j, (i + 1) * y + j);
                // 右侧
                if(j + 1 < y && grid[i][j + 1] == '1') 
                    union.union(i * y + j, i * y + j + 1);
            }
        }
    }
    return union.count;
}

public class QuickUnion {
    private int[] parents;
    private int count = 0;
    QuickUnion(char[][] grid) {
        int x = grid.length;
        int y = grid[0].length;
        parents = new int[x * y];
        for(int i = 0; i < x; i++) {
            for(int j = 0; j < y; j++) {
                if(grid[i][j] == '1') {
                    // 初始化指向自己
                    parents[i * y + j] = i * y + j;
                    count++;
                }
            }
        }
    }
    public void union(int x, int y) {
        int xRoot = find(x);
        int yRoot = find(y);
        if(xRoot == yRoot) return;
        parents[xRoot] = yRoot;
        count--;
    }
    public int find(int element) {
        while(parents[element] != element) {
            parents[element] = parents[parents[element]];
            element = parents[element];
        }
        return element;
    }
}

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是对DFSBFS和UCS的介绍: DFS(深度优先搜索):总是扩展最深层的节点,使用的是LIFO队列,即使用的是stack栈。DFS在生成节点时做的goal test,因此在搜索树中,DFS总是沿着最深的路径搜索,直到找到目标状态或者无法继续搜索为止。DFS的优点是空间复杂度低,但是可能会陷入局部最优解。 BFS(广度优先搜索):总是扩展最浅层的节点,使用的是FIFO队列,即使用的是queue队列。BFS在扩展节点时做的goal test,因此在搜索树中,BFS总是沿着最浅的路径搜索,直到找到目标状态或者无法继续搜索为止。BFS的优点是可以找到最优解,但是空间复杂度较高。 UCS(最佳优先搜索):总是扩展当前代价最小的节点,使用的是priority queue优先队列。UCS在扩展节点时做的goal test,因此在搜索树中,UCS总是沿着代价最小的路径搜索,直到找到目标状态或者无法继续搜索为止。UCS的优点是可以找到最优解,且可以在frontier集中记录所有合适的解,但是空间复杂度较高。 以下是对DFSBFS和UCS的演示: 假设我们要在一个迷宫中找到从起点到终点的最短路径,其中1表示墙,0表示可以通过的路。迷宫如下所示: ``` 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 0 0 0 1 1 0 1 0 1 0 1 1 0 1 1 0 1 0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 ``` 我们可以使用DFSBFS和UCS来寻找最短路径。其中DFS使用stack栈,BFS使用queue队列,UCS使用priority queue优先队列。具体实现可以参考以下代码: ```python # DFS def dfs(maze, start, end): stack = [(start, [start])] visited = set() while stack: node, path = stack.pop() if node == end: return path if node not in visited: visited.add(node) for neighbor in get_neighbors(maze, node): stack.append((neighbor, path + [neighbor])) return None # BFS def bfs(maze, start, end): queue = [(start, [start])] visited = set() while queue: node, path = queue.pop(0) if node == end: return path if node not in visited: visited.add(node) for neighbor in get_neighbors(maze, node): queue.append((neighbor, path + [neighbor])) return None # UCS def ucs(maze, start, end): queue = [(0, start, [start])] visited = set() while queue: cost, node, path = heapq.heappop(queue) if node == end: return path if node not in visited: visited.add(node) for neighbor in get_neighbors(maze, node): new_cost = cost + 1 heapq.heappush(queue, (new_cost, neighbor, path + [neighbor])) return None # 获取邻居节点 def get_neighbors(maze, node): neighbors = [] row, col = node if row > 0 and maze[row-1][col] == 0: neighbors.append((row-1, col)) if row < len(maze)-1 and maze[row+1][col] == 0: neighbors.append((row+1, col)) if col > 0 and maze[row][col-1] == 0: neighbors.append((row, col-1)) if col < len(maze[0])-1 and maze[row][col+1] == 0: neighbors.append((row, col+1)) return neighbors ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值