【问题标题】:algorithmic puzzle for calculating the number of combinations of numbers sum to a fixed result用于计算数字组合数量总和到固定结果的算法难题
【发布时间】:2016-01-11 03:54:06
【问题描述】:

这是我从昨晚开始想到的一个谜题。我想出了一个解决方案,但效率不高,所以我想看看是否有更好的主意。

谜题是这样的:

给定正整数 N 和 T,您需要:

for i in [1, T], A[i] from { -1, 0, 1 }, such that SUM(A) == N

additionally, the prefix sum of A shall be [0, N], while when the prefix sum PSUM[A, t] == N, it's necessary to have for i in [t + 1, T], A[i] == 0

here prefix sum PSUM is defined to be: PSUM[A, t] = SUM(A[i] for i in [1, t])

the puzzle asks how many such A's exist given fixed N and T

例如,当N = 2T = 4,跟随As工作时:

1 1 0 0
1 -1 1 1
0 1 1 0

但以下不要:

-1 1 1 1  # prefix sum -1
1 1 -1 1  # non-0 following a prefix sum == N
1 1 1 -1  # prefix sum > N

以下python代码可以验证这样的规则,当给定N为expect和一个A的实例为seq时(有些人可能觉得阅读代码比阅读文字描述更容易):

def verify(expect, seq):
    s = 0
    for j, i in enumerate(seq):
        s += i
        if s < 0:
            return False
        if s == expect:
            break
    else:
        return s == expect
    for k in range(j + 1, len(seq)):
        if seq[k] != 0:
            return False
    return True

我已经编写了我的解决方案,但它太慢了。以下是我的:

我将问题分解为两部分,其中没有-1 的部分(只有{0, 1}-1 的部分。

所以如果SOLVE(N, T) 是正确答案,我定义了一个函数SOLVE'(N, T, B),其中正数B 允许我将前缀总和扩展到[-B, N] 的区间内,而不是[0, N]

事实上SOLVE(N, T) == SOLVE'(N, T, 0)

所以我很快意识到解决方案实际上是:

  1. A 的前缀是一些有效的{0, 1} 组合与正长度l,并在其中包含o 1s
  2. l + 1的位置,我开始添加1个或多个-1s并使用B来跟踪数字。最大值为 B + o 或取决于 A 中剩余的插槽数,以较小者为准。
  3. 递归调用SOLVE'(N, T, B)

在前面的N = 2, T = 4 示例中,在其中一个搜索案例中,我会这样做:

  1. 设A的前缀为[1],则有A = [1, -, -, -]
  2. 开始添加-1。这里我只添加一个:A = [1, -1, -, -]
  3. 递归调用SOLVE',这里我会调用SOLVE'(2, 2, 0)来解决最后两个点。在这里它只会返回[1, 1]。然后其中一种组合产生[1, -1, 1, 1]

但是这个算法太慢了。

我想知道如何优化它或以任何不同的方式来看待这个可以提高性能的问题?(我只需要这个想法,而不是实现)

编辑:

一些样本将是:

T N RESOLVE(N, T)
3 2 3
4 2 7
5 2 15
6 2 31
7 2 63
8 2 127
9 2 255
10 2 511
11 2 1023
12 2 2047
13 2 4095
3 3 1
4 3 4
5 3 12
6 3 32
7 3 81
8 3 200
9 3 488
10 3 1184
11 3 2865
12 3 6924
13 3 16724
4 4 1
5 4 5
6 4 18

指数时间解决方案通常如下(在 python 中):

import itertools
choices = [-1, 0, 1]
print len([l for l in itertools.product(*([choices] * t)) if verify(n, l)])

【问题讨论】:

  • 您能否提供一些测试输入和输出,并说明您当前的算法在这些方面需要多长时间?
  • @MarkDickinson 更新了一些示例输出。我当前的 impl 需要几秒钟才能解决 SOLVE(10, 100).
  • 谢谢!不过,我不确定我是否相信这些输入和输出。对于第一个(T = N = 3),不是 [1, 1, 1] 唯一的解决方案吗?我错过了哪两个解决方案?
  • @MarkDickinson 我将 N 加了一个。现在它已经修复了。
  • 好的,他们同意我的解决方案(我为solve(10, 100) 花费了大约 1 毫秒,结果为250639233987229485923025924628548154758061157)。我会发布我的解决方案。

标签: algorithm


【解决方案1】:

观察:假设n 至少为1,则您陈述的问题的每个解决方案都以[1, 0, ..., 0] 的形式结束:即单个1 后跟零个或多个0s .该点之前的解决方案部分是完全位于[0, n-1] 中的步行,从0 开始,在n-1 结束,并且少于t 步数。

因此,您可以将最初的问题简化为一个稍微简单的问题,即确定在[0, n] 中有多少个t-step walks,从0 开始,到n 结束(其中每个步骤可以是0+1-1,和以前一样)。

以下代码解决了更简单的问题。它使用lru_cache 装饰器来缓存中间结果;这是在 Python 3 的标准库中,或者有一个 recipe 你可以为 Python 2 下载。

from functools import lru_cache

@lru_cache()
def walks(k, n, t):
    """
    Return the number of length-t walks in [0, n]
    that start at 0 and end at k. Each step
    in the walk adds -1, 0 or 1 to the current total.

    Inputs should satisfy 0 <= k <= n and 0 <= t.
    """
    if t == 0:
        # If no steps allowed, we can only get to 0,
        # and then only in one way.
        return k == 0
    else:
        # Count the walks ending in 0.
        total = walks(k, n, t-1)
        if 0 < k:
            # ... plus the walks ending in 1.
            total += walks(k-1, n, t-1)
        if k < n:
            # ... plus the walks ending in -1.
            total += walks(k+1, n, t-1)
        return total

现在我们可以使用这个功能来解决您的问题了。

def solve(n, t):
    """
    Find number of solutions to the original problem.
    """
    # All solutions stick at n once they get there.
    # Therefore it's enough to find all walks
    # that lie in [0, n-1] and take us to n-1 in
    # fewer than t steps.
    return sum(walks(n-1, n-1, i) for i in range(t))

我的机器上solve(10, 100) 的结果和时间安排:

In [1]: solve(10, 100)
Out[1]: 250639233987229485923025924628548154758061157

In [2]: %timeit solve(10, 100)
1000 loops, best of 3: 964 µs per loop

【讨论】:

  • 天哪。这太妙了。让我仔细阅读你的想法。
  • 嗯。这个时间是假的,因为它使用了缓存的结果。我机器上的实际时间更像是 0.002 秒。
  • 可以使用缓存。但我不明白的是,即使你有缓存,整个区间最长步行的情况也将是(n - 1) * (n - 1) * t = O(t * n ^ 2)。这个程序似乎比这运行得更快。你会怎么解释?
  • 请注意,对于任何特定的solve 调用,walks 函数的第二个参数 (n) 不会发生变化。所以我认为只有(n-1) * t
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-29
相关资源
最近更新 更多