动态规划--凑硬币问题

凑硬币问题

题目详情为:有面值为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\n");
    for(iterx = 0; iterx <= money; iterx++)
    {
        printf("序号%-3d : %d\n", iterx, d[iterx]);
    }

    result = d[money];
    free(d);
    return result;
}
int main(void)
{
    const int coin[3] = {1, 3, 5};
    printf("\nThe result is %d \n", 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\n");
    for(iterx = 0; iterx <= money; iterx++)
    {
        printf("序号%-3d : %d\n", iterx, d[iterx]);
    }

    result = d[money];
    free(d);
    return result;
}
int main(void)
{
    const int coin[3] = {2, 3, 5};
    printf("\nThe result is %d \n", DP_leastcoin(coin, 112));
    return 0;
}
View Code

 

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

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

 

posted on 2013-10-11 18:59  大卫david  阅读(9524)  评论(10编辑  收藏  举报

导航