Awesome算法解题框架——广度优先搜索(BFS)

Awesome算法解题框架系列:致力于探讨算法问题的框架思维,形成解题框架,用一个框架以点带面解决一类问题。问题千变万化,算法思维框架是不变的,以不变应万变。

广度优先搜索(以下简称BFS)是图的搜索算法中的一种基础算法,其核心思想是从源点出发一圈一圈向外扩展、逐层地毯式推进搜索。使用的核心数据结构是队列,此外为了存储路径,还需要一个哈希表。

下面是两个使用BFS算法的模板题,本文将从这两个问题出发,总结出BFS算法的通用解题框架模板,力求使用这套模板,可以快速解决BFS问题。

岛屿的最大面积

问题分析

图中上下左右连在一起的1的区域就是陆地,要找到这些陆地区域中最大的面积,朴素的思路就是:先找到所有的陆地区域,然后计算出最大的那个区域的面积。那么核心问题就转换为如何找到图中所有的陆地区域?当然是用BFS了。

和其他的BFS寻找最短路径问题稍有不同的是,本题并不需要寻找一个最短路径,因此不需要存储路径有关的信息。

代码实现

用Java实现:

// AC@7ms 12% 89%
class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        // - 判空
        if (null == grid || null == grid[0]) {
            return 0;
        }

        // - 陆地是否处理过的标记数组
        int row = grid.length;
        int column = grid[0].length;
        boolean[][] marked = new boolean[row][column];

        // - 从左到右、从上到下第一个未被处理的陆地开始BFS处理
        int maxLandArea = 0;
        Queue<int[]> queue = new LinkedList<>();
        for(int i = 0; i < row; i++) {
            for (int j = 0; j < column; j++) {
            if (grid[i][j] == 1 && !marked[i][j]) {
                // 当前陆地面积
                int curArea = 0;
                queue.add(new int[]{i, j});
                while (!queue.isEmpty()) {
                    // 当前点
                    int[] cur = queue.poll();
                    curArea++;
                    // 标记当前点已处理
                    marked[cur[0]][cur[1]] = true;
                    // 获取当前点的上下左右未被处理的邻居陆地节点
                    List<int[]> neighbors = unmarkedLandNeighbors(grid, cur, marked);
                    if (null != neighbors && neighbors.size() > 0) {
                        for (int[] nei: neighbors) {
                            queue.offer(nei);
                            // 标记当前邻居点已处理,否则会被重复添加
                            marked[nei[0]][nei[1]] = true;
                        }
                    }
                }

                // 更新最大面积
                if (curArea > maxLandArea) {
                    maxLandArea = curArea;
                }
              }
            }
        }

        return maxLandArea;
    }

    // 获取一个点上下左右的未被处理的邻居陆地节点的坐标列表,(行,列)下标二元组
    private List<int[]> unmarkedLandNeighbors(int[][] grid, int[] point, boolean[][] marked) {
        int row = grid.length;
        int column = grid[0].length;

        int r = point[0];
        int c = point[1];
        List<int[]> neighbors = new ArrayList<>();
        // 上
        if (r > 0 && grid[r-1][c] == 1 && !marked[r-1][c]) {
            neighbors.add(new int[]{r-1, c});
        }

        // 下
        if (r < row - 1 && grid[r+1][c] == 1 && !marked[r+1][c]) {
            neighbors.add(new int[]{r+1, c});
        }

        // 左
        if (c > 0 && grid[r][c - 1] == 1 && !marked[r][c-1]) {
            neighbors.add(new int[]{r, c-1});
        }

        // 右
        if (c < column - 1 && grid[r][c+1] == 1 && !marked[r][c+1]) {
            neighbors.add(new int[]{r, c+1});
        }
        return neighbors;
    }

}

迷宫问题

问题分析

这是一个典型的BFS算法问题,迷宫中有通路,也有障碍,从起点开始,运用BFS的逐层地毯式搜索,过程中记录每个搜索过的点的上一个节点的哈希表,直到遇到终点,最终就可以根据这个哈希表、从后往前"按图索骥"找到从起点到终点的最短路径。

代码实现

用Java实现:

// poj-3984 迷宫问题
import java.util.*;
class Main {
    private static Main main = new Main();

    public static void main(String[] argv) {
        Scanner sc = new Scanner(System.in);
        int[][] maze = new int[5][5];
        for(int i = 0; i < 5; i++){
            for(int j = 0; j < 5; j++){
                maze[i][j] = sc.nextInt();
            }
        }

        main.printShortestPath(maze);
    }

    // 打印从左上角到右下角的最短路径
    private static void printShortestPath(int[][] maze) {
        // - 判空
        if (null == maze || null == maze[0] || maze.length == 0 || maze[0].length == 0) {
            return;
        }

        // - 找到最短路径前驱map
        Map<List<Integer>, int[]> shortestPathPreMap = findShortestPathPreMap(maze);

        // - 解析最短路径前驱map为最短路径列表
        List<int[]> path = parsePath(shortestPathPreMap, new int[]{4,4});

        // - 打印最短路径
        for (int[] p: path) {
            System.out.println("(" + p[0] + ", " + p[1] + ")");
        }
    }

    // 找到从左上角到右下角的最短路径,返回路径前驱节点map
    private static Map<List<Integer>, int[]> findShortestPathPreMap(int[][] maze) {
        int row = maze.length;
        int col = maze[0].length;

        // - 标记某个点的前驱节点的哈希表
        Map<List<Integer>, int[]> preMap = new HashMap<List<Integer>, int[]>();
        // - 标记迷宫中的点有没有被搜索过的标记数组
        boolean[][] searched = new boolean[row][col];
        // - 待查找队列,存放待查找的点的(行,列)坐标,先把左上角的点入队
        Queue<int[]> searchQueue = new LinkedList<int[]>();
        searchQueue.offer(new int[]{0,0});
        // - 遍历待查找队列
        while (!searchQueue.isEmpty()) {
            // - 出队当前点
            int[] cur = searchQueue.poll();
            // - 标记当前点为处理过
            searched[cur[0]][cur[1]] = true;
            // - 遍历当前点的邻居节点列表
            List<int[]> neighbors = getNeighbors(maze, cur, searched);
            if (neighbors.size() > 0) {
                for (int[] nei: neighbors) {
                    // 记录前驱节点
                    preMap.put(arr2List(nei), cur);
            
                    // 如果是终点,则返回前驱关系哈希表
                    if (nei[0] == row-1 && nei[1] == col-1) {
                        return preMap;
                    }
            
                    // 如果不是终点,入队,并标记为已经搜索过
                    searchQueue.offer(nei);
                    searched[nei[0]][nei[1]] = true;
                }
            }
        }

        return null;
    }
    
    // 获取cur点当前上下左右未被查找过且不是障碍物的邻居节点列表
    private static List<int[]> getNeighbors(int[][] maze, int[] cur, boolean[][] searched) {
        List<int[]> neighbors = new ArrayList<int[]>();
    
        int rowLen = maze.length;
        int colLen = maze[0].length;
        int row = cur[0];
        int col = cur[1];

        // 上
        if (row > 0 && !searched[row-1][col] && maze[row-1][col] != 1) {
            neighbors.add(new int[]{row-1, col});
        }
        // 下
        if (row < rowLen - 1 && !searched[row+1][col] && maze[row+1][col] != 1) {
            neighbors.add(new int[]{row+1, col});
        }
        // 左
        if (col > 0 && !searched[row][col-1] && maze[row][col-1] != 1) {
            neighbors.add(new int[]{row, col-1});
        }
        // 右
        if (col < colLen - 1 && !searched[row][col+1] && maze[row][col+1] != 1) {
            neighbors.add(new int[]{row, col+1});
        }
        return neighbors;
    }

    // 从preMap中解析出从起点到终点的路径列表
    private static List<int[]> parsePath(Map<List<Integer>, int[]> preMap, int[] end) {
        // 从起点到终点的路径列表
        List<int[]> path = new ArrayList<int[]>();
    
        // 当前节点:终点
        int[] cur = end;
        while (null != cur && !(cur[0] == 0 && cur[1] == 0)) {
            path.add(cur);
            cur = preMap.get(arr2List(cur));
        }
        // 加入起点
        path.add(new int[]{0,0});
        // 反转path
        Collections.reverse(path);
        return path;
    }

    // 把整型数组转为整型列表
    private static List<Integer> arr2List(int[] arr) {
        List<Integer> nums = new ArrayList<Integer>();
        for (int n: arr) {
            nums.add(n);
        }
        return nums;
    }

}

BFS算法框架总结

核心框架

把起点加入待搜索队列
while 待搜索队列不为空:
    出队当前元素,并标记为已处理
    获取当前元素的可达、合法、未被标记的邻居列表
    for neighbor in [邻居列表]:
        把neighbor加入待搜索队列
        标记neighbor为已处理

核心框架伪代码

根据上面两个题目的代码,可以抽象出BFS算法解体框架的伪代码,伪代码中采用模块化的思想,封装了3个核心函数,每一个函数力求只做一件(简单的)事情:

// BFS算法主函数
BFS(matrix, start, end):
    pre_map <- GET-SHORTEST-PATH-PRE-MAP(matrix, start, end)
    path <- PARSE-PRE-MAP(pre_map, start, end)
    return path

// 找到从起点到终点的最短路径的前驱关系哈希表
GET-SHORTEST-PATH-PRE-MAP(matrix, start, end):
    // 待搜寻队列
    search_queue <- empty queue
    // 已搜寻过的点
    searched_map <- empty hash map, all default false
    // <某个点: 前一个点>的关系表
    pre_map <- empty hash map
    
    // 一开始将起点放入待搜寻队列
    search_queue <- enqueue start
    while search_queue is not empty:
        cur <- dequeue from search_queue
        // 标记当前点为已搜索过
        searched_map[cur] <- true
        neighbors <- GET-NEIGHBORS(cur)
        for nei in neighbors:
            // 记录前驱点关系
            pre_map[nei] <- cur
            // 判断是否找到终点
            if nei is end:
                return pre_map
            search_queue <- enqueue nei
            searched_map[nei] <- true

// 找到一个点的可达、且未被标记过的邻居点列表
GET-NEIGHBORS(matrix, cur, searched_map):
    neighbors <- empty list
    for point in up, down, left, right:
        if point can access and searched_map[point] is false:
            neighbors <- add point
    return neighbors

// 把前驱关系哈希表解析为一个从起点到终点的路径列表
PARSE-PRE-MAP(pre_map, start, end):
    path <- empty list

    cur <- end
    while cur is not start:
        path <- add cur
        cur <- pre_map[cur]
    path <- add start
    path <- reverse path
    return path

Java实现时的几点注意事项

使用Java实现上述框架的时候,需要注意如下几点事项:

  • Java中的队列可以使用LinkedList,其实现了Queue接口,其入队方法为offer,出队方法为poll,判断是否为空的方法为isEmpty
  • 为了方便,可以优先使用int[2]数组来表示一个点在矩阵中的(行, 列)坐标。
  • 不能用数组作为Map的key,因为数组没有实现hashCode方法,而用整型List是可以作为Map的key的,有完全相同元素的两个List,其hachCode也是相同的。
  • 寻找一个点的可达、未搜寻过的邻居节点的时候,可以按照上下左右的顺序进行查找,防止思路混乱。

总结

BFS算法的框架是非常明晰的,没有多少弯弯绕的东西,本文中用伪代码给出的解题框架适用于用二维矩阵表示的图,如果是以其他形式表示的图,那么代码只需稍作修改,也可以同样适用,框架思想是相通的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容