BFS&DFS答题记录与总结

学习与参考资料:
[1] 2013王道论坛计算机考研机试指南
[2] DFS和BFS讲解及Leetcode刷题小结(1)(JAVA)
[3] DFS和BFS讲解及Leetcode刷题小结(2)(JAVA)
[4] AcWing立体推箱子题解

写在前面:

对于状态的定义不要局限于地图的坐标,在地图题中,正确定义状态后才能解决“立体推箱子”以及“允许对称转移的迷宫问题”等题,而在非地图题中,连地图都没有,所以状态的定义就很关键了。

DFS

地图类

LC200. Number of Islands

LC200. Number of Islands

在这里插入图片描述

from collections import deque

def dfs(grid,x,y):
    if 0<=x<len(grid) and 0<=y<len(grid[0]) and grid[x][y]=="1":
        grid[x][y]=0  # 将访问之后的陆地变成水,后面便不会再去访问了,省去了记录访问信息的数组
        dfs(grid,x-1,y)
        dfs(grid,x+1,y)
        dfs(grid,x,y-1)
        dfs(grid,x,y+1)

# 也可以用bfs
def bfs(grid, x, y):
    queue = deque()
    queue.append((x, y))
    grid[x][y] = '0'  # 将访问之后的陆地变成水,后面便不会再去访问了,省去了记录访问信息的数组
    while queue:
        directions = [(0,1), (0,-1), (-1,0), (1,0)]
        x, y = queue.popleft()
        for d in directions:
            nx, ny = x + d[0], y + d[1]    
            if 0<=nx<len(grid) and 0<=ny<len(grid[0]) and grid[nx][ny] == '1':
                queue.append((nx, ny))
                grid[nx][ny] = '0'  # 将访问之后的陆地变成水
            
        
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        if len(grid)==0:
            return 0
        cnt=0
        for x in range(len(grid)):
            for y in range(len(grid[0])):
                if grid[x][y]=="1":
                    # bfs(grid,x,y)
                    dfs(grid,x,y)
                    cnt+=1
        return cnt

BFS

地图类

POJ2251. 地牢大师

AcWing1096. 地牢大师

在这里插入图片描述

import sys
from collections import deque


def parse_nums(nums_str):
    return [int(x) for x in nums_str.strip().split()]


def bfs(grid, x, y, z):
    queue = deque()
    queue.append((x, y, z, 0))
    grid[x][y][z] = '#'
    while queue:
        directions = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]]
        x, y, z, t = queue.popleft()
        for d in directions:
            nx, ny, nz, nt = x + d[0], y + d[1], z + d[2], t + 1
            if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]) and 0 <= nz < len(grid[0][0]) and grid[nx][ny][nz] != '#':
                if grid[nx][ny][nz] == 'E':
                    return nt
                queue.append((nx, ny, nz, nt))
                grid[nx][ny][nz] = '#'
    return -1


for ns in sys.stdin:
    if ns == '0 0 0':  # 终止信号
        break
    L, R, C = parse_nums(ns)
    grid = []
    x, y, z = 0, 0, 0
    for i in range(L):
        level = []
        for j in range(R):
            row = [c for c in input()]
            if 'S' in row:
                x, y, z = i, j, row.index('S')
            level.append(row)
        grid.append(level)
        input()  # 丢弃空行
    time = bfs(grid, x, y, z)
    print("Trapped!" if time == -1 else f"Escaped in {time} minute(s).")

AcWing172. 立体推箱子

AcWing172. 立体推箱子

在这里插入图片描述

import sys
from collections import deque


def parse_nums(nums_str):
    return [int(x) for x in nums_str.strip().split()]


def check(grid, x, y, s):
    if x < 0 or x > len(grid) or y < 0 or y > len(grid[0]):
        return False  # 如果出界,不合法
    if grid[x][y] == '#':
        return False  # 如果箱子的坐标对应的格子是禁地,不合法
    if s == 0 and grid[x][y] == 'E':
        return False  # 如果箱子是竖着的,则箱子对应坐标是易碎也不合法
    if s == 1 and grid[x][y + 1] == '#':
        return False  # 如果箱子是左右的,则箱子的另一个格子如果是禁地也不合法
    if s == 2 and grid[x + 1][y] == '#':
        return False  # 如果箱子是上下的,则箱子的另一个格子如果是禁地也不合法
    return True


def bfs(grid, x, y, s):
    queue = deque()
    # 事实上有了res表,不用再使用最后一位来记录结果了,而且(x,y,s)才是定义的搜索状态。但是这样写可以与不用res表的代码统一起来
    queue.append((x, y, s, 0))
    # 转移表,每个三元组的前两个表示x和y的增量,第三个表示箱子姿势的转移
    transitions = [[[-2, 0, 2], [1, 0, 2], [0, -2, 1], [0, 1, 1]],  # 姿势为竖着对应的4个方向的x,y,s如何转移
                   [[-1, 0, 1], [1, 0, 1], [0, -1, 0], [0, 2, 0]],  # 姿势为左右对应的4个方向的x,y,s如何转移
                   [[-1, 0, 0], [2, 0, 0], [0, -1, 2], [0, 1, 2]]]  # 姿势为上下对应的4个方向的x,y,s如何转移
    # 搜索状态定义为(x,y,s),所以不再适合直接使用原矩阵记录是否已经访问
    # 这里设置res来保存搜索结果,默认值-1还可以用于判断是否已经访问
    res = [[[-1 for _ in range(3)] for _ in range(len(grid[0]))] for _ in range(len(grid))]
    res[x][y][s] = 0  # 设置起点的最小时间代价为0
    while queue:
        x, y, s, t = queue.popleft()
        for d in transitions[s]:  # 用s取出转移表中对应的行
            nx, ny, ns, nt = x + d[0], y + d[1], d[2], t + 1  # 状态转移与结果记录(注意d[2]不是增量,所以ns=d[2]而不是=s+d[2])
            if check(grid, nx, ny, ns) and res[nx][ny][ns] == -1:  # 如果下一个状态是合法的,且未访问过
                if grid[nx][ny] == 'O' and ns == 0:  # 如果是所求的终点,返回对应的结果时间
                    return nt
                queue.append((nx, ny, ns, nt))  # 否则入队,后续再访问其邻接结点
                res[nx][ny][ns] = nt  # 记录到达该结点的最小时间,同时也表示已经访问过
    return -1


# 读取2维地图
for ns in sys.stdin:
    if ns == '0 0':  # 终止信号
        break
    N, M = parse_nums(ns)
    grid = []
    # 起点状态,x、y为箱子的左上角格子坐标,s为箱子姿势(0为竖着,1为左右躺着,2为上下躺着),t为步数/时间
    x, y, s, t = 0, 0, 0, 0
    first_X = True
    for i in range(N):
        row = [c for c in input()]
        if 'X' in row:
            if first_X:  # 第一次碰到X,就能判断是否为左右躺着,否则先默认竖着
                x, y, s = i, row.index('X'), 0
                if row[y + 1] == 'X':
                    s = 1
                first_X = False
            else:  # 如果碰到了两行中都有X,则是上下躺着
                s = 2
        grid.append(row)
    time = bfs(grid, x, y, s)
    print(time if time != -1 else 'Impossible')

阿里2020.03.23笔试第二题——允许对称转移的迷宫问题

牛客回忆贴——阿里2020.03.23笔试题

在这里插入图片描述

写完上一题AcWing的立体推箱子,对于BFS中 状态 的理解肯定比较深入了。对于本题,只需要定义状态为 (x,y,s),其中x,y为地图坐标,s为已经使用飞行器的次数,s的合法范围为[0,5]

经过测试,下面代码可以通过给出的用例。

import sys
from collections import deque


def parse_nums(nums_str):
    return [int(x) for x in nums_str.strip().split()]


def check(grid, x, y, s):
    return 0 <= x < len(grid) and 0 <= y < len(grid[0]) and 0 <= s <= 5 and grid[x][y] != '#'


def bfs(grid, x, y, s):
    n, m = len(grid), len(grid[0])
    queue = deque()
    # 事实上有了res表,不用再使用最后一位来记录结果了,而且(x,y,s)才是定义的搜索状态。但是这样写可以与不用res表的代码统一起来
    queue.append((x, y, s, 0))
    # 转移表,每个二元组表示x和y的增量
    transitions = [[-1, 0], [1, 0], [0, -1], [0, 1]]  # s坐标后面手动控制其转移
    # 搜索状态定义为(x,y,s),所以不再适合直接使用原矩阵记录是否已经访问
    # 这里设置res来保存搜索结果,默认值-1还可以用于判断是否已经访问
    res = [[[-1 for _ in range(5)] for _ in range(m)] for _ in range(n)]
    res[x][y][s] = 0  # 设置起点的最小时间代价为0
    while queue:
        x, y, s, t = queue.popleft()
        for i in range(5):  # 5种走法:上下左右以及对称
            if i < 4:  # 上下左右可以使用transitions统一处理
                d = transitions[i]
                nx, ny, ns, nt = x + d[0], y + d[1], s, t + 1
            else:  # 对称转移时,新坐标不是简单用一个增量加到原坐标,而是原坐标的函数,所以手动控制转移结果
                nx, ny, ns, nt = n - 1 - x, m - 1 - y, s + 1, t + 1
            if check(grid, nx, ny, ns) and res[nx][ny][ns] == -1:  # 如果下一个状态是合法的,且未访问过
                if grid[nx][ny] == 'E':  # 如果是所求的终点,返回对应的结果时间
                    return nt
                queue.append((nx, ny, ns, nt))  # 否则入队,后续再访问其邻接结点
                res[nx][ny][ns] = nt  # 记录到达该结点的最小时间,同时也表示已经访问过
    return -1


# 读取2维地图
for ns in sys.stdin:
    N, M = parse_nums(ns)
    grid = []
    # 起点状态,x、y为箱子的左上角格子坐标,s为已经使用飞行器的次数,t为步数/时间
    x, y, s, t = 0, 0, 0, 0
    for i in range(N):
        row = [c for c in input()]
        if 'S' in row:
            x, y = i, row.index('S')
        grid.append(row)
    time = bfs(grid, x, y, s)
    print(time)

其他涉及状态转移的最优化问题

LC752. 打开转盘锁

本题的一个教训是,检查新状态的操作必须是时间复杂度O(1)的,否则总体的时间复杂度将会很高,见注释。

在这里插入图片描述

from collections import deque


def bfs(target, deadends):
    queue = deque()
    queue.append((0, 0, 0, 0, 0))
    res = [[[[-1 for _ in range(10)] for _ in range(10)] for _ in range(10)] for _ in range(10)]
    res[0][0][0][0] = 0
    for d in deadends:
        if d=='0000':return -1
        a,b,c,d=[int(x) for x in d]
        # 表示deadends中的状态无法到达,但已经访问过(不等于-1)
        # 这样后面就不用每次检查新状态是否合法时都检查是否在deadends中
        res[a][b][c][d]= -2  
    directions = [[-1, 0, 0, 0], [1, 0, 0, 0], [0, -1, 0, 0], [0, 1, 0, 0],
                  [0, 0, -1, 0], [0, 0, 1, 0], [0, 0, 0, -1], [0, 0, 0, 1]]
    while queue:
        a, b, c, d, t = queue.popleft()
        for ds in directions:
            na, nb, nc, nd, nt = (a + ds[0] + 10) % 10, (b + ds[1] + 10) % 10, (c + ds[2] + 10) % 10, (
                    d + ds[3] + 10) % 10, t + 1
            tmp = str(na) + str(nb) + str(nc) + str(nd)
            # 如果把deadends的状态加入res数组,这里每次都用tmp not in deadends的话
            # 非常耗时,因为 list 的 in 操作复杂度是 O(N) 的,在deadends很长时特别慢(7s vs 0.6s)
            if res[na][nb][nc][nd] == -1:  
                if tmp == target:
                    return nt
                res[na][nb][nc][nd] = nt
                queue.append((na, nb, nc, nd, nt))
    return -1

class Solution:
    def openLock(self, deadends, target):
        return bfs(target, deadends)

HDU1495. 非常可乐

题目链接:HDU1495. 非常可乐

在这里插入图片描述
使用三元组(a,b,c)来表示状态,分别代表三个瓶子中现有的可乐体积。

import sys
from collections import deque


def parse_nums(nums_str):
    return [int(x) for x in nums_str.strip().split()]

def XtoY(now, volumn, ix, iy):
    x, y = now[ix], now[iy]
    Vx, Vy = volumn[ix], volumn[iy]
    if Vy - y >= x:  # 如果可以倒空x
        y += x
        x = 0
    else:  # 否则可以倒满y
        x = x - (Vy - y)
        y = Vy
    now[ix], now[iy] = x, y
    return now

def bfs(s, n, m):
    res = [[[-1 for _ in range(m + 1)] for _ in range(n + 1)] for _ in range(s + 1)]
    res[s][0][0] = 0
    queue = deque()
    queue.append((s, 0, 0, 0))
    avg = s / 2
    while queue:
        a, b, c, t = queue.popleft()
        # 0,1,2分别代表三个瓶子,例如[0,1]代表把a瓶倒入b瓶,[1,2]代表把b瓶倒入c瓶
        directions = [[0, 1], [1, 0], [0, 2], [2, 0], [1, 2], [2, 1]]
        for d in directions:
            (na, nb, nc), nt = XtoY([a, b, c], [s, n, m], *d), t + 1
            if res[na][nb][nc] == -1:
            	# 如果找到了平分状态
                if (na == avg and nb == avg) or (na == avg and nc == avg) or (nb == avg and nc == avg):
                    return nt
                res[na][nb][nc] = nt
                queue.append((na, nb, nc, nt))
    return -1

for nums in sys.stdin:
    if nums == '0 0 0':
        break
    s, n, m = parse_nums(nums)
    res = bfs(s, n, m)
    print(res if res != -1 else 'NO')

LC279. Perfect Squares

在这里插入图片描述

本题可以把状态定义为自然数,搜索的起点为0,终点为n,而允许转移的方向为所有小于等于n的完全平方数。

from collections import deque
import math

def bfs(n):
    res=[-1 for _ in range(n+1)]
    res[0]=0
    queue=deque()
    queue.append((0,0))
    # 从[1,根号n]遍历,它们的平方就得到所有小于等于n的平方数,都可以作为转移方向
    directions=[i**2 for i in range(1,int(math.sqrt(n))+1)]
    while queue:
        a,t=queue.popleft()
        for d in directions:
            na,nt=a+d,t+1
            if 0<=na<=n and res[na]==-1:
                if na==n:return nt
                res[na]=nt
                queue.append((na,nt))
    return -1
    
class Solution:
    def numSquares(self, n: int) -> int:
        if n==0:return 0;
        return bfs(n)

本题也可以用动态规划来做,把dp[i]定义为数字i需要的最小平方数。PS:BFS比DP快很多。

class Solution {
public:
    int numSquares(int n) {
        if(n<=0)return 0;
        vector<int> dp(n+1,INT_MAX);
        dp[0]=0;
        for(int i=1;i<=n;i++){
            for(int j=1;j*j<=i;j++){
                dp[i]=min(dp[i],dp[i-j*j]+1);
            }
        }
        return dp[n];
    }
};

阿里2020.04.01笔试第一题——二进制串的翻转问题

给一个二进制串,每次操作可以选择翻转其中任意一位,但同时它的左边和右边的1个位置也会进行翻转,翻转是指0变为1,1变为0。例如对于 11011 的第三个位置翻转,会得到 10101。若翻转位置在最左侧,则只有右侧相邻元素跟着翻转,若翻转位置在最右侧,则只有左侧相邻元素跟着翻转。

问题是,给定一个二进制传,求能不能通过有限次翻转使得所有位置都为0,若可以,求出最少需要使用的操作次数,若不可以,输出“NO”。

第一行输入整数T,代表有T组测试数据,接下来T行,每行是一个二进制传。对于每一组测试数据,输出最少操作次数,或输出“NO”

示例:

输入:
2
01
001

输出:
NO
1

答:本题在leetcode上有更高级一点的类题,转化为全零矩阵的最少反转次数

在这里插入图片描述参考评论区答案:评论区帖子

import sys
from collections import deque

def parse_nums(nums_str):
    return [int(x) for x in nums_str.strip().split()]

def bit2int(bits):
    # 将二进制串转为int
    # 事实上如果只需要二进制串与int一一对应,只需要写成sum(cell << i for i, cell in enumerate(bits))
    # 此时相当于将二进制串先逆序,然后再转为int
    return sum(cell << (len(bits) - i - 1) for i, cell in enumerate(bits))

def bit_not(num, i):
    # num的从右到左第i位取反(i从0开始)
    return num ^ (1 << i)

def transfer(num, i, left=True, right=True):
    # left表示对i的左边也翻转,right表示对right的右边也翻转
    if left and right:
        num = bit_not(bit_not(bit_not(num, i + 1), i), i - 1)
    elif left and not right:
        num = bit_not(bit_not(num, i + 1), i)
    elif right and not left:
        num = bit_not(bit_not(num, i), i - 1)
    return num

def bfs(start, bit_len):
    queue = deque()
    queue.append((start, 0))
    seen = {start}
    while queue:
        curr, t = queue.popleft()
        for i in range(bit_len):
            # i<bit_len-1表示除了i=bit_len-1,其他时候都需要对i的右边也翻转;
            # i>0表示除了i=0,其他时候都需要对i的右边也翻转;
            next, nt = transfer(curr, i, i < bit_len - 1, i > 0), t + 1
            if next not in seen:
                if next == 0:
                    return nt
                queue.append((next, nt))
                seen.add(next)
    return 'NO'

# 基本输入输出
for line in sys.stdin:
    for _ in range(int(line)):
        nums = [int(x) for x in input().strip()]
        start = bit2int(nums)
        print(bfs(start, len(nums)))

发布了67 篇原创文章 · 获赞 27 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/xpy870663266/article/details/105084223