回溯法/深度优先遍历的简单优化技巧

深度优先遍历配合回溯,是解决不少问题的好方法,好比八皇后问题。 python

皇后的排布规则:n个皇后放在n*n的矩阵里,要求一列只有一个,一行只有一个,任一斜线上只有一个(/和\算法

一般,咱们会把皇后做为一个数组,行号做为数组的下标,而列号是数组元素的值,由此,二维平面的排布问题就成了一维数组的求解,配合检验函数以及回溯,就能够求解了。 数组

这里,我使用一个状态表(一个好的状态表能够比检验函数要有效率的多)来维护某个行可能的填入皇后的列号,每次放下皇后或者拿起皇后,都会对状态表进行更新。按行号的顺序依次安置皇后,安放完最后一个皇后,就获得了问题的一个解。 函数

可是这会带来一个问题,那就是求解的难度会随着皇后的数量增长,耗时会剧增,差很少是O(n^3)的程度。 优化

因此,咱们要找一个能够更快解决问题的方法。 spa

注意,八皇后问题的一个隐含的条件是:咱们能够在任意位置安放皇后,没有次序的要求。可是当咱们抽象到二维数组的时候,每每会忽略这一点。如今,考虑到这个状况,咱们有以下解决方案: scala

当咱们放下一个皇后以后,能够从剩余的全部行中,选择一个候选列号最少的,进行下一步的安置。 指针

这样作的好处时,能够大大减弱搜索树的规模,这种减弱越靠近根部,就越明显。 code

所以,咱们能够加入一个交换的函数。固然,这样一来,咱们的状态表里就应该加上关于行号和列号的记录了。 排序

最终代码以下:


num=8

class Queen(object):
	def __init__(self,n):
		self.lct= n
		self.prs=-1
		self.cdt=[1 for i in range(num)]

def Count(q):
	s=0
	for i in range(num):
		if q.cdt[i]>0:s+=1
	return s

def FindIt(q):
	u=q.prs+1;
	while u<num and q.cdt[u]<=0:u+=1
	if u<num:
		q.prs=u
		return True
	return False

def Settle(q,n):
	x=q[n].lct
	y=q[n].prs
	for i in range(n+1,num,1):
		p=q[i].cdt
		p[y]-=1
		a=q[i].lct-x
		b=y-a
		if b>=0 and b<num:p[b]-=1
		b=y+a
		if b>=0 and b<num:p[b]-=1

def Pickup(q,n):
	x=q[n].lct
	y=q[n].prs
	for i in range(n+1,num,1):
		p=q[i].cdt
		p[y]+=1
		a=q[i].lct-x
		b=y-a
		if b>=0 and b<num:p[b]+=1
		b=y+a
		if b>=0 and b<num:p[b]+=1

def Select(q,n):
	j,k=0,num+1
	for i in range(n,num):
		t=Count(q[i])
		if k>t:j,k=i,t
	if j!=n:q[n],q[j]=q[j],q[n]

def ShowIt(q):
	for i in range(num):
		for j in range(num):
			if q[j].lct==i:
				for k in range(num):
					if q[j].prs==k:
						print '*',
					else:
						print '-',
				print ''
	print ''

def Locate1():
	q=[Queen(i) for i in range(num)]
	i=0
	j=0
	while 1:
		if q[i].prs<0:
			Select(q,i)
		else:
			Pickup(q,i)
		if FindIt(q[i]):
			if i<num-1:
				Settle(q,i)
				i+=1
			else:
				j+=1
				yield j
				#ShowIt(q)
		else:
			q[i].prs=-1
			i-=1
			if i<0:break

def Locate2():
	q=[Queen(i) for i in range(num)]
	i=0
	j=0
	while 1:
		if q[i].prs>=0:Pickup(q,i)
		if FindIt(q[i]):
			if i<num-1:
				Settle(q,i)
				i+=1
			else:
				j+=1
				yield j
				#ShowIt(q)
		else:
			q[i].prs=-1
			i-=1
			if i<0:break
	
if __name__=='__main__':
	q=[Queen(i) for i in range(num)]
	import time
	
	t=time.time()
	Locate1().next()
	print 'once cost %.6f'%(time.time()-t)
	
	print '-----------------'
	
	t=time.time()
	Locate2().next()
	print 'once cost %.6f'%(time.time()-t)
算法很简单,也没有注释。我是用一个列表同时保存了皇后的行号、列号和状态表。当皇后放下后,状态表表示她放下时的全部可能位置(也就是放下后就不更新了),未放下的皇后的状态表会不断更新。一个指针,该指针左边都是放下的,右边都是未放下的,指针所指的皇后元素,是要进行挪动的那个。


那么,结果呢?以下:


(08)     92 ,   0.027 <=>   0.024 , ----- <=> ------
(09)    352 ,   0.113 <=>   0.096 , ----- <=> ------
(10)    724 ,   0.467 <=>   0.432 , ----- <=> ------
(11)   2680 ,   1.919 <=>   1.956 , ----- <=> ------
(12)  14200 ,   9.786 <=>  10.647 , ----- <=> ------
(13)  73712 ,  52.080 <=>  59.131 , ----- <=> ------
(14) 365596 , 326.946 <=> 462.586 , ----- <=> ------
(15) ------ , ------- <=> ------- , ----- <=> ------
(16) ------ , ------- <=> ------- , 0.002 <=>  0.175
(17) ------ , ------- <=> ------- , 0.002 <=>  0.103
(18) ------ , ------- <=> ------- , 0.003 <=>  0.770
(19) ------ , ------- <=> ------- , 0.002 <=>  0.053
(20) ------ , ------- <=> ------- , 0.005 <=>  4.075
(21) ------ , ------- <=> ------- , 0.004 <=>  0.195
(22) ------ , ------- <=> ------- , 0.002 <=> 36.618
(23) ------ , ------- <=> ------- , 0.003 <=>  0.563
(24) ------ , ------- <=> ------- , 0.003 <=>  9.280
(25) ------ , ------- <=> ------- , 0.013 <=>  1.157
(26) ------ , ------- <=> ------- , 0.022 <=>  9.391
(27) ------ , ------- <=> ------- , 0.008 <=> 11.390
(28) ------ , ------- <=> ------- , 0.003 <=> 74.833
(29) ------ , ------- <=> ------- , 0.029 <=> 42.061
(30) ------ , ------- <=> ------- , 0.018 <=> ------
(40) ------ , ------- <=> ------- , 0.158 <=> ------
(50) ------ , ------- <=> ------- , 0.040 <=> ------
(60) ------ , ------- <=> ------- , 0.032 <=> ------
(70) ------ , ------- <=> ------- , 0.077 <=> ------
(80) ------ , ------- <=> ------- , 0.054 <=> ------
(90) ------ , ------- <=> ------- , 2.162 <=> ------


上表的最左边是皇后的个数;第一栏是解的数量;随后的一对数据依次是优化和非优化版本的求所有解的耗时;最后一对数据依次是优化和非优化版本的求第一个解的耗时。

可见这种优化的效果仍是很好的。(虽然小数量是反而慢一些,但这是必然的,算法越精巧,也就要作越多的处理操做,天然会在处理小规模数据时不利)

这种改进,对于没有次序要求的深度搜索/回溯求解很是有效,好比,数独。

本质上,这种方法和A*算法有些殊途同归,一个是旨在“剪枝”,一个则使用经验函数“抄近路”,而它们的使用上,其实都是同样的:对全部可能的下一步进行排序以后,选择效果最好/概率最高的进行。

相关文章
相关标签/搜索