我看过很多关于这个主题的帖子,但没有一个是我想要的。
我想找到所有方法将大于 1 的正整数 N 表示为从 1 到 N 的最多 N 个整数之和,就像平常一样。
例如,在标准表示法中,这些都是 6 的所有分区:
[(1, 1, 1, 1, 1, 1),
(1, 1, 1, 1, 2),
(1, 1, 1, 2, 1),
(1, 1, 1, 3),
(1, 1, 2, 1, 1),
(1, 1, 2, 2),
(1, 1, 3, 1),
(1, 1, 4),
(1, 2, 1, 1, 1),
(1, 2, 1, 2),
(1, 2, 2, 1),
(1, 2, 3),
(1, 3, 1, 1),
(1, 3, 2),
(1, 4, 1),
(1, 5),
(2, 1, 1, 1, 1),
(2, 1, 1, 2),
(2, 1, 2, 1),
(2, 1, 3),
(2, 2, 1, 1),
(2, 2, 2),
(2, 3, 1),
(2, 4),
(3, 1, 1, 1),
(3, 1, 2),
(3, 2, 1),
(3, 3),
(4, 1, 1),
(4, 2),
(5, 1),
(6,)]
现在,这种表示法的熵非常低,首先,每次数字出现都会增加特定分区的大小,这效率低下,而且当数字多次出现时,很难计算它们的出现次数。我想用一个二元素元组来替换所有数字的出现,其中第一个元素是数字,第二个元素是计数,例如 等价(1, 1, 1, 1, 1, 1)
于(1, 6)
,它们都包含相同的信息,但显然前者更简洁。
其次,输出中有很多重复元素,例如,有五个分区,每个分区包含四个 1 和一个 2,它们会被算作五个独立的元素。这也很低效,因为加法是可交换的,改变数字的顺序不会改变结果,所以它们都是等价的,都是同一个元素。
然而,如果我们用一个元素替换所有五个元素,就会丢失信息。
我想用以下格式替换它:
Counter({((1, 2), (2, 2)): 6,
((1, 1), (2, 1), (3, 1)): 6,
((1, 4), (2, 1)): 5,
((1, 3), (3, 1)): 4,
((1, 2), (4, 1)): 3,
((1, 1), (5, 1)): 2,
((2, 1), (4, 1)): 2,
((1, 6),): 1,
((2, 3),): 1,
((3, 2),): 1,
((6, 1),): 1})
所以我希望结果是,Counter
其中键是唯一的分区,而值是数字可以排列的方式数。
是的,我已经为此编写了一个函数,使用了暴力破解和记忆化技术。结果证明它非常高效。
首先这是以标准格式输出的实现,我将其发布在这里以供比较:
def partitions(number: int) -> list[tuple[int, ...]]:
result = []
stack = [(number, ())]
while stack:
remaining, path = stack.pop()
if not remaining:
result.append(path)
else:
stack.extend((remaining - i, path + (i,)) for i in range(remaining, 0, -1))
return result
在 CPython 中查找所有 20 个分区需要 582 毫秒,在 PyPy3 中需要 200 毫秒:
CPython
In [22]: %timeit partitions(20)
582 ms ± 4.22 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
PyPy3
In [36]: %timeit partitions(20)
199 ms ± 3.17 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
现在,使用记忆法进行暴力破解,以预期的格式输出:
PARTITION_COUNTERS = {}
def partition_counter(number: int) -> Counter:
if result := PARTITION_COUNTERS.get(number):
return result
result = Counter()
for i in range(1, number):
for run, count in partition_counter(number - i).items():
new_run = []
added = False
for a, b in run:
if a == i:
new_run.append((a, b + 1))
added = True
else:
new_run.append((a, b))
if not added:
new_run.append((i, 1))
result[tuple(sorted(new_run))] += count
result[((number, 1),)] = 1
PARTITION_COUNTERS[number] = result
return result
CPython
In [23]: %timeit PARTITION_COUNTERS.clear(); partition_counter(20)
10.4 ms ± 72.1 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PyPy3
In [37]: %timeit PARTITION_COUNTERS.clear(); partition_counter(20)
9.75 ms ± 58.3 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
找到所有 20 个分区只需要 10 毫秒,比第一个函数快得多,而且 PyPy3 并没有使其更快。
但我们如何才能做得更好呢?毕竟,我只是在用蛮力,我知道有很多用于整数分割的智能算法,但它们都无法生成预期格式的输出。