【问题标题】:permutations without repetition无重复排列
【发布时间】:2015-11-09 03:26:40
【问题描述】:

我想知道,解决这个问题的最佳方法是什么:

给定 x、y 和 y 整数:a1、a2、a3 .. y 找出所有的组合 a1 ± a2 ± ... ± ay = x, y

我最近的方法是找到存储在表 T 中的 1 和 0 的所有排列,然后根据数字 T[i] 是否为 1 和 0,从 sum 中添加或减去 ai。问题是有n! n 元素数组的排列。因此,对于 20 个元素的数组,我必须检查 20!大多数重复的可能性。您能否建议我解决我的问题的任何潜在方法?

【问题讨论】:

  • 我认为您可以使用标准的分支定界流程生成它们。
  • 你有重复,因为有些 a 是相同的?因为如果它们都不同,我认为您在生成解决方案时不应该重复。
  • @maraca 是的,我注意到了。这就是为什么我认为,我的解决方案不是最佳选择。

标签: algorithm permutation


【解决方案1】:

只有 2^20(略超过一百万)个长度为 20 的二进制向量,而不是不可行的 20!。使用应该能够在不到一秒的时间内暴力破解,特别是如果您使用 Gray Code 这将允许您在一个步骤中从一个候选总和传递到另一个候选总和(例如从 a + b - c -d 到 @ 987654324@加2*d即可。

如果y 变得更大,@MikeWise 的出色分支和绑定想法会很好。生成以0 为根节点的树。给它-a1+a1 的孩子。然后通过添加和减去a2 等来创建 4 个孙子。如果您从目标 x 得到的距离超过剩余 ai 的总和 - 您可以修剪该分支。在最坏的情况下,这可能比基于格雷码的暴力破解稍差(因为您需要在每个节点上进行更多的处理),但在最好的情况下,您可能能够消除大多数可能性。

编辑时:这是一些 Python 代码。首先我定义了一个生成器,给定一个整数n,依次返回需要翻转的位位置以逐步通过格雷码:

def grayBit(n):
    code = [0]*n
    odd = True
    done = False
    while not done:
        if odd:
            code[0] = 1 - code[0] #flip bit
            odd = False
            yield 0
        else:
            i = code.index(1)
            if i == n-1:
                done = True
            else:
                code[i+1] = 1 - code[i+1]
                odd = True
                yield i+1

(这使用了我多年前在 Stanton 和 White 的优秀著作“Constructive Combinatorics”中学到的算法)。

然后——我用它来返回所有解决方案(作为由输入的数字列表组成的列表,并根据需要插入负号)。关键是我可以将当​​前的位翻转并加或减相应数字的两倍:

def signedSums(nums, target):
    n = len(nums)
    patterns = []
    total = sum(nums)
    pattern = [1]*n
    if target == total: patterns.append([x*y for x,y in zip(nums,pattern)])
    deltas = [2*i for i in nums]
    for i in grayBit(n):
        if pattern[i] == 1:
            total -= deltas[i]
        else:
            total += deltas[i]
        pattern[i] = -1 * pattern[i]
        if target == total: patterns.append([x*y for x,y in zip(nums,pattern)])
    return patterns

典型输出:

>>> signedSums([1,2,3,4,5,9],6)
[[1, -2, -3, -4, 5, 9], [1, 2, 3, -4, -5, 9], [-1, 2, -3, 4, -5, 9], [1, 2, 3, 4, 5, -9]]

评估只需要大约一秒钟:

>>> len(signedSums([i for i in range(1,21)],100))
2865

因此,有 2865 种方法可以对 1,2,..,20 范围内的整数进行加减运算,得到净和 100。

我假设a1 可以添加或减去(而不是仅添加,如果从字面上理解,这就是您的问题所暗示的)。请注意,如果您真的想坚持a1 是肯定发生的,那么您可以从x 中减去它,并将上述算法应用于列表的其余部分和调整后的目标。

最后,不难看出,如果您使用权重集{2*a1, 2*a2, 2*a3, .... 2*ay} 和目标总和为x + a1 + a2 + ... + ay 求解subset sub problem,那么选择的子集将完全对应于正迹象出现在原问题的解决方案中。因此,您的问题很容易简化为子集和问题,因此确定它是否有任何解决方案是 NP 完全的(并且 NP 很难将它们全部列出)。

【讨论】:

    【解决方案2】:

    我们有条件:

    a1 ± a2 ± ... ± ay = x, y<20   [1]
    

    首先,我将概括条件[1],允许包括“a1”在内的所有“a”为±:

    ±a1 ± a2 ± ... ± ay = x   [2]
    

    如果我们有[2]的解决方案,我们可以很容易地得到[1]的解决方案

    为了解决[2],我们可以使用以下方法:

    combinations list x 
        | x == 0 && null list = [[]]
        | null list = []
        | otherwise = plusCombinations ++ minusCombinations where
        a = head list
        rest = tail list
        plusCombinations = map (\c -> a:c) $ combinations rest (x-a)
        minusCombinations = map (\c -> -a:c) $ combinations rest (x+a)
    

    解释:

    1. 第一个条件检查 x 是否达到零并使用列表中的所有数字。这意味着找到了解决方案,我们返回单个解决方案:[[]]

    2. 第二个条件检查列表是否为空,只要 x 不为 0,这意味着找不到解决方案,返回空解决方案:[]

    3. 第三个分支意味着我们可以有两种选择:将 ai 与“+”或“-”一起使用,因此我们连接正负组合

    示例输出:

    *Main> combinations [1,2,3,4] 2
    [[1,2,3,-4],[-1,2,-3,4]]
    *Main> combinations [1,2,3,4] 3
    []
    *Main> combinations [1,2,3,4] 4
    [[1,2,-3,4],[-1,-2,3,4]]
    

    【讨论】:

    • 我使用了 John Coleman 先生提供的答案,但我也会尝试您的答案。 :) 不过它对我来说使用了一些未知的概念。
    • 这是一个不错、优雅的递归解决方案,并强调了 Haskell 的简洁性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-14
    • 2019-09-18
    • 2020-02-02
    相关资源
    最近更新 更多