Haskell: 八皇后问题的求解和效率优化

  • Post author:
  • Post category:其他




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



版权声明:本文为WinterShiver原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。