文章目录
学习与参考资料:
[1] 2013王道论坛计算机考研机试指南
[2] DFS和BFS讲解及Leetcode刷题小结(1)(JAVA)
[3] DFS和BFS讲解及Leetcode刷题小结(2)(JAVA)
[4] AcWing立体推箱子题解
写在前面:
对于状态的定义不要局限于地图的坐标,在地图题中,正确定义状态后才能解决“立体推箱子”以及“允许对称转移的迷宫问题”等题,而在非地图题中,连地图都没有,所以状态的定义就很关键了。
DFS
地图类
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. 地牢大师
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. 立体推箱子
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笔试第二题——允许对称转移的迷宫问题
写完上一题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)))