第1关:盲目搜索之宽度优先搜索算法
任务描述
本关任务:给定迷宫地图以及在迷宫中的起始位置,利用宽度优先搜索算法求解走出迷宫的最短路径长度,走出迷宫意味着达到迷宫地图的边界(所有位置下标0开始)。
例如以下4×4地图中,0表示墙,1表示可行走的路,起始位置为(2,1),则走出迷宫的一条最短路径为(2,1)→(2,0),步长为1。
0 1 0 1
0 1 1 1
1 1 0 0
0 1 0 1
相关知识
为了完成本关任务,你需要掌握:1.宽度优先搜索,2.一致代价搜索,3.求解思路。
宽度优先搜索
宽度优先搜索 Breadth-First-Search 是一种简单的搜索策略,首先扩展根结点,接着扩展根结点的所有后继结点,然后再扩展它们的后继,以此类推。一般地,在下一层的任何结点扩展之前,搜索树上本层深度的所有节点都应该被扩展过。示例如下:
宽度优先搜索每次总是扩展深度最浅的结点,这可以通过先进先出队列 FIFO, First In First Out 来实现,也就是将新结点添加到队列尾部,这也意味着浅层的老结点会在深层结点之前被扩展。算法伪代码如下:
一致代价搜索
宽度优先搜索是每一步的行动代价一致的搜索策略,因为它总是扩展深度最浅的未扩展节点。当单步代价不一致时,宽度优先搜索则不是最优的搜索策略。一致代价搜索 Uniform-Cost-Search 扩展的是路径消耗g(n)最小的节点n,通过将边缘结点按g值排序的优先队列实现,因此可以很好的解决单步代价不一致的问题。算法伪代码如下:
求解思路
设迷宫地图中的起点为(x,y),并以该点为根结点,上下左右四个相连的点为后继结点,由此构建一棵4叉树,然后从根结点开始层次遍历,最先达到边界的合法路径的步长即为答案。
编程要求
本关的编程任务是补全右侧代码片段 solveMaze 中 Begin 至 End 中间的代码,具体要求如下:
- 在 solveMaze 中,利用宽度优先搜索算法,求解走出迷宫的最短步数,并返回结果(无解返回 0 )。
测试说明
平台将自动编译补全后的代码,并生成若干组测试数据,接着根据程序的输出判断程序是否正确。
以下是平台的测试样例:
测试输入: 4 4 0 1 0 1 0 1 1 1 1 1 0 0 0 1 0 1 2 1
预期输出: 1
输入格式: 第 1 行:n 行 m 列,1<=n,m<=10 第 2~n+1 行:迷宫地图 第 n+2 行:起点(x,y),地图左上是(0,0),右下是(n-1,m-1) 输出格式: 最短步数(无解输出 0 )
代码
# -*- coding:utf-8 -*-
class Maze:
def __init__(self, map, n, m, x, y):
self.ans = 0 #最短步长结果
self.map = map #迷宫地图map[0,n-1][0,m-1](下标0开始)
self.n = n #迷宫地图行数n
self.m = m #迷宫地图列数m
self.x = x #起点,行坐标(下标0开始)
self.y = y #起点,列坐标(下标0开始)
class Solution:
def solveMaze(self, maze):
"""求解迷宫问题
:type: maze: class Maze #迷宫的数据结构类
:rtype: maze.ans: int #返回最短路径长度
"""
minpath=[] # 存储最短路径长度的列表
dir=[[1,0],[-1,0],[0,-1],[0,1]] # 上下左右四个方向
row=maze.n # 迷宫的行数
clm=maze.m # 迷宫的列数
sx=maze.x # 起点的行坐标
sy=maze.y # 起点的列坐标
vis=[[False for i in range(clm+1)] for i in range(row+1)] # 记录每个位置是否已经访问过
queue=[(sx,sy,0)] # 初始化队列,存储当前位置和步数
vis[sx][sy]=True # 标记起点已访问
while queue: # 当队列不为空时循环
t=queue.pop() # 弹出队首元素
tx=t[0] # 当前位置的行坐标
ty=t[1] # 当前位置的列坐标
ans=t[2] # 当前步数
for i in range(4): # 遍历上下左右四个方向
if 0<=tx+dir[i][0]<row and 0<=ty+dir[i][1]<clm: # 如果新位置在迷宫内
dx,dy=tx+dir[i][0],ty+dir[i][1] # 计算新位置的坐标
if not vis[dx][dy] and maze.map[dx][dy]: # 如果新位置未访问且可通行
queue.append((dx,dy,ans+1)) # 将新位置和步数加入队列
vis[dx][dy]=True # 标记新位置已访问
if dx==0 or dx==row-1 or dy==0 or dy==clm-1: # 如果新位置在迷宫边界上
minpath.append(ans+1) # 将当前步数加入最短路径列表
if minpath:return min(minpath) # 如果最短路径列表不为空,返回最小值
else: return 0 # 如果最短路径列表为空,返回0
第2关:盲目搜索之深度优先搜索算法
任务描述
本关任务:利用深度优先搜索算法的核心思想,求解N皇后问题的方案总数,其中N∈[1,10]。
N皇后问题是国际象棋的扩展,在N×N的棋盘上放置N个皇后,使得每一个皇后都不会攻击到其余的任一皇后(皇后可以攻击和它在同一行、同一列或同一对角线上的任何棋子)。
相关知识
为了完成本关任务,你需要掌握:1.深度优先搜索算法,2.深度受限搜索,3.迭代加深的深度优先搜索,4.求解思路。
深度优先搜索算法
深度优先搜索 Depth-First-Search 总是扩展搜索树的当前边缘结点中最深的节点。搜索很快的推进到搜索树的最深处的结点,该结点称之为叶子结点,它没有后继,当这些结点扩展完之后,就从边缘结点中去掉,然后回溯到下一个未扩展后继的深度稍浅的结点,搜索过程如下图所示。
与宽度优先搜索使用 FIFO 队列不同,深度优先搜索使用 LIFO, Last In First Out 的队列结构,即最新生成的结点最早被选择扩展,因此,为了实现方便,深度优先搜索算法一般抛弃 LIFO 队列而采用递归式结构实现。
深度受限搜索
在无限状态空间里,深度优先搜索可能会陷入无解的分支里跳不出来。深度受限搜索 Depth-Limited-Search 通过设置一个深度界限l来避免此问题,即深度为l的结点被当做为没有后继的叶子结点。
虽然深度界限解决了无穷路径的问题,但是该算法是不完备的。假设正确解在深度为d的结点,若设置的l小于d,则目标结点的深度超过了深度限制,那么该算法无法得到目标解,若设置的l大于d,深度受限搜索则不是最优的。深度优先搜索可以看作是特殊的深度受限搜索,其受限深度l=∞。
迭代加深的深度优先搜索
迭代加深的深度优先搜索 Iterative-Deepening-Search 结合了深度受限搜索,它不断的增大深度限制,首先是l=0,接着为1,然后为2,以此类推,直到找到目标解,即当深度界限达到d时,最浅的目标结点被找到。迭代加深的深度优先搜索结合了深度优先搜索和宽度优先搜索的优点。
求解思路
N皇后问题是一个典型的有限的树搜索空间求解问题。用增量形式化方法描述为:
- 状态:棋盘上 0-N 个皇后的任一摆放都是一个状态;
- 初始状态:NxN 的棋盘上没有皇后;
- 行动:在棋盘上的任一空格摆放一个皇后;
- 转移模型:将增加了一个皇后的棋盘返回;
- 目标测试:N 个皇后都在棋盘上,并且无法互相攻击。
以 8 皇后为例,这种形式化方法需要验证64×63×⋯×57个棋盘状态序列,即使在行动中增加皇后摆放的限制,棋盘状态序列的数量仍然是巨大的。
设棋盘某一位置为(i,j),将第i+1行的N个空格位置都作为(i,j)的后继结点,那么N×N的棋盘则可以通过一个根结点连接为一棵N叉搜索树,棋盘第i行对应搜索树深度为i的结点层,树的根结点到叶子节点的路径序列就是一个棋盘的状态序列,应用深度优先搜索算法可以非常方便的求解。
考虑到皇后的属性,可以增加一些限制条件来加快深度优先搜索的过程,剪掉一些无解的搜索路径。对问题具体分析如下:
-
每一行只能放置一个皇后,那么对这一行其余的结点就不需要放置皇后了,也就是上面的建树过程,只以下一行棋盘空格作为后继结点,忽略本行其余空格;
-
每一列只能放置一个皇后,下次搜索时就不要搜索已经放置过皇后的列了;
-
每一斜对角线只能放置一个皇后。
棋盘的行可以用树的深度来表达,因此每一行只有一个皇后,她们不会同行。棋盘的列可以用一个长度为N的一维数组来记录是否放置过皇后。观察棋盘的行和列的特点,可以发现如下规律:
列的索引减去行的索引: 列的索引加上行的索引:
0 1 2 3 4 ... 0 1 2 3 4 ...
-1 0 1 2 3 ... 1 2 3 4 5 ...
-2 -1 0 1 2 ... 2 3 4 5 6 ...
-3 -2 -1 0 1 ... 3 4 5 6 7 ...
-4 -3 -2 -1 0 ... 4 5 6 7 8 ...
... ... ... ... ... ... ... ... ...
棋盘的左斜线\
可以用一个数字表达,因此所有的左斜线可以用一个长度为2N−1的一维数组来记录是否放置过皇后,同理,右斜线/
也可以用一个长度为2N−1的一维数组来记录是否放置过皇后。
编程要求
本关的编程任务是补全右侧代码片段 solveNQueens 和 DFS 中 Begin 至 End 中间的代码,具体要求如下:
-
在 solveNQueens 中,利用 DFS 函数实现N皇后问题求解,并返回方案总数;
-
在 DFS 中,利用深度优先搜索算法的思想,递归求解N皇后问题。
测试说明
平台将自动编译补全后的代码,并生成若干组测试数据,接着根据程序的输出判断程序是否正确。
以下是平台的测试样例:
测试输入:1
预期输出:1
输入格式:整数,表示皇后数量。
输出格式:整数,表示N皇后放置方案总数。
代码
# -*- coding:utf-8 -*-
class Solution:
def __init__(self, n=0):
self.vis = [[]] #用于标记是否存在皇后的二维列表(初始值全为0)
self.ans = 0 #用于存储答案(N皇后方案数,初始值0)
self.n = n #用于存储皇后数量n
def solveNQueens(self):
"""求解N皇后问题(调用self.DFS函数)
:rtype: self.ans: int #返回N皇后放置方案数
"""
#请在这里补充代码,完成本关任务
#********** Begin **********#
# 产生一个3行50列的数组,第一行代表左斜线,第二行代表所在的列,第三行代表有索引,初始值都为0
self.vis = [[0 for j in range(50)] for i in range(3)]
self.DFS(1,self.n) # 递归
return self.ans
#********** End **********#
def DFS(self, row, n):
"""深度优先搜索N皇后问题的解空间
:type: row: int #NxN棋盘的第row行
:type: n: int #皇后数量n
:rtype: None #无返回值
"""
#请在这里补充代码,完成本关任务
#********** Begin **********#
if row == n+1: # 行数大于皇后的个数,说明这个方案可行,方案数加1,递归结束(递归结束条件),
self.ans += 1
return
for i in range(1,n+1,1):
# vis[0][row-i+n] == 0 row-i即行的索引减去列的索引,左斜线等于0,左斜线没有放置皇后,+n是为了防止产生负数
# vis[1][i] == 0 第i列为0
# vis[2][row+i] == 0 即所在的右斜线上并没有存放皇后,row+i即行索引加上列索引,同一右斜线的行索引加列索引所得的值相等。
if self.vis[0][row-i+n] == 0 and self.vis[1][i] == 0 and self.vis[2][row+i] == 0: # 第row行第i列可以存放皇后
self.vis[0][row-i+n] = self.vis[1][i] = self.vis[2][row+i] = 1 # 将对应的列,左斜线、右斜线都置为1
self.DFS(row+1,n) # 存放下一个皇后
# 如果下一个皇后的位置无法正确放置,则调整当前皇后的放置位置
self.vis[0][row-i+n] = self.vis[1][i] = self.vis[2][row+i] = 0
# 继续本次循环,知道找到本行的列中有合适的存放位置为止,如果本行无合适位置,则继续返回上一层
#********** End **********#