回溯法/深度優先遍歷的簡單優化技巧

深度優先遍歷配合回溯,是解決不少問題的好方法,好比八皇后問題。 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*算法有些殊途同歸,一個是旨在「剪枝」,一個則使用經驗函數「抄近路」,而它們的使用上,其實都是同樣的:對全部可能的下一步進行排序以後,選擇效果最好/概率最高的進行。

相關文章
相關標籤/搜索