题目
arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
每个值都认为是一种面值,且认为张数是无限的。
返回组成aim的最少货币数
暴力递归
依然是面值张数的问题,暴力递归尝试的过程是从数组arr index = 0位置出发,一直到 index = arr.length。如果能够正好凑齐aim,则return 0,否则 return Integer.Max_Value用来进行区分。
需要注意的是:因为是求最少货币数,所以需要通过数组中下一个面值的张数使用 + 当前面值的张数使用,取最小值。
代码
如果index == arr.length时,没有面值可以取了,如果此时rest = 0,说明正好凑齐钱数,return 0,否则return MAX_VALUE代表搞不了。
next != MAX_VALUE,则说明使用下一张面值刚好可以凑整aim钱数,那么就用当前面值的张数 zhang + 下一张面值的张数 next,和ans取最小值。
所以要通过MAX_VALUE来进行区分。
public static int minCoins(int[] arr, int aim) {
return process(0, arr, aim);
}
public static int process(int index, int[] arr, int rest) {
if (index == arr.length) {
return rest == 0 ? 0 : Integer.MAX_VALUE;
} else {
int ans = Integer.MAX_VALUE;
for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
//要去index + 1位置看下一张面值的钱的使用情况
int next = process(index + 1, arr, rest - zhang * arr[index]);
// 如果使用下一张面值 恰好可以将 rest凑齐
if (next != Integer.MAX_VALUE) {
//则,将当前使用面值张数 + 下一张面值使用张数 与 ans取最小值
ans = Math.min(ans, next + zhang);
}
}
return ans;
}
动态规划
根据暴力递归的代码改写动态规划,可变参数为 剩余金额 rest 和 数组下标 index,因为 rest 可以到达0位置,index可以到达arr.length。所以可以确定dp[][]的大小是 的 dp[arr.length + 1 ][aim + 1]。
确定好dp表大小后,根据暴力递归中的base case来填充dp表的默认值。
可以看到。暴力递归代码中,当index == arr.length时, 如果此时剩余金额 rest = 0,则return 0 ,否则 return MAX_VALUE。又因为int型数组,创建后,每个格子默认值就是0,所以只需要将dp[arr.length][1 ~ aim]的位置填充即可。其余代码照抄,dp表就填充完成了。
代码
暴力递归代码中,每次都是依赖index + 1位置 rest - zhang * arr[index],所以外层循环从N -1向0处遍历, 内循环 rest从0 向aim处遍历。
public static int dp(int[] arr, int aim) {
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
for (int rest = 1; rest <= aim;rest ++){
dp[N][rest] = Integer.MAX_VALUE;
}
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int ans = Integer.MAX_VALUE;
for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
int next = dp[index + 1][rest - zhang * arr[index]];
if (next != Integer.MAX_VALUE) {
ans = Math.min(ans, next + zhang);
}
dp[index][rest] = ans;
}
}
}
return dp[0][aim];
}
再次优化
可以看到动态规划的代码中,每填充1个dp表中格子都有枚举行为(zhang的for循环)。所以还可以再次进行优化。找到dp表中的依赖关系,替换掉这个for循环即可。
找依赖关系最直观的方法就是画图。
假设当前我剩余钱数 = 11,面值为3。
根据暴力递归代码带入看一下,我依赖的a0 ~ d3,当前面值使用0张,1张。。。的各种情况向下递归。
那如果此时rest = 8,依赖的就是b1 ~ d3,不过此时使用的张数也是0张、1张。。。。以此类推。
所以想要求 √ 位置的值,就是 a0 + (x + 1)。
为什么要加1呢? 还是回到暴力递归中的代码来看 这行代码 ans = Math.min(ans, next + zhang);
如果我next有效,我用的是 next + zhang,是在我当前面值使用了1,2,3若干张之后的情况,所以x位置,也是 √ 使用的一张arr[index]后剩余的rest = 8。
代码
首先保证左侧有值,并且左侧的值是有效的才进行比较。
public static int bestDP(int[] arr, int aim) {
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
for (int rest = 1; rest <= aim;rest ++){
dp[N][rest] = Integer.MAX_VALUE;
}
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
if (rest - arr[index] >= 0 && dp[index][rest - arr[index]] != Integer.MAX_VALUE) {
dp[index][rest] = Math.min(dp[index][rest], dp[index][rest - arr[index]] + 1);
}
}
}
return dp[0][aim];
}