2. 完全背包
零钱兑换(完全背包-求极值)
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
回到凑零钱问题,为什么说它符合最优子结构呢?比如你想求 amount = 11 时的最少数(原问题),如果你知道凑出 amount = 10 的最少数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的XXX)就是原问题的答案。因为XXX的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。
1.暴力尝试
写出当面额是amount时,最少兑换数目
// 返回当面额是amount时,最少的兑换零钱数目
public static int way1(int[] coins, int amount){
// base case: 当面额为0时,不需要兑换
if (amount == 0){
return 0;
}
if (amount < 0){
// 无法兑换
return -1;
}
// 第一轮的时候,挨个尝试,找到最小的
int res = Integer.MAX_VALUE;
for(int coin : coins){
int subProblem = way1(coins, amount-coin);
if(subProblem != -1) {
res = Math.min(res, subProblem + 1);
}
// 子问题等于-1,是无效的
}
// 可能所有的子问题都是无效的,res还是没有改变过,就返回-1,表示无效解法
return res == Integer.MAX_VALUE ? -1 : res;
} 2.记忆化搜索-备忘录
把已经得到的递归子树中的答案提前返回,避免不必要的计算, 其中数组初始化为-10,就是一种没有处理过的标志位,可以自行修改设计
// 记忆化搜索,优化上述的方法
public static int way2(int[] coins, int amount){
int[] map = new int[amount+1];
Arrays.fill(map, -10);
return dp_2(coins, amount, map);
}
public static int dp_2(int[] coins, int amount, int[] map){
if (amount == 0){
map[amount] = 0;
return 0;
}
if (amount < 0){
return -1;
}
// 可能会返回amount 为负数的情况,所以要把base case放在前面,用HashMap就不用担心数组越界
if (map[amount] != -10){
return map[amount];
}
// 第一轮尝试
int res = Integer.MAX_VALUE;
for (int coin : coins){
int subProblem = dp_2(coins, amount-coin, map);
if (subProblem == -1){
continue;
}
// 也可以在这里记录
map[amount-coin] = subProblem;
res = Math.min(res, subProblem + 1);
}
map[amount] = (res == Integer.MAX_VALUE ? -1 : res);
return map[amount];
} 3.动态规划-从底向上
// 动态规划,用DP数据代替记忆化搜索
public static int way3(int[] coins, int amount){
// 可变参数amount 0~amount
int[] dp = new int[amount+1];
// 设置数组的数, 都是正整数,最多用amount张
Arrays.fill(dp, amount+1);
// base case:
dp[0] = 0;
for(int i=1; i<=amount; i++){
for(int coin : coins){
// 无效尝试,直接下一个
if(i-coin < 0){
continue;
}
// dp[i]的答案由,dp[i-coin]方案中最少的+1得到
dp[i] = Math.min(dp[i], 1+dp[i-coin]);
}
}
return dp[amount] == amount+1 ? -1: dp[amount];
} 零钱兑换(完全背包-求方案数)
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
1.暴力尝试
每个面值都要挨个尝试,因为不是0-1问题,所以会出现计数选择
// 暴力递归方法: 时间复杂度->O(aimˆN), 非常高
// @ arr: 面值数组; @ index: 对第index的面试的决策; @ res: 还有多少钱凑够面值
public static int way1_process(int[] arr, int index, int res){
// base case: arr数组都选择完毕,res如果能成功为0,成功方法数返回1,如果不为0,不返回
if (index == arr.length) {
return res==0 ? 1: 0;
}
// 每次对index的决策,都包括从0张到最接近res的张数
int ways = 0;
for(int zhang = 0; res - arr[index]*zhang>=0; zhang++){
ways += way1_process(arr, index+1, res-zhang*arr[index]);
}
return ways;
}
public static int way1(int[]arr, int aim){
if(arr==null || aim<0 || arr.length==0){
return 0;
}
return way1_process(arr, 0, aim);
} 2.动态规划
// 动态规划
// 时间复杂度: O(N*aimˆ2)
// 牛客记录: 124ms, 26000K
public static int way2(int[] arr, int aim){
// 可变参数: index 和 res
// index:[0~arr.length] res:[0~aim], 设置数组dp[arr.length+1][aim+1]
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
// 初始位置
dp[N][0] = 1;
// 普通位置计算
for (int index = N-1; index>=0; index--){
for (int res = 0; res<=aim; res++){
int ways = 0;
for(int zhang = 0; res - arr[index]*zhang>=0; zhang++){
ways += dp[index+1][res-arr[index]*zhang];
}
dp[index][res] = ways;
}
}
return dp[0][aim];
} 3.斜率优化后的DP
通过观察可以得到,dp[index][res]是由下面的点dp[index+1][res] 和 左面的点dp[index][res-arr[index]]组成, 如果左面的点存在
// 直接改进成,空间优化的动态规划
// 牛客记录: 52ms, 26000K
public static int way3(int[] arr, int aim){
// 可变参数: index 和 res
// index:[0~arr.length] res:[0~aim], 设置数组dp[arr.length+1][aim+1]
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
// 初始条件
dp[N][0] = 1;
// 一般位置
for (int index = N-1; index>=0; index--){
for (int res = 0; res <= aim; res++){
// 时间优化之 斜率优化,观察之前循环累加,优化得到
dp[index][res] = dp[index+1][res];
if(res - arr[index] >= 0){
dp[index][res] += dp[index][res-arr[index]];
}
}
}
return dp[0][aim];
} 4.空间压缩TODO
j

京公网安备 11010502036488号