4. 搜索问题的求解

搜索算法的标准设定

过程:

  1. 搜索开始:从搜索树的根节点开始
  2. 节点扩展:访问一个未被访问的但已经被发现的节点
  3. 节点生成:发现新的节点
  4. 目标测试:判断当前节点对应的状态是不是目标状态

实现模块:

  1. 搜索边缘:已经生成但是未被访问的节点集合
  2. 节点扩展:访问一个节点,并枚举它所有邻居节点
  3. 节点生成:将新节点加入搜索边缘

搜索算法的评价

image.png

搜索树中的一些符号

  • b表示分支因子
  • m表示最大搜索高度
  • s目标状态对应的节点(可以为多个)

搜索树中的总节点个数:

1 + b + (b^2) + (b^3) + ... + (b^m) = (1 - b^(m + 1)) / (1 - b) = O(b^m)

评价搜索算法的性质:

  • 完备性:如果存在目标节点,是否可以保证搜索到
  • 最优性:最先找到的目标节点是不是损耗最小的
  • 时间复杂性:生成的节点个数
  • 空间复杂性:需要的存储空间(搜索边缘大小)

树搜索

树搜索无法发现树中的重复结构,因此会产生额外的损耗。

image.png

我们不需要扩展重复节点,因为,我们已经访问过该节点对应的状态了,而且代价更小。

image.png

图搜索

  • 思路:同一个状态相关的节点不进行重复访问
  • 实现:树搜索 + 保存访问过的节点集合(已搜索集合)
  • 更新:每次扩展完一个节点,都更新已搜索集合
  • 使用:在扩展一个节点之前,检查已搜索集合,如果该节点被包含,那就跳过对它的扩展。

搜索算法的大体框架伪代码

树搜索

// problem: 整个搜索问题
// fringe: 存储整个未搜索的节点
function TreeSearch(problem, fringe) return a solution, or failure {
	//压入初始状态到未搜索的节点数据结构中
	fringe = Insert(MakeNode(InitlalState[problem]), fringe) 
	
	// 循环的从该结构中压入或弹出节点进行操作
	loop do{
		//如果未访问的节点为空,还没有找到正确的节点,因此该问题没有解,则返回失败
		if fringe is empty then return failure
		
		//从fringe弹出一个节点
		node = RemoveFront(fringe) 
		//该状态为想要的节点状态,则找到了,直接返回
		if GoalTest(problem, STATE[node]) then return node
		
		//扩展,生成和该节点相关的邻居节点,挨个遍历,然后将其插入到未访问的节点fringe数据结构中
		for childNode in Expand(State[node], problem) {
			fringe = Insert(childNode, fringe)
		}
	}
}

将上面的树搜索转换未图搜索, 引入一个closed数据结构,该结构未一个集合,用来保存已经访问过的节点,然后在加入到fringe中的时候查看下是否已经被访问过了,如果被访问过了,就不需要重复去访问了。

// problem: 整个搜索问题
// fringe: 存储整个已发现但是还未搜索访问过的节点
function GraphSearch(problem, fringe) return a solution, or failure {
	closed = an empty set
	//压入初始状态到未搜索的节点数据结构中
	fringe = Insert(MakeNode(InitlalState[problem]), fringe) 
	
	// 循环的从该结构中压入或弹出节点进行操作
	loop do{
		//如果未访问的节点为空,还没有找到正确的节点,因此该问题没有解,则返回失败
		if fringe is empty then return failure
		
		//从fringe弹出一个节点
		node = RemoveFront(fringe) 
		//该状态为想要的节点状态,则找到了,直接返回
		if GoalTest(problem, STATE[node]) then return node
		
		if STATE[node] is not in closed then
			add STATE[node] to closed
			//扩展,生成和该节点相关的邻居节点,挨个遍历,然后将其插入到未访问的节点fringe数据结构中
			for childNode in Expand(State[node], problem) {
				fringe = Insert(childNode, fringe)
			}
	}
}

深度优先搜索(DFS)

在上面的算法框架中,从fringe中找一个节点来访问的策略不同,则有不同的搜索算法。选择边缘中深度最大的节点进行搜索(距离根节点步数量最多)为深度优先搜索DFS

  • 搜索边缘(fringe):保存已经生成但未访问(扩展)节点的数据结构
  • 扩展(RemoveFront):从搜索边缘中选择一个节点进行扩展,获得它所有的邻居节点
  • 生成(Insert):将一个新的节点插入到搜索边缘中
  • 搜索策略:选择边缘中深度最大的节点进行搜索(距离根节点步数量最多)
  • 实现:采用后进先出的栈来维护fringe实现。

深度优先算法DFS的指标

image.png

  • DFS生成的节点个数(时间复杂度):如果最大树高M是有限的,则O(b^m)。
  • DFS需要的搜索边缘的大小(空间复杂度):每层需要存储b个节点, 则O(b^m)。
  • DFS是完备的吗:不完备,树高m可能是无限的,它会朝着一个深度往下搜。
  • DFS是最优的吗:不,他会找到“最左边”的解,不管这个解的树高是多少。

广度优先搜索(BFS)

选择边缘中深度最小的节点进行扩展(距离根节点步数最少)为深度优先搜索DFS

  • 搜索边缘(fringe):保存已经生成但未访问(扩展)节点的数据结构
  • 扩展(RemoveFront):从搜索边缘中选择一个节点进行扩展,获得它所有的邻居节点
  • 生成(Insert):将一个新的节点插入到搜索边缘中
  • 搜索策略:选择边缘中深度最小的节点进行扩展(距离根节点步数最少)
  • 实现:采用先进先出的队列来维护fringe实现。

广度优先算法BFS的指标

image.png

  • BFS生成的节点个数(时间复杂度):
    • 假设树高最小的目标节点在s层,需要扩展所有树高大于等于s的节点
    • 需要生成的节点个数的复杂度为:O(b^(s + 1))
    • 为什么是s + 1? 因为虽然在s层,但是s+1层已经生成了
  • BSF需要的搜索边缘的大小(空间复杂度):边缘需要保存生成的最后一层的所有节点,即O(b^(s+1))
  • BFS是完备的吗?是
  • BFS是最优的吗?是(前提是每一步的损耗都是一样的)

迭代加深算法(IDS)

  • 深度优先(DFS)的空间复杂度更小
  • 广度优先算法的时间复杂度更小,而且更健壮(完备且最优)

image.png

结合这两个的优点,可以得到一个算法,迭代加深算法,因为深度优先算法如果树很深就会一直往深度去搜,因此可以给其加一个深度限制,当搜索到限制的深度后在搜索其他的,就不要在继续往下了。

  • 设定最大搜索深度为1,运行深度优先搜索算法,未发现目标
  • 设定最大搜索深度为2,运行深度优先搜索算法,未发现目标
  • 审定最大搜索深度为3, ...

一致代价搜索(UCS)

前面的算法默认每一步的代价都是一样的,但是大部分情况可能每一步的代价各不相同,比如如下: image.png

  • 搜索边缘(fringe):保存已经生成但未访问(扩展)节点的数据结构
  • 扩展(RemoveFront):从搜索边缘中选择一个节点进行扩展,获得它所有的邻居节点
  • 生成(Insert):将一个新的节点插入到搜索边缘中
  • 搜索策略:选择边缘中代价最小的节点(从根节点到当前节点的累积代价)
  • 实现:采用优先队列来维护fringe实现。

image.png

一致代价搜索(UCS)的指标

image.png

image.png

搜索算法对比

image.png