Java数据结构与算法:动态规划

小师弟:刚刚在牛客网上刷了一道算法题,一脸懵逼,看评论说是用动态规划做,可是都看不懂别人的代码。。。

大师兄:什么题呀说来听听。

小师弟:给你六种面额1、5、10、20、50、100元的纸币,假设每种币值的数量都足够多,编写程序求组成N元(N为0-10000的非负整数)的不同组合的个数。
这里写图片描述

大师兄:这个题呀,确实可以用动态规划。我来考考你,还记得动态规划的三要素吗?

小师弟:【最优子结构】、【边界】、【状态转移公式】

大师兄:是的。那我来考考你,假如输入的是1000元,那么这个问题的最优子结构是什么?
为了问题讨论方便,咱们约定一下格式:

  • n元钱用最大面值不超过m元的基本面值组合起来的个数,记为A(n,m),那么1000元用{1,5,10,20,50,100}组合个数记为A(1000,100),1000元用{1,5,10,20,50}组合个数记为A(1000,50)……1000元用{1}组合个数记为A(1000,1);
  • i元钱的组合中,包含的最大面值为j元(显然j可以取1,5,10,20,50,100)的组合,记为B(i,j),那么1000元的分配组合中,最大面值为100的记为B(1000,100),最大面值为1元的记为B(1000,1)。

小师弟:1000元钱的组合可以分为,最大面值为100的,和最大面值小于100的。

大师兄:是的,按照咱们的约定方式,在基本面值为{1,5,10,20,50,100}时,1000元钱的组合记为A(1000,100),最大面值为100的记为B(1000,100),而最大面值小于100的组合就相当于用基本面值为{1,5,10,20,50}来组合,所以记为A(1000,50)。

小师弟:所以是不是可以得到,A(1000,100) = B(1000,100) + A(1000,50)。

大师兄:是的,另外B(1000,100)是不是还可以继续化简呢,你想想,1000元钱的组合中最大面值为100的组合,如果拿去了一张100元钱,剩下的组合有什么特点?

小师弟:最大面值为100,说明至少有一张100,现在拿去了一张100,那么就可能一张毛爷爷也没有了,剩下的组合中,基本面值还是{1,5,10,20,50,100},但是包含的最大面值不一定是100了,因为还剩下900元钱,所以是不是可以认为是用最大面值不超过100元的基本面值来组合900元?也就是A(900,100) 。

大师兄:非常好,所以我们就得到了A(1000,100) =A(900,100) + A(1000,50)。

小师弟:我知道了,这是递推公式,用这个我们就可以用递归来解决问题了。
这里写图片描述
A(0,100),A(1000,1)应该就属于边界条件了吧。

大师兄:是的。A(n,m)表示n元钱用最大面值不超过m元的基本面值组合起来的个数。

  • 一种情况是A(n,m) (n<m),就像A(0,100)。组合的最大面值肯定小于m元,如果还存在比m小的面值m-,那么A(n,m)=A(n,m-),如果m已经是最小的面值也就是1时,n肯定为0,这种组合方式的终极问题是A(0,1)。
  • 另一种情况是A(n,1) (n>=1),就像A(1000,1)。n>=1,而m=1时,根据常识,任意正数n元都可以用n张1元钱来组合,这种组合方式个数为1。

小师弟:那这个A(0,1)怎么处理呢?

大师兄:我们来看一下A(0,100)是怎么产生的,它的父节点是A(100,100) = A(0,100) + A(100,50),含义是100元钱用最大面值不超过100元的基本面值组合起来的个数,可以分为最大面值为100元,和最大面值小于100元两种情况,后一种情况就是B(100,100) = A(100,50),而前一种情况,100元用最大面值为100元的基本面值来表示,只有一种组合,就是一张100元,故A(0,100) = 1。

同理,m>1时,A(0,m)问题可以从A(m,m)延伸来的( A(m,m) = A(0,m) + A(m,m-) ),它对应用一张m元面值表示m元,也就是只有一种组合,所以A(0,m)一定为1。

而A(1,1)=1,所以迭代过程中不会用上A(0,1),但为了方便我们仍可以记A(0,1) = 1;

我们来总结一下得到的所有推论:

【边界】

  • A(n,1) = 1 (n>=0)
  • A(0,m) = 1 (m=1,5,10,20,50,100)

【状态转移方程】

  • A(n,m) = A(n-m,m) + A(n,m-) (n>=m, m-为小于m的面值)
  • A(n,m) = A(n,m-) (n<m)

后面就交给你了,把你的代码秀一秀。

小师弟:有了递推公式和边界条件就可以用递归解决了,但是如果N太大,会使递归数深度很大,效率很低,所以这里更适合使用动态规划。动态规划从简单过程往复杂过程计算,并把后面会用到的数据保存下来,避免重复计算。计算备忘录如下:
这里写图片描述
以A(10,10)为例,A(10,10) = A(0,10) + A(10,5) = 1 + 3 = 4;可见,每一行的计算只需要在上一行的值和这一行前面的值,因此,用一个数组就可以完成。

// @author 小师弟
import java.util.Scanner;
public class Main {
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);
        int num = in.nextInt();

        int[] m = {1, 5, 10, 20, 50, 100}; // 保存基本面额的数组
        long[] data = new long[num+1]; // 保存计算数据的数组
        for(int i = 0; i <= num; i++) // 边界条件A(n,1) = 1 (n>=0)
            data[i] = 1;
        for(int i = 1; i < 6; i++) // 基本面额从5开始,因为1元情况在数组初始化时已经写入了
            for(int n = 1; n <= num; n++) // n从1开始,保证了边界条件A(0,m)=1 (m=1,5,10,20,50,100)
                if(n >= m[i])
                    data[n] += data[n - m[i]]; // 状态转移方程
        System.out.println(data[num]);
        in.close();
    }
}

大师兄:很好很好,撸起袖子加油干。

猜你喜欢

转载自blog.csdn.net/weixin_40255793/article/details/79634651