Haskell: 八皇后問題的求解和效率優化

Haskell: 八皇后問題的求解和效率優化

編程思路

n皇后問題,在前m步保留已經放了m個之後的所有可能情況。在第m+1步再試圖放1個上去,保留所有不產生衝突的情況,也就是已經放了m+1個之後的所有可能情況。以此類推,放滿n個皇后爲止。

初步的實現和問題

queen :: Int -> Int -> [[Int]]
queen n 1 = [[i] | i <- [1..n]]
queen n num = [ls ++ [index] | index <- [1..n], ls <- queen n (num-1), 
                             not (elem index ls) 
                          && not (elem (num - index - 1) [i - (ls !! i) | i <- [0..(num-2)]])
                          && not (elem (num + index - 1) [i + (ls !! i) | i <- [0..(num-2)]])

但是很失敗,執行queen 8 8的運行時間爲一分鐘以上。
之所以出現這種情況,原因是queen n (num-1)被計算了好多次。

細節原因待探尋,不過應該和dfs中的計算順序有關。在queen n num裏面,queen n (num-1)應該是被計算了n次。

標準實現

兩個解決方法:
1.用let儲存queen n (num-1)的結果,避免反覆計算。

queen :: Int -> Int -> [[Int]]
queen n 1 = [[i] | i <- [1..n]]
queen n num = let ls_ls = queen n (num-1) in 
                          [ls ++ [index] | index <- [1..n], ls <- ls_ls, 
                             not (elem index ls) 
                          && not (elem (num - index - 1) [i - (ls !! i) | i <- [0..(num-2)]])
                          && not (elem (num + index - 1) [i + (ls !! i) | i <- [0..(num-2)]])
                          ]

2.換一下循環內主次變量的順序,讓求解難度較大,取值較多的做遍歷的主變量(放在前面)

queen :: Int -> Int -> [[Int]]
queen n 1 = [[i] | i <- [1..n]]
queen n num = [ls ++ [index] | ls <- queen n (num-1), index <- [1..n], 
                             not (elem index ls) 
                          && not (elem (num - index - 1) [i - (ls !! i) | i <- [0..(num-2)]])
                          && not (elem (num + index - 1) [i + (ls !! i) | i <- [0..(num-2)]])
                          ]

除此之外,發現,終止條件其實可以寫得更簡潔一些。

queen n 0 = [[]]

運行結果:

*Main> :l eight-queen.hs
[1 of 1] Compiling Main             ( eight-queen.hs, interpreted )
Ok, modules loaded: Main.
(0.02 secs,)

*Main> length $ queen 8 8
92
(0.05 secs, 20,472,904 bytes)

如果直接執行queen 8 8,可以輸出符合八皇后問題要求的所有排列情況。
附贈一個Python版本對照參考,基本就是照着上面Haskell代碼翻譯的:

def queen(n, num):
    pos_lst = list(range(1, n+1))
    if num == 0: return [[]]
    # if num == 1: 
        # return [[i] for i in range(1, n+1)]
    else:
        ls_lst = queen(n, num-1)
        result = [(ls, pos) for ls in ls_lst for pos in pos_lst]
        return [ls+[pos] for (ls, pos) in result \
                if not (pos in ls) \
                and not ((num - pos - 1) in [(i - ls[i]) for i in range(num-1)]) \
                and not ((num + pos - 1) in [(i + ls[i]) for i in range(num-1)]) \
        ]

print(len(queen(8,8))) # 92

改善效率

把判定條件提煉出來,爲safe函數,另外:
1.考慮到ls的長度就是num-1,所以優化掉之前顯式出現的num-2,num-1.
2.對之前的ls ++ [index]改爲index:ls,通過使用默認構造器改善效率。同時,因爲這種情況導致列表裏面的元素(對應已放置皇后的位置)爲反序,需要對條件進行一些恆等變換:

safe :: [Int] -> Int -> Bool
safe ls index = notElem index ls 
    && notElem (index + 1) [ls !! i - i | i <- take (length ls) [0..]] 
    && notElem (index - 1) [ls !! i + i | i <- take (length ls) [0..]]

queen :: Int -> Int -> [[Int]]
queen n 0 = [[]]
queen n num = [index:ls | ls <- queen n (num-1), index <- [1..n], safe ls index] 

再用zipWith改善列表推導式,優化掉take。

safe :: [Int] -> Int -> Bool
safe ls index = notElem index ls 
    && notElem (index + 1) (zipWith (-) ls [0..])
    && notElem (index - 1) (zipWith (+) ls [0..]) 

queen :: Int -> Int -> [[Int]]
queen n 0 = [[]]
queen n num = [index:ls | ls <- queen n (num-1), index <- [1..n], safe ls index] 

優化掉列表解析式中的ls <- queen n (num-1),顯式地保證queen n (num-1)在求queen n num的時候只運行一次:

safe :: [Int] -> Int -> Bool
safe ls index = notElem index ls 
    && notElem (index + 1) (zipWith (-) ls [0..])
    && notElem (index - 1) (zipWith (+) ls [0..]) 

oneMoreQueen :: Int -> [[Int]] -> [[Int]]
oneMoreQueen n lss = [index:ls | ls <- lss, index <- [1..n], safe ls index]

queen :: Int -> Int -> [[Int]]
queen n 0 = [[]]
queen n num = oneMoreQueen n (queen n $ num-1) 

程序在這樣的優化下基本達到了最優執行效率:從最初的26秒優化到10秒。

q :: Int -> [[Int]]
q n = iterate (. (oneMoreQueen n)) id !! n $ [[]]

以上的代碼代替queen,把遞歸顯式化,但是並沒有改善執行效率。

main :: IO ()
main = putStrLn $ show $ length $ queen 12 12 -- for test
-- main = putStrLn $ show $ length $ q 12

在以上的時間測試中,我們執行main獲得12皇后的結果,作爲此程序的基準測試。

最終代碼

safe :: [Int] -> Int -> Bool
safe ls index = notElem index ls 
    && notElem (index + 1) (zipWith (-) ls [0..])
    && notElem (index - 1) (zipWith (+) ls [0..]) 

oneMoreQueen :: Int -> [[Int]] -> [[Int]]
oneMoreQueen n lss = [index:ls | ls <- lss, index <- [1..n], safe ls index]

-- q :: Int -> [[Int]]
-- q n = iterate (. (oneMoreQueen n)) id !! n $ [[]]

queen :: Int -> Int -> [[Int]]
queen n 0 = [[]]
queen n num = oneMoreQueen n (queen n $ num-1) 

q :: Int -> [[Int]]
q n = queen n n

main = putStrLn $ show $ length $ q 10 -- for test

有靈魂解法

建立相應數據結
構,模擬皇后行爲,對基於皇后的data使用描述性的語言判定safe,在遞歸中自然地進行遍歷+剪枝。
依然待補。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章