Featured image of post 01背包

01背包

动态规划

# 前言

最基本的01背包:

「01背包」是指给定物品价值与体积(对应了「给定价值与成本」),在规定容量下(对应了「限定决策规则」)如何使得所选物品的总价值最大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
    public int maxValue(int N, int C, int[] v, int[] w) {
        int[][] dp = new int[N][C+1];
        // 先处理「考虑第一件物品」的情况
        for (int i = 0; i <= C; i++) {
            dp[0][i] = i >= v[0] ? w[0] : 0;
        }
        // 再处理「考虑其余物品」的情况
        for (int i = 1; i < N; i++) {
            for (int j = 0; j < C + 1; j++) {
                // 不选该物品
                int n = dp[i-1][j]; 
                // 选择该物品,前提「剩余容量」大于等于「物品体积」
                int y = j >= v[i] ? dp[i-1][j-v[i]] + w[i] : 0; 
                dp[i][j] = Math.max(n, y);
            }
        }
        return dp[N-1][C];
    }
}

滚动数组版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
    public int maxValue(int N, int C, int[] v, int[] w) {
        int[][] dp = new int[2][C+1];
        // 先处理「考虑第一件物品」的情况
        for (int i = 0; i < C + 1; i++) {
            dp[0][i] = i >= v[0] ? w[0] : 0;
        }
        // 再处理「考虑其余物品」的情况
        for (int i = 1; i < N; i++) {
            for (int j = 0; j < C + 1; j++) {
                // 不选该物品
                int n = dp[(i-1)&1][j]; 
                // 选择该物品
                int y = j >= v[i] ? dp[(i-1)&1][j-v[i]] + w[i] : 0;
                dp[i&1][j] = Math.max(n, y);
            }
        }
        return dp[(N-1)&1][C];
    }
}

一维数组版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
    public int maxValue(int N, int C, int[] v, int[] w) {
        int[] dp = new int[C + 1];
        for (int i = 0; i < N; i++) {
            for (int j = C; j >= v[i]; j--) {//从大往小
                // 不选该物品
                int n = dp[j]; 
                // 选择该物品
                int y = dp[j-v[i]] + w[i]; 
                dp[j] = Math.max(n, y);
            }
        }
        return dp[C];
    }
}

参考自https://mp.weixin.qq.com/s/xmgK7SrTnFIM3Owpk-emmg

就是决策第i种物品选不选

# 【01背包存在问题】分割等和子集

# 题目

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。 示例 2:

输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和相等的子集。

提示:

1 <= nums.length <= 200

1 <= nums[i] <= 100

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/partition-equal-subset-sum 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

令sum为数组总和,原问题可转化为是否可以找出子序列,使得其和为sum/2(可预先判断sum是否为偶数)

进一步地,可转换为01背包问题,能否在背包容量为sum/2的前提下装满背包,与普通的01背包的区别在于这里的物品价值与体积是一致的

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n=nums.size();
        int sum=0,maxv=*max_element(nums.begin(),nums.end());
        for(auto &x:nums) sum+=x;
        if(sum&1 || maxv>sum/2) return false;
        vector<vector<int>> dp(n,vector<int>(sum/2+1,0));
        //dp[i][j] 前i个数不超过j的最大和
        //如果有dp[i][sum/2]=sum/2说明可以分割出等和子集
        
        for(int j=0;j<=sum/2;j++){
            if(j>=nums[0]) dp[0][nums[0]]=nums[0];
        }

        for(int i=1;i<n;i++){
            for(int j=0;j<=sum/2;j++){
                if(j-nums[i]>=0)
                    dp[i][j]=max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
                else dp[i][j]=dp[i-1][j];
            }
        }
        
        if(dp[n-1][sum/2]==sum/2) return true;
        else return false;
    }
};

也可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int n = nums.size();
        if(n==1) return false;
        int sum=0;
        for(auto &x:nums) sum+=x;
        if(sum&1) return false;

        vector<vector<int>> dp(n,vector<int>(sum+1,0));
        //dp[i][j] nums[0..i]中是否可以加起来等于j
        for(int i=0;i<n;i++){
            dp[i][0] = 0;
        }
        dp[0][nums[0]] = 1;
        for(int i=1;i<n;i++){
            for(int j=1;j<sum/2+1;j++){
                if(nums[i]>j) dp[i][j] = dp[i-1][j];
                else dp[i][j] = dp[i-1][j]|dp[i-1][j-nums[i]];
            }
        }
        return dp[n-1][sum/2];
    }
};

# 【01背包最值问题】最后一块石头的重量II

# 题目

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎; 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

示例 1:

输入:stones = [2,7,4,1,8,1] 输出:1 解释: 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。 示例 2:

输入:stones = [31,26,33,21,40] 输出:5

提示:

1 <= stones.length <= 30

1 <= stones[i] <= 100

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/last-stone-weight-ii 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

任意选i块石头,使得他们的重量趋近于总重量的一半,因为这样和另一半抵消的差值就是最小的

从而转化为01背包问题

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int n = stones.size(), sum = 0;
        for(auto &x:stones) sum+=x;
        int target = sum/2;
        
        vector<int> dp(target+1,0);
        for(int i=0;i<n;i++){
            for(int j=target;j>=stones[i];--j){
                dp[j] = max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        //dp[target] 背包容量为target的最大石头重量
        
        return abs(sum-dp[target]-dp[target]);
    }
};

# 【01背包组合问题】目标和

# 题目

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3 示例 2:

输入:nums = [1], target = 1 输出:1

提示:

1 <= nums.length <= 20

0 <= nums[i] <= 1000

0 <= sum(nums[i]) <= 1000

-1000 <= target <= 1000

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/target-sum 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

原数组需要分为两部分:负数部分neg 和 非负数部分pos,再令原数组总和为sum,由题意有:

pos+neg=target;

pos-neg=sum;

去掉pos,联立可得neg=(target-sum)/2 (注意,这里求得的neg是带符号的)

因此原问题转化成了 在原数组中找出子序列的和等于-neg(使其变为非负数,后用neg代替)的方案数

首先预处理判断neg的合法性,然后进行动态规划

和普通01背包的定义不同,这里定义dp[i,j]表示前i个(不包括第i个)数中子序列的和等于j的方案数

有两个不同的点:1)第一维不包括第i个;2)第二维表示刚好等于j,而不是不超过j

这是为了方便表示什么都没选的时候,也是一种方案

初始化dp[0,0]=1,dp[0,y]=0(y>0),表示哪个数都没选时,子序列和和只可能等于0且只有1种方案。

转移方程:

1
2
3
4
5
6
for(int i=1;i<=n;i++){
	for(int j=0;j<=neg;j++){
    	if(j-nums[i-1]>=0) dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]];//可以选第i-1个数,选中的方案数+不选的方案数
        else dp[i][j]=dp[i-1][j];//选不了第i-1个数
    }
}

最后结果即为dp[n,neg]

其实第一维还是可以像原来背包那样,定义dp[i,j]表示前i个(包括第i个)数中子序列的和等于j的方案数

但初始化要改一下:

1
2
3
//dp[i][j] 前i个数总和为j的方案数
dp[0][0]=1;
dp[0][nums[0]]=nums[0]==0?2:1;

即第0个数也有选还是不选两种情况,不选的话,和为0,即dp[0,0]=1;如果选,不能简单的令dp[0, nums[0]]=1,因为此时nums[0]可能等于0,选或不选是不同的方案。

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size(), sum=0;
        for(auto &x:nums) sum+=x;
        if(sum<target||(target-sum)&1) return 0;
        int neg = -(target-sum)/2;
        vector<vector<int>> dp(n+1,vector<int>(neg+1,0));
        // cout<<neg<<endl;
        //dp[i][j] 前i-1个数总和为j的方案数
        dp[0][0]=1;

        for(int i=1;i<=n;i++){
            for(int j=0;j<=neg;j++){
                if(j-nums[i-1]>=0) dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]];
                else dp[i][j]=dp[i-1][j];
            }
        }
        
        return dp[n][neg];
    }
};

或者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size(), sum=0;
        for(auto &x:nums) sum+=x;
        if(sum<target||(target-sum)&1) return 0;
        int neg = -(target-sum)/2;
        //这里将第2维拓展到了最大值,是为了避免后面赋值dp[0][nums[0]]越界
        vector<vector<int>> dp(n,vector<int>(1001,0));
        // cout<<neg<<endl;
        //dp[i][j] 前i个数总和为j的方案数
        dp[0][0]=1;
        dp[0][nums[0]]=nums[0]==0?2:1;

        for(int i=1;i<n;i++){
            for(int j=0;j<=neg;j++){
                if(j-nums[i]>=0) dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]];
                else dp[i][j]=dp[i-1][j];
            }
        }
        
        return dp[n-1][neg];
    }
};

# 空间优化

01背包空间优化,内循环变为从大到小

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size(), sum=0;
        for(auto &x:nums) sum+=x;
        if(sum<target||(target-sum)&1) return 0;
        int neg = -(target-sum)/2;
        vector<int> dp(neg+1,0);
        // cout<<neg<<endl;
        //dp[j] 总和为j的方案数
        dp[0]=1;

        for(int i=1;i<=n;i++){
            for(int j=neg;j>=nums[i-1];j--){
                dp[j]+=dp[j-nums[i-1]];
            }
        }
        return dp[neg];
    }
};

# 【01二维费用背包最值问题】一和零

# 题目

给你一个二进制字符串数组 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 。

提示:

1 <= strs.length <= 600

1 <= strs[i].length <= 100

strs[i] 仅由 ‘0’ 和 ‘1’ 组成

1 <= m, n <= 100

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/ones-and-zeroes 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

要将此问题转换为背包问题,需要抽象出【价值】与【容量】

每个字符串的【价值】都是1,而【容量】对应的则是0的总数量以及1的总数量。

和普通01背包受到单维度容量限制不同,此题容量限制有两个维度,0的总数量和1的总数量,所以定义dp数组时需要考虑到

定义dp[i,j,k]表示前i个字符串(物品)中0的总数量不超过j,1的总数量不超过k的最大子集长度

初始化:

1
2
3
4
5
6
for(int j=0;j<=m;j++){
	for(int k=0;k<=n;k++){
		if(j>=zero[0]&&k>=one[0])
        	dp[0][j][k]=1;
	}
}

转移方程:

1
2
3
4
5
6
7
8
9
 for(int i=1;i<len;i++){
	for(int j=0;j<=m;j++){
		for(int k=0;k<=n;k++){
        	if(j>=zero[i] && k>=one[i])
            	dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-zero[i]][k-one[i]]+1);
                else dp[i][j][k]=dp[i-1][j][k];
		}
	}
}

最后结果即为dp[len-1, m, n]

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len=strs.size();
        int dp[len][m+1][n+1];
        //dp[i][j][k] 前i个字符串中,0的个数不大于m,1的个数不大于0 的最多字符串个数
        memset(dp,0,sizeof(dp));
        //每个字符串中0和1的个数
        vector<int> zero,one;
        for(auto &str: strs){
            int le = str.size(),cnt=0;
            for(auto &ch: str){
                if(ch=='0') cnt++;
            }
            zero.push_back(cnt);
            one.push_back(le-cnt);
        }
        //初始化
        for(int j=0;j<=m;j++){
            for(int k=0;k<=n;k++){
                if(j>=zero[0]&&k>=one[0])
                    dp[0][j][k]=1;
            }
        }

        for(int i=1;i<len;i++){
            for(int j=0;j<=m;j++){
                for(int k=0;k<=n;k++){
                    if(j>=zero[i] && k>=one[i])
                        dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-zero[i]][k-one[i]]+1);
                    else dp[i][j][k]=dp[i-1][j][k];
                }
            }
        }

        return dp[len-1][m][n];

    }
};

为了避免对初始化的分类讨论,可以假设添加一个空字符串到最前面

此时dp[i,j,k]表示前i-1个

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len=strs.size();
        int dp[len+1][m+1][n+1];
        //dp[i][j][k] 前i-1个字符串中,0的个数不大于m,1的个数不大于0 的最多字符串个数
        memset(dp,0,sizeof(dp));
        vector<int> zero,one;
        for(auto &str: strs){
            int le = str.size(),cnt=0;
            for(auto &ch: str){
                if(ch=='0') cnt++;
            }
            zero.push_back(cnt);
            one.push_back(le-cnt);
        }
        
        for(int i=1;i<=len;i++){
            for(int j=0;j<=m;j++){
                for(int k=0;k<=n;k++){
                    if(j>=zero[i-1] && k>=one[i-1])
                        dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-zero[i-1]][k-one[i-1]]+1);
                    else dp[i][j][k]=dp[i-1][j][k];
                }
            }
        }

        return dp[len][m][n];

    }
};

# 空间优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int len=strs.size();
        int dp[m+1][n+1];
        //[j][k] 0的个数不大于m,1的个数不大于0 的最多字符串个数
        memset(dp,0,sizeof(dp));
        vector<int> zero,one;
        for(auto &str: strs){
            int le = str.size(),cnt=0;
            for(auto &ch: str){
                if(ch=='0') cnt++;
            }
            zero.push_back(cnt);
            one.push_back(le-cnt);
        }
        //每个字符串中0和1的个数
        for(int j=0;j<=m;j++){
            for(int k=0;k<=n;k++){
                if(j>=zero[0]&&k>=one[0])
                    dp[j][k]=1;
            }
        }

        for(int i=1;i<len;i++){
            for(int j=m;j>=zero[i];j--){
                for(int k=n;k>=one[i];k--){
                    dp[j][k]=max(dp[j][k],dp[j-zero[i]][k-one[i]]+1);
                }
            }
        }

        return dp[m][n];

    }
};

# 【01二维费用背包组合问题】盈利计划

# 题目

集团里有 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 <= n <= 100

0 <= minProfit <= 100

1 <= group.length <= 100

1 <= group[i] <= 100

profit.length == group.length

0 <= profit[i] <= 100

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/profitable-schemes 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

令dp[i,j,k] 表示前i项工作,人数不超过j,利润至少为k的总方案数

这题也有两个约束,一个是人数,一个是利润,但其中的利润又不是绝对性的限制

这种组合问题的初始化有点麻烦,需要想清楚:

1
2
3
4
5
6
7
8
for(int j=0;j<=n;j++){//成员人数
	dp[0][j][0]=1;//第0项工作不做,利润恒为0,方案数为1
	if(j>=group[0]){//人数够做第0项工作了
		//做一下吧
        for(int k=0;k<=profit[0];k++) dp[0][j][k]=1;
        dp[0][j][0]++;//还是选择不做,利润为0的方案数加1
    }
}

状态转移方程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for(int i=1;i<len;i++){
	for(int j=0;j<=n;j++){
    	for(int k=0;k<=100;k++){
        	dp[i][j][k]=dp[i-1][j][k];//第i个工作不选
            	if(j>=group[i]){//选第i个工作
                	if(k>=profit[i])//如果总利润要达到k,第i份工作产生利润pi小于总利润,那么只需要前i-1份工作达到至少k-pi的利润
                        dp[i][j][k] += dp[i-1][j-group[i]][k-profit[i]];
                    //如果要达到的总利润k小于第i份工作产生的利润,那么前i-1份工作利润随便,不小于0即可,事实上,pi>=0
                    else dp[i][j][k] += dp[i-1][j-group[i]][0];
                    dp[i][j][k] %= MOD;
                }
		}
	}
}

最后的结果即为dp[len-1,n, minProfit]

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const int MOD = 1e9+7;
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size();
        
        long long dp[len+1][n+1][101];//dp[i,j,k] 表示前i项工作,人数不超过j,利润至少为k的总方案数
        memset(dp,0,sizeof(dp));
        
        for(int j=0;j<=n;j++){//成员人数
            dp[0][j][0]=1;//第0项工作不做,利润恒为0,方案数为1
            if(j>=group[0]){//人数够做第0项工作了
                //做一下吧
                for(int k=0;k<=profit[0];k++) dp[0][j][k]=1;
                dp[0][j][0]++;//还是选择不做,利润为0的方案数加1
            }
                
        }

        for(int i=1;i<len;i++){
            for(int j=0;j<=n;j++){
                for(int k=0;k<=100;k++){
                    dp[i][j][k]=dp[i-1][j][k];//第i个工作不选
                    if(j>=group[i]){//选第i个工作
                        if(k>=profit[i])//如果总利润要达到k,第i份工作产生利润pi小于总利润,那么只需要前i-1份工作达到至少k-pi的利润
                            dp[i][j][k] += dp[i-1][j-group[i]][k-profit[i]];
                        //如果要达到的总利润k小于第i份工作产生的利润,那么前i-1份工作利润随便,不小于0即可,事实上,pi>=0
                        else dp[i][j][k] += dp[i-1][j-group[i]][0];
                        dp[i][j][k] %= MOD;
                    }

                }
            }
        }

        return dp[len-1][n][minProfit];

    }
};

# 代码2.0

上一题也提到过,为了避免讨论初始化的麻烦,可以假想存在一个空工作,即所需人数为0,产生利润也为0,将该工作放在首位

那么dp[i,j,k]就用来表示前i-1个(真)工作,人数不超过j,产生利润至少为k的方案数

初始化就只需要令 dp[0,x,0]=1,其中x的范围为[0,n],表示这项空工作,人数无论是多少,利润只能是0,方案数只有1

后面循环注意将需要修改的i变为i-1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const int MOD = 1e9+7;
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size();
        
        long long dp[len+1][n+1][101];
        memset(dp,0,sizeof(dp));
        for(int j=0;j<=n;j++) dp[0][j][0]=1;
        for(int i=1;i<=len;i++){
            for(int j=0;j<=n;j++){
                for(int k=0;k<=100;k++){
                    dp[i][j][k]=dp[i-1][j][k];//第i个工作不选
                    if(j>=group[i-1]){//选第i个工作
                        if(k>=profit[i-1])//如果总利润要达到k,第i份工作产生利润pi小于总利润,那么只需要前i-1份工作达到至少k-pi的利润
                            dp[i][j][k] += dp[i-1][j-group[i-1]][k-profit[i-1]];
                        //如果要达到的总利润k小于第i份工作产生的利润,那么前i-1份工作利润随便,不小于0即可,事实上,pi>=0
                        else dp[i][j][k] += dp[i-1][j-group[i-1]][0];
                        dp[i][j][k] %= MOD;
                    } 
                }
            }
        }
        return dp[len][n][minProfit];

    }
};

# 空间优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const int MOD = 1e9+7;
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size();
        
        long long dp[n+1][101];
        memset(dp,0,sizeof(dp));
        for(int j=0;j<=n;j++) dp[j][0]=1;
        for(int i=1;i<=len;i++){
            for(int j=n;j>=group[i-1];j--){
                for(int k=100;k>=0;k--){
                    dp[j][k]=dp[j][k];
                    if(k>=profit[i-1])
                        dp[j][k] += dp[j-group[i-1]][k-profit[i-1]];
                    else dp[j][k] += dp[j-group[i-1]][0];
                    dp[j][k] %= MOD;
                }
            }
        }

        return dp[n][minProfit];

    }
};

# 另外一种定义方式

如果将dp[i,j,k]定义成前i-1项工作,人数恰好为j,利润至少为k的方案数

初始化dp[0,0,0]=1,表示空工作时,不需要人数,人数恰好为0,利润为0,方案数为1

转移方程和前面的方式没啥区别

最后的需返回dp[len,x,minProfit]的和,其中x的范围是[0,n]

官方题解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size(), MOD = (int)1e9 + 7;
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1)));
        dp[0][0][0] = 1;
        for (int i = 1; i <= len; i++) {
            int members = group[i - 1], earn = profit[i - 1];
            for (int j = 0; j <= n; j++) {
                for (int k = 0; k <= minProfit; k++) {
                    if (j < members) {
                        dp[i][j][k] = dp[i - 1][j][k];
                    } else {
                        dp[i][j][k] = (dp[i - 1][j][k] + dp[i - 1][j - members][max(0, k - earn)]) % MOD;
                    }
                }
            }
        }
        int sum = 0;
        for (int j = 0; j <= n; j++) {
            sum = (sum + dp[len][j][minProfit]) % MOD;
        }
        return sum;
    }
};
Licensed under CC BY-NC-SA 4.0
Last updated on May 11, 2022 00:00 UTC
Built with Hugo
Theme Stack designed by Jimmy