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

news2024/12/24 10:55:33

目录

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 算两种选法的,就不是背包问题,因为这种在数学上称之为排列,而背包问题解决的是组合问题. 


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1066728.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

天眼销|企业数据查询必备

首先&#xff0c;得介绍一下天眼销是一个什么样的平台&#xff0c;请往下看&#xff1a; 可能会有一些还是觉得懒得看&#xff0c;简单理解&#xff0c;它与我们熟知的某查查&#xff0c;天眼某相类似&#xff0c;有很多共同之处。比如&#xff1a;某查查的服务模式&#xff08…

数据结构与算法(七)--使用链表实现栈

一、前言 之前我们已经学习了链表的所有操作及其时间复杂度分析&#xff0c;我们可以了解到对于链表头的相关操作基本都是O(1)的&#xff0c;例如链表头增加、删除元素&#xff0c;查询元素等等。那我们其实有一个数据结构其实可以完美利用到这些操作的特点&#xff0c;都是在…

Idea升级版本后踩坑关于Local History

版本升级&#xff1a;IntelliJ IDEA 2022.1.2 (Ultimate Edition) Local History本地历史修改记录在idea升级后默认只保留 5天 以内的修改记录&#xff0c;导致时间过长的一些内容就自动被清除掉了。可通过File->Setting-Advanced Setting 进行修改。

数据结构——常见的十种排序算法

一、常见的十种排序算法&#xff1a; 冒泡排序、选择排序、插入排序、归并排序、快速排序、希尔排序、堆排序、计数排序、桶排序、基数排序 1.【知识框架】 补充&#xff1a; 内部排序&#xff1a;整个排序过程完全在内存中进行。 外部排序&#xff1a;由于待排序记录数据量太…

python进行接口自动化测试

一、接口自动化测试的流程 1、需求分析 1、1请求&#xff08;url,方法、数据&#xff09; 2、挑选需要做自动化测试的接口 3、设计自动化测试用例 4、搭建自动化测试环境 5、设计自动化执行框架&#xff08;报告、参数化、 用例执行框架&#xff09; 6、编写代码 7、执…

工程派工单,建筑工程派工单

工程派工单是指建设项目管理人员或工程维修人员发出的文件&#xff0c;用于标明工人或维修人员在建设项目或设备中处理或维修问题的任务。派工单包括建设项目的实际维护任务、所需材料、工具等信息&#xff0c;以及具体的执行人员和完成时间。工程派工单是保证建设项目顺利开展…

用《斗破苍穹》的视角打开C#3 标签与反射(人物创建与斗技使用)

随着剧情的发展&#xff0c;主线人物登场得越来越多&#xff0c;时不时跳出一个大佬&#xff0c;对我张牙舞爪地攻击。眼花缭乱的斗技让我不厌其烦&#xff0c;一个不小心&#xff0c;我就记不清楚在哪里遇上过什么人&#xff0c;他会什么斗技了。这时候&#xff0c;我就特别希…

Centos中清除因程序异常终止,导致的残留的Cache/buff_drop_caches命令---linux工作笔记063

我这里因为nifi程序背压设置的不合理,导致,内存和CPU消耗过高,系统崩溃,但是重启NIFI以后,发现 对应的执行top命令,看到,系统的buff/cache 依然没有减少,说明内存被浪费了,残留在这里没有被回收. 用这个办法执行这个命令; linux会自动触发清理,但是只有在内存不够用的时候才会…

计算机竞赛 题目:基于机器视觉opencv的手势检测 手势识别 算法 - 深度学习 卷积神经网络 opencv python

文章目录 1 简介2 传统机器视觉的手势检测2.1 轮廓检测法2.2 算法结果2.3 整体代码实现2.3.1 算法流程 3 深度学习方法做手势识别3.1 经典的卷积神经网络3.2 YOLO系列3.3 SSD3.4 实现步骤3.4.1 数据集3.4.2 图像预处理3.4.3 构建卷积神经网络结构3.4.4 实验训练过程及结果 3.5 …

沈阳陪诊系统|沈阳陪诊系统开发|沈阳陪诊系统功能和优势

在现代医疗服务中&#xff0c;陪诊系统服务正变得越来越重要。这项创新的服务提供了一种全新的方式&#xff0c;帮助患者在医院就诊时获得更好的照顾和支持。无论是面对复杂的医学流程还是需要心理支持&#xff0c;陪诊系统服务都能够为患者提供方便、专业的帮助。陪诊系统服务…

自学SLAM(3)---保姆教程教你如何使用摄像头运行ORB-SLAM2

前言 上一篇文章我讲述了如何使用自己的视频运行ORB-SLAM2 链接如下&#xff1a; 链接: 上一篇&#xff0c;环境搭建及使用自己的视频运行ORB-SLAM2 没有搭建环境的朋友看上面我的链接哦&#xff0c;里面有超详细的环境搭建&#xff0c;一步一步来保姆级别的哦 那么本篇&#…

【mysql 大表清理】磁盘占用太多,清理无效大表

在使用MySQL数据库时&#xff0c;有时候由于数据量增加或者磁盘空间限制&#xff0c;会导致数据库磁盘空间不足的问题。这会影响到数据库的正常运行&#xff0c;需要及时清理磁盘空间来解决问题。本文将介绍如何清理MySQL数据库的磁盘空间&#xff0c;并给出示例以帮助读者更好…

wget出现无法建立SSL连接的问题

出现这个问题的原因&#xff0c;这是因为wget在使用https协议的时候&#xff0c;默认会去验证网站的证书&#xff0c;而这个证书验证经常会失败&#xff0c;加上"--no-check-certificate"选项&#xff0c;就能排除掉这个错误

MongoEngine 简介安装、连接、数据类型及其参数详解

文章目录 前言一、MongoEngine 简介二、MongoEngine的安装与连接1. 安装MongoEngine2. 连接到MongoDB3. 定义数据模型 三、MongoEngine模型介绍1. 常见数据类型2. 数据类型参数 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客&#xff0c;方…

除静电离子风棒的工作原理及应用

除静电离子风棒是一种常见的除静电设备&#xff0c;它的工作原理是通过产生大量的负离子来中和物体表面的静电电荷&#xff0c;从而达到除静电的目的。 静电离子风棒内部装有一个电离器&#xff0c;电离器会将空气中的氧气分子或水分子电离成正、负离子。这些带电的离子在空气…

ubuntu疑难杂症

1.ubuntu 使用apt 安装软件时提示出现不能获得lock $ sudo rm /var/lib/dpkg/lock$ sudo dpkg --configure -a$ sudo rm /var/lib/apt/lists/lock

什么是防抖和节流

防抖和节流都是前端开发中常用的优化性能的技术。 一、定义 防抖&#xff1a; 防抖指的是在事件触发后&#xff0c;在规定的时间内若再次触发&#xff0c;则重新计时&#xff0c;直到规定时间内没有再次触发事件&#xff0c;才执行事件处理。这样可以避免在短时间内频繁地触发…

vtk之【vtkPolyData、vtkCell、vtkPoints】

文章目录 一,vtkPolyData、cell、point1) 例子2) vtkPolyData、vtkCell、vtkPoints 二,vtkNew<>与vtkSmartPointer<>的区别:三&#xff0c;补充 一,vtkPolyData、cell、point 1) 例子 /*** vtkNew 是一个类模板* vtkNew<> 是一个简单的 RAII&#xff08;Res…

【ARM CoreLink 系列 5 -- CI-700 控制器介绍 】

文章目录 1.1 什么是 CI-700?1.1.1 关于 CI-7001.1.2 CI-700 特点1.2 全局配置参数1.2.1 寻址能力1.3 组件和配置1.3.1 CI-700 互联的结构1.3.2 Crosspoint(XP)1.3.3 外部接口1.4 组件(Components)1.1 什么是 CI-700? CI-700是一种AMBA 5 CHI互连,具有可定制的网状拓扑结构…

onlyOfice取消上传文件大小的限制

进入docker容器 docker exec -it 容器名ID bash 编辑配置文件 #如果不能编辑,需安装vim apt-get update apt-get install vim #如果不能安装vim&#xff0c;可以在容器外部编辑配置文件后上传至容器&#xff1a; docker cp /home/file/文件 容器id:/etc/onlyoffice/d…