在Haskell找到Knight巡回赛的一个解决方案

时间:2017-08-22 07:17:56

标签: haskell lazy-evaluation knights-tour

我正在尝试在Haskell中解决Knight's Open Tour,并提出一个解决方案来生成所有可能的解决方案:

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<h3></h3>

然而,当使用8乘8的棋盘测试时,上述功能永远不会停止,这是因为解决方案空间非常大(根据1,19,591,828,170,979,904不同的开放游览)。所以我想找到一个解决方案。 Fisrt,我试过了:

knightsTour :: Int -> [[(Int, Int)]]
knightsTour size = go 1 [(1, 1)]
  where
    maxSteps = size^2
    isValid (x, y) = x >= 1 && x <= size && y >= 1 && y <= size

    go :: Int -> [(Int, Int)] -> [[(Int, Int)]]
    go count acc | count == maxSteps = return $ reverse acc
    go count acc = do
      next <- nextSteps (head acc)
      guard $ isValid next && next `notElem` acc
      go (count + 1) (next : acc)


fs = replicateM 2 [(*1), (*(-1))]
nextSteps :: (Int, Int) -> [(Int, Int)]
nextSteps (x, y) = do
  (x', y') <- [(1, 2), (2, 1)]
  [f, f'] <- fs
  return (x + f x', y + f' y')

希望Haskell的懒惰评估能够挽救这一天。但这没有发生,解决方案仍然永远存在。 然后,我试过了:

-- First try    
head (knightsTour 8)

但上面的解决方案仍无法实现,因为它仍然可以永久运行。 我的问题是:

  1. 为什么不能进行懒惰的评估工作,因为我预计只能生成第一个解决方案?在我看来,在两次尝试中,只需要第一个解决方案。
  2. 如何更改上面的代码才能生成第一个解决方案?

1 个答案:

答案 0 :(得分:2)

首先是好消息:您的代码正在按照您的期望进行,并且只生成第一个解决方案!

这也是一个坏消息:即使找到第一个解决方案也需要很长时间。我认为你低估的是有多少&#34;死胡同&#34;需要遇到才能产生解决方案。

例如,您可以使用Debug.Trace模块调整初始版本,以便让我们知道您在尝试查找第一条路径时遇到的死角数量:

import Control.Monad
import Debug.Trace (trace)
import System.Environment (getArgs)

knightsTour :: Int -> [[(Int, Int)]]
knightsTour size = go 1 [(1, 1)]
  where
    maxSteps = size * size
    isValid (x, y) = x >= 1 && x <= size && y >= 1 && y <= size

    go :: Int -> [(Int, Int)] -> [[(Int, Int)]]
    go count acc | count == maxSteps = return $ reverse acc
    go count acc = do
      let nextPossible' = [ next |
                            next <- nextSteps (head acc)
                            , isValid next && next `notElem` acc]
          nextPossible = if null nextPossible'
            then trace ("dead end; count: " ++ show count) []
            else nextPossible'
      next <- nextPossible
      -- guard $ isValid next && next `notElem` acc
      go (count + 1) (next : acc)


fs = replicateM 2 [(*1), (*(-1))]
nextSteps :: (Int, Int) -> [(Int, Int)]
nextSteps (x, y) = do
  (x', y') <- [(1, 2), (2, 1)]
  [f, f'] <- fs
  return (x + f x', y + f' y')

main :: IO ()
main = do
  [n] <- getArgs
  print (head $ knightsTour (read n))

现在,让我们看看为不同的电路板尺寸提供了多少输出:

/tmp$ ghc -o kntest -O2 kntest.hs 
[1 of 1] Compiling Main             ( kntest.hs, kntest.o )
Linking kntest ...
/tmp$ ./kntest 5 2>&1 | wc
   27366  109461  547424
/tmp$ ./kntest 6 2>&1 | wc
  783759 3135033 15675378
/tmp$ ./kntest 7 2>&1 | wc
  818066 3272261 16361596

好的,所以我们在电路板尺寸为7的电路板上遇到了27,365个死角,在电路板尺寸为7的情况下遇到了超过80万个死角。对于8的电路板,我将其重定向到文件:

/tmp$ ./kntest 8 2> kn8.deadends.txt

它还在运行。在这一点上,它遇到了超过3800万个死胡同:

/tmp$ wc -l kn8.deadends.txt 
 38178728 kn8.deadends.txt

这些死胡同中有多少人真的接近尾声?

/tmp$ wc -l kn8.deadends.txt ; fgrep 'count: 61' kn8.deadends.txt | wc -l ; fgrep 'count: 62' kn8.deadends.txt | wc -l; fgrep 'count: 63' kn8.deadends.txt | wc -l ; wc -l kn8.deadends.txt
 52759655 kn8.deadends.txt
    1448
       0
       0
 64656651 kn8.deadends.txt

现在它已达到6400多万个死胡同,它仍然没有找到超过61步的死胡同。

现在它已经达到了8500万,如果我花了太长时间来写下剩余的内容,那么当我完成这个答案时它可能会超过1亿。

你可能会做一些事情来加速你的程序(例如使用向量来跟踪已经访问过的点而不是O(n)notElem查找),但从根本上说它是这样做的我只想得到第一个答案,因为第一个答案真的要比你最初想的要长得多。

编辑:如果你添加一个非常简单,天真的Warnsdorf's rule实现,那么即使对于非常大的(40x40)电路板,你几乎可以立即获得第一次骑士之旅:

import Control.Monad
import System.Environment (getArgs)
import Data.List (sort)

knightsTour :: Int -> [[(Int, Int)]]
knightsTour size = go 1 [(1, 1)]
  where
    maxSteps = size * size
    isValid (x, y) = x >= 1 && x <= size && y >= 1 && y <= size

    getValidFor from acc = do
      next <- nextSteps from
      guard $ isValid next && next `notElem` acc
      return next

    go :: Int -> [(Int, Int)] -> [[(Int, Int)]]
    go count acc | count == maxSteps = return $ reverse acc
    go count acc = do
      let allPoss = getValidFor (head acc) acc
          sortedPossible = map snd $ sort $
                           map (\x -> (length $ getValidFor x acc, x))
                           allPoss
      next <- sortedPossible
      go (count + 1) (next : acc)

fs = replicateM 2 [(*1), (*(-1))]
nextSteps :: (Int, Int) -> [(Int, Int)]
nextSteps (x, y) = do
  (x', y') <- [(1, 2), (2, 1)]
  [f, f'] <- fs
  return (x + f x', y + f' y')

main :: IO ()
main = do
  [n] <- getArgs
  print (head $ knightsTour (read n))