动态编程 - 硬币更改决策

时间:2010-04-13 23:16:20

标签: algorithm computer-science dynamic-programming

我正在审查算法课程中的一些旧笔记,动态编程问题对我来说似乎有点棘手。我有一个问题,我们有无限的硬币供应,有一些面额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;

转换这个动态程序对我来说并不容易。我该怎么做呢?

7 个答案:

答案 0 :(得分:12)

您的代码是一个良好的开端。将递归解决方案转换为动态编程解决方案的常用方法是“自下而上”而不是“自上而下”。也就是说,如果您的递归解决方案使用较小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

答案 1 :(得分:4)

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

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);
    }
}

答案 2 :(得分:1)

只需在递归解决方案中添加一个memoization步骤,动态算法就可以了。以下示例在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比您的稍长的原因。

答案 3 :(得分:1)

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

采用x [2] = 51,x [1] = 50,x [0] = 1,X = 100的病理学例子。然后要求算法“考虑”用硬币进行改变的可能性x [2],或者以x [1]开始进行更改。与国家造币一起使用的第一步,也称为Greedy Algorithm - 以及,“使用小于工作总数的最大硬币”,将不适用于病态造币。相反,这种算法经历了组合爆炸,使他们有资格进入NP完全。

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

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

动态编程通常给出的一个例子是乘以几个矩阵的应用程序。根据矩阵的大小,选择 A · B · C 作为两种等价形式之一:(( A · B )· C )或( A ·( B · C ))导致不同数量的乘法和加法的计算。也就是说,一种方法比另一种方法更优化(更快)。动态编程是一个主题,它列出了不同方法的计算成本,并根据在运行时动态计算程序计划执行实际计算。

一个关键特征是计算是根据计算的计划执行的,而不是通过所有可能组合的枚举来执行的 - 无论是递归地还是迭代地执行枚举。在乘法矩阵的示例中,在每个步骤中,仅选择最小成本乘法。结果,从不计算中间成本次优计划的可能成本。换句话说,计划不是通过搜索所有可能的最优计划来计算的,而是通过从零开始逐步建立最佳计划来计算的。

命名法“动态编程”可以与“线性编程”进行比较,其中“程序”也用于意义上的“计划”。

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

Cover of the recommended book. http://ecx.images-amazon.com/images/I/41hJ7gLDOmL._SL500_AA300_.jpg

答案 4 :(得分:1)

本文非常相关:http://ecommons.library.cornell.edu/handle/1813/6219

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

答案 5 :(得分:1)

这是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 :(得分:0)

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

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;
}