动态规划1:找零钱练问题

动态规划

很多实际的问题往往需要使用动态规划来解决,动态规划的题目很难,如果按照自己的想法去实现会非常复杂,不仅逻辑复杂而且时间复杂度很高,解决动态规划的问题往往是有套路的,即动态规划的问题题型都差不多,解决思路比较相似,因此有时候可以套用已有的阶梯思路来解决问题。对于动态规划题目,首先需要理解动态规划的含义是什么,动态规划的本质究竟是什么,然后掌握动态规划问题解决的思路、策略和方法,最后多做几个题目来灵活应用动态规划的套路解决实际的问题。

题目:有数组penny,penny中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。

测试样例:[1,2,4],3,3  返回:2

思路:

方法一:暴力递归搜索

对于一个目标值aim,使用若干个给定的整数来拼凑出这个目标值aim,元素在数组penny[]中给定,每个元素可以使用0次,1次或者任意次,求出拼凑方案的数目有多少种。解决这个问题的思路显然是使用递归,不使用递归如果仅仅使用循环的话逻辑是非常复杂的,基本不可能解决,必须使用递归。即对数组的第一个元素的情况进行分情况枚举,假设目标值为10,零钱数组为[5,3,2],则考虑5元使用0次时的方案有多少种res1,5元使用1次的方案有多少种res2,5元使用2次的方案有多少种res3,将这3个结果加起来就得到了结果总的方案数目。这样通过一次枚举其实就将问题剥去了一层,使得问题有所简化,在计算res1,res2,res3时,面对的问题和初始时相同,只是使用的参数条件发生了变化,例如求res2时,此时可以使用的零钱数组是[3,2],目标值不再是10,而是aim-1*5=5,于是问题就是使用[3,2]来拼凑5,解决方法和之前是一样的,因此设计一个递归函数,输入一个目标值aim,和可以使用的零钱数组的范围index(表示小标为index及之后的元素),返回使用给定零钱可以拼凑出目标值的方案数目res。

递推关系:对penny[index]的使用的次数情况进行枚举,for(int i=0,penny[index]*i<=aim,i++),对于每一个i都对应一个枚举的情况,在该枚举情况下可以重新调整参数,此时可以使用的零钱数组是penny中index+1开始及之后的数组部分,新的目标值是aim=(aim-penny[index]*i),于是调用递归方法dp(penny,index+1,aim-penny[index]*i)即可返回当前枚举条件下的方案数目,对每一种情况下的方案数目进行累加即可得到总的方案数目。

边界条件:这里关键是对于边界条件或者叫作初始条件的确定,每次递归方法返回的都是使用数组元素arr[]拼凑得到aim的方案数目,那么到底怎么计算拼凑的方案数目呢?其实需要理解,使用递归时,问题不断展开,即相当于枚举出了使用penny[]数组元素各种组合情况,此时需要判断是否满足拼凑的要求,即怎样算是拼凑成功了,怎样算是拼凑没有成功,显然在某种枚举情况下,当aim值变成0时说明拼凑成功,否则拼凑不成功;并且由于前面的若干个元素的组成总是要保证和<=aim,因此最终的填坑的工作总是要交给最后一个元素来进行,即通过递归,使用前面的元素[5,3]可以组合成的各种价值已经都被列举完了,次时就要看最后一个元素2是否能在for(int i=0,penny[index]*i<=aim,i++)的枚举过程中使得总和达到aim值,如果恰好达到则算作一个方案,否则不能算作方案。因此边界条件是当index>penny.length-1时,判断aim是否等于0。理解:不管前面的元素如何组合(各种组合方案通过递归已经展现出来了),要想形成一个完整的方案(5使用x次,3使用y次,2使用z次),必须确定最后一个元素使用了多少次,因此必须对最后一个元素使用的次数进行枚举,每一次枚举的过程相同,枚举的同时判断剩余的目标值是否为0,如果为0表示当前的递归形成1个拼凑方案参与计数,否则不形成方案不参与计数。于是递归的边界条件就是当index==length时判断aim==0是否成立,若成立+1,否则+0;

import java.util.*;
//找零钱问题:方法一:使用暴力递归搜索,注意边界条件的确定
public class Exchange {
    public int countWays(int[] penny, int n, int aim) {
        //特殊输入
        if(penny==null||penny.length<=0||aim<=0) return 0;
        //调用递归方法解决问题
        return this.process(penny,0,aim);
    }
//递归方法:输入一个零钱数组部分和目标值,返回拼凑的方案数目
    private int process(int[] penny,int index,int aim){
        int res=0;
//边界条件:初始条件,递归的边界条件满足后要立即返回,所谓边界条件是指能够使得方法return的条件
        if(index==penny.length){
            res=aim==0?1:0;
            return res;
        }
        
        //对第一个元素的出现次数进行枚举
        for(int i=0;i*penny[index]<=aim;i++){
            res+=this.process(penny,index+1,aim-penny[index]*i);
        }
        return res;
    }
}

方法二:记忆搜索

在实际面试的题目中,对于动态规划的问题,必然可以使用暴力搜索、记忆搜索的方法来解决,实际上所谓的动态规划就是在暴力搜索、记忆搜索的基础之上通过优化而得到的一种解题套路。

暴力搜索方法--记忆搜索方法动态规划方法状态继续化简后的动态规划方法

虽然动态规划的问题千奇百怪,但是它的解题思路和策略以及进行优化的轨迹是高度一致的,可以按照套路来进行。对于一个复杂的问题,使用暴力搜索方法通常是容易的,要将其改进为动态规划并进一步进行状态化简就比较麻烦(要向降低时间复杂度总是会使得编程难度增

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值