天天看點

算法求解思路培養-2. 八皇後問題(說說遞歸)

算法求解思路培養-2. 八皇後問題(說說遞歸)

按道理來說,遞歸是很簡單的。例如求斐波那契數的公式,

fibonaci(N) = fibonaci(N-1)+fibonaci(N-2)           

不複雜吧,再比方,階乘公式:

Factorial(N) = N*Factorial(N-1)           

有啥難的呢?

但真正在面試、比賽或者工作中遇到的題目是這樣的。

1、八皇後問題(面試):

在8×8格的國際象棋上擺放八個皇後,使其不能互相攻擊,即任意兩個皇後都不能處于同一行、同一列或同一斜線上,問一共有多少種擺法。

2、遞歸周遊檔案夾(工作)

3、列印一個數組的全排列(面試)

4、棋盤覆寫問題(競賽)

那麼,如何求解這些問題呢?

還是先從簡單的解法入手引出遞歸。下面我們以八皇後來舉例。其暴力求解方法如下:

def is_valid(position):
    dim = len(position)
    for i in range(dim):
        for j in range(i+1,dim):
            if position[i] == position[j]:  #在同一列的情況
                return False
            if abs(position[i]-position[j]) == abs(i-j): #在同一個對角線的情況
                return False
    return True

def eight_queen():
    solution_count = 0
    for c1 in range(0,8):
        for c2 in range(0,8):
            for c3 in range(0,8):
                for c4 in range(0,8):
                    for c5 in range(0,8):
                        for c6 in range(0,8):
                            for c7 in range(0,8):
                                for c8 in range(0,8):
                                    if is_valid([c1,c2,c3,c4,c5,c6,c7,c8]):
                                        solution_count+=1
                                    else:
                                        pass
    return solution_count

print("八皇後解法總數:",eight_queen())def is_valid(position):           

最後程式輸出解法總數為92。

這裡有必要解釋一下is_valid函數,該函數判斷給定的8個棋子布局是否滿足題目中的要求,也就是不能互相攻擊。

大家需要注意的是,eight_queen()程式中,傳遞給is_valid函數的[c1,c2,c3,c4,c5,c6,c7,c8] 代表了第一行在第c1列放皇後,第二行在第c2列放皇後,第三行在第c3列放皇後,以此類推。

也就是說,這種擺法已經避免了出現2個皇後 在同一行的局面。

是以,is_valid函數主要判斷這8個皇後的位置是否在同一列,或者同一條斜線上。

判斷兩個棋子是否在同一列比較簡單,隻需要判斷position[i]是否等于position[j]即可。

那麼,如何判斷兩個棋子是否在同一條斜線上呢?我們探索一些在同一斜線的情形:

算法求解思路培養-2. 八皇後問題(說說遞歸)

      圖1-向右下方的斜線

算法求解思路培養-2. 八皇後問題(說說遞歸)

      圖2-向右上方的斜線

一共有兩種情況,一種是向右下方的斜線,一種向右上方的斜線。

看看向右下方的斜線,

算法求解思路培養-2. 八皇後問題(說說遞歸)

                  圖3-點P右下方的斜線

假設第一個皇後的位置是P,其坐标為(i,j),則向下傾斜時,該斜線上下一個位置的坐标是(i+1,j+1),第二個位置的坐标是(i+2,j+2),不失一般性,第n個位置的坐标是(i+N,j+N)

從上可知,點P和與其在一條斜線上的點K的坐标分别是(i,j),(i+k,j+k),下面嘗試找到這兩個位置的坐标有什麼關聯,也就是找這些幾個整數的關聯,我們從加減乘除的角度來考慮。

先試着把二者的坐标進行相加,得到:

x坐标相加得到結果X= i+i+k = 2*i + k

y坐标相加得到結果Y= j+j+k = 2*j + k

發現2個結果都有k,基于直覺,把二者相減,得到2*(j-i),猜測,是否Y-X是(j-i)的倍數,就說明二者在一條斜線上呢?很可惜猜錯了,例如點(4,2)和(6,6),二者雖然不在一條斜線上,卻有Y-X=2 ,是點(4,2)的y和x的內插補點的倍數。

看起來加法的嘗試失敗了。

減法呢?P和K這兩點的X和Y坐标分别相減,得到

(i+k-i, j+k-j)= (k,k),就是說減法得到的結果,其X和Y值相等。

我們試了幾個不在這條斜線上的點,發現這些點的值與P的x和y坐标相減,得到的X和Y确實不相等,是以,我們認推測如果某點Q(m,n)在(i,j)所在的向下斜線上,其x和y坐标值和點P的x,y坐标值相減後,m-i = n-j,即X和Y相等。

本結論實際上也很容易證明。

假設一個點Q在從P開始的從左到右向下傾斜的線上,其坐标表示為(i+k,j+k),由于這個斜線與Q所在的行隻有Q一個交點(兩條直線最多一個交點),隻需要證明Q所在行的其他點不滿足X=Y即可。

不失一般性,假設與Q在同一行的另一個點S的坐标是(i+k,j+m),這裡m≠k(否則S和Q是同一個點),則S和P坐标相減後,X=i+k-i=k, Y=j+m-j=m,由于剛才的結論m≠k,得到X≠Y。

到這裡,可以得到如下結果:

結論①:一個點K的坐标x和y分别減去P的坐标x和y,如果相等,則說明K在從P出發向右下方傾斜的斜線上。

下面研究向右上方傾斜的斜線。

算法求解思路培養-2. 八皇後問題(說說遞歸)

              圖4-點P向右上方的直線

我們發現K-P的結果是(k,-k),x和y坐标相反,大家也可以結合前面的分析,驗證這個結論:

結論②:一個點K的坐标x和y分别減去P的坐标x和y,如果結果相反,則說明K在從P出發從左到右向上傾斜的斜線上。

彙總結論①和②,得到如下結論:

結論③:一個點K的坐标x和y分别減去P的坐标x和y,如果絕對值相等,則說明K在從P出發的斜線上。

注意:這裡斜線的斜率都是±1(八皇後題目中限制了斜率為±1)

條件判斷代碼很好寫,如下:

if abs(position[i]-position[j]) == abs(i-j): #在同一條斜線的情況
     return False           

下面我們看下主流程代碼:

solution_count = 0
    for c1 in range(0,8):
        for c2 in range(0,8):
            for c3 in range(0,8):
                for c4 in range(0,8):
                    for c5 in range(0,8):
                        for c6 in range(0,8):
                            for c7 in range(0,8):
                                for c8 in range(0,8):
                                    if is_valid([c1,c2,c3,c4,c5,c6,c7,c8]):
                                        solution_count+=1
                                    else:
                                        pass           

同樣分析,這段代碼很容易了解,但是擴充性上就很差了。如果問題改成100個皇後,就得改成100重循環,這可是很大的工作量,更主要的是,代碼顯得太醜陋了。

有什麼解決辦法呢?

我們這樣分析,假設第一行已經放好了一個皇後(這個皇後放哪個列無所謂)。那麼,剩下的七個皇後實際上也要滿足“不能處于同一行、同一列或同一斜線上”的條件,也就是說假設我們有個eight_queen_2函數,實作了八皇後的解法,那麼該函數同樣也能夠實作剩下的七皇後問題,就是說我們完全可以複用這個eight_queen2函數來完成剩下的七皇後問題,這也是遞歸的常見用法。

那麼,這裡的七皇後和八皇後的問題有什麼聯系呢?七皇後除了滿足自身的七個皇後不在同一行、同一列或同一斜線上的條件外,唯一的聯系就是這七個皇後不能和第一行的皇後在同一行、同一列或者統一斜線上。于是,我們得到的遞歸實作如下:

'''八皇後問題'''
 
occupied=[]
dim =8
solutions = 0
def is_valid(x,y,occupied):
    global dim
    for pos in occupied:
        if pos[1] == y or abs(x-pos[0])==abs(y-pos[1]):
            return False
    return True

def sol_eight_queen(cur_row):
    global occupied,solutions
    if cur_row >= dim:
        solutions+=1
        return True
    for j in range(dim):
        if is_valid(cur_row,j,occupied): 
            occupied.append([cur_row,j])
            sol_eight_queen(cur_row+1)
            del occupied[-1]
 
def test_eight_queen():
    sol_eight_queen(0)
    print("total valid answers is:",solutions)           

運作test_eight_queen()函數,程式輸出為92。

下面我們看看代碼。判斷布局是否符合要求的代碼見is_valid程式,代碼比較容易了解,這裡不再詳細介紹。

核心是sol_eight_queen(cur_row)程式。

其中,global eight_queen,occupied,dim,solutions定義了一些全局變量,用全局變量大多數時候不是很好的程式設計習慣,筆者這裡為了簡化代碼編寫采用了全局變量的方式。occupied儲存了從第1行到目前行的王後擺放位置。solutions儲存了可以成功擺放8個王後的布局數,也就是我們要求的結果。

if cur_row == dim:
    solutions += 1
    return True           

這裡是遞歸退出條件,可以了解成當擺放到棋盤的第9行時,認為前面已經成功拜訪完畢前8行的王後。為此我們将solution加1。

for j in range(dim):
        if is_valid(cur_row,j,occupied): 
            occupied.append([cur_row,j])
            sol_eight_queen(cur_row+1)
            del occupied[-1]           

這一段代碼是核心邏輯。

for j in range(dim):

意味着我們對于目前行cur_row的每一列,執行下面的操作:

首先通過is_valid(cur_row,j,occupied)來判斷在第cur_row行的第j列擺放王後,是否和之前擺放的王後沖突,如果沖突,則繼續判斷第j+1列;如果不沖突,則調用如下三行代碼:

occupied.append([cur_row,j])
sol_eight_queen(cur_row+1)
del occupied[-1]           

通過occupied.append([cur_row,j])表明本行我們在(cur_row,j)位置擺放王後,然後遞歸調用sol_eight_queen(cur_row+1)進入下一行,也就是cur_row+1行;遞歸調用結束後,需要清理現場,也就是這裡的del occupied[-1],即删除occupied數組中的最後一個元素。為什麼呢?

如果不把[cur_row,j]這個位置的王後拿走,大家可以想象,後續放王後時相當于憑空多了這個不應該存在的限制條件,導緻程式無解。大家可以自行删除del occupied[-1]後再運作,程式輸出解的個數為0。

到這裡,讓我們暫停下,首先整理一下遞歸和循環的關系:

一、遞歸程式本身相當于1個循環,像這種:

factorial(n) = n*factorial(n-1)           

等價于:

result =1
for i in range(1,n+1):
    result *= i           

二、2個遞歸調用,相當于順序執行2個循環,例如:

factorial(n)+factorial(k)           

三、一個循環内部調用了遞歸,例如上述的八皇後問題,則相當于N重循環。再例如下面的檔案夾周遊程式:

import os
def browser_dir(rootDir): 
    for lists in os.listdir(rootDir): 
        path = os.path.join(rootDir, lists) 
        if os.path.isfile(path): 
            print(path)
        else:
            browser_dir(path)            

可以看出,循環内調用遞歸可以等價于多重循環的效果。大家自己寫下代碼好好體會下。

四、多重循環内部調用了遞歸,常見于圖相關類型的題目,例如:

有一個H*W大小的遊戲闆,由黑白兩種格子組成。現要用3個機關格子的L狀闆塊把白色格子全部覆寫掉。闆塊可以自由旋轉,但不能重疊覆寫黑色格子,或超出遊戲版。

基本上能遇到的就是這些了,大家要銘記的是對于不同的題目,應用不同的循環+遞歸的政策。

建議大家好好體會下

for j in range(dim):
        if is_valid(cur_row,j,occupied): 
            occupied.append([cur_row,j])
            sol_eight_queen(cur_row+1)
            del occupied[-1]           
這段代碼,一個循環,一個進入下層遞歸的條件,同時注意恢複現場。

繼續閱讀