【算法】找零钱-动态规划实现过程解析

来源:互联网 发布:vue.js 实现网页下载 编辑:程序博客网 时间:2024/06/06 12:35

本文章素材来自:https://www.nowcoder.com/study/vod/1/12/1


题目要求:

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

假设测试数据如下:
int[] penny=new int[]{5, 15, 25, 1};
int aim=1000;

1、暴力求解法(利用递归的思想)

实现思想:根据上面的测试的数据,有如下想法
这里写图片描述

因此可以定义递归过程:
这里写图片描述

具体的算法在这里就不贴出来来,可以参考第2点的算法实现。


2、记忆化搜索法(在暴力求法的基础上修改的)

public static int countWays(int[] penny, int aim) {        // write code here        if (penny == null || penny.length == 0 || aim < 0) return 0;        else {            HashMap<String, Integer> map = new HashMap<>();            return fun( map,penny, 0, aim);        }    }    public static int fun(HashMap<String,Integer> map, int[] arr, int index, int aim) {        if (index == arr.length) {            //如果aim刚好为0了,就表示找到了一个方案,否则penng数组中的币值用完了aim还有剩余就表示方案行不通,即没有对应的方案            return aim == 0 ? 1 : 0;        }        String key = index + "-" + aim;        if (map.containsKey(key)) return map.get(key);        int res = 0;        for (int i = 0; arr[index] * i <= aim; i++)            res += fun(map, arr, index + 1, aim - arr[index] * i);        map.put(key, res);        return res;    }

因为暴力求解时使用的递归,因此会存在重复的计算,因此在原基础上使用一个数据结构将已经计算好的结果保存起来,如果需要再次计算时就直接从里面取出结果,这样就可以避免递归的重复计算。

因此,在上述代码中,使用来一个HashMap 来存储计算好的结果,其中Map的key是由indexaim 组成的,对应的value 即为已经计算过的结果。


3、由 记忆化搜索引出的 动态规划

这里写图片描述
O(n * aim^2)

public static int countWays(int[] penny, int aim) {        if (penny == null || penny.length == 0 || aim < 0) return 0;        int dp[][]=new int[penny.length][aim+1];        for (int i = 0; i < penny.length; i++)    dp[i][0] = 1;        for (int i = 1; i <= aim; i++)        {            if (i % penny[0] == 0)  dp[0][i] = 1;            else                    dp[0][i] = 0;        }        for (int i = 1; i < penny.length; i++)        {            for (int j = 1; j <= aim; j++)            {                int count = 0;                for (int k = 0; penny[i]*k <= j; k++)                    count += dp[i-1][j-penny[i]*k];                dp[i][j] = count;            }        }        return dp[penny.length-1][aim];    }

这里写图片描述

由记忆搜索法与其引出的动态规划法的核心算法的比较,发现并无太大的差异,而主要的区别就是如上图所说的,动态规划规定好了计算的顺序(计算dp[i][j] 就要先计算出 dp[i-1][0 ~ j] 的结果,然后在枚举求和,而dp[i-1][0 ~ j] 每个元素的结果又需要由对应的上一排的枚举求和实现…),而记忆搜索法本质还是递归,只不过优化了其过程,避免的重复的递归计算。


4、优化后的动态规划

这里写图片描述

根据上图的可以知道,需要累加的项只有dp[i-1][j-1*arr[i]]以及其所在那一排的且位于它前面的某些项(这一部分就相当于dp[i][j-arr[i]),以及dp[i-1][i] 这一项。因此可以优化原有动态规划的实现,避免上一排枚举求和的操作。

注意:为什么说只有部分项需要累加呢,这因为这些项之间都是依次相差 (arr[i]-1)个位置,而两个项中间的元素的值,其实是为0。举例说明,因为dp[i-1][j-1*arr[i]] (表示使用1 张arr[i]货币),到 dp[i-1][j] 之间(表示完全不用使用arr[i]货币),其中包含了 j - arr[i]+1j - arr[i]+2…、、j - arr[i]+(arr[i]-1),而这些是凑不齐1张arr[i]货币的,因此会有(零钱)剩余,而剩余就表示该方案行不通,即为0。

public static int countWays(int[] penny, int aim) {        if (penny == null || penny.length == 0 || aim < 0) return 0;        int dp[][]=new int[penny.length][aim+1];        for (int i = 0; i < penny.length; i++)     dp[i][0] = 1;        for (int i = 1; i <= aim; i++)        {            if (i % penny[0] == 0)      dp[0][i] = 1;            else                        dp[0][i] = 0;        }        for (int i = 1; i < penny.length; i++)        {            for (int j = 1; j <= aim; j++)            {                if (j < penny[i])   dp[i][j] = dp[i-1][j];                else                dp[i][j] = dp[i-1][j] + dp[i][j-penny[i]];            }        }        return dp[penny.length-1][aim];    }

这里写图片描述

这里写图片描述

原创粉丝点击