【问题标题】:Divide and conquer recursive solution for making change用于进行更改的分而治之的递归解决方案
【发布时间】:2019-09-27 12:56:02
【问题描述】:

我正在尝试实现一个分而治之的程序,当给定硬币集 c = {c0, c1,...,cn} 和金额 A 时,它会找到可以支付 A 的多种不同方式,如以及函数被递归了多少次。

我的想法是做这样的事情:

callsMade = 0
coins = [1,5,10,25]

def makeChange(A, c):
    global callsMade
    callsMade += 1
    if(A == 0):
        return 1
    if(A < 0):
        return 0
    combos = 0
    for i in range(len(coins)):
        combos += makeChange(A - coins[i], i)
    return combos

其中 A 是传入的金额,c = len(coins)-1。 不过,这段代码的 sn-p 行为并不像我预期的那样。我的思考过程是循环遍历硬币数组,用数组中那个位置的硬币减去当前数量,然后递归调用 makeChange 函数,用较低的数量和数组中的下一个硬币,然后将全局调用每个增加 1时间。

使用硬币集 = [1,5,10,25] 和金额 A = 200,组合的数量应该是 1463,大约有 1500 次调用。

【问题讨论】:

  • 我不清楚你在问什么。在中间你说这不能按预期工作。发生什么了?你需要什么帮助? (难道你还需要检查coins[I]是否小于A?
  • 传入c有什么意义?它没有被使用。
  • 目前它给了我错误的组合数量,我什至不能用超过 50 的值调用它,因为程序慢到爬行。所以首先,我想帮助了解我对这个分而治之问题的思考过程是否正确,然后希望能在那个 sn-p 中找到我的错误。
  • @MisterMiyagi 正确。我的想法是用它来遍历硬币列表,并在进行另一个递归调用之前增加它。我很困惑如何将这一切结合在一起才能正常工作。

标签: python divide-and-conquer


【解决方案1】:

重复关系看起来像这样(为简洁起见,我删除了调用计数器):

def makeChange(A, coins, k=0):
    if A == 0: return 1
    if A <  0: return 0
    return sum(makeChange(A - coins[i], coins, i) for i in range(k, len(coins)))

也就是说,你不要考虑小于你已经拿走的硬币,否则你会得到像[1, 1, 5][1, 5, 1]这样的组合。有了这个,我得到了 makeChange(200, (1,5,10,25)) 的 1463 个组合,总共有 111491 个调用——比你预期的要多一点。

请注意,此函数将多次计算许多组合。例如,您可以通过[1,5][1,1,1,1,1,1] 访问A=194,以此类推,但makeChange(194, coins, k=1) 的结果对于两种方式都是相同的。您可以使用functools.lru_cache 自动记住这些值。这样,您只需 801 次调用即可获得相同的结果。

@functools.lru_cache(None)
def makeChange(A, coins, k=0):
    # same as above

(对于记忆,你必须包含coins作为参数(作为tuple,而不是list,以便可散列),否则它会将结果重用于一组不同的硬币.)

【讨论】:

    【解决方案2】:

    基本思路是对的,但是你需要考虑递归的一些问题,以及你想把什么算作正确答案。

    如果你简单开始,你可以问[1,5,10,25]有多少个组合应该等于6

    应该是3:
    [1, 1, 1, 1, 1, 1], [5, 1], [1, 5]?
    或 2:
    [1, 1, 1, 1, 1, 1], [1, 5]?

    两个对我来说最有意义。为此,您需要将硬币数组的一个子集传递回递归,因此当您在 for 循环中查看 5 时,您不会再次考虑 [5, 1] — 假设您此时已经计算了 [1, 5] .不要传递未使用的c 参数,而是传递coins 列表。然后在循环中管理该列表。在这里我添加了一个额外的参数cur 来收集组合只是为了帮助检查工作。如果您只想要计数,则不需要它。

    def makeChange(A, coins, cur = None):
        ''' This will also collect the combinations to check the work'''
        if cur is None:
            cur = []
        global callsMade
        callsMade += 1
        if(A == 0):
            print(cur)
            return 1
        if(A < 0):
            return 0
        combos = 0
        for i, coin in enumerate(coins):
            if coin > A: # don't bother if coin is too big
                continue
            # pass a subset of the list into recursion
            combos += makeChange(A - coin, coins[i:], cur + [coin])
        return combos
    
    coinset = [1,5,10,25]
    A = 10
    makeChange(A, coinset)
    

    结果:

    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    [1, 1, 1, 1, 1, 5]
    [5, 5]
    [10]
    Out[456]:
    4
    

    A 设置为200 显示1463 组合。

    【讨论】:

    • 感谢您的描述,传递子集是有意义的,并且确实将正常的递归调用减少了大约一半。但是这个实现仍然进行了 78,903 次递归调用,这让我觉得除了完全记忆之外还有一种更有效的方法。拨打的电话应该在 1570 左右。
    • @pweave 的组合比 1570 多得多。要减少调用次数,您可以记住该函数。实际上,您可以使用lru_cache 或自己编写来将呼叫次数减少到 764。
    猜你喜欢
    • 2020-01-19
    • 1970-01-01
    • 1970-01-01
    • 2021-08-17
    • 2020-09-21
    • 2020-02-09
    • 2012-02-25
    • 2013-03-17
    • 1970-01-01
    相关资源
    最近更新 更多