畅游面试中的动态规划套路-完全背包系列

2.完全背包

索引链接:畅游面试中的动态规划套路

在这里插入图片描述

Cutting Rod

方法1:暴力递归
  • 代码略
方法2:自顶向下记忆化递归(Top-down)
        Integer[][] cache;
        int[] lengths;
        int[] prices;

        public int rodCutting(int[] lengths, int[] prices, int rodlength) {
            this.lengths = lengths;
            this.prices = prices;
            cache = new Integer[lengths.length][rodlength + 1];//
            return helper(rodlength, 0);
        }


        private int helper(int rodlength, int curIdx) {
            if (curIdx >= lengths.length) return 0;
            if (cache[curIdx][rodlength] != null) return cache[curIdx][rodlength];
            int choose = 0;//选
            if (lengths[curIdx] <= rodlength) {
                choose = prices[curIdx] + helper(rodlength - lengths[curIdx], curIdx);
            }
            int non_choose = helper(rodlength, curIdx + 1);//不选
            return cache[curIdx][rodlength] = Math.max(choose, non_choose);
        }
方法3:自底向上填表DP(Bottom-up)
     public int rodCutting(int[] lengths, int[] prices, int n) {
            int m = lengths.length;
            int[][] f = new int[m][n + 1];
            for (int i = 0; i < m; i++) {
                for (int j = 1; j <= n; j++) {
                    int choose = 0, non_choose = 0;
                    if (lengths[i] <= j) choose = prices[i] + f[i][j - lengths[i]];
                    if (i > 0) non_choose = f[i - 1][j];
                    f[i][j] = Math.max(choose, non_choose);
                }
            }
            return f[m - 1][n];
        }
  • 测试
            _2nd handler = new _2nd();
            int[] lengths = {1, 2, 3, 4, 5};//每一根木棒的长度
            int[] prices = {2, 6, 7, 10, 13};//每一根木棒的利润
            int rodlength = 5;
            System.out.println(handler.rodCutting(lengths, prices, rodlength));

零钱兑换

题目链接:322. 零钱兑换

题解链接:畅游面试中的动态规划套路-完全背包系列之零钱兑换

零钱兑换问题的Follow Up 1

Follow Up指的是面试过程中的追问环节,包括但不限于已有问题的举一反三,边界条件,优化方案

JEty1e.png

  • b a s e base base题是基于这一题:给定不同面额的硬币$ coins$ 和一个总金额 a m o u n t amount amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1, 你可以认为每种硬币的数量是无限的
Step 1 零钱兑换问题(01背包)

题目描述:给定不同面额的硬币$ coins$ 和一个总金额 a m o u n t amount amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1, 但是每个硬币只能选择一次

题目分析
  • 因为提到了每个硬币只能选择一次,这个与**01背包**很吻合

这是01背包的抽象模型

参数定义:
  • N N N件物品
  • V V V背包的总容量
  • C i Ci Ci放入第 i i i件物品耗费的费用
  • W i Wi Wi放入第 i i i件物品得到的价值
定义状态

F [ i , v ] F[i,v] F[i,v]表示前 i i i件物品恰好放入一个容量为 v v v的背包可以获得的最大价值

选择

转移方程应为是: F [ i , v ] = m a x ( F [ i − 1 , v ] , F [ i − 1 , v − C i ] + W i ) F[i,v]=max(F[i-1,v],F[i-1,v-Ci]+Wi) F[i,v]=max(F[i1,v],F[i1,vCi]+Wi)

  • 解释:将第 i i i件物品放入容量为 v v v的背包中,只需要基于 i − 1 i-1 i1件物品的基础上做第 i i i件物品的放与不放的问题
    • 不放入第 i i i件物品,获得的最大价值是 F [ i − 1 , v ] F[i-1,v] F[i1,v]
    • 放入第 i i i件物品,获得的最大价值是 F [ i − 1 , v − C i ] + W i F[i-1,v-Ci]+Wi F[i1,vCi]+Wi,因为第 i i i件物品已经放进去背包了,留给前 i − 1 i-1 i1件物品的背包容量只有 v − C i v-Ci vCi,而通过放入第 i i i件物品,获取的价值是 W i Wi Wi
边界条件
  • d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]表示当选择是 0 0 0个物品时,在没有物品,背包体积为 0 0 0时,不装任何东西的时候 d p [ 0 ] [ 0 ] = 0 dp[0][0]=0 dp[0][0]=0
  • F [ i − 1 , v − C i ] F[i-1,v-Ci] F[i1,vCi]其中 v > = C i v>=Ci v>=Ci,不然为负数,没有意义

回到本题

定义状态
  • F [ i , v ] F[i,v] F[i,v]表示前 i i i件物品恰好放入一个容量为 v v v的背包可以获得的最大价值,套用到本题即, d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个硬币,组成目标金额为 j j j的最少硬币数量
选择
  • 根据上文中的01背包的抽象模型, d p [ i ] [ j ] dp[i][j] dp[i][j]依赖于对于第 i i i个硬币拿与不拿这两种选择
    • 不拿: d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],相当于前 i − 1 i-1 i1个硬币选择,组成目标金额 j j j的最少硬币
    • 拿: d p [ i − 1 ] [ j − c o i n s [ i ] ] + 1 dp[i-1][j-coins[i]]+1 dp[i1][jcoins[i]]+1,从前 i − 1 i-1 i1个硬币选择,组成目标金额 j − c o i n s [ i ] j-coins[i] jcoins[i],因为拿了第 i i i个硬币,所有选择方案上的硬币数量需要 + 1 +1 +1
    • 最终的动态转移方程: d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − c o i n s [ i ] + 1 ] ) dp[i][j]=min(dp[i-1][j],dp[i-1][j-coins[i]+1]) dp[i][j]=min(dp[i1][j],dp[i1][jcoins[i]+1])
边界条件
  • d p [ a m o u n t + 1 ] dp[amount+1] dp[amount+1],表示从 0 0 0 a m o u n t amount amount的所有状态,初始化时,全部置为 i n f inf inf,为了最后找到有些情况不可达,返回-1的结果
  • d p [ 0 ] = 0 dp[0]=0 dp[0]=0,组成目标金额为 0 0 0的硬币选择方案,即什么硬币都不选,最少硬币为 0 0 0

只给出了一维数组 d p dp dp版,其他版本可参见底部的阅读

    def coinChange(self, coins: List[int], amount: int) -> int:
        n = len(coins)
        # 初始化数组 dp[amount+1] 为 float('inf')
        dp = [float('inf') for i in range(amount + 1)]
        dp[0] = 0
        for i in range(1, n + 1):
            for j in range(amount, coins[i - 1], -1):
                dp[j] = min(dp[j], dp[j - coins[i]] + 1)
        return dp[amount] if dp[amount] != float('inf') else -1
Step 2 零钱兑换问题(完全背包)

题目描述:给定不同面额的硬币$ coins$ 和一个总金额 a m o u n t amount amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1, 每个硬币可以选择无数次

题目分析
  • 因为提到了每个硬币可以选择无数次,这个与**完全背包**很吻合

这是完全背包的抽象模型

参数定义:
  • N N N件物品
  • V V V背包的总容量
  • C i Ci Ci放入第 i i i件物品耗费的费用
  • W i Wi Wi放入第 i i i件物品得到的价值动态规划的几大要素:状态,选择以及边界条件

每件物品可以 取用0件,1件,2件… V / C i V/Ci V/Ci件,

定义状态
  • F [ i , v ] F[i,v] F[i,v]表示前 i i i个物品恰好放入容量为 v v v的背包时获取到的最大价值,很容易得到如下的动态转移方程:
    • F [ i , v ] = m a x ( F [ i − 1 , v − k C i ] + k W i ∣ k C i ∈ [ 0 , v ] ) F[i,v]=max(F[i-1,v-kCi]+kWi|kCi∈[0,v]) F[i,v]=max(F[i1,vkCi]+kWikCi[0,v])
边界条件
  • 如果创建一个 d p = i n t [ N + 1 ] [ V + 1 ] dp=int[N+1][V+1] dp=int[N+1][V+1]的二维动态数组,当 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]时,表示的时物品数与背包的容量都是 0 0 0的,显然其结果为 0 0 0

回到本题

  • d p [ a m o u n t + 1 ] dp[amount+1] dp[amount+1],初始化 d p [ 0 ] = 0 dp[0]=0 dp[0]=0,表示欲组成总金额为 0 0 0硬币的最少个数,为 0 0 0,一个硬币都不取
  • d p [ j ] dp[j] dp[j]:使用 c o i n s coins coins组成目标金额为 j j j的最少硬币数量
  • 动态转移方程: d p [ j ] = m i n ( d p [ j ] , d p [ j − c o i n s [ i − 1 ] ] + 1 ) dp[j]=min(dp[j],dp[j-coins[i-1]]+1) dp[j]=min(dp[j],dp[jcoins[i1]]+1) 其中 i − 1 i-1 i1表示的是第 i i i个硬币
    def change(self, coins: List[int], amount: int) -> int:
        # 初始化数组 dp[amount+1] 为 float('inf')
        dp = [float('inf') for i in range(amount + 1)]
        dp[0] = 0
        for coin in coins:
            for j in range(coin, amount + 1):
                dp[j] = min(dp[j], dp[j - coin] + 1)
        return dp[amount] if dp[amount] != float('inf') else -1
Step 3 零钱兑换问题(多重背包)

题目描述:给定不同面额的硬币$ coins$ 和一个总金额 a m o u n t amount amount每个硬币的选择次数有限制,上限 t t t,编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

题目分析
  • 因为提到了每个硬币的选择次数有限制,上限 t t t,这个与**多重背包**很吻合
定义状态
  • 对于第 i i i中硬币,可以由0 ,1 2…k 到t 次 的选择, d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个硬币组成金额为 j j j时的最少硬币数量
dp[i][j] = 
min(dp[i-1]dp[j],
	dp[i-1][j-c]+1, 
	dp[i-1][j-2*c]+2, 
	...,
	dp[i-1][j-k*c]+k)
	其中k∈[1,t]
  • 代码:
    def change(self, coins: List[int], amount: int, t: List[int]) -> int:
        dp = [float('inf') for i in range(amount + 1)]
        dp[0] = 0
        for i in range(len(coins)):
            for j in range(amount, coins[i] - 1, -1):
                for k in range(1, t[i] + 1): 
                    if j >= k * coins[i]:  
                        dp[j] = min(dp[j], dp[j - k * coins[i]] + k)
        return -1 if dp[amount] > amount else dp[amount]
  • 选str[k-1]这个字符时
  • 不选str[k-1]这个字符时

其中 c o s t _ z e r o [ k ] cost\_zero[k] cost_zero[k] c o s t _ o n e [ k ] cost\_one[k] cost_one[k],是到达第 k − 1 k-1 k1个字符串的时候,这个字符串中0和1的数量,因为数据的下标索引小于0是无意义的

边界

每个维度+1,第0个字符是空字符串

    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        int[][][] dp = new int[len + 1][m + 1][n + 1];
        for (int k = 1; k < len + 1; ++k) {
            int[] counter = counter(strs[k - 1]);
            for (int i = 0; i < m + 1; ++i) {
                for (int j = 0; j < n + 1; ++j) {
                    if (i >= counter[0] && j >= counter[1]) {
                        dp[k][i][j] = Math.max(dp[k][i][j],
                                dp[k - 1][i - counter[0]][j - counter[1]] + 1);
                    }
                    dp[k][i][j] = Math.max(dp[k][i][j], dp[k - 1][i][j]);
                }
            }
        }
        return dp[len][m][n];
    }

    /**
     * 计算str字符串中的 0和1的个数, counter[0] 是 "0"的个数,counter[1]是"1"的个数
     *
     * @param str
     * @return
     */
    public int[] counter(String str) {
        int[] counter = new int[2];
        for (char c : str.toCharArray()) counter[c - '0']++;
        return counter;
    }

方法4:自底向上填表DP(Bottom-up)-空间压缩

动态规划的几大要素:状态,选择以及边界条件

参数定义:
  • N N N件物品
  • V V V背包的总容量
  • C i Ci Ci放入第 i i i件物品耗费的费用
  • W i Wi Wi放入第 i i i件物品得到的价值
定义状态

F [ i , v ] F[i,v] F[i,v]表示前 i i i件物品恰好放入一个容量为 v v v的背包可以获得的最大价值,而转移方程应为是: F [ i , v ] = m a x ( F [ i − 1 , v ] , F [ i − 1 , v − C i ] + W i ) F[i,v]=max(F[i-1,v],F[i-1,v-Ci]+Wi) F[i,v]=max(F[i1,v],F[i1,vCi]+Wi)

  • 解释:将第 i i i件物品放入容量为 v v v的背包中,只需要基于 i − 1 i-1 i1件物品的基础上做第 i i i件物品的放与不放的问题
    • 不放入第 i i i件物品,获得的最大价值是 F [ i − 1 , v ] F[i-1,v] F[i1,v]
    • 放入第 i i i件物品,获得的最大价值是 F [ i − 1 , v − C i ] + W i F[i-1,v-Ci]+Wi F[i1,vCi]+Wi,因为第 i i i件物品已经放进去背包了,留给前 i − 1 i-1 i1件物品的背包容量只有 v − C i v-Ci vCi,而通过放入第 i i i件物品,获取的价值是 W i Wi Wi
边界条件
  • d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]表示当选择是 0 0 0个物品时,在没有物品,背包体积为 0 0 0时,不装任何东西的时候 d p [ 0 ] [ 0 ] = 0 dp[0][0]=0 dp[0][0]=0
  • F [ i − 1 , v − C i ] F[i-1,v-Ci] F[i1,vCi]其中 v > = C i v>=Ci v>=Ci,不然为负数,没有意义
核心代码
dp[N+1][V+1]
dp[0][0...V]=0
dp[0...N][0]=0
for i in range(1,N+1)
	for j in range(1,V+1)
    	dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])

上面是01背包的模型

定义状态

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示遍历到第 k k k个字符的时候,使用 i i i个0, j j j个1的时候,能够拼出的字符串的数量,即个数

    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        int[][] dp = new int[m + 1][n + 1];
        for (String str : strs) {
            int[] counter = counter(str);
            for (int i = m; i >= counter[0]; i--) {
                for (int j = n; j >= counter[1]; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - counter[0]][j - counter[1]] + 1);
                }
            }
        }
        return dp[m][n];
    }

    /**
     * 计算str字符串中的 0和1的个数, counter[0] 是 "0"的个数,counter[1]是"1"的个数
     *
     * @param str
     * @return
     */
    public int[] counter(String str) {
        int[] counter = new int[2];
        for (char c : str.toCharArray()) counter[c - '0']++;
        return counter;
    }

零钱兑换II

题目链接:518. 零钱兑换 II

题解链接:背包思想解决零钱兑换问题(逐步优化,多方法)

题目分析
  • 假设每一种面额的硬币有无限个 这是题目描述的,符合完全背包问题:每件物品可以无限制的取用,只要不超过总的背包容量,把背包撑爆

  • 完全背包的抽象模型,参见前文

方法1:朴素版(三层循环)
定义状态
  • d p [ i ] [ j ] dp[i][j] dp[i][j]: c o i n s [ 0.. i ] coins[0..i] coins[0..i]范围内的硬币,组成目标金额为 j j j,能得到的组合数
  • 当遇到一种新的硬币 c o i n s [ i ] coins[i] coins[i]时,可以选 0 0 0种, 1 1 1种, 2 2 2种,直到 k k k种,因为可以对于一种硬币进行无数种选择,只要 j − c o i n s [ i − 1 ] ≥ 0 j-coins[i-1]≥0 jcoins[i1]0即可,不满足这个条件,背包撑爆,对于这个状态本身没有什么意义,将上面的每个子状态的和累加,即是所求 d p [ i ] [ j ] dp[i][j] dp[i][j]
    • 动态转移方程:$ \sum_{0}^k dp[i-1][j-k*coins[i-1]] $
边界条件
  • 多设置一行, d p = i n t [ n + 1 ] [ . . . ] dp=int[n+1][...] dp=int[n+1][...] d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]置为 1 1 1,其他为 0 0 0
    public int change1st(int amount, int[] coins) {
        int n = coins.length;
        int[][] dp = new int[n + 1][amount + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= amount; j++) {
                for (int k = 0; k * coins[i - 1] <= j; k++) {
                    dp[i][j] += dp[i - 1][j - k * coins[i - 1]];
                }
            }
        }
        return dp[n][amount];
    }
复杂度分析
  • 时间复杂度: O ( N ∗ a m o u n t 2 ) O(N*amount^2) O(Namount2)其中 N N N是硬币的个数即 c o i n s coins coins数组的长度, a m o u n t amount amount是金额
  • 空间复杂度: O ( N ∗ a m o u n t ) O(N*amount) O(Namount),即 d p dp dp使用的空间
方法2:优化(两层循环)
定义状态
  • d p [ i ] [ j ] dp[i][j] dp[i][j] c o i n s [ 0.... i − 1 ] coins[0....i-1] coins[0....i1]这前 i i i个硬币,组成金额为 j j j,能得到的组合数
  • d p [ i ] [ j ] dp[i][j] dp[i][j]依赖于两个状态,选不选 c o i n s [ i − 1 ] coins[i-1] coins[i1]这个硬币,
    • 不选的时候为 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],不选 c o i n s [ i − 1 ] coins[i-1] coins[i1]时,只能在前 i − 1 i-1 i1个硬币中组成 j j j
    • 选的时候为 d p [ i ] [ j − c o i n s [ i − 1 ] ] dp[i][j-coins[i-1]] dp[i][jcoins[i1]],选 c o i n s [ i − 1 ] coins[i-1] coins[i1]时,总的金额减少到 j − c o i n s [ i − 1 ] j-coins[i-1] jcoins[i1],但由于是完全背包问题,每个物品可以选无限次,所以,剩下的可以选的硬币还是应该是 c o i n s [ 0... i − 1 ] coins[0...i-1] coins[0...i1] i i i
    • 状态转移方程: d p [ i ] [ j ] dp[i][j] dp[i][j]= d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]+ d p [ i ] [ j − c o i n s [ i − 1 ] ] dp[i][j-coins[i-1]] dp[i][jcoins[i1]],这其中可以做个优化, j − c o i n s [ i − 1 ] < 0 j-coins[i-1]<0 jcoins[i1]<0的情况无意义,可以过滤
边界条件
  • d p [ i ] [ 0 ] dp[i][0] dp[i][0] i i i个硬币形成金额为 0 0 0的组合数,为 1 1 1那就是不选任何硬币,只有这 1 1 1种选法
  • d p [ 0 ] [ j ] dp[0][j] dp[0][j]其中 j ≠ 0 j≠0 j=0,前 0 0 0种硬币组成目标金额为 j j j的组合数,初始化时, d p = = i n t [ n + 1 ] [ . . . ] dp==int[n+1][...] dp==int[n+1][...] 0 0 0种硬币其实是没有硬币的,翻译下来是没有硬币,如何组成 j j j,答案显然是 0 0 0
  • 注意在初始化 d p [ 0 ] [ j ] dp[0][j] dp[0][j]时,不要将 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]的状态覆盖掉了
结果
  • d p [ n ] [ a m o u n t ] dp[n][amount] dp[n][amount] n n n中硬币形成了 a m o u n t amount amount的组合数,即 c o i n s [ 0... n − 1 ] coins[0...n-1] coins[0...n1]形成了 a m o u n t amount amount
    public int change2nd(int amount, int[] coins) {
    	int n = coins.length;
        int[][] dp = new int[n + 1][amount + 1];
        for (int i = 0; i <= n; i++) dp[i][0] = 1;
        for (int j = 1; j <= amount; j++) dp[0][j] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= amount; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j - coins[i - 1] >= 0) dp[i][j] += dp[i][j - coins[i - 1]];
            }
        }
        return dp[n][amount];
    }
复杂度分析
  • 时间复杂度: O ( N ∗ a m o u n t ) O(N*amount) O(Namount)其中 N N N是硬币的个数即 c o i n s coins coins数组的长度, a m o u n t amount amount是金额

  • 空间复杂度: O ( N ∗ a m o u n t ) O(N*amount) O(Namount),即 d p dp dp使用的空间

  • 相比于方法1,优化了一层循环,时间复杂度变好

方法3:优化(一维数组)
定义状态
  • 将方法2中的 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − c o i n s [ i − 1 ] ] ; dp[i][j] =dp[i - 1][j]+ dp[i][j - coins[i - 1]]; dp[i][j]=dp[i1][j]+dp[i][jcoins[i1]];去掉一维 i i i得到, d p [ j ] = d p [ j + d p [ j − c o i n s [ i ] ] ] dp[j]=dp[j+dp[j-coins[i]]] dp[j]=dp[j+dp[jcoins[i]]]这里 i i i 0 0 0开始的,不需要取 c o i n s [ i − 1 ] coins[i-1] coins[i1]
边界条件
  • 没有硬币时候,不选任何硬币,可以组成金额为 0 0 0,只有这么一种组合, d p [ 0 ] dp[0] dp[0]等价于 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]
 public int change3rd(int amount, int[] coins) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int i = 0; i < n; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
        return dp
复杂度分析
  • 时间复杂度: O ( N ∗ a m o u n t ) O(N*amount) O(Namount)其中 N N N是硬币的个数即 c o i n s coins coins数组的长度, a m o u n t amount amount是金额
  • 空间复杂度: O ( a m o u n t ) O(amount) O(amount),即 d p dp dp使用的空间,将二维降低为一维
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值