【问题标题】:Dynamic programming - Coin change decision动态规划 - 硬币变化决策
【发布时间】:2011-02-07 16:43:53
【问题描述】:

我正在复习我的算法课程中的一些旧笔记,动态编程问题对我来说似乎有点棘手。我有一个问题,我们有无限供应的硬币,有一些面额 x1,x2,... xn,我们想找一些价值 X。我们正在尝试设计一个动态程序来决定 X 的找零是否可以制造与否(不是最小化硬币的数量,或者归还哪些硬币,只是对或错)。

我已经对这个问题进行了一些思考,我可以看到一种递归方法,它类似于......

MakeChange(X, x[1..n this is the coins])
    for (int i = 1; i < n; i++)
    {
        if ( (X - x[i] ==0) || MakeChange(X - x[i]) )
            return true;
    }
    return false;

将其转换为动态程序对我来说并不容易。我该如何处理?

【问题讨论】:

  • 忽略Heath,这是dp

标签: algorithm computer-science dynamic-programming


【解决方案1】:

您的代码是一个好的开始。将递归解决方案转换为动态编程解决方案的常用方法是“自下而上”而不是“自上而下”。也就是说,如果您的递归解决方案使用较小 x 的值计算特定 X 的值,则改为从较小的 x 开始计算相同的值 ,并将其放入表中。

在您的情况下,将您的 MakeChange 递归函数更改为 canMakeChange 表。

canMakeChange[0] = True
for X = 1 to (your max value):
   canMakeChange[X] = False
   for i=1 to n:
     if X>=x[i] and canMakeChange[X-x[i]]==True: 
       canMakeChange[X]=True

【讨论】:

  • 自下而上的方法绝对是动态编程的标志,也是我试图翻转的东西。谢谢!
  • @Heath 不正确。上面的算法最初只为 0 返回 true。然后当其中一个硬币 (X - xi) = 9 时,它将返回 true。这些将自下而上建立。这就是动态规划的全部意义
  • 好的,我明白你的意思了。但这也不是动态规划。相关问题“你将哪些硬币作为零钱返还”无法通过这种方式解决。我还以为你也想解决这个问题。对于任何包含 x[n]=1 的硬币来说,这是一个更简单的问题;返回真。
  • 是什么让这不是动态编程?当你做类矩阵乘法问题时,很多时候你只是从返回最小乘法数开始。那仍然是最真实形式的动态规划,不是吗?
  • 具体的质量,imo 使这种方法不是动态编程:计算所有中间结果,而不仅仅是通过追求最佳子结果的“路径”形成的子集;因为实际上在此实现中没有“最佳”子结果的概念。例如,如果可以用四个四分之一、十个硬币、二十个镍币或一百个便士进行更改,那么所有这些可能性都将在此算法返回之前计算出来。
【解决方案2】:

我下面的解决方案是一种贪婪的方法,计算所有解决方案并缓存最新的最优解决方案。如果当前执行的解决方案已经大于缓存的解决方案,则中止路径。请注意,为获得最佳性能,面额应按降序排列。

import java.util.ArrayList;
import java.util.List;

public class CoinDenomination {

    int denomination[] = new int[]{50,33,21,2,1};
    int minCoins=Integer.MAX_VALUE;
    String path;

    class Node{
        public int coinValue;
        public int amtRemaining;
        public int solutionLength;
        public String path="";
        public List<Node> next;

        public String toString() { return "C: "+coinValue+" A: "+amtRemaining+" S:"+solutionLength;}
    }

    public List<Node> build(Node node)
    {
        if(node.amtRemaining==0)
        {
            if (minCoins>node.solutionLength) {
                minCoins=node.solutionLength;
                path=node.path;
            }
            return null;
        }

        if (node.solutionLength==minCoins) return null;
        List<Node> nodes = new ArrayList<Node>();
        for(int deno:denomination)
        {
           if(node.amtRemaining>=deno)
           {
               Node nextNode = new Node();
               nextNode.amtRemaining=node.amtRemaining-deno;
               nextNode.coinValue=deno;
               nextNode.solutionLength=node.solutionLength+1;
               nextNode.path=node.path+"->"+deno;
               System.out.println(node);
               nextNode.next = build(nextNode);
               nodes.add(node);

           }
        }

        return nodes;
    }

    public void start(int value)
    {
        Node root = new Node();
        root.amtRemaining=value;
        root.solutionLength=0;
        root.path="start";
        root.next=build(root);
        System.out.println("Smallest solution of coins count: "+minCoins+" \nCoins: "+path);
    }

    public static void main(String args[])
    {
        CoinDenomination coin = new CoinDenomination();
        coin.start(35);
    }
}

【讨论】:

    【解决方案3】:

    只需在递归解决方案中添加一个记忆步骤,动态算法就会脱离它。以下示例使用 Python:

    cache = {}
    def makeChange(amount, coins):
        if (amount,coins) in cache:
            return cache[amount, coins]
        if amount == 0:
            ret = True
        elif not coins:
            ret = False
        elif amount < 0:
            ret = False 
        else:
            ret = makeChange(amount-coins[0], coins) or makeChange(amount, coins[1:])
        cache[amount, coins] = ret
        return ret
    

    当然,您可以使用装饰器来自动记忆,从而获得更自然的代码:

    def memoize(f):
        cache = {}
        def ret(*args):
            if args not in cache:
                cache[args] = f(*args)
            return cache[args]
        return ret
    @memoize
    def makeChange(amount, coins):
        if amount == 0:
            return True
        elif not coins:
            return False
        elif amount < 0:
            return False
        return makeChange(amount-coins[0], coins) or makeChange(amount, coins[1:])
    

    注意:即使您发布的非动态编程版本也存在各种边缘情况错误,这就是为什么上面的 makeChange 比您的稍长。

    【讨论】:

    • 它仍然有一个错误:如果你将硬币作为列表而不是元组传递,你会得到一个“TypeError: unhashable type”。
    • 这不是一个错误——它是一个特性(如果你将“amount”作为字符串传递,你也会得到一个异常)。当你传入不正确的类型时出现类型错误是一个特性,而不是一个错误。
    【解决方案4】:

    这篇论文很相关:http://ecommons.library.cornell.edu/handle/1813/6219

    基本上,正如其他人所说,使任意 X 与任意面额集合相加的最优变化是 NP-Hard,这意味着动态规划不会产生及时的算法。本文提出了一种多项式时间(即输入大小的多项式,是对先前算法的改进)算法,用于确定贪心算法是否总是针对给定的面额集产生最优结果。

    【讨论】:

      【解决方案5】:

      这里是 c# 版本,仅供参考,以找到给定总和所需的最小硬币数量:

      (详情可参考我的博客@http://codingworkout.blogspot.com/2014/08/coin-change-subset-sum-problem-with.html

      public int DP_CoinChange_GetMinimalDemoninations(int[] coins, int sum)
              {
                  coins.ThrowIfNull("coins");
                  coins.Throw("coins", c => c.Length == 0 || c.Any(ci => ci <= 0));
                  sum.Throw("sum", s => s <= 0);
                  int[][] DP_Cache = new int[coins.Length + 1][];
                  for (int i = 0; i <= coins.Length; i++)
                  {
                      DP_Cache[i] = new int[sum + 1];
                  }
                  for(int i = 1;i<=coins.Length;i++)
                  {
                      for(int s=0;s<=sum;s++)
                      {
                          if (coins[i - 1] == s)
                          {
                              //k, we can get to sum using just the current coin
                              //so, assign to 1, no need to process further
                              DP_Cache[i][s] = 1;
                          }
                          else 
                          {
                              //initialize the value withouth the current value
                              int minNoOfCounsWithoutUsingCurrentCoin_I = DP_Cache[i - 1][s];
                              DP_Cache[i][s] = minNoOfCounsWithoutUsingCurrentCoin_I;
                              if ((s > coins[i - 1]) //current coin can particiapte
                                  && (DP_Cache[i][s - coins[i - 1]] != 0))
                              {
                                  int noOfCoinsUsedIncludingCurrentCoin_I = 
                                      DP_Cache[i][s - coins[i - 1]] + 1;
                                  if (minNoOfCounsWithoutUsingCurrentCoin_I == 0)
                                  {
                                      //so far we couldnt identify coins that sums to 's'
                                      DP_Cache[i][s] = noOfCoinsUsedIncludingCurrentCoin_I;
                                  }
                                  else
                                  {   
                                      int min = this.Min(noOfCoinsUsedIncludingCurrentCoin_I, 
                                          minNoOfCounsWithoutUsingCurrentCoin_I);
                                      DP_Cache[i][s] = min;
                                  }
                              }
                          }
                      }
                  }
                  return DP_Cache[coins.Length][sum];
              }
      

      【讨论】:

        【解决方案6】:

        在一般情况下,硬币值可以是任意的,您提出的问题称为Knapsack Problem,并且已知属于 NP-complete (Pearson, D. 2004),因此无法在多项式时间内解决比如动态规划。

        以 x[2] = 51, x[1] = 50, x[0] = 1, X = 100 的病态为例。那么就需要算法“考虑”用硬币找零的可能性x[2],或者从 x[1] 开始进行更改。用于国家造币的第一步,也称为Greedy Algorithm -- to wit,“使用小于工作总量的最大硬币”,不适用于病态造币。相反,此类算法经历了组合爆炸,使它们符合 NP-complete 的条件。

        对于某些特殊的硬币价值安排,例如几乎所有实际使用的硬币价值安排,包括虚构系统 X[i+1] == 2 * X[i],有非常快的算法,甚至 O(1)在给定的虚构情况下,确定最佳输出。这些算法利用硬币价值的属性。

        我不知道动态编程解决方案:一种利用编程主题所需的最佳子解决方案的解决方案。一般来说,一个问题只能通过动态规划来解决,如果它可以分解为子问题,当优化解决时,可以重新组合成一个可证明是最优的解决方案。也就是说,如果程序员不能在数学上证明(“证明”)重新组合问题的最优子解会产生最优解,则不能应用动态规划。

        动态规划的一个常见示例是多个矩阵相乘的应用。根据矩阵的大小,选择将 A·B·C 评估为两种等效形式之一:((A·BC) 或 (A·(B· C)) 导致计算不同数量的乘法和加法。也就是说,一种方法比另一种方法更优化(更快)。动态编程是将不同方法的计算成本制成表格的主题,并根据运行时动态计算的时间表(或程序)执行实际计算。

        一个关键特性是计算是根据计算出的计划执行的,而不是通过枚举所有可能的组合来执行的——无论枚举是递归执行还是迭代执行。在矩阵相乘的例子中,在每一步,只选择成本最低的乘法。因此,永远不会计算中间成本次优计划的可能成本。换句话说,调度不是通过搜索所有可能的调度以获得最优调度来计算的,而是通过从无到有逐步构建最优调度来计算的。

        术语“动态规划”可以与“线性规划”相比较,其中“程序”也用于表示“调度”的意思。

        要了解有关动态编程的更多信息,请参阅我所知道的关于算法的最伟大的书籍,"Introduction to Algorithms" by Cormen, Leiserson, Rivest, and Stein.“Rivest”是“RSA”的“R”,而动态编程只是分数的一章。

        【讨论】:

        • 你说的很多东西都非常有用和真实。但是,决定是否可以进行更改的行为肯定具有最佳子结构。如果我可以将我的面额 X 分解为因子,那么这些因子的解组合起来将产生我的解。是的,背包问题是 NP Complete,但这更具体一点,不是吗?
        • 现实生活中的改变只比背包问题更具体,因为本国货币(例如美国硬币)具有特殊属性。这些属性可能让您使用动态编程,但这里看到的所有示例(包括您的原始示例)实际上都是贪心算法,如果可以识别的话。考虑我的例子,有 51、50 和 1 个“美分”的硬币……这是一种病态的造币,加上 49 美分、48 美分的硬币等等会变得更糟。这些无意义的造词,作为一个集合,引起了普遍的问题。
        • 抛开现实生活中的变化不谈,存在最优子结构,因为它们是可能有因子的整数。如果这些因素分解为您的硬币组中的因素,那么您将有一个解决方案,否则您没有。如果我有一个找到硬币面额的解决方案给我 13,我有一个解决方案给我 7,那么我有一个解决方案给我 20。
        • 因子是乘法,对吧?这是减法,当我们做出改变时,我想。如果我欠你一美元,我付给你 0.25 美元,我现在是欠你 0.75 美元(减法),还是我现在欠你无量纲数“4”(除法)?我想我们要减法,而不是因式分解。
        • 有趣的想法。我个人称他的解决方案为动态编程。据我个人所知,确实如此。但是,我还有很多东西要学。感谢您的讨论。这真的让我想到了 DP
        【解决方案7】:

        i如果您以递归方式编写,那很好,只需使用基于内存的搜索即可。你必须存储你计算过的,不会再计算了

        int memory[#(coins)]; //initialize it to be -1, which means hasn't been calculated
        MakeChange(X, x[1..n this is the coins], i){
            if(memory[i]!=-1) return memory[i];
            for (int i = 1; i < n; i++)
            {
                if ( (X - x[i] ==0) || MakeChange(X - x[i], i) ){
                    memory[i]=true;
                    return true;
                }
            }
            return false;
        }
        

        【讨论】:

          猜你喜欢
          • 2023-03-20
          • 2018-02-25
          • 2012-11-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-10-25
          • 2016-01-23
          相关资源
          最近更新 更多