簡體   English   中英

按遞增順序查找所有正整數組合,這些組合加起來等於給定的正數 n

[英]Find all combinations of positive integers in increasing order that adds up to a given positive number n

如何編寫一個 function 接受n (其中 n > 0)並返回總和為n的正整數的所有組合的列表? 這是 web 上的常見問題。並且提供了不同的答案,例如123 但是,在提供的答案中,他們使用兩個函數來解決問題。 我只想用一個function來做。因此,我編碼如下:

def all_combinations_sum_to_n(n):
    from itertools import combinations_with_replacement

    combinations_list = []

    if n < 1:
        return combinations_list

    l = [i for i in range(1, n + 1)]
    for i in range(1, n + 1):
        combinations_list = combinations_list + (list(combinations_with_replacement(l, i)))

    result = [list(i) for i in combinations_list if sum(i) == n]
    result.sort()
    return result

如果我將 20 傳遞給我的 function 即all_combinations_sum_to_n(20) ,我機器的操作系統會終止該進程,因為它非常昂貴。 我認為我的 function 的空間復雜度是 O(n*n.)? 我該如何修改我的代碼,以便我不必創建任何其他 function 而我的單個 function 具有改進的時間或空間復雜性。 我認為使用 itertools.combinations_with_replacement 是不可能的。

更新

Barmar、ShadowRanger 和 pts 提供的所有答案都很棒。 當我在 memory 和運行時方面尋找有效答案時,我使用https://perfpy.com並選擇 python 3.8 來比較答案。 我使用了六個不同的n值,在所有情況下,ShadowRanger 的解決方案得分最高。 因此,我選擇 ShadowRanger 的答案作為最佳答案。 分數如下:

在此處輸入圖像描述

您有兩個主要問題,一個導致您當前的問題(內存不足),另一個即使您解決了該問題也會繼續存在該問題:

  1. 你在過濾之前積累了所有組合,所以你的 memory 要求是巨大的 如果你的 function 可以是一個生成器(迭代一次產生一個值)而不是返回一個完全實現的list ,你甚至不需要一個list ,即使你必須返回一個list ,你也不需要生成如此巨大的中間list 您可能認為您至少需要一個list來進行排序,但是combinations_with_replacement已經保證根據輸入順序生成可預測的順序,並且由於range是有序的,因此生成的值也將被排序。

  2. 即使您解決了 memory 問題,由於縮放比例不佳,僅生成那么多組合的計算成本也令人望而卻步; 對於 memory,但不是 CPU,下面代碼的優化版本,它在 0.2 秒內處理 11 個輸入,在 ~2.6 秒內處理 12 個輸入,在~11 秒內處理 13 個輸入; 按照這個比例,20 將接近宇宙時間框架的熱寂。

Barmar 的答案是解決這兩個問題的一種方法,但它仍然在熱切地工作並在甚至可能不需要完整的工作時存儲完整的結果,並且它涉及排序和重復數據刪除,如果你足夠小心的話,這並不是絕對必要的你如何產生結果。

這個答案將解決這兩個問題,首先是 memory 問題,然后是速度問題,而無需 memory 線性以上的要求n

單獨解決 memory 問題實際上使代碼更簡單,它仍然使用相同的基本策略,但不會不必要地消耗所有 RAM。 訣竅是編寫一個生成器 function,避免一次存儲多個結果(調用者可以list ify,如果他們知道 output 足夠小並且他們實際上一次需要它,但通常,只是循環遍歷生成器更好):

from collections import deque  # Cheap way to just print the last few elements
from itertools import combinations_with_replacement  # Imports should be at top of file,
                                                     # not repeated per call
def all_combinations_sum_to_n(n):
    for i in range(1, n + 1):  # For each possible length of combinations...
        # For each combination of that length...
        for comb in combinations_with_replacement(range(1, n + 1), i):
            if sum(comb) == n:    # If the sum matches...
                yield list(comb)  # yield the combination

# 13 is the largest input that will complete in some vaguely reasonable time, ~10 seconds on TIO
print(*deque(all_combinations_sum_to_n(13), maxlen=10), sep='\n')

在線試用!

再次說明,對於輸入 20,這不會在任何合理的時間內完成; 有太多多余的工作要做,組合的增長模式與輸入的階乘成比例; 你必須在算法上更聰明。 但是對於不太嚴重的問題,這種模式比構建巨大的list並將它們連接起來的解決方案更簡單、更快,並且內存效率顯着提高。

為了在合理的時間內解決問題,使用相同的基於生成器的方法(但沒有itertools ,這在這里不實用,因為當你知道它們無用時你不能告訴它跳過組合),這是一個改編Barmar 的答案不需要排序,不產生重復項,因此可以在不到 20 秒的時間內生成解決方案集,即使對於n = 20也是如此:

def all_combinations_sum_to_n(n, *, max_seen=1):
    for i in range(max_seen, n // 2 + 1):
        for subtup in all_combinations_sum_to_n(n - i, max_seen=i):
            yield (i,) + subtup
    yield (n,)


for x in all_combinations_sum_to_n(20):
    print(x)

在線試用!

這不僅會生成具有內部排序順序的單個元組( 1總是在2之前),而且會按排序順序生成元組序列(因此遍歷sorted(all_combinations_sum_to_n(20))相當於直接遍歷all_combinations_sum_to_n(20) ,后者只是避免了臨時list和無操作排序過程)。

使用遞歸而不是生成所有組合然后過濾它們。

def all_combinations_sum_to_n(n):
    combinations_set = set()
    for i in range(1, n):
        for sublist in all_combinations_sum_to_n(n-i):
            combinations_set.add(tuple(sorted((i,) + sublist)))

    combinations_set.add((n,))

    return sorted(combinations_set)

我有一個更簡單的解決方案,它不使用sorted()並將結果放在列表中,但它會產生順序不同的重復項,例如[1, 1, 2][1, 2, 1]n == 4 我添加了那些以消除重復項。

在我的 MacBook M1 all_combinations_sum_to_n(20)在大約 0.5 秒內完成。

這是一個快速迭代的解決方案:

def csum(n):
  s = [[None] * (k + 1) for k in range(n + 1)]
  for k in range(1, n + 1):
    for m in range(k, 0, -1):
      s[k][m] = [[f] + terms
                 for f in range(m, (k >> 1) + 1) for terms in s[k - f][f]]
      s[k][m].append([k])
  return s[n][1]

import sys
n = 5
if len(sys.argv) > 1: n = int(sys.argv[1])
for terms in csum(n):
  print(' '.join(map(str, terms)))

解釋:

  • 讓我們將terms定義為一個非空的、遞增的(可以包含多次相同的值)正整數列表。

  • n的解決方案是按字典順序遞增的所有項的列表,其中每個項的總和為n

  • s[k][m]是所有術語的列表,按字典順序遞增,其中n中每個術語的總和,每個術語中的第一個(最小)integer 至少為m

  • 解決方案是s[n][1] 在返回此解決方案之前, csum function 使用迭代動態規划填充s數組。

  • 在內循環中,使用了以下遞歸: s[k][m]中的每個項要么至少有 2 個元素( f和其余元素),要么有 1 個元素( k )。 在前一種情況下, rest 是一個terms ,其中總和是k - f ,最小的 integer 是f ,因此它來自s[k - f][f]

如果n至少為 20,此解決方案比 @Barmar 的解決方案快很多。例如,在我的 Linux 筆記本電腦上, n = 25 時,它快了大約 400 倍,而n = 28 時,它快了大約 3700 倍。 對於較大的n值,它會變得非常快。

該解決方案比@ShadowRanger 的解決方案使用更多的 memory,因為該解決方案創建了大量臨時列表,並且它使用所有臨時列表直到最后。

如何想出這么快的解決方案?

  • 嘗試找到一個遞歸公式。 (先不要寫代碼!)

  • 有時遞歸僅適用於具有多個變量的遞歸函數。 (在我們的例子中, s[k][m]是遞歸的 function, k是問題隱含的明顯變量, m是我們必須發明的額外變量。)所以嘗試在遞歸公式中添加一些變量:添加最少數量的變量以使其工作。

  • 編寫您的代碼,以便它只計算每個遞歸 function 值一次(而不是更多)。 為此,您可以向遞歸 function 添加緩存(記憶),或者您可以使用動態編程,即以正確的順序填充(多維)數組,以便已經填充所需的內容。

暫無
暫無

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

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