P98 - 数织游戏(Nonogram)

2026-09-07 00:00    #Haskell   #99题   #约束搜索  

P98 - 数织游戏(Nonogram)

Nonograms

官方模块:Problems.P98 核心函数:nonogram


← P97 数独 | P99 填字游戏 →


题目描述

每行、每列给出连续实心块的长度。相邻块之间至少有一个空格;求满足全部行列线索的布尔矩阵。

生成单行候选

 1import Data.List (isPrefixOf, transpose)
 2
 3linePatterns :: Int -> [Int] -> [[Bool]]
 4linePatterns width [] = [replicate width False]
 5linePatterns width (block:rest)
 6  | block <= 0 = []
 7  | otherwise =
 8      [ replicate start False ++ replicate block True ++ separator ++ suffix
 9      | start <- [0..width - block - minimumTail]
10      , let separator = if null rest then [] else [False]
11      , suffix <- linePatterns (width - start - block - length separator) rest]
12  where
13    minimumTail = sum rest + length rest

minimumTail 是剩余块及它们所需分隔空格的最小长度,因此起点枚举不会产生越界布局。

求解

方法一:按行回溯并检查列前缀

 1nonogram :: [[Int]] -> [[Int]] -> Maybe [[Bool]]
 2nonogram rowClues columnClues = build [] rowOptions
 3  where
 4    height = length rowClues
 5    width  = length columnClues
 6    rowOptions = map (linePatterns width) rowClues
 7    columnOptions = map (linePatterns height) columnClues
 8
 9    build rows []
10      | transpose rows `matches` columnOptions = Just rows
11      | otherwise = Nothing
12    build rows (options:remaining) = firstJust
13      [build rows' remaining | row <- options
14                             , let rows' = rows ++ [row]
15                             , compatible rows']
16
17    compatible rows = and
18      [any (column `isPrefixOf`) options
19      | (column, options) <- zip (transpose rows) columnOptions]
20
21    actual `matches` options = and (zipWith elem actual options)
22
23firstJust [] = Nothing
24firstJust (Just value:_) = Just value
25firstJust (Nothing:rest) = firstJust rest

方法二:从候选较少的方向搜索

 1nonogramAdaptive :: [[Int]] -> [[Int]] -> Maybe [[Bool]]
 2nonogramAdaptive rowClues columnClues
 3  | rowCost <= columnCost = nonogram rowClues columnClues
 4  | otherwise = transpose <$> nonogram columnClues rowClues
 5  where
 6    height = length rowClues
 7    width = length columnClues
 8    rowCost = product
 9      [toInteger (length (linePatterns width clue)) | clue <- rowClues]
10    columnCost = product
11      [toInteger (length (linePatterns height clue)) | clue <- columnClues]

横纵转置后仍是同一个 Nonogram。先估算逐行和逐列候选组合数,从更小的一侧调用基准求解器;按列求解得到的棋盘最后再转置回来。线索不对称时,这个选择可以显著减少搜索分支。

方法对比

方法搜索方向
固定按行实现直接,表现取决于行线索强弱
自适应比较候选组合数后选择行或列

测试

1>>> fmap (map (map (\b -> if b then '#' else '.')))
2... (nonogram [[1],[3],[1]] [[1],[3],[1]])
3Just [".#.","###",".#."]
4>>> nonogram [[3]] [[1],[1]]
5Nothing
6>>> nonogramAdaptive [[1],[3],[1]] [[1],[3],[1]] ==
7... nonogram [[1],[3],[1]] [[1],[3],[1]]
8True

逐行加入后立即检查所有列前缀,可以在错误行布局出现时马上回溯。

参考


← P97 数独 | P99 填字游戏 →