P91 - 骑士巡游

2026-09-07 00:00    #Haskell   #99题   #回溯  

P91 - 骑士巡游

Knight’s tour

官方模块:Problems.P91 核心函数:knightsTourclosedKnightsTour


← P90 N 皇后 | P92 优雅树标号 →


Warnsdorff 规则

回溯时优先尝试“下一步可走位置最少”的格子,让最受约束的位置尽早决定,显著减少死路搜索。

实现

方法一:Warnsdorff 排序回溯

 1import Data.List (sort, sortOn)
 2import qualified Data.Set as Set
 3
 4type Position = (Int, Int)
 5
 6knightsTour :: Int -> Position -> Maybe [Position]
 7knightsTour n end
 8  | not (inside n end) = Nothing
 9  | otherwise = search n [end] (Set.delete end board)
10  where board = Set.fromList [(x,y) | x <- [1..n], y <- [1..n]]
11
12search n path@(current:_) remaining
13  | Set.null remaining = Just path
14  | otherwise = firstJust
15      [search n (next:path) (Set.delete next remaining)
16      | next <- ranked]
17  where
18    ranked = sortOn (length . moves remaining) (moves remaining current)
19search _ [] _ = Nothing
20
21moves remaining (x,y) =
22  filter (`Set.member` remaining)
23    [(x+dx,y+dy) | (dx,dy) <- jumps]
24  where jumps = [(1,2),(1,-2),(-1,2),(-1,-2),
25                 (2,1),(2,-1),(-2,1),(-2,-1)]
26
27firstJust [] = Nothing
28firstJust (Just x:_) = Just x
29firstJust (Nothing:xs) = firstJust xs
30
31inside n (x,y) = 1 <= x && x <= n && 1 <= y && y <= n

从终点反向搜索,最终得到的列表自然以指定位置结尾。

闭合巡游

 1closedKnightsTour :: Int -> Maybe [Position]
 2closedKnightsTour n
 3  | n < 1     = Nothing
 4  | otherwise = firstJust
 5      [pathTo end [(1,1)] (Set.delete end withoutStart)
 6      | end <- moves withoutStart (1,1)]
 7  where
 8    board = Set.fromList [(x,y) | x <- [1..n], y <- [1..n]]
 9    withoutStart = Set.delete (1,1) board
10
11    pathTo end path@(current:_) remaining
12      | Set.null remaining =
13          if knightMove current end
14            then Just (reverse path ++ [end])
15            else Nothing
16      | otherwise = firstJust
17          [pathTo end (next:path) (Set.delete next remaining)
18          | next <- sortOn (length . moves remaining)
19                           (moves remaining current)]
20    pathTo _ [] _ = Nothing
21
22    knightMove (x,y) (x',y') =
23      sort [abs (x-x'), abs (y-y')] == [1,2]

方法二:朴素回溯

 1knightsTourPlain :: Int -> Position -> Maybe [Position]
 2knightsTourPlain n end
 3  | not (inside n end) = Nothing
 4  | otherwise = searchPlain [end] (Set.delete end board)
 5  where
 6    board = Set.fromList [(x, y) | x <- [1..n], y <- [1..n]]
 7
 8    searchPlain path@(current:_) remaining
 9      | Set.null remaining = Just path
10      | otherwise = firstJust
11          [searchPlain (next : path) (Set.delete next remaining)
12          | next <- moves remaining current]
13    searchPlain [] _ = Nothing

朴素版本保持相同的回溯状态,却完全按照坐标顺序尝试下一步。它适合说明 Warnsdorff 排序没有改变解集合,只是把最可能形成死路的选择提前。

方法对比

方法候选顺序实际表现
Warnsdorff后续出口少的格子优先大棋盘通常很快
朴素回溯坐标顺序搜索树巨大,只适合小棋盘对照

测试

1>>> fmap length (knightsTour 6 (3,5))
2Just 36
3>>> fmap length (closedKnightsTour 6)
4Just 36
5>>> knightsTour 3 (1,1)
6Nothing
7>>> knightsTourPlain 1 (1,1)
8Just [(1,1)]

参考


← P90 N 皇后 | P92 优雅树标号 →