目录
一、前言
二、DFS与排列组合
1、DFS:自写排列算法1
(1)基础模板
(2)基于(1)输出前n个数任意m个都全排列
2、DFS:自写排列算法2(这个写法更常见)
(1)从小到大打印 n 个数的全排列
(2)从小到大打印n个数中任意m个数的排列
3、DFS:自写组合算法
4、例题——迷宫(2017年省赛,lanqiaoOJ题号641)
5、例题——寒假作业(2016年省赛,lanqiaoOJ题号1388)
(1)简单做法 (超时)
(2)自写全排列
三、DFS连通性判断
1、例题——全球变暖(2018年省赛,lanqiaoOJ题号178)
一、前言
该篇博文主要讲利用Python实现排列组合与连通性判断。
二、DFS与排列组合
求排列的系统函数,例如 Python 的 permutations()。
在某些场景下,permutations() 不能用,需要手写代码实现排列。本讲给出例题。
手写排列组合代码、暴力法、二进制法。
1、DFS:自写排列算法1
(1)基础模板
让第一个数不同,得到 n 个数列
把第 1 个和后面每个数交换。
1 2 3 4 5 ...... n
2 1 3 4 5 ...... n
......
n 2 3 4 5 ...... 1
这 n 个数列,只要第一个数不同,不管后面 n-1 个数怎么排,这 n 个数列都不同。
在上面的每个数列中,去掉第一个数,对后面的 n-1 个数进行类似的排列。例如从第 2 行的 { 2 1 3 4 5 ..... n } 进入第二层。先去掉首位 2,然后对 n-1 个做排列
1 3 4 5 ...... n
3 1 4 5 ...... n
......
n 3 4 5 ...... 1
这 n-1 个数列,只要第一个数不同,不管后面 n-2 个数怎么排,这n-1个数列都不同。这是递归的第二层。(自顶向下:这就是 DFS 的思想)
def dfs(s,t):
if s==t: #递归结束,产生一个全排列
print(a[0:n])
else:
for i in range(s,t+1):
a[s],a[i]=a[i],a[s] #交换
dfs(s+1,t) #缩小范围
a[i],a[s]=a[s],a[i] #恢复
a=[1,2,3,4,5,6,7,8,9]
n=3
dfs(0,n-1) #求前n个数的全排列
上面的代码为什么这样写,自己画棵树出来看看就明白其中的逻辑了。很多时候递归代码为什么没看懂也写不出来,就是因为你不会画树也不理解树。
但上面的代码有一个致命的缺点:不能按从小到大的顺序打印排列。
(2)基于(1)输出前n个数任意m个都全排列
如前4个数任选3个。
def dfs(s,t):
#global cnt
if s==3: #递归结束,产生一个全排列
print(a[0:3])
#cnt+=1
else:
for i in range(s,t+1):
a[s],a[i]=a[i],a[s] #交换
dfs(s+1,t) #缩小范围
a[i],a[s]=a[s],a[i] #恢复
#cnt=0
a=[1,2,3,4,5,6,7,8,9]
n=4
dfs(0,n-1) #求前n个数的全排列,C43A33
#print(cnt)
2、DFS:自写排列算法2(这个写法更常见)
(1)从小到大打印 n 个数的全排列
例:前 3 个数的全排列
def dfs(s,t):
if s==t:
print(b[0:n]) #递归结束,输出一个全排列
else:
for i in range(t):
if vis[i]==False:
vis[i]=True
b[s]=a[i] #存排列
dfs(s+1,t)
vis[i]=False
a=[1,2,3,4,5,6,7,8,9]
b=[0]*10 #记录生成的一个全排列
vis=[False]*10 #记录第i个数是否用过
n=3
dfs(0,n)
(2)从小到大打印n个数中任意m个数的排列
def dfs(s,t):
if s==3:
print(b[0:3]) #递归结束,输出一个排列
else:
for i in range(t):
if vis[i]==False:
vis[i]=True
b[s]=a[i] #存排列
dfs(s+1,t)
vis[i]=False
a=[1,2,3,4,5,6,7,8,9]
b=[0]*10 #记录生成的一个全排列
vis=[False]*10 #记录第i个数是否用过
n=4
dfs(0,n) #前n个数的排列
3、DFS:自写组合算法
dfs 时,选或不选第 k 个数,就实现了各种组合
例(1) :打印二进制数。以打印 000~111 为例
(如果要反过来打印,只需要交换 8、10 行)
vis=[0]*10
def dfs(k): #深搜到第k个
if k==3:
for i in range(3):
print(vis[i],end='')
print()
else:
vis[k]=0 #不选第k个
dfs(k+1) #继续搜下一个
vis[k]=1 #选第k个
dfs(k+1) #继续搜下一个
dfs(0)
例(2):打印组合。
以 3 个数 {1, 2, 3} 为例,把上面的代码与需要打印的数列结合
def dfs(k): #深搜到第k个
if k==3:
for i in range(3):
if vis[i]==1:
print(a[i],end='')
print()
else:
vis[k]=0 #不选第k个
dfs(k+1) #继续搜下一个
vis[k]=1 #选第k个
dfs(k+1) #继续搜下一个
vis=[0]*10
a=[1,2,3,4,5,6,7,8,9,10]
dfs(0)
4、例题——迷宫(2017年省赛,lanqiaoOJ题号641)
【问题描述】
给出一个迷宫,问迷宫内的人有多少能走出来。迷宫如下:其中 L 表示向左走,R 表示向右走,U 表示向上走,D 表示向下走。
函数 dfs(i, j):
判断从坐标点 (i,j) 出发,是否能走出去。
能走出去:返回1
否则:返回0
dfs():递归,在每个点,它根据指示牌向上、下、左、右四个方向走。
dfs() 结束的条件:
1)走出了迷宫,返回1。
2)走不出迷宫,返回0。
什么情况下走不出迷宫?
兜圈子,回到了曾经走过的点。
用 vis[i][j] 记录点 (i, j) 是否曾经走过,如果走过,就是兜圈子。
def dfs(x,y):
if x<0 or y<0 or x>=10 or y>=10:
return 1
if vis[x][y]==1:
return 0
vis[x][y]=1
if mp[x][y]=="L":
return dfs(x,y-1)
if mp[x][y]=="R":
return dfs(x,y+1)
if mp[x][y]=="U":
return dfs(x-1,y)
if mp[x][y]=="D":
return dfs(x+1,y)
mp=[[''*10] for i in range(10)] #二维矩阵存迷宫
for i in range(10):
mp[i]=list(input()) #读迷宫
ans=0
for i in range(10):
for j in range(10):
vis=[[0]*10 for _ in range(10)] #初始化vis[][]
if dfs(i,j)==1:
ans+=1
print(ans)
迷宫有 n 行 n 列,做一次 dfs(),最多需要走遍所有的点,即 O(n^2) 次;
每个点都做一次 dfs(),总复杂度 O(n^4)。
能优化吗?
- 用不着对每个点都做一次 dfs()。
- 从一个点出发,走过一条路径,最后走出了迷宫,那么以这条路径上所有的点为起点,都能走出迷宫;
- 若这条路径兜圈子了,那么这条路径上所有的点都不能走出迷宫。如果对路径进行记录,就能大大减少计算量。
5、例题——寒假作业(2016年省赛,lanqiaoOJ题号1388)
(1)简单做法 (超时)
用 permutations() 函数,生成所有的排列,检查是否合法
运行时间极长!
13个数的排列:13!= 6,227,020,800
可能需要数小时
(2)自写全排列
不需要生成一个完整排列。例如一个排列的前 3 个数,如果不满足 “□+□=□”,那么后面的 9 个数不管怎么排列都不对。这种提前终止搜索的技术叫 “剪枝”。
def dfs(num):
global ans
if num==13:
if b[10]==b[11]*b[12]:
ans+=1
return
if num==4 and b[1]+b[2]!=b[3]:
return #剪枝
if num==7 and b[4]-b[5]!=b[6]:
return
if num==10 and b[7]*b[8]!=b[9]:
return
for i in range(1,14):
if not vis[i]:
b[num]=i
vis[i]=1
dfs(num+1)
vis[i]=0
ans=0
b=[0]*15
vis=[0]*15
dfs(1)
print(ans)
三、DFS连通性判断
连通性判断:图论的一个简单问题,给定一张图,图由点和连接点的边组成,要求找到图中互相连通的部分。
1、例题——全球变暖(2018年省赛,lanqiaoOJ题号178)
【题目描述】
你有一张某海域 NxN 像素的照片,"." 表示海洋、"#" 表示陆地,如下所示:
其中 "上下左右 " 四个方向上连在一起的一片陆地组成一座岛屿。例如上图就有 2 座岛屿。由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。具体来说如果一块陆地像素与海洋相邻 (上下左右四个相邻像素中有海洋),它就会被淹没。例如上图中的海域未来会变成如下样子:
请你计算:照片中有多少岛屿会被完全淹没。照片保证第 1 行、第 1 列、第 N 行、第 N 列的像素都是海洋。
【输入描述】
第一行包含一个整数 N (1<N<1000)。以下 N 行 N 列代表一张海域照片。
【输出描述】
输出一个整数表示答案。
连通性问题,计算步骤:
- 遍历一个连通块 (找到这个连通块中所有的'#',标记已经搜过,不用再搜);
- 再遍历下一个连通块……;
- 遍历完所有连通块,统计有多少个连通块。
- 什么岛屿不会被完全淹没?若岛中有个陆地 (称为高地),它周围都是陆地,那么这个岛不会被完全淹没。
- 用 DFS 搜出有多少个岛(连通块),检查这个岛有没有高地,统计那些没有高地的岛 (连通块) 的数量,就是答案。
- 计算复杂度:每个像素点只用搜一次且必须至少搜一次,共 N^2 个点,DFS 的复杂度是 O(N^2),不可能更好了。
- 从图上任意一个点 u 开始遍历,标记 u 已经搜过。
- 递归 u 的所有符合连通条件的邻居点。
- 递归结束,找到了与 u 连通的所有点,这是一个连通块。
- 不与 u 连通的、其他没有访问到的点,继续用上述步骤处理,找到所有的连通块。
import sys
sys.setrecursionlimit(60000) #设置递归深度,否则不能通过100%的测试
def dfs(x,y):
d=[(0,1),(0,-1),(1,0),(-1,0)]
global flag
global vis
global mp
vis[x][y]=-1
if mp[x][y+1]=='#' and mp[x][y-1]=='#' and mp[x+1][y]=='#' and mp[x-1][y]=='#':
flag=1
for i in range(4):
nx=x+d[i][0]
ny=y+d[i][1]
if vis[nx][ny]==0 and mp[nx][ny]=='#':
dfs(nx,ny)
n=int(input())
mp=[]
for i in range(n):
mp.append(list(input()))
vis=[]
for i in range(n):
vis.append([0]*n)
ans=0
for i in range(n):
for j in range(n):
if vis[i][j]==0 and mp[i][j]=='#':
flag=0
dfs(i,j)
if flag==0:
ans+=1
print(ans)
以上,DFS排列组合与连通性
祝好