美文网首页
图和广度优先搜索

图和广度优先搜索

作者: sweetBoy_9126 | 来源:发表于2021-11-30 17:31 被阅读0次

    你经常要找出最短路径,这可能是前往朋友家的最短路径,也可能是国际象棋中把对方将死的最少步数。解决最短路径问题的算法被称为广度优先搜索
    需要两个步骤
    1). 使用图来构建问题模型。
    2). 使用广度优先搜索解决问题。

    1. 是什么

    • 网络结构的抽象模型,是一组由边连接的节点。
    • 用于模拟不同的东西是如何相连的。
    • js 里可以用 Object 和 Array 构建图。
    • 图的表示法:邻接矩阵、邻接表。关联矩阵......

    1.1. 广度优先搜索

    • 一种用于图的查找算法,可帮助解决两类问题
      1). 从节点 A 出发,有前往节点 B 的路径吗?
      2). 从节点 A 出发,前往节点 B 的哪条路径最短?

    1.1.1

    有路径吗?
    假设你经营一个芒果农场,需要找芒果经销商,以便将芒果卖给他。在 Facebook,你与芒果销售商有联系吗?为此,你可以在朋友中查找。


    然后依次检查名单中的每个人,看看他是否是芒果销售商。

    假设你没有朋友是芒果销售商,那么你必须在朋友的朋友中查找。

    检查名单中的每个人时,你都将其朋友加入名单。

    1.1.2 查找最短路径

    以上面的案例为例,广度优先搜索可回答的两类问题

    • 从节点 A 出发,有前往节点 B 的路径吗?(在你的人际关系网中,有芒果销售商吗?)
    • 从节点 A 出发,前往节点 B 的哪条路径最短?(哪个芒果销售商与你的关系最近?)
      那么第二类哪个芒果销售商与你的关系最近
      例如:朋友是一度关系,朋友的朋友是二度以此类推

    我们应先在一度中搜索,如果没有才在二度中搜索。也就是一度关系在二度关系之前加入查找名单。



    而我们想保证我们的查找顺序就需要用到队列

    2. 图的表示法

    2.1. 邻接矩阵

    2.2. 邻接表

    3. 常用操作

    • 深度优先遍历
      尽可能深的搜索图的分支。
    • 广度优先遍历
      先访问离根节点最近的节点。

    3.1. 深度优先遍历算法口诀

    • 访问根节点
    • 对根节点的没访问过的相邻节点挨个进行深度优先遍历

    graph.js

    const graph = {
      0: [1, 2],
      1: [2],
      2: [0, 3],
      3: [3]
    };
    module.exports = graph;
    
    • dfs.js
    const graph = require('./graph');
    const visited = new Set();
    const dfs = (node) => {
      console.log(node)
      visited.add(node);
      graph[node].forEach(c => {
        if (!visited.has(c)) {
          dfs(c);
        }
      })
    };
    dfs(2); // 2 0, 1, 3 
    

    从2开始首先遍历0,0遍历完了遍历0里面的1,1下面的2已经访问过了,所以不再继续遍历,开始遍历回到第一次的遍历遍历2里面的3

    3.2. 广度优先遍历算法口诀

    • 新建一个队列,把根节点入队。
    • 把队头出队并访问。
    • 把队头的没访问过的相邻节点入队。
    • 重复第二三步,直到队列为空。
    const graph = require('./graph');
    const visited = new Set();
    visited.add(2);
    const q = [2];
    while(q.length) {
      const n = q.shift();
      console.log(n);
      graph[n].forEach(c => {
        if (!visited.has(c)) {
          q.push(c);
          visited.add(c);
        }
      })
    }
    

    4. 场景

    4.1. 有效数字 leetCode 65


    上面图中左侧状态0为起点,每次都从状态0开始,并且只有3 5 6是有效的
    比如第一个 "0" => true
    开始的时候是0,值也是0,0-9之间的数字也就是到了左图中的6了,也就是有效的
    第二个 " 0.1 " => true
    从状态0开始,空格后还是状态0,然后是0到了状态6,点到了状态3,1还是状态3,后面的空格我们就可以忽略所以是true
    第三个 "abc" => false
    从状态0开始,第一位是a没有路可以走所以是 false

    解题步骤


    var isNumber = function(s) {
        const graph = {
            // 0通过空格是0,+/-是1,.是2,0-9是6
            0: {'blank': 0, '+/-': 1, '.': 2, '0-9': 6},
            1: {'0-9': 6, '.': 2},
            2: { '0-9': 3 },
            3: { '0-9': 3, 'e': 4 },
            4: { '0-9': 5, '+/-': 7 },
            5: { '0-9': 5 },
            6: { '0-9': 6, '.': 3, 'e': 4 },
            7: { '0-9': 5 }
        };
        // 起始状态为0
        let state = 0;
        // s.trim() 去除首尾空格,因为首尾空格不影响有效值
        for (c of s.trim()) {
            if (c === '') {
                c = 'blank'
            } else if (c === '+' || c === '-') {
                c = '+/-'
            } else if (c >= 0 && c <= 9) {
                c = '0-9'
            }
            // c.toLowerCase() 'E'也是有效数字
            state = graph[state][c.toLowerCase()]
            if (state === undefined) {
                return false;
            }
        }
        return [3, 5, 6].includes(state);
    };
    

    4.2. 太平洋大西洋水流问题 leetCode 417

    给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。

    规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。

    请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

    提示:

    输出坐标的顺序不重要
    m 和 n 都小于150

    示例:

    给定下面的 5x5 矩阵:
    
      太平洋 ~   ~   ~   ~   ~ 
           ~  1   2   2   3  (5) *
           ~  3   2   3  (4) (4) *
           ~  2   4  (5)  3   1  *
           ~ (6) (7)  1   4   5  *
           ~ (5)  1   1   2   4  *
              *   *   *   *   * 大西洋
    
    返回:
    
    [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
    

    解题思路

    解题步骤

    var pacificAtlantic = function(heights) {
        if (!heights || !heights.length) {return []}
        let m = heights.length; // m 矩阵的行数
        let n = heights[0].length; // n 矩阵的列数
    
        // 能流到太平洋的矩阵(得到长度为 m 里面每一项的长度是n并且值都是 false 的矩阵,false 节点不能流入大洋里)
        let flow1 = Array.from({ length: m }, () => new Array(n).fill(false));
        // 能流到大西洋的矩阵
        let flow2 = Array.from({ length: m }, () => new Array(n).fill(false));
        
        // 深度遍历row: 当前节点位于第几行,col: 第几列, flow: 用到哪个 flow
        const dfs = (row, col, flow) => {
            // 可以流入对应大洋的设置为 true
            flow[row][col] = true;
            // 遍历相邻节点(上下左右四个节点)
            // 上节点行数-1列数不变,下节点:行数加1列数不变,左节点:行数不变列数-1,右节点:行数不变,列数加1
            [[row -1, col], [row + 1, col], [row, col -1], [row, col + 1]].forEach(([nextRow, nextCol]) => {
                if (
                    // 保证下一个节点在矩阵中(因为m和n是length,而nextRow 和 nextCol 是 index,所以只能小于)
                    nextRow >= 0 && nextRow < m &&
                    nextCol >= 0 && nextCol < n &&
                    // 保证下一个节点没有访问过
                    !flow[nextRow][nextCol] &&
                    // 保证逆流而上(下一个节点大于等于上一节点)
                    heights[nextRow][nextCol] >= heights[row][col]
                ) {
                    dfs(nextRow, nextCol, flow)
                }
            })
        }
    
        // 沿着海岸线逆流而上
        // 对于太平洋来说海岸线是第一行和第一列,对于大西洋来说是最后一行和最后一列
        for (let row = 0; row < m; row++) {
            // 太平洋:递归遍历第一列
            dfs(row, 0, flow1)
            // 大西洋:递归遍历最后一列
            dfs(row, n - 1, flow2)
        }
        for (let col = 0; col < n; col++) {
            // 太平洋:递归遍历第一行
            dfs(0, col, flow1);
            // 大西洋:递归遍历最后一行
            dfs(m -1, col, flow2);
        }
        // 收集能同时流入两个大洋的坐标
        const res = [];
        for (let row = 0; row < m; row++) {
            for (let col = 0; col < n; col++) {
                if (flow1[row][col] && flow2[row][col]) {
                    res.push([row, col])
                }
            }
        }
        return res;
    };
    

    时间复杂度mn 因为最后遍历的都是 mn 个格子,空间复杂度用到了两个临时变量 flow1 和 flow2 存的都是 mn 个格子,所以空间复杂度也是mn

    4.3. 克隆图 leetCode 133

    解题思路

    • 拷贝所有节点。
    • 拷贝所有的边。

    解题步骤


    var cloneGraph = function(node) {
        if (!node) return;
        const visited = new Map();
        const dfs = (n) => {
            // 拷贝当前节点
            const nodeCopy = new Node(n.val);
            // 建立一个当前节点和拷贝后的当前节点的映射关系
            visited.set(n, nodeCopy);
            n.neighbors?.forEach(ne => {
                if (!visited.has(ne)) {
                    dfs(ne)
                }
                nodeCopy.neighbors.push(visited.get(ne))
            })
        }
        dfs(node);
        return visited.get(node);
    };
    

    相关文章

      网友评论

          本文标题:图和广度优先搜索

          本文链接:https://www.haomeiwen.com/subject/rwudxrtx.html