这个题是我们纸币问题的第三题
题目大意:
arr是货币数组,其中的值都是正数。再给定一个正数aim。 每个值都认为是一张货币, 认为值相同的货币没有任何不同, 返回组成aim的方法数 例如:arr = {1,2,1,1,2,1,2},aim = 4 方法:1+1+1+1、1+1+2、2+2 一共就3种方法,所以返回3 |
这个题跟我们上一道题的区别是每种纸币在数组中出现了多少次就是多少张,你只能用这么多张(假设张数是N)进行0~N的尝试,尝试过程不能超过剩余金额,所以限制条件有两个:
1. 张数只能从0~N
2.张数*当前面值不能超过剩余金额
而我们之前的题目是每个面值的有无数张,限制条件只有一个:
张数*当前面值不能超过剩余金额,不限制张数
一步步改的代码如下,注释特别详细
package dataStructure.recurrence.practice;
import java.util.HashMap;
/**
* arr是货币数组,其中的值都是正数。再给定一个正数aim。
* 每个值都认为是一张货币,
* 认为值相同的货币没有任何不同,
* 返回组成aim的方法数
* 例如:arr = {1,2,1,1,2,1,2},aim = 4
* 方法:1+1+1+1、1+1+2、2+2
* 一共就3种方法,所以返回3
*/
public class CoinsWaySameValueSamePaper {
//辅助的Info类,用来保存张数和面值
public static class Info {
//张数和面值数组,二者长度相等
int[] nums;
int[] values;
public Info(int[] nums, int[] values) {
this.nums = nums;
this.values = values;
}
}
public static int coinsWay(int[] arr, int aim) {
Info info = generateInfo(arr);
return process1(info.nums, info.values, 0, aim);
}
/**
* 根据暴力递归改的动态规划-普通动态规划
* @param arr
* @param aim
* @return
*/
public static int coinsWayDp(int[] arr, int aim) {
Info info = generateInfo(arr);
int[] nums = info.nums;
int[] values = info.values;
int N = nums.length;
//上面的部分和原来一样
//dp是二维的,行代表index, 从0 到 N, 列代表剩余钱数,从0到aim
int[][] dp = new int[N + 1][aim + 1];
//下标为N的行(也就是最后一行)只有rest=0的时候是1,其他都是0
dp[N][0] = 1;
for(int index = N - 1; index >= 0; index --) {
//从倒数第一行开始依次往上天,列的顺序从左到右或者从右到左都行
for(int rest = 0; rest <= aim; rest ++) {
int ways = 0;
//递归中的枚举过程,i * values[index] <= rest保证rest - i * values[index]不会越界
for(int i = 0; i * values[index] <= rest && i <= nums[index]; i++) {
//System.out.println("rest=" + rest + "value[index]=" + values[index] + " i=" + i);
ways += dp[index + 1][rest - i * values[index]];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}
/**
* 动态规划的本题最好版本-省去枚举过程
* @param arr
* @param aim
* @return
*/
public static int coinsWayDpBest(int[] arr, int aim) {
Info info = generateInfo(arr);
int[] nums = info.nums;
int[] values = info.values;
int N = nums.length;
int[][] dp = new int[N + 1][aim + 1];
//下标为N的行(也就是最后一行)只有rest=0的时候是1,其他都是0
dp[N][0] = 1;
//以上过程和普通动态规划版本完全一致
for(int index = N - 1; index >= 0; index --) {
for(int rest = 0; rest <= aim; rest ++) {
//dp[index][rest] = dp[index+1][rest] + dp[index+1][rest - values[index]] + ...
// 一直加到rest - (nums[index] + 1) * values[index]或者nums[index]用完
//dp[index+1][rest]是使用0张,肯定不会超过两个限制,可以用来赋初始值
dp[index][rest] = dp[index+1][rest];
//如果rest小于当前的的面值,下面就没有必要进行遍历了(这个也是为了保证同行使用一张那个位置不越界)
if(rest - values[index]>=0) {
//一个位置先用它下面同列+同行列左移values[index]列
dp[index][rest] += dp[index][rest - values[index]];
//这个结果可能算重复了一个位置,这个位置就是dp[index][rest - values[index]]的计算过程中下一行最左边要计算的位置
//如果rest - (nums[index] + 1) * values[index] >= 0才需要减,如果满足这个条件,说明:
//dp[index][rest] = dp[index+1][rest] + dp[index+1][rest - values[index]] + ...+dp[index+1][rest - values[index]*nums[index]]
//dp[index][rest - values[index]] = dp[index+1][rest - values[index]] + ...+dp[index+1][rest - values[index]*nums[index]] + dp[index+1][rest - values[index]*(nums[index]+1)]
//使用dp[index][rest] += dp[index][rest - values[index]];就多算了一个dp[index + 1][rest - (nums[index] + 1) * values[index]],这里要减去
if(rest - (nums[index] + 1) * values[index] >= 0) {
dp[index][rest] -= dp[index + 1][rest - (nums[index] + 1) * values[index]];
}
}
}
}
return dp[0][aim];
}
/**
* 递归方法使用index下标开始的硬币拼成rest
* @param nums 原始的数量数组,nums[i]代表i位置的货币(面值为values[i])有几张
* @param values 原始的面值数组,values[i]表示i位置的货币是多大面值
* @param index 当前要尝试的位置的下标
* @param rest 目前剩余的金额
* @return
*/
public static int process1(int[] nums, int[] values, int index, int rest) {
if(index == nums.length) {
return rest == 0? 1 : 0;
}
int ways = 0;
//两种限制:1. 张数只能从0~nums[index]
//2.张数*当前面值不能超过剩余金额
for(int i = 0; i * values[index] <= rest && i <= nums[index]; i++) {
//System.out.println("rest=" + rest + "value[index]=" + values[index] + " i=" + i);
//所有可能性的总和就是我们要的结果
ways += process1(nums, values, index + 1, rest - i * values[index]);
}
return ways;
}
/**
* 根据原始数组获取对应的Info信息,主要是获取那两个数组:张数和面值的信息
* @param arr
* @return
*/
private static Info generateInfo(int[] arr) {
//使用hashmap进行去重和统计数量
HashMap<Integer, Integer> map = new HashMap<>();
//遍历原数组每个位置
for(int i = 0; i < arr.length; i++) {
//统计每种金额数量
if(map.containsKey(arr[i])) {
map.put(arr[i],map.get(arr[i]) + 1);
} else {
map.put(arr[i], 1);
}
}
int[] nums = new int[map.size()];
int[] values = new int[map.size()];
int index = 0;
//key是面值,value是张数,依次填充两个数组
for (Integer key : map.keySet()) {
//System.out.println(key + " " + map.get(key));
values[index] = key;
nums[index ++] = map.get(key);
}
return new Info(nums, values);
}
public static void main(String[] args) {
int[] arr = {1,2,1,1,2,1,2};
int aim = 4;
int coinsWay = coinsWay(arr, aim);
System.out.println(coinsWay);
int coinsWayDp = coinsWayDp(arr, aim);
System.out.println(coinsWayDp);
int coinsWayDpBest = coinsWayDpBest(arr, aim);
System.out.println(coinsWayDpBest);
}
}
过程分析图:
不明白有问题可以私信我,保证你懂