在 Haskell 中使用列表 Monad 进行回溯

Backtraking with list Monad in Haskell

我正在尝试通过回溯解决 分解问题 并在 Haskell 中列出 Monad。问题陈述如下:给定一个正整数 n,找到所有连续整数列表(范围 i..j),其总和相等至 n.

我提出了以下似乎工作正常的解决方案。有人可以建议 better/more 使用 list Monad 和回溯的有效实现吗?

欢迎提出任何建议。提前致谢。

import Control.Monad

decompose :: Int -> [[Int]]
decompose n = concatMap (run n) [1 .. n - 1]
  where
    run target n = do
        x <- [n]
        guard $ x <= target
        if x == target
            then return [x]
            else do
                next <- run (target - n) (n + 1)
                return $ x : next

test1 = decompose 10 == [[1,2,3,4]]
test2 = decompose 9 == [[2,3,4],[4,5]]

一系列数字k .. lk≤l的总和等于(l ×(l+1)-k×(k-1))/2。例如:1 .. 4等于(4×5-1×0)/2=(20-0)/2=10;和 4 .. 5(5×6-4×3)/2=(30-12)/2=9 .

如果我们有一个总和S和一个偏移量k,我们就可以找出是否有一个 l 总和适用于:

2×S = l×(l+1)-k×(k-1)

0=l2+l-2×S-k×(k-1)

因此我们可以用以下公式求解这个方程:

l=(-1 + √(1+8×S+4×k×(k-1)))/2

如果这是整数,则序列存在。例如对于 S=9k=4,我们得到:

l = (-1 + √(1+72+48))/2 = (-1 + 11)/2 = 10/2 = 5.

我们可以利用一些函数,比如Babylonian method [wiki]来快速计算整数平方根:

squareRoot :: Integral t => t -> t
squareRoot n 
   | n > 0    = babylon n
   | n == 0   = 0
   | n < 0    = error "Negative input"
   where
   babylon a   | a > b = babylon b
               | otherwise = a
      where b  = quot (a + quot n a) 2

我们可以通过对根进行平方来检查找到的根是否确实是精确的平方根,看看我们是否获得了原始输入。

现在我们有了它,我们可以遍历序列的下限,并寻找上限。如果存在,我们 return 序列,否则,我们尝试下一个:

decompose :: Int -> [[Int]]
decompose s = [ [k .. div (sq-1) 2 ]
              | k <- [1 .. s]
              , let r = 1+8*s+4*k*(k-1)
              , let sq = squareRoot r
              , r == sq*sq
              ]

因此,我们可以获得例如具有以下项的项目:

Prelude> decompose 1
[[1]]
Prelude> decompose 2
[[2]]
Prelude> decompose 3
[[1,2],[3]]
Prelude> decompose 3
[[1,2],[3]]
Prelude> decompose 1
[[1]]
Prelude> decompose 2
[[2]]
Prelude> decompose 3
[[1,2],[3]]
Prelude> decompose 4
[[4]]
Prelude> decompose 5
[[2,3],[5]]
Prelude> decompose 6
[[1,2,3],[6]]
Prelude> decompose 7
[[3,4],[7]]
Prelude> decompose 8
[[8]]
Prelude> decompose 9
[[2,3,4],[4,5],[9]]
Prelude> decompose 10
[[1,2,3,4],[10]]
Prelude> decompose 11
[[5,6],[11]]

我们可以进一步限制范围,例如指定k,其中:

decompose :: Int -> [[Int]]
decompose s = [ [k .. l ]
              | k <- [1 .. <b>div s 2</b> ]
              , let r = 1+8*s+4*k*(k-1)
              , let sq = squareRoot r
              , r == sq*sq
              , let l = div (sq-1) 2
              , <b>k < l</b>
              ]

然后我们得到:

Prelude> decompose 1
[]
Prelude> decompose 2
[]
Prelude> decompose 3
[[1,2]]
Prelude> decompose 4
[]
Prelude> decompose 5
[[2,3]]
Prelude> decompose 6
[[1,2,3]]
Prelude> decompose 7
[[3,4]]
Prelude> decompose 8
[]
Prelude> decompose 9
[[2,3,4],[4,5]]
Prelude> decompose 10
[[1,2,3,4]]
Prelude> decompose 11
[[5,6]]

NB 这个答案有点离题,因为这个问题特别要求 Haskell 中的直接回溯解决方案。发布它以防对解决此问题的其他方法感兴趣,特别是使用现成的 SMT 求解器。

现成的约束求解器可以轻松处理这类问题,Haskell 中有多个库可以访问它们。无需赘述太多细节,以下是如何使用 SBV 库 (https://hackage.haskell.org/package/sbv) 对其进行编码:

import Data.SBV

decompose :: Integer -> IO AllSatResult
decompose n = allSat $ do
                 i <- sInteger "i"
                 j <- sInteger "j"

                 constrain $ 1 .<= i
                 constrain $ i .<= j
                 constrain $ j .<  literal n

                 constrain $ literal n .== ((j * (j+1)) - ((i-1) * i)) `sDiv` 2

对于给定的n,我们简单地表达对ij的约束,使用求和公式。其余的由 SMT 求解器简单处理,为我们提供所有可能的解决方案。这里有一些测试:

*Main> decompose 9
Solution #1:
  i = 4 :: Integer
  j = 5 :: Integer
Solution #2:
  i = 2 :: Integer
  j = 4 :: Integer
Found 2 different solutions.

*Main> decompose 10
Solution #1:
  i = 1 :: Integer
  j = 4 :: Integer
This is the only solution.

虽然这可能无法深入了解如何解决问题,但它确实利用了现有技术。同样,虽然这个答案没有按要求使用 list-monad,但希望在考虑 SMT 求解器在常规编程中的应用时它会引起一些兴趣。