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