簡體   English   中英

Haskell生成n個數的所有組合

[英]Haskell generating all combinations of n numbers

我正在嘗試生成n個數字的所有可能組合。 例如,如果n = 3,我會想要以下組合:

(0,0,0), (0,0,1), (0,0,2)... (0,0,9), (0,1,0)... (9,9,9).

這篇文章描述了如何在n = 3時這樣做:

[(a,b,c) | m <- [0..9], a <- [0..m], b <- [0..m], c <- [0..m] ]

或者為了避免重復(即同一個n-uple的多個副本):

let l = 9; in [(a,b,c) | m <- [0..3*l],
                         a <- [0..l], b <- [0..l], c <- [0..l],
                         a + b + c == m ]

然而,對於n > 3遵循相同模式將非常快速地變得非常愚蠢。 假設我想找到所有組合: (a, b, c, d, e, f, g, h, i, j)等。

任何人都能指出我在正確的方向嗎? 理想情況下,我寧願不使用內置功能,因為我正在嘗試學習Haskell,我寧願花時間去理解一些代碼而不僅僅是使用其他人編寫的包。 不需要元組,列表也可以。

三位數的所有組合是什么? 我們手動寫幾個。

000, 001, 002 ... 009, 010, 011 ... 099, 100, 101 ... 998, 999

我們最終還算 我們列舉了0到999之間的所有數字。對於任意數字的數字,這直截了當地說:上限是10^n (不包括),其中n是數字位數。

數字是故意這樣設計的。 如果有三個數字的可能組合不是有效數字,或者如果有一個三位數的數字無法通過組合三位數來表達,那將是非常奇怪的!

這對我來說是一個簡單的計划,它只涉及算術,不需要深入理解Haskell *:

  1. 生成0到10^n之間的數字列表
  2. 將每個數字轉換為數字列表。

第2步是有趣的部分。 要提取三位數字的數字(在基數10中), 請執行以下操作

  1. 取數字的商和余數相對於100.商是數字的第一個數字。
  2. 從第1步取余數,取其商和余數相對於10.商是第二位數。
  3. 第2步的其余部分是第三位數。 這與取1的商相同。

對於n位數,我們取n次,從10^(n-1)開始到1結束。 每次,我們使用最后一步的余數作為下一步的輸入。 這表明我們將數字轉換為數字列表的函數應該實現為折疊:我們將通過操作線程化其余部分並構建一個列表。 (如果你不在10號基地,我會留給你弄清楚這個算法是如何變化的!)


現在讓我們實現這個想法。 我們想要計算給定數字的指定位數,必要時的零填充。 digits的類型應該是什么?

digits :: Int -> Int -> [Int]

嗯,它接受一些數字和一個整數,並產生一個表示輸入整數數字的整數列表。 該列表將包含一位數整數,每個整數將是輸入數字的一位數。

digits numberOfDigits theNumber = reverse $ fst $ foldr step ([], theNumber) powersOfTen
    where step exponent (digits, remainder) =
              let (digit, newRemainder) = remainder `divMod` exponent
              in (digit : digits, newRemainder)
          powersOfTen = [10^n | n <- [0..(numberOfDigits-1)]]

令我印象深刻的是,這段代碼與我想要執行的算術的英文描述非常相似。 我們通過從0向上取冪來生成十次冪表。 然后我們將那張桌子折疊起來; 在每一步,我們將商放在數字列表上,並將余數發送到下一步。 我們必須在結尾處reverse輸出列表,因為它是從右到左的方式構建的。

順便說一下,生成列表,轉換它,然后將其折疊起來的模式是Haskell中慣用的事情。 它甚至有自己的高f'數字名稱, hylomorphism GHC也了解這種模式,並將其編譯成一個緊密的循環,優化了你正在使用的列表的存在。

我們來試試吧!

ghci> digits 3 123
[1, 2, 3]
ghci> digits 5 10101
[1, 0, 1, 0, 1]
ghci> digits 6 99
[0, 0, 0, 0, 9, 9]

它就像一個魅力! (好吧,當numberOfDigits對於theNumber來說太小時,它會出錯,但是不要theNumber 。)現在我們只需要生成一個數字的計數列表來使用digits

combinationsOfDigits :: Int -> [[Int]]
combinationsOfDigits numberOfDigits = map (digits numberOfDigits) [0..(10^numberOfDigits)-1]

......我們已經完成了!

ghci> combinationsOfDigits 2
[[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[1,0],[1,1] ... [9,7],[9,8],[9,9]]

*對於一個版本,它確實需要哈斯克爾的深刻理解,看到我的其他答案

我的另一個答案給出了一個算術算法來枚舉所有數字組合。 這是一個替代解決方案,通過推廣您的示例而產生。 它也適用於非數字,因為它只使用列表的結構。

首先,讓我們提醒自己如何使用列表理解來實現三位數組合。

threeDigitCombinations = [[x, y, z] | x <- [0..9], y <- [0..9], z <- [0..9]]

這里發生了什么? 列表推導對應於嵌套循環。 z從0到9計數,然后y上升到1, z再次從0開始計數。 x是最慢的。 如您所知,當您需要不同數量的數字時,列表推導的形狀會發生變化(盡管是統一的)。 我們將利用這種一致性。

twoDigitCombinations = [[x, y] | x <- [0..9], y <- [0..9]]

我們想要抽象列表推導中的變量數量(等效地,循環的嵌套)。 讓我們開始玩吧。 首先,我將把這些列表推導重寫為等效的monad理解

threeDigitCombinations = do
    x <- [0..9]
    y <- [0..9]
    z <- [0..9]
    return [x, y, z]
twoDigitCombinations = do
    x <- [0..9]
    y <- [0..9]
    return [x, y]

有趣。 看起來threeDigitCombinationsthreeDigitCombinations大致相同的twoDigitCombinations動作,但有一個額外的聲明。 再次重寫......

zeroDigitCombinations = [[]]  -- equivalently, `return []`
oneDigitCombinations = do
    z <- [0..9]
    empty <- zeroDigitCombinations
    return (z : empty)
twoDigitCombinations = do
    y <- [0..9]
    z <- oneDigitCombinations
    return (y : z)
threeDigitCombinations = do
    x <- [0..9]
    yz <- twoDigitCombinations
    return (x : yz)

現在應該清楚我們需要參數化的內容:

combinationsOfDigits 0 = return []
combinationsOfDigits n = do
    x <- [0..9]
    xs <- combinationsOfDigits (n - 1)
    return (x : xs)

ghci> combinationsOfDigits' 2
[[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[1,0],[1,1] ... [9,8],[9,9]]

它有效,但我們還沒有完成。 我想告訴你,這是一個更一般的monadic模式的實例。 首先,我將更改combinationsOfDigits的實現,以便它折疊一個常量列表。

combinationsOfDigits n = foldUpList $ replicate n [0..9]
    where foldUpList [] = return []
          foldUpList (xs : xss) = do
              x <- xs
              ys <- foldUpList xss
              return (x : ys)

看看foldUpList :: [[a]] -> [[a]] ,我們可以看到它實際上並不需要使用列表本身:它只使用列表的monad -y部分。 它可以適用於任何monad,事實上確實如此! 它在標准庫中,它被稱為sequence :: Monad m => [ma] -> m [a] 如果您對此感到困惑,請將m替換為[] ,您應該看到這些類型意味着相同的事情。

combinationsOfDigits n = sequence $ replicate n [0..9]

最后,注意到那個sequence . replicate n sequence . replicate n是這樣定義的replicateM ,我們把它降低到一個非常活潑的一個班輪。

combinationsOfDigits n = replicateM n [0..9]

總而言之, replicateM n給出了輸入列表的n個組合。 這適用於任何列表,而不僅僅是數字列表。 事實上,它適用於任何monad - 雖然“組合”解釋只有在你的monad代表選擇時才有意義。

這段代碼確實非常簡潔! 因此,我認為它的工作方式並不完全明顯,這與我在其他答案中向您展示的算術版本不同。 列表monad一直是我發現不太直觀的monad之一,至少當你使用高階monad組合器而不是do -notation時。

另一方面,它比數字運算版本運行得快得多。 在我的(高規格)MacBook Pro上,用-O2編譯,這個版本計算的5位數組合比壓縮數字的版本快4倍。 (如果有人能解釋我正在聽的原因!)

基准

combos 1 list = map (\x -> [x]) list
combos n list = foldl (++) [] $ map (\x -> map (\y -> x:y) nxt) list
    where nxt = combos (n-1) list

在你的情況下

combos 3 [0..9]

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM