聊聊深度优先搜索算法及应用

Posted by 甘家城 on 2018-03-31 Viewed times

之前看算法书对于深搜一直是一个坎,有点难以理解或者理解了一些但没法下手写,这回来重新探讨一下个人目前的理解及解决方案。文章从全排入手,到解决八皇后,和尚妖怪过河等问题。

首先,看一个全排序,传入一个数组(其中有n个可不等长的数组),得到的结果是其中每个数组取一个数组成的新数组的集合,如下

result=[0]*3
arr=[[1,2,3],[4,5],[7,8,9]]
res=[]
#算法部分,遍历其中每个数组,在result保存临时一个数组。
#copy是因为append只是插入了一个类似指针,而result内容会变,因此要用copy换一个地址。
def dfs(arr, depth):
    global res
    for i in range(len(arr[depth])):
        result[depth] = arr[depth][i]
        if depth != len(arr) - 1:
            dfs(arr, depth + 1)
        else:
            res.append(result.copy())
dfs(arr,0)
print(res)
#结果为
[[1, 4, 7], [1, 4, 8], [1, 4, 9], [1, 5, 7], [1, 5, 8], [1, 5, 9], [2, 4, 7], [2
, 4, 8], [2, 4, 9], [2, 5, 7], [2, 5, 8], [2, 5, 9], [3, 4, 7], [3, 4, 8], [3, 4
, 9], [3, 5, 7], [3, 5, 8], [3, 5, 9]]

在来看个升级版,一个数组如[1,2,3,4,5]的全排,这里for中递归的思路则和上次基本不一样,用的是交换,遍历完所有可能的交换值,但因为只有一个数组,所以交换完保存数后还得在交换回来,也就是递归下面一行和上面一行的区别。

COUNT=0  
res=[]

#for第一次是0和所有交换,递归后是1和所有的交换
#但运行过程则会0和0换,之后跳到递归中1和1换,直到最后,得到第一情况,也就是没换,这时begin=end
#然后回退一步,那时begin应该是end-1,所以最后两个换下,又得到一种情况。
#就这样回退到最初的for,就可以遍历所有交换情况
def perm(n,begin,end):  
    global COUNT,res
    if begin>=end:  
        res.append(n)
    else:  
        i=begin  
        for num in range(begin,end):  
            n[num],n[i]=n[i],n[num] 
            perm(n.copy(),begin+1,end)
            n[num],n[i]=n[i],n[num]

N=4
n=[i for i in range(N)]
perm(n,0,len(n))
print(res)

然后看几个应用,基本也是自己学了之后用上去!

第一个,九宫格中文输入得到输入项的所有组合。用到上面第一个全排序,只需要把输入转换成已按按键的字母数组。如

arr=[[a,b,c],[c,d,e],[g,h,i]]

第二个,八皇后,也就是8*8棋盘上放8个皇后,每个皇后横竖左斜右斜都不能有其他皇后。总共有92中情况。这里的解法便是先得到长为8的数组的全排,在逐个检验,当然检验是有技巧的。

#这里把数组如[1,2,3,4,5,6,7,8]下标作为棋盘x,值作为棋盘y
#因为横竖都不在线上,所以求全排
#然后检查的就是左斜右斜便可
#全排,请看前面的讲解
COUNT=0  
def perm(n,begin,end):  
    global COUNT,res
    if begin>=end:  
        res.append(n)
    else:  
        i=begin  
        for num in range(begin,end):  
            n[num],n[i]=n[i],n[num] 
            perm(n.copy(),begin+1,end)
            n[num],n[i]=n[i],n[num]
res=[]
N=8
n=[i for i in range(N)]
perm(n,0,len(n))

#检验的是数组中有没有重复值
def check(l):
    return len(set(l))==len(l)

result = []

#左斜的位置x-y相同的会在一条线上,右斜的位置x+y相同的会在一条线上。
#由此检验
for i in res:
    s = [i[j]+j for j in range(N)]
    c = [i[j]-j for j in range(N)]
    if check(s) and check(c):
        print(i)
        result.append(i)
#得到92个解
print(len(result))

最后一个应用:和尚与妖怪过河问题。

问题大致是河左岸有三个妖怪三个和尚,要全部过河到右岸,有一条能载两人的船,只要左岸或右岸妖怪数大于和尚数,妖怪就会把和尚吃掉。需得所有简单解法(中间不包括重复循环步骤)。

关键点和上面的其实是差不多,只是比较难抽象出遍历的东西,一不小心就会死在中间的死循环。

class River():
    def __init__(self):
        self.ship = 1 #1--左岸,-1--右岸
        self.left = [3,3] #和尚,妖怪
        self.right = [0,0] #和尚,妖怪
        #状态改变只有这五种和取反后的五种
        self.change = [ [-1, -1],
                        [-2, 0],
                        [0, -2],
                        [-1, 0],
                        [0, -1]]
        #保存上一个状态,直接排除一样的来回
        self.lastState = -1
        #如果遍历是一棵树,保存的则根到某个枝条
        self.lineHis = [[3, 3, 0, 0, 1]]
    #改变状态
    def move(self,n):
        self.left = [self.left[i]+self.ship*self.change[n][i] for i in range(2)]
        self.right = [self.right[i]-self.ship*self.change[n][i] for i in range(2)]
        self.ship = -self.ship
        self.lastState = n
    #获取下一步可移动点,比如只有一个怪兽在左岸,就要排除两个妖怪过河的方案,这个剪枝很重要。
    def getState(self):
        states = []
        if self.ship == 1:
            state = self.left
        else:
            state = self.right
        for i in range(len(self.change)):
            if min(state[0]+self.change[i][0],state[1]+self.change[i][1])>=0:
                states.append(i)
        return states
    #主算法部分,断掉妖怪大于和尚的分支。
    #在遍历中,最重要的一步就是在这条支线上,如果这一种状态之前出现过,就不要继续深入。
    def dfs(self):
        if self.left==[0,0] and self.right==[3,3]:
            print(self.lineHis)
            return 
        elif (self.left[1]>self.left[0] and self.left[0]!=0) or (self.right[1]>self.right[0] and self.right[0]!=0):
            return
        else:
            states = self.getState()
            for i in states:
                if i != self.lastState:
                    self.move(i)
                    self.lineHis.append(self.left+self.right+[self.ship])
                    if self.lineHis[-1] not in self.lineHis[0:-1]:
                        self.dfs()
                    self.move(i)
                    self.lineHis.pop()
        
if __name__ == "__main__":
    s = River()
    s.dfs()
    #可得4个最简解。

总结:深度优先搜索主要部分是提炼出搜索的树,然后好坏在于剪枝。


版权声明:本文为原创文章,转载请注明出处和作者,不得用于商业用途,请遵守 CC BY-NC-SA 4.0协议。

支付宝打赏 微信打赏

赞赏一下