【问题标题】:Recursive Algorithm Time Complexity: Coin Change递归算法时间复杂度:硬币变化
【发布时间】:2016-11-20 12:38:19
【问题描述】:

我正在研究一些算法,并遇到了coin change 问题。

在考虑这个问题时,我想出了这个幼稚的递归解决方案:

int coinChange(const vector<int>& coins, int start, int n) {
  if (n == 0) return 1;
  if (n < 0) return 0;

  int total = 0;

  for (int i = start; i < coins.size(); ++i) {
    if (coins[i] <= n) total += coinChange(coins, i, n-coins[i]);
  }

  return total;
}

然后我意识到“接受”的解决方案如下:

int count( int S[], int m, int n )
{
    // If n is 0 then there is 1 solution (do not include any coin)
    if (n == 0)
        return 1;

    // If n is less than 0 then no solution exists
    if (n < 0)
        return 0;

    // If there are no coins and n is greater than 0, then no solution exist
    if (m <=0 && n >= 1)
        return 0;

    // count is sum of solutions (i) including S[m-1] (ii) excluding S[m-1]
    return count( S, m - 1, n ) + count( S, m, n-S[m-1] );
}

起初我以为两者本质上是一样的。我很清楚我的递归树要宽得多,但这似乎只是因为我的算法在每个级别上都做了更多的工作,所以它变得平衡了。看起来这两种算法都在考虑用当前硬币进行更改的方法的数量(假设它 start 与m 在第二个算法中的作用基本相同。

我越看越觉得不管前面的文字,我的算法是O(n^n),第二个是O(2^n)。我已经研究这个太久了,但如果有人能解释一下我的算法与第二个算法相比做了哪些额外的工作,那就太好了。

编辑

我了解这个问题的动态规划解决方案,这个问题纯粹是一个基于复杂性的问题。

【问题讨论】:

  • 这可能是题外话,但虽然算法是 O(2^n),但这不是一个严格的界限。也就是说,无论 S[] 的值如何,算法都不是 Theta(2^n)。例如,当 m=2 时,最坏的情况是 S=[1, 2],复杂度是 Theta(Fib(n)) = Theta(phi^n),其中 phi 是黄金分割率。
  • @PaulHankin 好点。所以说这些算法是 Theta(2^n),我的理解是我们实际上需要找到一个 n_sub_0 使得 f(n) = O(2^n) 和 f(n) = Omega(2 ^n),对于所有n &gt;= n_sub_0,在这种特殊情况下可以这样做吗?
  • Dom,我不确定我明白你在问什么。如果你问是否有可能找到一个常数 c,使得所有大 n 的 c * phi^n >= 2^n,那么证明它不是相对容易。
  • @PaulHankin 对不起,让我试着澄清一下。我想知道是否可以对该算法进行严格限制,使得 f(n) = Theta(x) 或者我们是否可以证明对于大于某个阈值的所有输入,f(n) = Theta(2^ n)。我提出这个是因为您的示例似乎引入了一些小的输入来表明 f(n) = 不是 Theta(2^n) 而是 Theta(phi^n)。我们能否确定在输入什么大小(数组大小和/或更改值)后算法确实是 Theta(2^n)?
  • 我相信没有输入是 Theta(2^n),尽管随着硬币数量的增加它越来越接近。我相信(有一些证据,但没有严格的证据)复杂性是 Theta(r^n) 其中 r+r^(-|S|) = 2 and 1

标签: c++ algorithm recursion time-complexity coin-change


【解决方案1】:

这两段代码是相同的,只是第二段使用递归而不是 for 循环来遍历硬币。这使得它们的运行时复杂度相同(尽管由于额外的递归调用,第二段代码可能具有更差的内存复杂度,但这可能会丢失)。

例如,这里是在 S = [1, 5, 10] 和 m=3 的情况下对第二个 count 的部分评估。在每一行上,我扩展了count 最左边的定义。

  count(S, 3, 100)
= count(S, 2, 100) + count(S, 3, 90)
= count(S, 1, 100) + count(S, 2, 95) + count(S, 3, 90)
= count(S, 0, 100) + count(S, 1, 99) + count(S, 2, 95) + count(S, 3, 90)
= 0 + count(S, 1, 99) + count(S, 2, 95) + count(S, 3, 90)

您可以看到这与您的 for 循环的计算相同,总和 total

这两种算法都很糟糕,因为它们在指数时间内运行。这是一个(我的)答案,它使用了一种简洁的动态编程方法,该方法在 O(nm) 时间内运行并使用 O(n) 内存,并且非常简洁——在大小上与您的幼稚递归解决方案相当。 https://stackoverflow.com/a/20743780/1400793 。它在 Python 中,但可以轻松转换为 C++。

【讨论】:

  • 非常感谢。是的,我了解解决这个问题的 DP 方法,我只是单纯地想知道递归版本是否具有相同的复杂性,因为我已经到了让自己相信它可以去任何一种方式的地步,哈哈。
  • 不错的py解决方案顺便说一句
【解决方案2】:

您没有阅读整篇文章 (?)。

动态编程背后的想法是存储一些已经获得的值,这样就不需要再次计算它们。在文末您可以看到实际正确的解决方案。

至于为什么你的解是 n^n 而他们原来的解是 2^n。两种解决方案实际上都是 2^(n+#coins)。他们只是用 m-1 调用函数,而不是有一个循环遍历每个硬币。虽然您的解决方案在开始时尝试每枚硬币,然后越来越少,但他们的尝试尝试取一个 m 类型的硬币,然后是另一个,然后是另一个,直到某个时候它切换到类型 m-1 并且对它做同样的事情,所以在。基本上两种解决方案都是一样的。

另一种证明它们具有相同复杂性的方法是这样的:

两种解法都是正确的,因此它们将达到所有可能的解法,并且在递归的特定分支达到负数时都停止增长。因此,它们具有相同的复杂性。

如果您不相信,请尝试每个解决方案,除了添加一些计数器并在每次输入函数时递增它。对每个解决方案执行此操作,您会看到得到相同的数字。

【讨论】:

  • 我确实阅读了整篇文章。我的问题是围绕文章的递归部分,而不是 DP 部分。我也理解文章的 DP 部分,我只是有点困惑我的递归版本是否与他们的复杂性相同。这是一个比任何事情都更普遍的复杂性问题。谢谢!
  • 我明白了。我只是想确保您了解 DP 解决方案。无论如何,我是否设法回答了您的问题,还是需要再解释一些?
  • 不,我想你解决了我的困惑。我到了可以看到它是一个 n^n 算法的地步,即使它似乎做的工作量与显然是 2^n 的解决方案相同。谢谢!
【解决方案3】:

基准测试 在我的计算机上,基准测试如下:

coinChange(v, 0, 500);// v=[1, 5, 10, 15, 20, 25]

耗时 1.84649 秒完成。 但是

count(s, 6, 500); //s = [1, 5, 10, 15, 20, 25]

执行耗时 0.853075 秒。
编辑
我将结果解释为两种算法的时间复杂度相同。

【讨论】:

  • 不要让我开始讨论在实际应用时理论算法复杂性是如何被抛到窗外的......这里提供的信息可能已经过时,因此有用性可能会降低.如果您愿意,请留下它,但我不乐意将其作为问题的答案...想想问题的含义。
  • 如果您要进行基准测试,您应该评估随着输入的增长时间增长的情况。这将更好地建议运行时。现在这两个数字并不能说明任何事情
  • @KartikChugh 是的。
猜你喜欢
  • 1970-01-01
  • 2011-02-12
  • 1970-01-01
  • 2017-10-28
  • 1970-01-01
  • 2017-07-15
相关资源
最近更新 更多