目录
一、DFS搜索基础
1、dfs简介
搜索算法:穷举问题解空间部分/全部情况,从而求出问题的解
深度优先搜索:①:本质上是暴力枚举
②:尽可能一条路走到底,走不了再回退。(一条路走到底,走不了再回退)
2、dfs和n重循环
数字拆分的例子
给定一个数字x,将其拆分成3个正整数,后一个要求大于等于前一个,给出方案。
最简单的思路:三重循环暴力求解。
当将数字拆分为n个数字时,显然使用n重循环是不合理的。
n重循环 = 特定的数形结构 = dfs搜索
直接上代码
import sys
sys.setrecursionlimit(100000)
x, n = map(int, input().split())
li = []
def dfs(depth):
# depth:表示拆分的第depth个数字
if depth == n:
if sum(li) == x:
# 判断是否符合后面的大于等于前面的数字
for i in range(n - 1):
if li[i] > li[i + 1]:
return
else:
continue
# 答案
print(li)
return
return
for i in range(1, x + 1):
# 加入元素
li.append(i)
dfs(depth + 1)
# 恢复现场
li.pop()
dfs(0)
"""
输入样例:7 3
输出样例:
[1, 1, 5]
[1, 2, 4]
[1, 3, 3]
[2, 2, 3]
"""
代码的优化:
x, n = map(int, input().split())
li = []
# 优化的过程多加了一个形参,保证当前选的数字大于等于前一个数字
def dfs(depth, val):
if sum(li) > x:
return
if depth == n:
if sum(li) == x:
print(li)
return
for i in range(val, x + 1):
li.append(i)
dfs(depth + 1, i)
li.pop()
# 拆分的数字是正整数所以val传入的数字是1而非0
dfs(0, 1)
数字拆分例题变形
题目分析:
①、 由于数字的范围不大直接使用dfs即可
②、本题与上面例题的区别就在于没有规定拆分数字的个数, 并在输出中加入了等号。
③、直接看代码代码的注释比较详细
import locale
n = int(input())
# li:用来存储拆分的数字
li = []
def dfs(depth, val):
# depth:表示拆分的第的普惠个数字,
# val与上一个例题优化的代码相似,为了保证当下拆分的数字大于等于前面拆分的数字
if depth == n:
if sum(li) == n:
print('+'.join(map(str, li)))
return
return
# 在下面的循环中注意是不包含n个(有题目的样例输出可以知道)
for i in range(val, n):
# 选择第i个数字
li.append(i)
dfs(depth + 1, i)
li.pop()
# 不选择第i个数字
dfs(n, val)
dfs(0, 1)
dfs和n重循环的模版
由于dfs并没有通用的模版,因此只能给出相对模糊的模版概念
def dfs(depth):
"""
:param depth: 当前为第几重循环
:return:
"""
if depth == n:
# n重循环最内层执行的代码
return
# 每重循环进行的枚举选择
dfs和n重循环的典型例题
下面的代码运行的较慢,由于本题是一个填空题,只需要在本地跑出结果再提交即可。
li = []
ans = 0
def dfs(depth):
# depth:表示第depth个小朋友
if depth == 7:
x = 0
y = 0
# 统计方案的第一种和第二种糖果的总数量,如果符合题目则ans += 1
for x_d, y_d in li:
x += x_d
y += y_d
if x == 9 and y == 16:
global ans
ans += 1
return
return
# 枚举第一种糖果
for i in range(6):
for j in range(6):
# 枚举第二种糖果
if 2 <= i + j <= 5:
li.append((i, j))
dfs(depth + 1)
li.pop()
dfs(0)
print(ans)
"""
运行的结果:5067671
"""
代码的优化:
代码进行了优化后比上面的运行速度快10倍不止。
ans = 0
def dfs(depth, n, m):
# depth:表示第depth个小朋友
# n:表示第一种糖果
# m:表示第二种糖果
if depth == 7:
if n == 0 and m == 0:
global ans
ans += 1
return
# 枚举第一种糖果
for i in range(6):
# 枚举第二种糖果
for j in range(6):
# 根据题目:每个小朋友得到的糖果总数最少为2个最多为个
if 2 <= i + j <= 5 and n - i >= 0 and m - j >= 0:
dfs(depth + 1, n - i, m - j)
dfs(0, 9, 16)
print(ans)
"""
运行结果:5067671
"""
最后一题:
这一题类似为集合问题,直接看题目
本题给的代码并不能通过全部的样例,大概只能通过65%
直接看代码,通过代码来理解做题的思路
import sys
sys.setrecursionlimit(10000)
inpurt = sys.stdin.readline
def dfs(depth, weight, ant):
# depth:表示第depth个瓜
# weight:表示买的瓜的重量
# ant:表示劈瓜的次数
# 剪枝:当质量大于所要的质量m直接返回
if weight > m:
return
global ans
# 统计答案最少劈的次数
if weight == m:
ans = min(ans, ant)
if ans < ant:
return
# 递归出口
if depth == n:
return
# 不买
dfs(depth + 1, weight + 0, ant)
# 买一半
dfs(depth + 1, weight + a[depth] // 2, ant + 1)
# 买一个
dfs(depth + 1, weight + a[depth], ant)
n, m = map(int, input().split())
# 在这里使用特殊的技巧,让质量乘2,避免质量为奇数时除以2,出现精度误差
m *= 2
a = list(map(int, input().split()))
a = [x * 2 for x in a]
# ans:初始化表示每个瓜都劈一刀
ans = n + 1
dfs(0, 0, 0)
if ans == n + 1:
ans = -1
print(ans)
二、回溯法
1.回溯法简介
回溯:就是dfs的一种,在搜索尝试过程中寻找问题的解,当发现已经不满足要求条件时,就,“回溯”返回,尝试别的路径。
回溯更强调:此路不通,另行他路,走过的路需要打标记
回溯法是一般dfs的好基础上加上一些剪枝策略。
2、回溯模版——求排列
排列要求数字不重复——每次选择的数字需要打标记——vis数组
要输出当排列——记录路径——path数组。
回溯:先打标记,记录路径、然后下一层,回到上一层,清除标记。
代码模版:
def dfs(depth):
# depth:第depth个数字
if depth == n:
print(li)
return
for i in range(1, n + 1):
if vis[i] == 0:
li.append(i)
vis[i] = 1
dfs(depth + 1)
vis[i] = 0
li.pop()
n = int(input())
# vis:数组
vis = [0] * (n + 1)
li = []
dfs(0)
"""
输入样例:3
输出样例:
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]
"""
根据上面的模版来道例题
上面这个全排列问题就是全排列数的模版,通过例题课题看出全排列数的用处
n = int(input())
lis = []
vis = [0] * (n + 1)
li = [0] * n
def dfs(depth):
if depth == n:
for i in li:
print('{:>5}'.format(i), end='')
print()
return
for i in range(1, n + 1):
if vis[i] == 0:
vis[i] = 1
li[depth] = i
dfs(depth + 1)
vis[i] = 0
dfs(0)
3、回溯模版——求组合
排列和组合的不同之处:组合与顺序无关,排列和顺序有关
组合的代码模版:
# 在n个数中选出k个数
n, k = map(int, input().split())
li = []
vis = [0] * (n + 1)
def dfs(depth, val):
# depth:表示第depth个数字
# val:为了避免顺序而考虑的形参
if depth == k:
print(li)
return
# 上面的代码中遇到过多次相似的情况,以下就不写注释了。
for i in range(val, n + 1):
if vis[i] == 0:
vis[i] = 1
li.append(i)
dfs(depth + 1, i + 1)
vis[i] = 0
li.pop()
dfs(0, 1)
来个例题熟悉一下组合模版
这个题就是组合的版子题,只是在熟悉一下组合的模版
# 在n个数中选出k个数
n, k = map(int, input().split())
li = []
vis = [0] * (n + 1)
def dfs(depth, val):
# depth:表示第depth个数字
# val:为了避免顺序而考虑的形参
if depth == k:
for i in li:
print('{:>3}'.format(i), end='')
print()
return
# 上面的代码中遇到过多次相似的情况,以下就不写注释了。
for i in range(val, n + 1):
if vis[i] == 0:
vis[i] = 1
li.append(i)
dfs(depth + 1, i + 1)
vis[i] = 0
li.pop()
dfs(0, 1)
4、集合问题
子集的问题相对于组合和排列用的更多,子集的模版特别简单直接看代码
n = int(input())
li = list(map(int, input().split()))
# lis:用来存储答案
lis = []
def dfs(depth):
# depth:表示第depth个数
if depth == n:
print(lis)
return
# 选第depth个
lis.append(li[depth])
dfs(depth + 1)
lis.pop()
# 不选depth个
dfs(depth + 1)
dfs(0)
"""
输入样例:
3
1 2 3
输出样例:
[1, 2, 3]
[1, 2]
[1, 3]
[1]
[2, 3]
[2]
[3]
[]
"""
我们来看一个有关子集的题目
import sys
sys.setrecursionlimit(5000)
def check(il):
S = s.copy()
for j in il:
if op[j][0] == 1:
x = op[j][1]
v = op[j][2]
S[x] = (S[x] + v) % 10
else:
x = op[j][1]
y = op[j][2]
S[x], S[y] = S[y], S[x]
if S == t:
print('Yes')
sys.exit()
def dfs(depth):
# depth:表示的depth个元素
if depth == k:
check(lis)
return
for i in range(k):
if vis[i] == 0:
lis.append(i)
vis[i] = 1
dfs(depth + 1)
vis[i] = 0
lis.pop()
dfs(k)
n = int(input())
s = list(map(int, list(input())))
t = list(map(int, list(input())))
k = int(input())
op = []
for i in range(k):
op.append(list(map(int, input().split())))
vis = [0] * (k + 1)
lis = []
dfs(0)
print('No')
5、二选一问题,二进制
模版
n = int(input())
vis = [0] * n
def dfs(depth):
# depth:第depth个数
if depth == n:
print(vis)
return
# 第depth个选0
dfs(depth + 1)
# 第depth个选1
vis[depth] = 1
dfs(depth + 1)
vis[depth] = 0
dfs(0)
"""
输入样例:3
输出样例:
[0, 0, 0]
[0, 0, 1]
[0, 1, 0]
[0, 1, 1]
[1, 0, 0]
[1, 0, 1]
[1, 1, 0]
[1, 1, 1]
"""
上一个例题来例题来理解这个算法怎使用
这题的官方题解好像使用的位运算来做的,当然我们完全可以使用上面的模版进行解题。这个代码相对于来说比较好理解,我就没有加注释,读者可自行阅读。
n = int(input())
a = list(map(int, input().split()))
ant = 0
m = 0
def check(li):
ans = 0
for i in range(n):
# 当为1是往左走,0时则相反。
if li[i] == 1:
ans = (ans - a[i]) % 7
else:
ans = (ans + a[i]) % 7
if ans == 0:
# ant:表示所以符合题目要求的方案数。即可以回到原点
global ant
ant += 1
vis = [0] * n
def dfs(depth):
if depth == n:
# m为所有可能出现的方案数
global m
m += 1
check(vis)
return
vis[depth] = 0
dfs(depth + 1)
vis[depth] = 1
dfs(depth + 1)
dfs(0)
print('{:.4f}'.format(ant / m + 0.0000001))
接下来来一个特别经典的题目。
6、N皇后问题
通过上图可以,发现i和j在主对角线和副对角线的规律。因此可以构造vis1标记数组来表示主对角线,vis2表示副对角线的标记数组。
不太好理解可以看一下面这个视频N皇后问题。
N = int(input())
def dfs(x):
# x表示第x层皇后,也可是说是第x行的皇后
# 如过能够走到最后一层则答案加1。
if x == N:
global ans
ans += 1
return
# 第x层的皇后枚举每一列
for y in range(N):
# 当前的坐标为(x, y),要求当前列,当前主对角线,副对角线不能被攻击到
if vis[y] == 0 and vis1[x - y + N] == 0 and vis2[x + y] == 0:
# 标记为1
vis[y] = 1
vis1[x - y + N] = 1
vis2[x + y] = 1
dfs(x + 1)
# 恢复现场
vis[y] = 0
vis1[x - y + N] = 0
vis2[x + y] = 0
# 三个标记数组分别表示列,主对角线,副对角线。
vis = [0] * N
vis1 = [0] * (2 * N)
vis2 = [0] * (2 * N)
# ans:用来存储答案。
ans = 0
dfs(0)
print(ans)
三、联通块问题
联通快问题是比较常见的问题,直接通过题目来体会这一类的问题
题目链接:小怂爱水洼
上图这题算是最为基础的联通块问题,重点就在于打标记。
import sys
sys.setrecursionlimit(100000)
# 联通块问题
n, m = map(int, input().split())
Map = [list(map(int, input().split())) for _ in range(n)]
vis = [[0] * m for _ in range(n)]
def dfs(x, y):
# 打标记,避免被重复遍历
vis[x][y] = 1
global ans
ans += Map[x][y]
# 扩展:把联通的一个块全部遍历,进行标记
for x_d, y_d in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
xx = x_d + x
yy = y_d + y
# 特判:是否过界,是否被标记,是否在符合题目条件
if 0 <= xx < n and 0 <= yy < m and vis[xx][yy] == 0 and Map[xx][yy] != 0:
dfs(xx, yy)
ant = 0
for i in range(n):
for j in range(m):
# 如果没有被标记, 并符合题目中的条件
if vis[i][j] == 0 and Map[i][j] != 0:
ans = 0
dfs(i, j)
# 找出最大值
ant = max(ant, ans)
print(ant)
在来一题
题目链接:全球变暖
import sys
# 一定要设置递归深度
sys.setrecursionlimit(100000)
input = sys.stdin.readline
# dfs的目的是判断是否为高地,以及打标记
def dfs(x, y):
vis[x][y] = 1
# 题目中描述,岛屿的边缘全是会被淹没的,所以地图的边缘全是。
# 不用去考虑越界情况
# 如果Map上下左右全部为# 则说明此处为高地
if Map[x - 1][y] == '#' and Map[x][y - 1] == '#' and Map[x + 1][y] == '#' and Map[x][y + 1] == '#':
global ok
ok = 1
# 扩展:
for d_x, d_y in [(1, 0), (0, 1), (0, -1), (-1, 0)]:
xx = d_x + x
yy = d_y + y
if Map[xx][yy] == '#' and vis[xx][yy] == 0:
dfs(xx, yy)
N = int(input().strip())
Map = [list(input().strip()) for _ in range(N)]
vis = [[0] * N for _ in range(N)]
ans = 0
for i in range(N):
for j in range(N):
if vis[i][j] == 0 and Map[i][j] == '#':
# ok = 0表示不是高地
# ok = 1表示是高地
ok = 0
dfs(i, j)
if ok == 0:
ans += 1
print(ans)
四、迷宫染色问题
import sys
input = sys.stdin.readline
sys.setrecursionlimit(1000000)
def dfs(x, y, color, vis):
vis[x][y] = color
for d_x, d_y in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
xx = d_x + x
yy = d_y + y
if 0 <= xx < n and 0 <= yy < m and Map[xx][yy] == '.' and vis[xx][yy] == 0:
dfs(xx, yy, color, vis)
def check(x, y):
ans = 0
for x_d, y_d in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
xx = x_d + x
yy = y_d + y
if 0 <= xx < n and 0 <= yy < m:
ans |= vis1[xx][yy]
if ans == 3:
print('Yes')
sys.exit()
else:
return 0
n, m = map(int, input().split())
A, B, C, D = map(int, input().split())
A -= 1
B -= 1
C -= 1
D -= 1
Map = [list(input().strip()) for _ in range(n)]
# vis1:起点标记数组
vis1 = [[0] * m for _ in range(n)]
dfs(A, B, 1, vis1)
if vis1[C][D] == 1:
print('Yes')
sys.exit()
dfs(C, D, 2, vis1)
for i in range(n):
for j in range(m):
if Map[i][j] == '#':
check(i, j)
print('No')