面试高频手撕算法 - 背包问题2

目录

1. 完全背包

1.1 【模板】完全背包

1.2 零钱兑换

1.3 零钱兑换 II

1.4 完全平方数

2. 二维费用的背包问题

2.1 一和零

2.2 盈利计划


 --- 如果背包问题原先没有基础的,建议先看上一篇博客 --- 面试高频手撕算法 - 01背包系列

1. 完全背包

1.1 【模板】完全背包

【题目链接】

【模板】完全背包_牛客题霸_牛客网你有一个背包,最多能容纳的体积是V。 现在有n种物品,每种物品有任意多个,第。题目来自【牛客题霸】icon-default.png?t=N7T8https://www.nowcoder.com/practice/237ae40ea1e84d8980c1d5666d1c53bc?tpId=230&&tqId=38965&sourceUrl=https%3A%2F%2Fwww.nowcoder.com%2Fexam%2Foj【题目描述】

你有一个背包,最多能容纳的体积是V。
现在有 n 种物品,每种物品有任意多个,第 i 种物品的体积为 vi,价值为 wi。

(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?

第一问:【算法原理】

再做动态规划系列问题的时候,无非就是这几大步骤:

① 状态定义

② 推导状态转移方程

③ 初始化 dp 表

④ 填表顺序

返回值

1. 状态定义

背包问题本质上还是一个线性 dp, 所以状态的定义根据线性 dp 的经验: 

状态定义: dp[i] 表示从前 i 个物品中挑选, 所有选法, 能挑选出来的最大价值  (试错)

但是这样定义状态之后, 发现推不出来, 因为不知道体积, 所以需要定义一个二维的 dp.

状态定义:dp[i][j]表示从前 i 个物品中挑选,总体积不超过 j,所有选法中,能挑选出来的最大价值

2. 推导状态转移方程 

根据最后一个位置的状况, 分情况讨论:

        像这种,当我们要表示一个状态的时候,发现它需要很多个状态拼接而成的时候,这个时候我们需要想一个策略,将这些状态用一个或两个等有限个状态来表示。

【数学思想】

        数学里面有种思想是错位相减,此处的做法有点类似,通过给 dp[i][j] 的列上减去一个体积,得到另一个表达式,然后寻找两个表达式之间的关联关系:

于是,我们的状态转移方程就转换成了:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i]] + w[i])

3. 初始化 dp 表

为了方便代码进行 dp 的一个过程, 我们都会根据情况给 dp 表多加一行, 一列.

由于使用到前面的列的时候, 会进行判断, 所以初始化的时候, 可以不用初始化列, 只需要初始化行.

当只有 0 个商品的时候,想要体积不超过 0,1,2,3..... 最大价值肯定都是 0 了.

4. 填表顺序

从状态转移方程来分析,dp[i] 会使用到上一行,以及当前行前面的列, 所以填表顺序从上往下,从左往右填.

5. 返回值

从状态表示:从前 i 个物品中挑选,总体积不超过 j 的最大价值, 再结合题目要求,

得出最终的状态:从前 n 个物品中挑选,总体积不超过 V 的最大价值,所以返回 dp[n][V] 即可.

第一问:【编写关键代码】

/**
 * (1)求这个背包至多能装多大价值的物品?
 * @param v 每个商品所对应的体积
 * @param w 每个商品所对应的价值
 * @param V 背包体积
 * @param n 商品数量
 * @return
 */
public int getMaxVlaue(int[] v, int[] w, int V, int n) {
    int[][] dp = new int[n + 1][V + 1];
    // 从前 i 个商品中挑选,总体积不超过 j,最大价值是多少
    for(int i = 1; i <= n; ++i) {
        for(int j = 1; j <= V; ++j) {
            // 不挑选 i 商品
            dp[i][j] = dp[i - 1][j];
            // 挑选 i 商品, 数学思想简化状态转移方程
            if(j - v[i] >= 0) {
                dp[i][j] = Math.max(dp[i][j],dp[i][j - v[i]] + w[i]);
            }
        }
    }
    return dp[n][V];
}

此处先理解最基础的代码,等看完第二问, 再统一做空间优化.

第二问:【算法原理】

1. 状态定义

有了前面的经验,接下来就直接定义状态了.

状态定义:dp[i][j]表示从前 i 个物品中挑选,总体积正好为 j,所有选法中,能挑选出来的最大价值

2. 推导状态转移方程

此处的状态转移方程和第一问几乎一模一样, 只需要处理一些细节:

根据上面的数学推导,得出的状态转移方程:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i] + w[i]])

        但是此处需要正好装满,所以 j - v[i] 就有可能不存在,所以需要像 01 背包那样,给第一行除第一个位置,全部设为 -1 或者 -0x3f3f3f3f,因为没有商品的时候,不可能凑出体积为 1,2,3 的情况.

4. 填表顺序

从上往下,从左往右

5.返回值

dp[n][V]

第二问:【编写关键代码】

/**
 * (2)若背包恰好装满,求至多能装多大价值的物品?
 * @param v 每个商品所对应的体积
 * @param w 每个商品所对应的价值
 * @param V 背包体积
 * @param n 商品数量
 * @return
 */
public static int getMaxVlaue(int[] v, int[] w, int V, int n) {
    int[][] dp = new int[n + 1][V + 1];
    // 状态定义:从前 i 个商品中挑选,体积恰好等于 j,最大价值为多少

    // 体积不能正好等于 j 的,统统初始化为 -0x3f3f3f3f
    for(int j = 1; j <= V; ++j) dp[0][j] = -0x3f3f3f3f;

    for(int i = 1; i <= n; ++i) {
        for(int j = 1; j <= V; ++j) {
            // 不选 i
            dp[i][j] = dp[i - 1][j];
            // 选 i,-0x3f3f3f3f 不需要做条件判断,因为它足够小,取 max 不影响
            if(j - v[i] >= 0) {
                dp[i][j] = Math.max(dp[i][j],dp[i][j - v[i]] + w[i]);
            }
        }
    }
    return dp[n][V] < 0 ? 0 : dp[n][V];
}

 【空间优化】

  • 利用滚动数组做空间上的优化
  • 直接在原始代码上稍加修改即可

        利用滚动数组优化的时候,需要注意内层循环的填表顺序,由于 dp[i][j] 只会用到上一行的当前列,以及当前行前面的列,所以内层循环填表和 01 背包不一样,完全背包空间优化后,内层循环需要从左往右填表.

第一问:空间优化后的代码

/**
 * (1)求这个背包至多能装多大价值的物品?
 * @param v 每个商品所对应的体积
 * @param w 每个商品所对应的价值
 * @param V 背包体积
 * @param n 商品数量
 * @return
 */
public int getMaxVlaue(int[] v, int[] w, int V, int n) {
    int[] dp = new int[V + 1];
    // 从前 i 个商品中挑选,总体积不超过 j,最大价值是多少
    for(int i = 1; i <= n; ++i) {
        for(int j = v[i]; j <= V; ++j) {
            dp[j] = Math.max(dp[j],dp[j - v[i]] + w[i]);
        }
    }
    return dp[V];
}

第二问:空间优化后的代码

/**
 * (2)若背包恰好装满,求至多能装多大价值的物品?
 * @param v 每个商品所对应的体积
 * @param w 每个商品所对应的价值
 * @param V 背包体积
 * @param n 商品数量
 * @return
 */
public static int getMaxVlaue(int[] v, int[] w, int V, int n) {
    int[] dp = new int[V + 1];
    // 状态定义:从前 i 个商品中挑选,体积恰好等于 j,最大价值为多少

    // 体积不能正好凑成 j 的,统统初始化为 -0x3f3f3f3f
    for(int j = 1; j <= V; ++j) dp[j] = -0x3f3f3f3f;

    for(int i = 1; i <= n; ++i) {
        for(int j = v[i]; j <= V; ++j) {
            dp[j] = Math.max(dp[j],dp[j - v[i]] + w[i]);
        }
    }
    return dp[V] < 0 ? 0 : dp[V];
}

       有了这两道完全背包的基础之后,后面的完全包相关的题目,可以先自己尝试去做,在看答案之前,自己能做出来,记忆还是非常深刻的.

1.2 零钱兑换

【题目链接】

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/coin-change/【题目描述】

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的最少的硬币个数 。
如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

【算法原理】

这道题还是很容易能看出来是完全背包问题.

1. 状态定义

        模仿完全背包的状态定义:dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过 j,所有选法中,最大价值.

状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,所有的选法中,最少的硬币个数

2. 推导状态转移方程

根据最后一个位置的状况, 分情况讨论:

        有了上题的经验,当出现这种定义一个状态需要无线个状态来拼接的时候,这个时候我们要想办法把它变成一个或两个状态.

大家可以自己动手写写递推关系,找到 dp[i][j] 与 dp[i][j - coins[i]] 的关系,得到最终的状态转移方程:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i] + 1])

3. 编写代码

/**
 * 零钱兑换 - 完全背包
 * @param coins 硬币数组
 * @param amount 目标值
 * @return
 */
public int coinChange(int[] coins, int amount) {
    int n = coins.length;
    int INF = 0x3f3f3f3f;
    int[][] dp = new int[n + 1][amount + 1];
    // 状态定义:dp[i][i] 表示从前i个硬币中挑选,总和正好等于j,所有的选法中,最少硬币个数

    // 不难正好凑成 j 元
    for(int j = 1; j <= amount; ++j) dp[0][j] = INF;

    for(int i = 1; i <= n; ++i) {
        for(int j = 0; j <= amount; ++j) {
            // 不选 i
            dp[i][j] = dp[i - 1][j];
            // 选 i, 具体选几个 i,用数学方法优化
            if(j >= coins[i - 1]) {
                dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
            }
        }
    }
    // 可能不存在正好凑出的情况
    return dp[n][amount] >= INF ? -1 : dp[n][amount];
}

4. 空间优化

  1. 删掉一维
  2. 内层循环从左往右
/**
 * 零钱兑换 - 完全背包
 * @param coins 硬币数组
 * @param amount 目标值
 * @return
 */
public int coinChange(int[] coins, int amount) {
    int n = coins.length;
    int INF = 0x3f3f3f3f;
    int[] dp = new int[amount + 1];
    // 状态定义:dp[i][i] 表示从前i个硬币中挑选,总和正好等于j,所有的选法中,最少硬币个数

    // 不能正好凑成 j 元
    for(int j = 1; j <= amount; ++j) dp[j] = INF;

    for(int i = 1; i <= n; ++i) {
        for(int j = coins[i - 1]; j <= amount; ++j) {
            dp[j] = Math.min(dp[j], dp[j - coins[i - 1]] + 1);
        }
    }
    // 可能不存在正好凑出的情况
    return dp[amount] >= INF ? -1 : dp[amount];
}

1.3 零钱兑换 II

【题目链接】

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/coin-change-ii/【题目描述】

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

【算法原理】

        零钱兑换 II 其实比零钱兑换还简单,此处求的是凑成 j 元的方法数,只要不是求最大价值或最小价值中的正好凑成某某某,初始化工作就很简单.

1.定义状态

状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,总共有多少种选法

2. 推导状态转移方程

有前面的基础了,数学思想直接得出状态转移方程: dp[i][j] = dp[i - 1][j] + dp[i][j- coins[i]]

        此处需要注意的是,第二个状体千万不能在后面 + 1,因为求得是选法,以 i 结尾,是把最后一个位置拼在前面的选法中,所以本质上是同一种选法,求个数才需要 + 1,可以好好体会为什么是这样.

3. 编写代码 

/**
 * 零钱兑换 II - 完全背包
 * @param coins 硬币数组
 * @param amount 目标值
 * @return
 */
public int change(int amount, int[] coins) {
    int n = coins.length;
    int[][] dp = new int[n + 1][amount + 1];
    // 状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,总共有多少种选法

    dp[0][0] = 1; // 初始化

    for(int i = 1; i <= n; ++i) {
        for(int j = 0; j <= amount; ++j) {
            // 不选 i
            dp[i][j] = dp[i - 1][j];
            if(j >= coins[i - 1]) {
                // 选 i, 数学思想优化状态转移方程
                dp[i][j] += dp[i][j - coins[i - 1]];
            }
        }
    }
    return dp[n][amount];
}

4. 空间优化

  1. 删掉一维
  2. 内层循环从左往右
/**
 * 零钱兑换 II - 完全背包
 * @param coins 硬币数组
 * @param amount 目标值
 * @return
 */
public int change(int amount, int[] coins) {
    int n = coins.length;
    int[] dp = new int[amount + 1];
    // 状态定义:dp[i][i] 表示从前 i 个硬币中挑选,总和正好等于 j,总共有多少种选法

    dp[0] = 1; // 初始化

    for(int i = 1; i <= n; ++i) {
        for(int j = coins[i - 1]; j <= amount; ++j) {
            dp[j] += dp[j - coins[i - 1]];
        }
    }
    return dp[amount];
}

1.4 完全平方数

【题目链接】

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/perfect-squares/【题目描述】

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。
例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

【算法原理】

        这道题如果想到的是贪心,那么就想错了,对于 13,贪心做法先找 9,再找 4,看似没问题,但是如果是 12,贪心做法先找 9, 再找三个 1,这样就需要 4 个完全平方数了,而三个 4 只需要 3 个完全平方数. 

所以我们干脆就从第一个开始往后挑,那么就是这样理解:从前 i 个完全平方数中挑选几个数,正好凑成 j,所有的选法中,使用最少的完全平方数.

这正好就是完全背包问题!

1.状态定义

状态定义:从前 i 个完全平方数中挑选,总和正好等于 j,所有的选法中,最小的数量

2. 推到状态转移方程

 使用数学思想得出最终的状态转移方程:dp[i][j] = Math.min(dp[i - 1][j],dp[i][j - i * i] + 1)

3. 编写代码

/**
 * 完全平方数
 * @param n 目标值
 * @return
 */
public int numSquares(int n) {
    int INF = 0x3f3f3f3f;
    // i^2 <= n
    int m = (int)Math.sqrt(n);
    int[][] dp = new int[m + 1][n + 1];
    // 状态定义:从前 i 个完全平方数中挑选,总和正好等于 j,所有的选法中,最小的数量
    for(int j = 1; j <= n; ++j) dp[0][j] = INF; // 不能正好凑成 j

    for(int i = 1; i <= m; ++i) {
        for(int j = 1; j <= n; ++j) {
            // 不选 i ^ 2
            dp[i][j] = dp[i - 1][j];
            if(j >= i * i) {
                // 选 i ^ 2
                dp[i][j] = Math.min(dp[i][j],dp[i][j - i * i]  + 1);
            }
        }
    }
    return dp[m][n] >= INF ? 0 : dp[m][n];
}

        此处二维 dp 表的横坐标,最多用到 1 ~ 根号 n,因为 13 只会用到 1,4,9,不可能用到 16,所以可以得出 i ^ 2 <= n。

4. 空间优化

/**
 * 完全平方数
 * @param n 目标值
 * @return
 */
public int numSquares(int n) {
    int INF = 0x3f3f3f3f;
    int m = (int)Math.sqrt(n);
    int[] dp = new int[n + 1];
    // 状态定义:从前 i 个完全平方数中挑选,总和正好等于 j,所有的选法中,最小的数量
    for(int j = 1; j <= n; ++j) dp[j] = INF; // 不能正好凑成 j

    for(int i = 1; i <= m; ++i) {
        for(int j = i * i; j <= n; ++j) {
            dp[j] = Math.min(dp[j],dp[j - i * i]  + 1);
        }
    }
    return dp[n] >= INF ? 0 : dp[n];
}

2. 二维费用的背包问题

        什么是二维费用的背包问题呢,简单来说就是有两个限定条件的背包问题,接下来结合题目来理解二维费用的背包问题.

2.1 一和零

【题目链接】

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/ones-and-zeroes/【题目描述】

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

题目意思:从前 i 个字符串中挑选,个数上满足 0 <= 5 && 1 <= n,所有选法中的最大子集.

【算法原理】

这题本质上就是二维费用的 01 背包,只不过比 01 背包多一维,做法大致相同.

1.状态定义

状态定义:dp[i][j][k] 表示从前 i 个字符串中挑选,字符 0 的个数不超过 j
         字符 1 的个数不超过 k,所有选法中,最大的长度.

2. 推导状态转移方程

最终的状态转移方程: dp[i][j][k] = Math.max(dp[i - 1][j][k],dp[i - 1][j - a][k - b] + 1)

3. 初始化

当没有字符串的时候,最大子集长度当然就是 0 了,所以不需要初始化.

4. 编写代码

/**
 * 一和零 - 二位费用的 01 背包
 * @param strs
 * @param m
 * @param n
 * @return
 */
public int findMaxForm(String[] strs, int m, int n) {
    int L = strs.length;
    int[][][] dp = new int[L + 1][m + 1][n + 1];
    // 从数组中前 i 个字符串中挑选,0的个数不超过 m,
    // 1的个数不超过 n 的所有子集中,长度最大的子集

    for(int i = 1; i <= L; ++i) {
        // 计算当前字符串  0,1 的个数
        int a = 0, b = 0;
        for(char ch : strs[i - 1].toCharArray()) {
            if(ch == '0') a++;
            else b++;
        }
        for(int j  = 0; j <= m; ++j) {
            for(int k = 0; k <= n; ++k) {
                // 不选 i
                dp[i][j][k] = dp[i -1][j][k];
                // 选 i
                if(j >= a && k >= b) {
                    dp[i][j][k] = Math.max(dp[i][j][k], 
                            dp[i - 1][j - a][k - b] + 1);
                }
            }
        }
    }
    return dp[L][m][n];
}

5. 空间优化

/**
 * 一和零 - 二位费用的 01 背包
 * @param strs
 * @param m
 * @param n
 * @return
 */
public int findMaxForm(String[] strs, int m, int n) {
    int L = strs.length;
    int[][] dp = new int[m + 1][n + 1];
    // 状态定义:从数组中前 i 个字符串中挑选, 0 的个数不超过 m, 
    // 1 的个数不超过 n 的所有子集中, 长度最大的子集
    for(int i = 1; i <= L; ++i) {
        // 计算当前字符串  0,1 的个数
        int a = 0, b = 0;
        for(char ch : strs[i - 1].toCharArray()) {
            if(ch == '0') a++;
            else b++;
        }
        for(int j = m; j >= a; --j) {
            for(int k = n; k >= b; --k) {
                dp[j][k] = Math.max(dp[j][k], dp[j - a][k - b] + 1);
            }
        }
    }
    return dp[m][n];
}

2.2 盈利计划

【题目链接】

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode.cn/problems/profitable-schemes/【题目描述】

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。
如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为盈利计划 。并且工作的成员总数最多为 n 。

有多少种计划可以选择?因为答案很大,所以返回结果模 10^9 + 7 的值。

示例 1:

输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。

示例 2:

输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。

题目的意思如下:

 【算法原理】

        这道题与 "一和零" 唯一的区别就是:此处有一个限定条件是满足 >= 某个值,等会需要注意状态转移方程上的差异.

1. 状态定义

状态定义:dp[i][j][k] 表示从前 i 个计划中挑选,总人数不超过 j,
总利润至少为 k,一共有多少种选法

2. 推导状态转移方程

        此处为什么 k - profit[i] 可以 <0 ? 因为题目要求的就是利润至少 >= x,如果 profit[i] 已经 > x,那不是更好,并且前面的挑选,我只需要保证利润 >= 0 就可以了.

所以对于选 i 的这种情况,状态转移方程是需要稍做处理的,因为像 dp[i][j][-3] 这种状态是没有意义的,所以使它利润为 0 即可,也就是 dp[i - 1][j - group[i]][max(0, k - profit(i))]

所以最终的状态转移方程 : dp[i][j][k] = dp[i - 1][j] + dp[i - 1][j - group[i]][max(0, k - profit(i))]

3. 编写代码

/**
 * 盈利计划 - 二维费用的背包问题
 * @param n 员工数量
 * @param minProfit 至少产生 minProfit 利润
 * @param group 员工数组
 * @param profit 工作所对应的利润数组
 * @return
 */
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
    int mod = (int)1e9 + 7;
    int L = group.length;
    int m = minProfit;
    int[][][] dp = new int[L + 1][n + 1][m + 1];
    // 初始化: 任务0, 利润0, 人数 0 <= j <= n, 都有一种选法,所以初始化为 1
    for(int j = 0; j <= n; ++j) dp[0][j][0] = 1;

    // 状态:从前 i 个任务中挑选, 人数不超过 j, 利润至少为 k 的选法种数
    for(int i = 1; i <= L; ++i) {
        for(int j = 0; j <= n; ++j) {
            for(int k = 0; k <= m; ++k) {
                // 不选 i
                dp[i][j][k] = dp[i - 1][j][k];
                // 选 i, j - g[i] 必须要 >= 0, 而 k - p[i] 可以小于 0
                if(j >= group[i - 1]) {
                    dp[i][j][k] += dp[i - 1][j - group[i - 1]][Math.max(0, k - profit[i - 1])];
                }
                // 每次让他模上 mod
                dp[i][j][k] %= mod;
            }
        }
    }
    return dp[L][n][m];
}

4. 空间优化

/**
 * 盈利计划 - 二维费用的背包问题
 * @param n 员工数量
 * @param minProfit 至少产生 minProfit 利润
 * @param group 员工数组
 * @param profit 工作所对应的利润数组
 * @return
 */
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit){
    int mod = (int)1e9 + 7;
    int L = group.length;
    int m = minProfit;
    int[][] dp = new int[n + 1][m + 1];
    // 初始化: 任务0, 利润0, 人数不超过 j,0 <= j <= n, 都有一种选法,所以初始化为 1
    for(int j = 0; j <= n; ++j) dp[j][0] = 1;

    // 状态:从前 i 个任务中挑选, 人数不超过 j, 利润至少为 k 的选法种数
    for(int i = 1; i <= L; ++i) {
        for(int j = n; j >= group[i - 1]; --j) {
            for(int k = m; k >= 0; --k) {
                dp[j][k] +=
                        dp[j - group[i - 1]][Math.max(0, k - profit[i - 1])];
                dp[j][k] %= mod;
            }
        }
    }
    return dp[n][m];
}

【总结 看完背包问题的这两篇文章,之后遇到类似的题目,也能有感觉往背包方面去靠,另外就是要理解背包问题的核心:它是用来解决有限制条件下的 " 组合 " 问题!而有些问题看似也是从一堆物品中挑选,并要求满足某个条件,实则并不是背包问题的,例如这道题:组合总和 IV

        像这种 1,2,1 和 1,1,2 算两种选法的,就不是背包问题,因为这种在数学上称之为排列,而背包问题解决的是组合问题. 


背包系列讲解到这就结束了,希望能帮助大家.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Master_hl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值