目录
前言
本文是笔者的学习笔记,记述的是我对搜索与图之间关系的个人见解,若有谬误还请读者谅解。
人工智能中的搜索
人工智能中的搜索不同于平时大家提到的搜索引擎的搜索,它是一种解决问题的算法。
1 定义
对于一个问题,在给出一系列状态和状态跳转的约束条件下求出从初始状态到最终状态的路径的算法。
2 状态
状态的定义我们根据例子来讲解。
例如,在汉诺塔问题中,移动圆盘前后的每一步中,三个柱子上的圆盘数量和大小就是一个状态。
图
1 定义
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
图中的连接线可以有向,也可以无向。图中的连接线可以有权重。
2 无向图
无向图是指节点与节点之间的边没有方向的图。
为了之后的算法,我们将无向图抽象为一个Ruby类Udigraph,将无向图中的节点抽象为一个Ruby类Point。
无向图类Udigraph:
class Udigraph #无向图的类,本类的实例表示一个无向图
attr_accessor :point #Point
def initialize
@point=Hash.new
end
def [](id) #重载 #这里的id与point的id同步
if @point[id]!=nil
return @point[id]
else
return false
end
end
def []=(id,point)
@point[id]=point
point.id=id
return
end
def length
return @point.length
end
def find(id) #查找图中的节点
@point.each do |m|
if m.id=id
p m.id
return m
end
end
end
def findall? #这个图里的所有节点都访问过了?
@point.each_key do |m|
unless @point[m].visited?
return false
end
end
return true
end
def next(id) #返回第一个相邻的未访问的节点
@point.keys.each do |i|
unless @point[i].visited? #此节点未被访问
if @point[i].neighbor? id
return @point[i]
end
end
end
return false
end
end
节点类Point:
class Point #无向图节点的类,本类的实例表示一个无向图中的结点
attr_accessor :adjoin #邻接表 1为相邻 0为不相邻 #这是一张哈希表
attr_accessor :value #值
attr_accessor :id #编号 从0开始 #没有被加入进无向图的节点编号为-1
attr_accessor :visited #访问过与否
def initialize(adj,val)
@adjoin=adj
@value=val
@visited=false
@id=-1
end
def visited? #访问过了?
return @visited
end
def neighbor?(id) #是否与节点id是邻居?
if adjoin[id]==1
return true
else
return false
end
end
def neighbor #邻节点个数
count=0
@adjoin.each_key do |m|
if(@adjoin[m]==1) then
count=count+1
end
end
return count
end
end
3 无向图的遍历
图的遍历方式与树有相似之处(可参考http://theodor.top/article/63)。但是,由于无向图中节点与节点之间的连接是无向的,所以可能存在遍历中重复访问节点的情况。因此,我们需要判定图中的某个节点是否被访问过,可以用一个visited数组存储。
4 深度优先遍历
可以参考树的先根遍历,避免不必要的口舌我们直接上方法:
def preOrder(udigraph,point) #深度优先遍历一张无向图,point为开始的节点
if udigraph.findall?
return
end
if point==false
return
end
print point.value #输出节点的值,并标记此节点为访问过
point.visited=true
(0...point.neighbor).each do
preOrder(udigraph,udigraph.next(point.id)) #找到下一个相邻的点然后继续遍历
end
end
5 广度优先遍历
同样可以参考数的层序遍历。由于Ruby中没有队列,我们先构造一个队列类Queue。
class Queue #队列类,用于广度优先遍历
def initialize
@data=[]
@length=-1
end
def push(data)
@length=@length+1 #自加
@data[@length]=data
end
def front
if @length<0
return false
end
return @data[0]
end
def pop
if @length<0
return false
end
if @length==0
@data=[]
else
@data=@data[1..@length]
end
@length=@length-1
end
def size
return @length+1
end
def empty?
return @length==-1?true:false
end
def ergodic #用于测试的方法
p @data
end
end
则我们的广度优先遍历算法为:
def levOrder(udigraph,point) #广度优先遍历
queue=Queue.new
queue.push point #起点入栈
while !queue.empty? do
top=queue.front #取出一个节点
top.visited=true
print top.value
queue.pop
loop do #当其还有邻节点时
next_=udigraph.next(top.id)
if next_==false
break "break"
else
next_.visited=true #进入队列了就是被访问了
queue.push next_ #将其邻节点入队
end
end
end
end
#6 用于测试的方法和数据
如下所示。其中,source1为图中节点的值,source2为图中节点的邻接表。通过修改source1与source2,我们能够得出一组新的测试数据。
#赋值部分---------------------------------------------------------------------------------------------------------------
source1=[1,2,3,4,5] #节点值
source2=[{2=>1,3=>1,4=>1,1=>1},{0=>1,2=>0,3=>0,4=>0},{0=>1,1=>0,3=>1,4=>1},{0=>1,1=>0,2=>1,4=>0},{0=>1,1=>0,2=>1,3=>0}]
$udi=Udigraph.new
(0...5).each do |m|
$udi[m]=Point.new(source2[m],source1[m])
end
preOrder($udi,$udi[0])
levOrder($udi,$udi[0])
将图引入搜索中
我们可以将搜索中的状态转换成图的一个个节点,而将其状态相互跳转约束条件的满足与否作为判定此图中节点连接与否的条件(也就是说,当节点一可以通过一步到达节点二,我们就将节点一与节点二相连)。
通过如上的操作,我们可以把搜索算法转化为“无向图的可达问题”,即,是否有路径从初始状态所属的节点到目标状态所属的节点。
1 算法实现
我们设在搜索中判定是否能由一步从状态1到状态2的方法为reach?,其接受参数Point p1(状态1),Point p2(状态2)。
很明显,reach?是一个随着问题变化而变化的方法。
作为整个算法核心的搜索方法search,由深度优先遍历或者广度优先遍历改造而来。其接受三个参数Point orig(起始状态),Point dest(目标状态),Route r(Route为一个表示状态变化路径的类)。
我们创建一个继承Udigraph类的子类,用于描述搜索图的Sudigraph,将reach?、reach!、search作为其实例函数。由于搜索图在搜索到目标状态的过程中会不止一次经过节点,用于表示节点是否访问过的方法将被放在Route类中,以表示“在此路径中节点是否被访问过”。
2 Route类
如下:
class Route #搜索路线的类
#对于这个类,我并不会试图去回溯它,而是在情况出现分歧的时候复制它
attr_accessor :route
attr_accessor :step
def initialize
@route=[] #路线,为Point的数组,其下标为步数
@step=0 #长度
end
def copy(cp) #复制
@route=Array.new cp.route
@step=cp.step
return self
end
def push(data) #加入一个新状态
@route[@step]=data
@step=@step+1
end
def visited?(id) #id所属的点是否在路径内(被访问过)
if @route.index(id)
return true
end
return false
end
def say #输出路线
@route.each do |m|
print m,"=>"
end
puts "end"
end
end
3 reach?
在本步,我们并不给出其实际代码。但我们能给出reach?方法的标准:其接收两个参数p1与p2,应为本搜索图实例所拥有的Point的value(这样说也许比较抽象,也就是接收的两个参数是图的节点的值)。返回true或者false。
事实上,为了类的扩展性,可以考虑将reach?以字符串的形式作为类的属性传递(当然,作为属性的reach?没有最后的问号),然后构造一个同名方法使用eval执行。也就是如下:
def reach?(p1,p2)
eval @reach
end
4 reach!
很明显,搜索图内的点是否相邻要由reach?算出。reach!方法会对图中所有的点求邻点,并赋值给Point.adjoin(Point类的邻接数组)。
def reach!
@point.values.each do |i|
@point.values.each do |m|
unless i==m #排除遍历到自己的时候
if reach? i.value,m.value #可达的情况下,加入邻接数组
i.adjoin[m.id]=1
end
end
end
end
end
5 search
search也被放在Udigraph类中。我们设定采用广度优先搜索(深度优先搜索也可以由图中的深度优先遍历改写而成,并不困难)。
def search(orig,dest,r) #这是广度优先搜索
if (orig.value==dest.value) #抵达终点
r.say #输出路径
@count=@count+1
end
if r.step==1
p orig.value
p dest.value
p r
end
orig.adjoin.each_key do |m| #寻找所有邻点,再发起一步搜索
unless r.visited? m #这个点没有访问过
r.push m
search(@point[m],dest,Route.new.copy(r))
end
end
end
6 Sudigraph类
之前我们提到过,Sudigraph类继承了Udigraph类,并且其实例变量reach指向的是确定一步从状态一到状态二可达与否的方法。
class Sudigraph<Udigraph #继承无向图类的搜索图类
attr_reader :count
def initialize(reach) #reach是提供给搜索图的判定可达方法
@reach=reach #reach是一个判断p1是否能一步到达p2的方法
@point=Hash.new
@count=0 #可达的路径数目
end
def reach?(p1,p2)
eval @reach
end
def reach!
@point.values.each do |i|
@point.values.each do |m|
unless i==m #排除遍历到自己的时候
if reach? i.value,m.value #可达的情况下,加入邻接数组
i.adjoin[m.id]=1
end
end
end
end
end
#reach! #确定所有点的连接性
def search(orig,dest,r) #这是广度优先搜索
if (orig.value==dest.value) #抵达终点
r.say #输出路径
@count=@count+1
end
orig.adjoin.each_key do |m| #寻找所有邻点,再发起一步搜索
unless r.visited? m #这个点没有访问过
r.push m
search(@point[m],dest,Route.new.copy(r))
end
end
end
end
例1 野人过河问题
河的两岸有三个传教士和三个野人需要过河,目前只有一条能装下两个人的船,在河的任何一方或者船上,如果野人的人数大于传教士的人数,那么传教士就会被野人攻击,怎么找出一种安全的渡河方案呢?
设状态为(传教士在起点河岸的人数,野人在起点河岸的人数,船的位置(起点为1,终点为0)),则初始状态为(3,3,1),目标状态为(0,0,0)。
1 32种可能的穷举
实际上,可能的类别就是(0..3).each,(0..3).each,(0..1).each。这三个变量之间在这一步中并没有相互之间的限制条件。
def river
re=Array.new
(0..3).each do |m|
(0..3).each do |n|
(0..1).each do |i|
re.push {"savage"=>m,"preacher"=>n,"boat"=>i} #一个关于野人、传教士、船的字典
end
end
end
$source1=re #赋给全局变量source1,之后由其实例化节点
end
2 reach方法
作为reach方法,我们需要考虑两个维度:1.人的数量(+-1..2)与船的数量(+-1)是否在允许范围内。作为有效的一步,其船只数量和人数必然会同时发生变化。2.到达这一步时,是否会发生题设中的“野人攻击传教士”情况,也就是某一个河岸中的野人数量大于传教士数量。
def reachForriver(p1,p2)
if p1["boat"]+p2["boat"]!=1 or p1["savage"]+p1["preacher"]==p2["savage"]+p2["preacher"] #船或者人的情况不符合
return false
end
[p1,p2].each do |m| #是否发生攻击事件?
if(!(m["preacher"]==0 or m["preacher"]==3) and m["preacher"]!=m["savage"])
#当传教士不在同一岸时,只要有一岸的传教士人数不等于野人的人数,就会发生野人攻击传教士的局面
#由于我们在生成状态时没有检查,所以对起点与终点都要执行检查 #其实当起点合法时这没有必要
return false
end
end
change=p1["savage"]-p2["savage"]+p1["preacher"]-p2["preacher"] #这一步变更的人数
if change.abs>2 #船超载了吗?
return false
end
return true
end
#3 赋值与计算
我的答案是一共有461条路径,但是没有找到真正的答案是多少。
river #遍历出所有状态
$sudi=Sudigraph.new x #赋值给搜索图
(0...$source1.length).each do |m|
$sudi[m]=Point.new(Hash.new,$source1[m])
end
$sudi.reach! #作出邻接哈希表
$orig={"preacher"=>3,"savage"=>3,"boat"=>1} #起点
$dest={"preacher"=>0,"savage"=>0,"boat"=>0} #终点
$sudi.search($sudi[$source1.index($orig)],$sudi[$source1.index($dest)],Route.new) #搜索
print "搜索完毕,共有#{$sudi.count}条路径。"
例2 8数码问题
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。
给出一个初始状态,一个目标状态,求初始状态到目标状态经过的所有状态。
1 9!种可能的穷举
STICK=[0,1,2,3,4,5,6,7,8]
$source1=Array.new
def all8(step,stick,p) #步数,已使用过的数组,目前的状态
if step==9 #穷举完了,返回
$source1.push p
return
else
STICK.each do |s| #对每个可以选取的元素调用all8
unless stick.index s #这个元素还未被选取
all8(step+1,stick+[s],p+s.to_s) #进一步遍历
end
end
end
end
八数码问题一共有9!(362880)个状态。在穷举状态时,第一个输入为all8(0,[],””)。
2 reach方法
def reachFor8num(p1,p2) #八数码问题的reach方法
index=p1.index "0"
p=[]
[0,2,4,8].each do |m| #四方向移动 #这里使用块传递会更好些,但是为了扩展性我们直接写出来
box=String.new p1 #使用拷贝值,防止更改
case m
when 8
if index>2
a=box[index-3]
box[index-3]=box[index]
box[index]=a
end
when 2
unless [0,3,6].index index
a=box[index-1]
box[index-1]=box[index]
box[index]=a
end
when 4
unless [2,5,8].index index
a=box[index+1]
box[index+1]=box[index]
box[index]=a
end
when 0
if index<6
a=box[index+3]
box[index+3]=box[index]
box[index]=a
end
end
p.push box #可移动情况下压入移动后的状态
end
p.compact! #去除nil元素
p.each do |m|
if m==p2
return true
end
end
return false
end
#3 计算
限于Ruby的速度以及笔者的水平问题,这道题我没有做实际计算(因为怎么想都会花费相当长的时间,并且递归是否会导致栈溢出呢?)。