您的位置:首页 > 其它

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

2017-09-29 12:07 591 查看
本文章素材来自: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是由
index
aim
组成的,对应的
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]+1
j - 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];
}




内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: