查找数组/序列中等于sum的最短组合

时间:2012-04-01 12:13:14

标签: python algorithm dynamic-programming coin-change

我完全陷入困境,不知道如何解决这个问题。假设我有一个数组

arr = [1, 4, 5, 10]

和一个数字

n = 8

我需要在arr内等于n的最短序列。因此,例如在arr等于n

之后的序列
c1 = 5,1,1,1
c2 = 4,4
c3= 1,1,1,1,1,1,1,1

所以在上面的例子中,我们的答案是c2,因为它是arr中等于sum的最短序列。

我不确定找到上述解决方案的最简单方法是什么?任何想法或帮助将非常感激。

谢谢!

编辑:

  • 修复了数组
  • 数组可能只有正值。
  • 我不确定子集问题如何解决这个问题,可能是由于我自己的无知。子集算法总是给出等于和的最短序列吗?例如,子集问题会将c2识别为上述场景中的答案吗?

5 个答案:

答案 0 :(得分:3)

为了将来发现这个问题的人的利益 -

正如Oscar Lopez和Priyank Bhatnagar所指出的那样,这就是硬币改变(改变,改变)的问题。

general 中,他们提出的动态编程解决方案是最佳解决方案 - 无论是(可证明!)总是使用最少的项目生成所需的总和,以及执行速度。如果基数是任意的,那么使用动态编程解决方案。

但是,如果你的基数是“不错的”,那么一个更简单的贪婪算法就可以了。

例如,澳大利亚货币系统使用$100, $50, $20, $10, $5, $2, $1, $0.50, $0.20, $0.10, $0.05的面额。通过重复给出最大的变化单位,直到剩余金额为零(或小于5美分),可以给出任何金额的最佳变化。

这是贪婪算法的一个有益的实现,说明了这个概念。

def greedy_give_change (denominations, amount):        
    # Sort from largest to smallest
    denominations = sorted(denominations, reverse=True)

    # number of each note/coin given
    change_given = list()

    for d in denominations:
        while amount > d:
            change_given.append(d)
            amount -= d

    return change_given

australian_coins = [100, 50, 20, 10, 5, 2, 1, 0.50, 0.20, 0.10, 0.05]
change = greedy_give_change(australian_coins, 313.37)
print (change)           # [100, 100, 100, 10, 2, 1, 0.2, 0.1, 0.05]
print (sum(change))      # 313.35

对于原始帖子(denominations = [1, 4, 5, 10]amount = 8)中的具体示例,贪婪的解决方案并非最佳 - 它会给出[5, 1, 1, 1]。但是贪婪的解决方案比动态编程解决方案更快更简单,所以如果可以使用它,你应该这样做!

答案 1 :(得分:2)

这个问题被称为最小硬币更换问题。

您可以使用动态编程解决它。 这是伪代码:

Set MinCoin[i] equal to Infinity for all of i
MinCoin[0] = 0

For i = 1 to N // The number N
For j = 0 to M - 1 // M denominations given
// Number i is broken into i-Value[j] for which we already know the answer
// And we update if it gives us lesser value than previous known.
   If (Value[j] <= i and MinCoin[i-Value[j]]+1 < MinCoin[i])
       MinCoin[i] = MinCoin[i-Value[j]]+1

Output MinCoin[N]

答案 2 :(得分:2)

正如之前所指出的那样minimum change coin problem,通常用动态编程解决。这是一个在时间复杂度O(nC)和空间复杂度O(C)中解决的Python实现,其中n是硬币数量,C所需金额:

def min_change(V, C):
    table, solution = min_change_table(V, C)
    num_coins, coins = table[-1], []
    if num_coins == float('inf'):
        return []
    while C > 0:
        coins.append(V[solution[C]])
        C -= V[solution[C]]
    return coins

def min_change_table(V, C):
    m, n = C+1, len(V)
    table, solution = [0] * m, [0] * m
    for i in xrange(1, m):
        minNum, minIdx = float('inf'), -1
        for j in xrange(n):
            if V[j] <= i and 1 + table[i - V[j]] < minNum:
                minNum = 1 + table[i - V[j]]
                minIdx = j
        table[i] = minNum
        solution[i] = minIdx
    return (table, solution)

在上述功能V中列出了可能的硬币和C所需的金额。现在,当您调用min_change函数时,输出符合预期:

min_change([1,4,5,10], 8)
> [4, 4]

答案 3 :(得分:1)

这是subset-sum问题的变体。在您的问题中,您可以多次选择一个项目。您仍然可以使用类似的想法通过使用动态prorgamming技术来解决此问题。基本思想是设计函数F(k,j),使得F(k,j)= 1意味着存在来自arr的序列,其和为j且长度为k。

形式上,基本情况是F(k,1)= 1,如果存在i,则arr [i] = k。对于归纳情形,F(k,j)= 1,如果存在i,则arr [i] = m,并且F(k-1,j-m)= 1。

F(k,n)= 1的最小k是你想要的最短序列的长度。

通过使用动态编程技术,您可以在不使用递归的情况下计算函数F. 通过跟踪每个F(k,j)的附加信息,您还可以重建最短的序列。

答案 4 :(得分:0)

您尝试解决的问题是硬币更改问题的变体。在这里,您需要查找最小的更改金额,或者总计达到给定金额的最小金币数量。

考虑一个简单的例子,你的数组是

c = [1, 2, 3]

你写了5作为C元素的组合,想知道什么是最短的组合。这里C是硬币值的集合,5是你想要改变的金额。

让我们记下所有可能的组合:

1 + 1 + 1 + 1 + 1
1 + 1 + 1 + 2
1 + 2 + 2
1 + 1 + 3
2 + 3

请注意,两个组合在重新排序时是相同的,因此例如2 + 3 = 3 + 2.

这里有一个令人敬畏的结果,一见钟情并不明显,但很容易证明。如果您有任何序列的硬币/值是最小长度的序列,总计达到给定的数量,无论您如何拆分此序列,这两个部分也将是相应金额的最小长度序列。

例如,如果c[3] + c[1] + c[2] + c[7] + c[2] + c[3]加起来为S,我们就知道6是来自c的最小元素长度序列,加起来为S然后,如果你拆分

                              |
S = c[3] + c[1] + c[2] + c[7] | + c[2] + c[3]
                              |

4是加起来为c[3] + c[1] + c[2] + c[7]2的序列的最小长度,这些序列的最小长度加起来为c[2] + c[3]

                              |
S = c[3] + c[1] + c[2] + c[7] | + c[2] + c[3] 
                              |
  =        S_left             +     S_right

如何证明这一点?相反,假设S_left的长度不是最佳的,那就是较短的序列加起来S_left。但是我们可以将S写成这个较短序列和S_right的总和,这与S的长度最小的事实相矛盾。 □

无论你如何拆分序列都是如此,你可以使用这个结果来构建一个遵循动态编程范式原则的递归算法(解决较小的问题,同时可能跳过不会被使用的计算, memoization 或跟踪计算值,最后合并结果。)

好的,所以在上面的小例子中,我们将如何使用动态编程方法解决问题:假设我们想要找到c = [1, 2, 3]中最短的元素序列来写入和{{1 }}。我们解决了通过减去一枚硬币获得的子问题:55 - 15 - 2,我们采用这些子问题的最小解,并加1(丢失的硬币)。

所以我们可以编写类似

的内容
5 - 3

从底部向上编写算法很方便,从较小的总和值开始,可以保存并用于形成更大的总和。我们只是解决了从1开始并上升到所需总和的所有可能值的问题。

这是Python中的代码:

shortest_seq_length([1, 2, 3], 5) = 
    min( shortest_seq_length([1, 2, 3], 5-1),
         shortest_seq_length([1, 2, 3], 5-2),
         shortest_seq_length([1, 2, 3], 5-3)
        ) + 1

现在除了我们无法为def shortest_seq_length(c, S): res = {0: 0} # res contains computed results res[i] = shortest_seq_length(c, i) for i in range(1, S+1): res[i] = min([res[i-x] for x in c if x<=i]) + 1 return res[S] 的所有值填充memoization结构的情况之外,这种情况有效。如果我们在i中没有值1,就会出现这种情况,例如,如果c并且具有上述功能,我们就无法形成总和1我们得到

c = [2, 5]

因此,为了解决这个问题,可以使用try / catch:

shortest_seq_length([2, 3], 5)
# ValueError: min() arg is an empty sequence

尝试一下:

def shortest_seq_length(c, S):
    res = {0: 0} # res is the dictionary containing results for each sum res[i] = shortest_seq_length(c, i)
    for i in range(1, S+1):
        try:
            res[i] = min([res[i-x] for x in c if x<=i and res[i-x] is not None]) +1
        except:
            res[i] = None # takes care of error when [res[i-x] for x in c if x<=i] is empty
    return res[S]

该算法的一个小改进是当总和等于其中一个值/硬币时跳过计算最小值的步骤,但如果我们编写一个循环来计算最小值,这可以做得更好。然而,这种改善并没有改善print(shortest_seq_length([2, 3], 5)) # 2 print(shortest_seq_length([1, 5, 10, 25], 37)) # 4 print(shortest_seq_length([1, 5, 10], 30)) # 3 print(shortest_seq_length([1, 5, 10], 25)) # 3 print(shortest_seq_length([1, 5, 10], 29)) # 7 print(shortest_seq_length([5, 10], 9)) # None O(mS) {/ 1}}的总体复杂性。