【队列 & 栈】(二) 队列和广度优先搜索

目录

一、队列和 BFS

二、广度优先搜索 - 模板

三、墙与门 (PLUS 会员专享)

四、岛屿数量

4.1 题目要求

4.2 求解过程

五、打开转盘锁

5.1 题目要求

5.2 求解过程

六、完全平方数

6.1 题目要求

6.2 解决过程


一、队列和 BFS


二、广度优先搜索 - 模板

// JAVA implementation
/**
 * Return the length of the shortest path between root and target node.
 */
int BFS(Node root, Node target) {
    Queue<Node> queue;  // store all nodes which are waiting to be processed
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    // BFS
    while (queue is not empty) {
        step = step + 1;
        // iterate the nodes which are already in the queue
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = the first node in queue;
            return step if cur is target;
            for (Node next : the neighbors of cur) {
                add next to queue;
            }
            remove the first node from queue;
        }
    }
    return -1;          // there is no path from root to target
}

// JAVA implementation
/**
 * Return the length of the shortest path between root and target node.
 */
int BFS(Node root, Node target) {
    Queue<Node> queue;  // store all nodes which are waiting to be processed
    Set<Node> used;     // store all the used nodes
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    add root to used;
    // BFS
    while (queue is not empty) {
        step = step + 1;
        // iterate the nodes which are already in the queue
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = the first node in queue;
            return step if cur is target;
            for (Node next : the neighbors of cur) {
                if (next is not in used) {
                    add next to queue;
                    add next to used;
                }
            }
            remove the first node from queue;
        }
    }
    return -1;          // there is no path from root to target
}

 


三、墙与门 (PLUS 会员专享)


四、岛屿数量

4.1 题目要求

4.2 解决过程

法一BFS 迭代实现。沿竖直和水平方向遍历输入矩阵,每发现一个陆地 (标号1),就对其进行 BFS,并将可由 BFS 访问/搜索到的陆地都置为已访问/已搜索 (标号0) 以表示属于同一个岛屿,且因为已访问/已搜索而不必在后续过程中再次访问/搜索 (意为不会访问同一个节点两次)。

2020/06/23 - 51.63%

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        land_nums = 0  # 岛屿数量
        for row in range(len(grid)):  # 遍历行
            for col in range(len(grid[0])):  # 遍历列
                if grid[row][col] == '1':  # 发现陆地
                    land_nums += 1  # 岛屿数量 +1
                    grid[row][col] = '0'  # 将已访问/已探索的陆地置为 0, 以后便不必再重复访问/搜索
                    # 对已发现陆地通过 BFS 扩张/辐射, 将与之相邻的陆地都标记为已访问/已探索状态
                    land_positions = collections.deque()  # 内置双端队列, 保存已访问/已探索相邻陆地坐标
                    land_positions.append([row, col])  # 当前已访问/已探索陆地坐标入队
                    while len(land_positions) > 0:  # 只要当前队列中还保存有已访问/已探索陆地坐标
                        x, y = land_positions.popleft()  # 出队探索
                        for new_x, new_y in [[x, y+1], [x, y-1], [x+1, y], [x-1, y]]:  # 向 4 个方向扩张/辐射
                            # 判断有效性, 要求是坐标界限范围内的陆地
                            if (0 <= new_x < len(grid)) and (0 <= new_y < len(grid[0])) and (grid[new_x][new_y] == '1'):
                                grid[new_x][new_y] = '0'  # 因为可由 BFS 访问到, 故属同一块岛,将其置 0 代表已访问/已探索
                                land_positions.append([new_x, new_y])  # 已访问/已探索陆地坐标入队
        return land_nums

法一改:BFS 迭代实现 - 函数封装。

2020/06/23 - 44.98%

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        from collections import deque  # 双端队列
        if not grid: 
            return 0
        row = len(grid)  # 行数
        col = len(grid[0])  # 列数
        cnt = 0  # 岛屿数

        def bfs(i, j):
            queue = deque()  # 保存已访问/已探索的陆地队列
            grid[i][j] = "0"  # 将已发现的陆地置为已访问/已探索状态 0 
            queue.appendleft((i, j))  # 已发现陆地坐标入队
            while queue:  # 只要队列中还有坐标
                i, j = queue.pop()  # 弹出一个坐标
                for x, y in [[-1, 0], [1, 0], [0, -1], [0, 1]]:  # 向周围 4 个方向以单位距离搜索
                    tmp_i = i + x
                    tmp_j = j + y
                    # 若在范围内发现新陆地
                    if (0 <= tmp_i < row) and (0 <= tmp_j < col) and (grid[tmp_i][tmp_j] == "1"):
                        grid[tmp_i][tmp_j] = "0"  # 将已发现的陆地置为已访问/已探索状态 0 
                        queue.appendleft((tmp_i, tmp_j))  # 已发现陆地坐标入队
        # 遍历所有元素
        for i in range(row):
            for j in range(col):
                if grid[i][j] == "1":  # 若发现新陆地
                    bfs(i, j)  # 进行 BFS
                    cnt += 1  # 岛屿数 +1
        return cnt

法二DFS 递归实现。抛开陆地、岛屿和水等概念,该问题的实质是:求一个只含 0/1 元素的矩阵中,元素为 1 的连通区域个数,如图所示:

为此,使用 DFS 的解决过程可表述为:

  • 建立一个已访问/已探索数组 (visited array) 以记录某位置是否被访问/探索过 (的状态);
  • 对于一个未被访问过且元素为 1 的位置,递归进入其上下左右元素为 1 之处,将其 visited 置为 true;
  • 重复上述过程;
  • 遍历完相邻区域后,令结果 cnt +1,然后继续搜素下一个被访问过且元素为 1 的位置,直至遍历完整个矩阵。

但本题目仅要求计算元素为 1 的连通区域个数,因此无需额外空间存储 visited 信息。此外,上述过程不会对元素为 0 的位置进行操作,故对于已访问/已探索的位置,将其元素置 0 即可,以节省 visited 信息的存储开销。

2020/06/23 - 29.04%

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid: 
            return 0
        row = len(grid)  # 行数
        col = len(grid[0])  # 列数
        cnt = 0  # 岛屿数

        def dfs(i, j):
            grid[i][j] = "0"  # 将已发现的陆地置为已访问/已探索状态 0
            for x, y in [[-1, 0], [1, 0], [0, -1], [0, 1]]:  # 向周围 4 个方向以单位距离搜索
                tmp_i = i + x
                tmp_j = j + y
                # 若在范围内发现新陆地
                if (0 <= tmp_i < row) and (0 <= tmp_j < col) and (grid[tmp_i][tmp_j] == "1"):
                    dfs(tmp_i, tmp_j)  # 递归进行 DFS
        # 遍历所有元素
        for i in range(row):
            for j in range(col):
                if grid[i][j] == "1":  # 若发现新陆地
                    dfs(i, j)  # 进行 DFS
                    cnt += 1  # 岛屿数 +1
        return cnt

法二改:DFS 递归实现

202006/23 - 51.63%

class Solution:
    def dfs(self, grid, i, j):
        # 等价的有效性条件
        if (i < 0) or (j < 0) or (i >= len(grid)) or (j >= len(grid[0])) or (grid[i][j] != '1'):
            return
        else:  # 若在范围内发现新陆地 (元素为1)
            grid[i][j] = '0'  # 将已发现的陆地置为已访问/已探索状态 0
            # 然后往上下左右4个方向依次进行递归 DFS
            self.dfs(grid, i+1, j)  # 右
            self.dfs(grid, i-1, j)  # 左
            self.dfs(grid, i, j+1)  # 下
            self.dfs(grid, i, j-1)  # 上
            
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid: 
            return 0
        
        cnt = 0  # 岛屿数
        # 遍历所有元素
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if grid[i][j] == '1':  # 若发现新陆地
                    self.dfs(grid, i, j)  # 进行 DFS
                    cnt += 1  # 岛屿数 +1
        return cnt

法三并查集

2020/06/23 - 35.34%

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        f = {}

        def find(x):
            # Python 字典 setdefault() 函数和 get()方法 类似, 若键不存在于字典中,将会添加键并将值设为默认值
            f.setdefault(x, x)  
            if f[x] != x:
                f[x] = find(f[x])
            return f[x]

        def union(x, y):
            f[find(x)] = find(y)

        if not grid: 
            return 0
        row = len(grid)
        col = len(grid[0])

        for i in range(row):
            for j in range(col):
                if grid[i][j] == "1":
                    for x, y in [[-1, 0], [0, -1]]:
                        tmp_i = i + x
                        tmp_j = j + y
                        if 0 <= tmp_i < row and 0 <= tmp_j < col and grid[tmp_i][tmp_j] == "1":
                            union(tmp_i * row + tmp_j, i * row + j)
        # print(f)
        res = set()
        for i in range(row):
            for j in range(col):
                if grid[i][j] == "1":
                    res.add(find((i * row + j)))
        return len(res)

小贴士:

问:已经有 BFS 了,为什么还要有 DFS ?

:BFS 可以找到最短距离,但 空间复杂度高;而 DFS 的空间复杂度则相对较低。因此,使用 BFS 需要付出一定代价。通常,寻找最短路径时会使用 BFS,其他情况则更多会使用 DFS (递归代码相对好写)。

参考文献

https://leetcode-cn.com/problems/number-of-islands/solution/dfs-bfs-bing-cha-ji-chao-ji-qing-xi-by-powcai/

https://leetcode-cn.com/problems/number-of-islands/solution/xiong-mao-shua-ti-python3-yi-jiu-shi-bfsmo-ban-ti-/

https://leetcode-cn.com/problems/number-of-islands/solution/pythonjavascript-tao-lu-dfs200-dao-yu-shu-liang-by/


五、打开转盘锁

5.1 题目要求

5.2 解决过程

法一BFS 迭代实现。可先将 0000 ~ 9999 共计 10000 种状态抽象为 10000 个图节点,每个节点存在 8 个相邻节点。而两个节点之间存在一条边,当且仅当这两个节点对应的状态只有 1 位不同,且不同的那位相差 1 (包括 0 和 9 也相差 1 的情况),并且这两个节点均不在数组 deadends 中。那么求从 0000 到 target 最小的旋转次数就相当于在图上搜索最短距离/路径,故可用 BFS 实现,并以 0000 为起始节点开始搜索。对于每个状态,它可以扩展到最多 8 个状态,即将其第 i = 0, 1, 2, 3 位进行 +1 或 -1,将这些状态中没有搜索过且不在数组 deadends 中的状态全部加入队列,并重复上述过程。注意 0000 本身也可能在 deadends 中。

2020/06/24 - 79.65%

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        # 生成器 - 生成相邻节点/数字字符串
        def neighbors(node):
            # 每个节点有至多4*2=8个相邻节点
            for i in range(4):  # 4个拨轮
                x = int(node[i])  # 取整
                for d in (-1, 1):  # 拨轮前转/后转1位
                    y = (x + d) % 10  # 拨轮位数循环0~9
                    yield node[:i] + str(y) + node[i+1:]  # 生成新数字字符串

        dead = set(deadends)  # 死亡数字集合 (用 set 比 list 查找效率更高)
        queue = collections.deque([('0000', 0)])  # 已发现未探索队列 (双端队列更普适)
        seen = {'0000'}  # 已探索集合
        while queue:  # 只要队列中还有节点, 就不断搜索直至队空
            node, depth = queue.popleft()  # 出队, 取出节点/数字字符串 + 深度/旋转次数
            if node == target:  # 到达目标数字
                return depth
            if node in dead:  # 到达死亡数字
                continue
            for nei in neighbors(node):  # 正常搜索
                if nei not in seen:  # 若不在已探索集合中
                    seen.add(nei)  # 便加入已探索集合
                    queue.append((nei, depth+1))  # 入队
        return -1  # 无论如何不能解锁则返回 -1

            

事实上,BFS 算法还有另一种优化思路:双向 BFS,能够进一步提高算法效率。简言之,传统的 BFS 从起点开始向四周扩散,直至遇到终点停止,相当于单向 BFS;而双向 BFS 则 从起点和终点同时开始扩散,当两边有交集时停止。

参考文献

https://leetcode-cn.com/leetbook/read/queue-stack/kj48j/

https://leetcode-cn.com/problems/open-the-lock/solution/da-kai-zhuan-pan-suo-by-leetcode/

https://leetcode-cn.com/problems/open-the-lock/solution/wo-xie-liao-yi-tao-bfs-suan-fa-kuang-jia-jian-dao-/

https://leetcode-cn.com/problems/open-the-lock/solution/python-bfs-qing-xi-ti-jie-by-knifezhu/


六、完全平方数

6.1 题目要求

      

6.2 解决过程

官方实现与说明

               

class Solution(object):
    def numSquares(self, n):
        square_nums = [i**2 for i in range(1, int(math.sqrt(n))+1)]  # 平方数列表

        def minNumSquares(k):
            """ recursive solution """
            # bottom cases: find a square number
            if k in square_nums:
                return 1
            min_num = float('inf')

            # Find the minimal value among all possible solutions
            for square in square_nums:
                if k < square:
                    break
                new_num = minNumSquares(k-square) + 1  # 关键语句
                min_num = min(min_num, new_num)
            return min_num

        return minNumSquares(n)

                


                

                

                                    

                

class Solution:
    def numSquares(self, n: int) -> int:
        square_nums = [i**2 for i in range(0, int(math.sqrt(n))+1)]  # 可选平方数
        dp = [float('inf') for _ in range(n+1)]  # dp数组 - index:需要凑齐的目标值, elem:最少个数
        dp[0] = 0  # bottom case
        
        for i in range(1, n+1):  # 需要凑齐的目标值 i
            for square in square_nums:  # 遍历可选平方数列表 square_nums
                if i < square:
                    break
                # 若最后一个数选择 square, 则共需 dp[i-square]+1 个
                dp[i] = min(dp[i], dp[i-square] + 1)  # 关键语句
        
        return dp[-1]

2020/08/30 - 38.08% (5276ms) - 这和凑钱问题 (322.零钱兑换) 十分相似 (均采用 DP 解法)

                


                

                      

                

class Solution:
    def numSquares(self, n: int) -> int:
        def is_divided_by(n, count):
            """
                return: true if "n" can be decomposed into "count" number of perfect square numbers.
                e.g. n=12, count=3:  true.
                     n=12, count=2:  false
            """
            if count == 1:
                return n in square_nums
            
            for k in square_nums:
                if is_divided_by(n - k, count - 1):  # 贪心算法, 最前找到的就是最少的
                    return True
            return False

        square_nums = set([i * i for i in range(1, int(n**0.5)+1)])  # 可选平方数
    
        for count in range(1, n+1):  # count 为最少需要个数
            if is_divided_by(n, count):
                return count

2020/08/30 - 92.99% (80ms) - 质的飞跃

                


                

                       

                

                       

class Solution:
    def numSquares(self, n: int) -> int:
        # list of square numbers that are less than `n`
        square_nums = [i * i for i in range(1, int(n**0.5)+1)]  # 可选平方数
    
        level = 0      # 层级
        queue = {n}  # 当前层
        while queue:
            level += 1
            # Important: use set() instead of list() to eliminate the redundancy,
            #            which would even provide a 5-times speedup, 200ms vs 1000ms.
            next_queue = set()  # 下一层
            # construct the queue for the next level
            for remainder in queue:  # 遍历当前层
                for square_num in square_nums:  # 遍历可选平方数
                    if remainder == square_num:
                        return level  # find the node!
                    elif remainder < square_num:
                        break
                    else:
                        next_queue.add(remainder - square_num)  # 下一层加入
            queue = next_queue  # 当前层下移
        return level

2020/08/30 - 90.70% (184ms) - 虽然性能差些,但思想很好

                


              

              

              

              

class Solution:
    def isSquare(self, n: int) -> bool:
        sq = int(math.sqrt(n))
        return sq*sq == n

    def numSquares(self, n: int) -> int:
        # four-square and three-square theorems
        while (n & 3) == 0:
            n >>= 2      # reducing the 4^k factor from number
        if (n & 7) == 7: # mod 8
            return 4

        if self.isSquare(n):
            return 1
        # check if the number can be decomposed into sum of two squares
        for i in range(1, int(n**(0.5)) + 1):
            if self.isSquare(n - i*i):
                return 2
        # bottom case from the three-square theorem
        return 3

2020/08/30 - 99.32% (44ms) - 最佳

              

参考文献

https://leetcode-cn.com/leetbook/read/queue-stack/kfgtt/

https://leetcode-cn.com/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值