• 动态规划--凑硬币问题


    凑硬币问题

    题目详情为:有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?

      最近在学习一些重要算法,作为五大算法之一的动态规划法,自然要认真学习,这是一道典型的动态规划问题,这里使用动态规划法的思想来解题;

      我们用d(i)=j来表示凑够i元最少需要j个硬币,通过题目,很容易得到:当i=0时,d(0)=0, 表示凑够0元最小需要0个硬币; 当i=1时,只有面值为1元的硬币可用, 因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的, 即d(0)=0,则有d(1) = d(1 - 1) + 1 = 1,凑够1元最少需要1个硬币,当i = 2时,d(2) = d(2 - 1) + 1= d(1) +1=2, 当i = 3时,d(3) = min{d(3 - 1) + 1 , d(3 - 3) + 1} = min(3, 1) = 1;动态规划算法通常基于一个递推公式及一个或多个初始状态。在这里d(i) 就是状态,通过分析推导的过程,可以得到,针对面值为1,3,5的硬币,可以得到递推公式(状态转移方程)为:

                d(i) = min{ d(i - Vj) + 1} ,i >= Vj。

      在动态规划中,得到了该问题的状态及其状态转移方程,问题已经解决了一大半了,然后,在分析的过程中,并不能一眼就看出递推公式,它需要更多的练习和更多的实践积累的,并不是一朝一夕能做到的,况且动态规划的关键就是找到状态和状态转移方程,那么容易找到,就不是动态规划了,就不是难点了。根据这个公式,我们可以比较轻易的写出实现的代码:

    /*
        @动态规划练习题
        如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    int DP_leastcoin(const int coin[], int money)
    {
        int *d = (int *)malloc(sizeof(int) * (money + 1));
        memset(d, 0, sizeof(int) * money);
        int iterx = 0, itery = 0;
        int MIN = 0;
        int result = 0;
        d[0] = 0;
        for(iterx = 1; iterx  <= money; iterx++)
        {
            for(itery = 0; itery < 3 && iterx >= coin[itery]; itery++)
            {
                if(itery == 0)
                {
                    MIN = d[iterx - coin[itery]] + 1;
                }
                else if(MIN > (d[iterx - coin[itery]] + 1))
                {
                    MIN = (d[iterx - coin[itery]] + 1);
                }
            }
            d[iterx] = MIN;
        }
    
        printf("要凑的钱  MIN
    ");
        for(iterx = 0; iterx <= money; iterx++)
        {
            printf("序号%-3d : %d
    ", iterx, d[iterx]);
        }
    
        result = d[money];
        free(d);
        return result;
    }
    int main(void)
    {
        const int coin[3] = {1, 3, 5};
        printf("
    The result is %d 
    ", DP_leastcoin(coin, 112));
        return 0;
    }
    View Code

      在研究凑硬币问题的时候,我把硬币的面值换为2,3,5,然后依旧使用这个状态转移方程,得到的结果是错的,由此也可以知道,状态转移方程是针对某一问题分析得到的,尽管只是修改了硬币的面值,该方程就不再成立了,首先我们要找到问题所在,是什么问题导致了该方程不再适用。

      我们手动分析面值为2,3,5的情况:

        d(0) = 0

        d(1) = 0

        d(2) = 1

        d(3) = 1

        d(4) = 2

        d(5) = 1

        d(6) = 2

        d(7) = 2

        d(8) = 2

        d(9) = 3

        d(10) = 2

        d(11) = 3

      我们先来看看面值为2,3,5的结果图片,看看是在哪里开始出错的:

      找出两者不一样的值,发现是从序号4开始的,d(4) , d(6) , d(9) , d(11) 这几个不同(当然后续还有其他不同的值),把这些状态代入上面的状态转移方程,看看那哪里不对:

        d(4) = min{ d(4 - 3) + 1, d(4 - 2) + 1} = min{ d(1)+1, d(2)+1 }=min(0+1, 1+1) = 1;

      问题来了,本来d(4)应该是等于2,由两个面值额外2的硬币凑成,这里怎么会有1呢?1的由来,是d(1)+1 = 1;d(6)也是有问题的,看下面

        d(6) = min{d(6 - 5)+1, d(6 - 3)+1, d(6 - 2)+1} = min(d(1)+1, d(3)+1, d(4)+1}=min(1, 2, 2);

      问题还是在d(1)上面,至于后面的d(9) 和d(11)是因为使用了错误的d(6)和d(4)才错的,那这个方程的罪魁祸首就是d(1)咯?

      假设我们把d(4) 和 d(6) 都纠正过来,即d(4)= 2, d(6) = 2,那么结果又如何,你可以自己从新列一遍,从d(6)开始,后面都是正确的。这里把我纠正的图片发一下,面值为2,3,5的,我给的需要凑得钱值是112,设一个更大的值,容易排查错误情况:

        

      上面没有把数据全部列出来。我检查了一下,对于面值为2,3,5的情况,没有发现错误的。

      很奇怪,这样一改程序就正确了,我猜想,把面值1,3,5的状态转移方程,拿到面值为2,3,5问题里面就出错,而修改一下(2,3,5)问题的前面某些值d(4)和d(6),状态方程依旧适用,主要原因还是在面值为1的硬币上,由于存在面值为1的情况,假设要凑的钱数为N,那么只要N>0,肯定可以凑出来,把硬币面值换为(2,3,5),那要凑出1块钱是不可能的,所以d(1)+1就有了问题,因为你无法凑到1块钱,是不能使用d(1)的,把存在d(1)的情况去掉,那结果就是正确的,现在知道为什么是4和6了吧,因为你的面值为2,3,5,一个数减掉2,3,5得到1的数就是3,4,6,所以,d(3), d(4) ,d(6)就是错的,那前面怎么没有指出d(3),因为恰好d(1)的结果不影响d(3),尽管如此,还是要把d(1)去掉,方法如下:

    d(4) = min{d(4 - 2) + 1}  = min(2) = 2,本来是d(4) = min{d(4 - 2) + 1, d(4 - 3) + 1}  = min(2 , 1) = 1

     程序修改十分简单,就在嵌套的for循环里面加上一条语句  “if(iterx - coin[itery] == 1) continue;//当硬币面值没有1时”   即可,如下:

    /*
        @动态规划练习题
        如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    int DP_leastcoin(const int coin[], int money)
    {
        int *d = (int *)malloc(sizeof(int) * (money + 1));
        memset(d, 0, sizeof(int) * money);
        int iterx = 0, itery = 0;
        int MIN = 0;
        int result = 0;
        d[0] = 0;
        for(iterx = 1; iterx  <= money; iterx++)
        {
            for(itery = 0; itery < 3 && iterx >= coin[itery]; itery++)
            {
                if(iterx - coin[itery] == 1) continue;//当硬币面值没有1时
                if(itery == 0)
                {
                    MIN = d[iterx - coin[itery]] + 1;
                }
                else if(MIN > (d[iterx - coin[itery]] + 1))
                {
                    MIN = (d[iterx - coin[itery]] + 1);
                }
            }
            d[iterx] = MIN;
        }
    
        printf("要凑的钱  MIN
    ");
        for(iterx = 0; iterx <= money; iterx++)
        {
            printf("序号%-3d : %d
    ", iterx, d[iterx]);
        }
    
        result = d[money];
        free(d);
        return result;
    }
    int main(void)
    {
        const int coin[3] = {2, 3, 5};
        printf("
    The result is %d 
    ", DP_leastcoin(coin, 112));
        return 0;
    }
    View Code

      啰啰嗦嗦的写了很多,我的表达能力有限,望见谅!此外,希望本博客对大家学习算法能有一点帮助!

      ps:把硬币面值换为其他值,可能要重新分析,这个题目也可以用贪心算法实现,但是貌似贪心算法有可能求出来的是局部最优解,我对贪心算法不大会,等后续学习到了,再来讨论!

  • 相关阅读:
    springboot中如何向redis缓存中存入数据
    elasticSearch索引库查询的相关方法
    java客户端的elasticSearch索引库的相关操作
    lucene索引的增、删、改
    lucene的索引查询
    框架
    GG
    总结
    阿里的代码规范检查工具
    传统架构与SOA架构的区别和特点:
  • 原文地址:https://www.cnblogs.com/bestDavid/p/DPleastcoin.html
Copyright © 2020-2023  润新知