Обратный поиск со списком Monad в Haskell

Я пытаюсь решить проблему декомпозиции с возвратом и перечислением монад в Haskell. Вот постановка задачи: по заданному положительному целому числу n найти все списки последовательных целых чисел (в диапазоне i..j), сумма которых равна n< /эм>.

Я пришел со следующим решением, которое, похоже, работает нормально. Может ли кто-нибудь предложить лучшую/более эффективную реализацию с использованием 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]]

person Daniel Gaspani    schedule 06.05.2020    source источник


Ответы (2)


Сумма диапазона чисел k .. l с kl равна (l(l+1)-k(k-1))/2 . Например: 1 .. 4 равно (45-10)/2=(20-0)/2=10; а сумма 4 .. 5 равна (56-43)/2=(30-12)/2=9.

Если у нас есть сумма S и смещение k, мы можем, таким образом, выяснить, существует ли l, для которого сумма верна:

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

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

таким образом, мы можем решить это уравнение с помощью:

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

Если это целое число, то последовательность существует. Например, для S=9 и k=4 мы получаем:

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

Мы можем использовать некоторые функции, такие как вавилонский метод [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

Мы можем проверить, действительно ли найденный корень является точным квадратным корнем, возведя корень в квадрат, и посмотреть, получим ли мы исходный ввод.

Итак, теперь, когда у нас есть это, мы можем перебрать нижнюю границу последовательности и найти верхнюю границу. Если она существует, мы возвращаем последовательность, в противном случае пробуем следующую:

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‹l, с помощью:

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

Затем это дает нам:

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]]
person Willem Van Onsem    schedule 06.05.2020
comment
Спасибо @Willem, ваш ответ содержит много полезных советов. - person Daniel Gaspani; 07.05.2020

Примечание. Этот ответ немного тангенциален, поскольку вопрос конкретно требует решения для прямого поиска с возвратом в 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

Мы просто выражаем ограничения на i и j для данного n, используя формулу суммирования. Остальное просто обрабатывается решателем 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.

Хотя это может не дать достаточного понимания того, как решить проблему, оно, безусловно, использует существующие технологии. Опять же, хотя в этом ответе монада списка не используется, как было задано, но, надеюсь, она представляет некоторый интерес при рассмотрении приложений решателей SMT в обычном программировании.

person alias    schedule 07.05.2020
comment
Спасибо @алиас. Я не знал о существовании модуля SBV, так что большое спасибо, что указали мне в этом направлении. Я посмотрю на это. - person Daniel Gaspani; 07.05.2020