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,在遞歸中自然地進行遍歷+剪枝。
依然待補。