背包问题

这篇博客记录在leetcode刷题中遇到的一些背包问题。

目录

完全背包问题

1.518. 零钱兑换 II

2 322. 零钱兑换

3. 377. 组合总和 Ⅳ

4. 1449. 数位成本和为目标值的最大数字

01背包问题

1. 494. 目标和 

2. 879. 盈利计划

3. 416. 分割等和子集

4.1049. 最后一块石头的重量 II

5. 474. 一和零

多重背包

1. 宝物筛选

2. POJ 1014 Dividing

 3. 1363. 形成三的最大倍数

混合背包

1.P1833 樱花

分组背包

1. P1757 通天之分组背包

有依赖的背包

1.P1064 金明的预算方案


完全背包问题

1.518. 零钱兑换 II

518. 零钱兑换 II 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 

示例 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

//思路:

这一题是典型的完全背包问题,即:在一组数中选出一些数,和为给定值,每个数字可以取无限个,求不同的组合(不同的取法)数。

对于背包问题,一般用二维dp,这里定义dp[i][j]的含义是,在[0, i-1]下标的数中,选一些数,可以组成和为j的组合数。

那么,对 0 <= i <= coins.size(),dp[i][0] = 1,意为无论有多少个数可以选择(包括没有数可以选的情况),1个数都不选,只有这一种取法能使和为0。

接下来考虑转移方程。dp[i][j]表示现在有[0, i-1]范围的数字可以选,对于下标为 i-1的数,可以选,也可以不选。如果不选,那么取法是dp[i-1][j]。如果选了下标是[i-1]的数,由于是完全背包,仍然可以继续选[i-1],所以取法是 dp[i][j - coins[i]],这里要保证 j >= coins[i]。由于这里的i, j是从左到右,从上到下计算的,因此 dp[i][j - coins[j]]的结果已经在 dp[i][j]之前计算出来了。

因此,转移方程即为 dp[i][j] = \left\{\begin{matrix} 1, j = 0& &\\dp[i-1][j], j < coins[i]]\\dp[i-1][j] + dp[i][j - coins[i]], j \geq coins[i] & \end{matrix}\right.

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size() + 1, vector<int>(amount + 1, 0));
        for (int i = 0; i <= coins.size(); ++i)
        {
            dp[i][0] = 1;
        }
        for (int i = 1; i <= coins.size(); ++i)
        {
            for (int j = 1; j <= amount; ++j)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= coins[i-1]) dp[i][j] += dp[i][j - coins[i-1]];
            }
        }
        return dp[coins.size()][amount];
    }
};

 

 由于 dp[i][j]的值只与 dp[i-1][j] 和 dp[i][j - coins[i-1]]有关,因此可以把dp数组优化成一维的,并且 j 从左向右更新。对照转移方程dp[i][j] = \left\{\begin{matrix} 1, j = 0& &\\dp[i-1][j], j < coins[i]]\\dp[i-1][j] + dp[i][j - coins[i]], j \geq coins[i] & \end{matrix}\right. 

空间优化成1维dp后,由于dp[i][j]取决于dp[i][j-coins[i]],所以j的遍历依然是从左向右,依次更新dp[i][j],而未更新的部分是dp[i-1][j]。。

参考这一篇题解https://leetcode-cn.com/problems/coin-change-2/solution/ling-qian-dui-huan-iihe-pa-lou-ti-wen-ti-dao-di-yo/

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int n = coins.size();
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 0; i < n; ++i)
        {
            for (int j = coins[i]; j <= amount; ++j)
            {
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};

相同的问题还有https://leetcode-cn.com/problems/coin-lcci/ 面试题 08.11. 硬币 

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

 

示例 1:

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

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

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

和上一题类似,不过这里要求的是最少需要的硬币个数。同样的,定义 dp[i][j]: 使用coins[0...i-1]来组合成j,最少需要的硬币个数。初始化 dp[i][0] = 0, for 0 <= i <= coins.size(),意为组合成0只需要0个硬币。之后,考虑 dp[i][j],如果不使用 coins[i],那么最少需要 dp[i-1][j];如果使用 coins[i],那么需要 dp[i][j-coins[i]] 个硬币,二者取较小的那个。即递推公式:

dp[i][j] = \left\{\begin{matrix} 0, j = 0& &\\dp[i-1][j], j < coins[i]]\\min(dp[i-1][j], dp[i][j - coins[i]] + 1), j \geq coins[i] & \end{matrix}\right.

当然这里需要注意的是可能不能组成需要的值,因此初始化的时候除了j=0的情况,其它值都应该初始化为 INT32_MAX/2,/2是为了避免在j >= coins[i]时因为要计算 +1操作发生溢出。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        vector<vector<int>> dp(n+1, vector<int>(amount+1, INT32_MAX/2));
        for (int i = 0; i <= n; ++i)
        {
            dp[i][0] = 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]) dp[i][j] = min(dp[i][j], dp[i][j-coins[i-1]] + 1);
            }
        }
        return dp[n][amount] < INT32_MAX/2 ? dp[n][amount] : -1;
    }
};

同样的,因为求dp[i] 只涉及到 dp[i-1],可以压缩成一维dp,j的遍历顺序是 coins[i] -> amount

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        vector<int> dp(amount + 1, INT32_MAX/2);
        dp[0] = 0;
        for (int i = 0; i < n; ++i)
        {
            for (int j = coins[i]; j <= amount; ++j)
            {
                dp[j] = min(dp[j], dp[j-coins[i]] + 1);
            }
        }
        return dp[amount] < INT32_MAX/2 ? dp[amount] : -1;
    }
};

 

3. 377. 组合总和 Ⅳ

377. 组合总和 Ⅳ 给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

//思路:

这一题看上去与完全背包问题很像,都是选出一些数,和为给定值,并且每个数可以无限选。但是不同之处在于,它要求的是排列数,因此,不能用完全背包去做,因为背包问题的结果是顺序无关的。

定义 dp[j] 是从所有数中选出和为 j的数的排列总数。需要考虑 dp[j] 和 dp[x] (x <= j)的关系。

对于 dp[j],这个 j 可以由 ? + nums[i]组成,而在最后的结果中,每一个 nums[i] 放在末尾,它能构成的排列总数就是 dp[j - nums[i]]。因此,转移方程就是

dp[j] = dp[j - nums[0]] + dp[j - nums[1]] + ....dp[j - nums[k]]

k\in \{j >= nums[k]\}

定义dp[0] = 1,语义是,nums[k] == j 时,dp[j - nums[k]] = 1,单独成为一种组合。

如果不好理解的话,可以类比爬楼梯问题:

https://leetcode-cn.com/problems/climbing-stairs/

70. 爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶
示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

对于这一问题,状态方程 dp[j] = dp[j-1] + dp[j-2]是怎么得到的呢?我们要爬 j 级台阶,可以先爬 j-1的台阶,最后爬1个台阶;也可以先爬 j-2 的台阶,最后爬2个台阶。因此,dp[j] = dp[j-1] + dp[j-2],可以看到方法是考虑顺序的。

对于这个问题,相当于我们一次能爬nums[0]...nums[k]级台阶,问要爬target级台阶,有多少种方法,当然是分为最后爬 nums[0]级、最后爬 nums[1]级、......最后爬 nums[k]级这些情况,并把它们相加。

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<int> dp(target + 1, 0);
        dp[0] = 1;
        for (int j = 1; j <= target; ++j)
        {
            for (int i = 0; i < nums.size(); ++i)
            {
                if(j >= nums[i]) dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
};

这个问题更详细的说明参考这篇题解:https://leetcode-cn.com/problems/combination-sum-iv/solution/dong-tai-gui-hua-python-dai-ma-by-liweiwei1419/

4. 1449. 数位成本和为目标值的最大数字

给你一个整数数组 cost 和一个整数 target 。请你返回满足如下规则可以得到的 最大 整数:

给当前结果添加一个数位(i + 1)的成本为 cost[i] (cost 数组下标从 0 开始)。
总成本必须恰好等于 target 。
添加的数位中没有数字 0 。
由于答案可能会很大,请你以字符串形式返回。

如果按照上述要求无法得到任何整数,请你返回 "0" 。

 

示例 1:

输入:cost = [4,3,2,5,6,7,2,5,5], target = 9
输出:"7772"
解释:添加数位 '7' 的成本为 2 ,添加数位 '2' 的成本为 3 。所以 "7772" 的代价为 2*3+ 3*1 = 9 。 "997" 也是满足要求的数字,但 "7772" 是较大的数字。
 数字     成本
  1  ->   4
  2  ->   3
  3  ->   2
  4  ->   5
  5  ->   6
  6  ->   7
  7  ->   2
  8  ->   5
  9  ->   5
示例 2:

输入:cost = [7,6,5,5,5,6,8,7,8], target = 12
输出:"85"
解释:添加数位 '8' 的成本是 7 ,添加数位 '5' 的成本是 5 。"85" 的成本为 7 + 5 = 12 。
示例 3:

输入:cost = [2,4,6,2,4,6,4,4,4], target = 5
输出:"0"
解释:总成本是 target 的条件下,无法生成任何整数。
示例 4:

输入:cost = [6,10,15,40,40,40,40,40,40], target = 47
输出:"32211"
 

提示:

cost.length == 9
1 <= cost[i] <= 5000
1 <= target <= 5000

本质是完全背包问题,即无限制地选一些数,它们的总和满足一定条件。但是与普通完全背包问题有两个不一样的地方:

(1) 要求选出的数的cost总和刚好等于target,而不是 <= target。

(2) 要求恢复选择的结果。

因为要使得组成的数字最大,显然长度要最长,所以dp数组应该记录最大长度的信息。定义 dp[i][j]: 从数字[1...i]里选,能够组成j的最大长度。

状态转移方程:

dp[i][j] = \left\{\begin{matrix} 0, i= 0, j = 0& &\\dp[i-1][j], j < cost[i-1]\\max(dp[i-1][j],dp[i][j-cost[i-1]]+1), j \geq cost[i-1] & \end{matrix}\right.

 对于第一点不同,就要在dp数组初始化的时候小心,初始化dp[0][0] = 0, 其它dp[i][j] 都初始化为接近INT32_MIN的数,这样才能使得在不能刚好组成j的时候,dp[i][j] 依然是个负数,也就是无效结果。

对于第二点不同,在长度最长的时候,应当从最大的数字开始循环找可行的解,也就是从最大的数字 ei = 9 和 最后的总和 ej = target 开始,循环判断 dp[ei][ej] == dp[ei][ej - cost[ei-1]] + 1,如果是,那么说明ei被选择,否则,ei--。

class Solution {
public:
    string largestNumber(vector<int>& cost, int target) {
        int dp[10][5001];
        //dp[i][j]: 从[1...i]里选,能组成代价和为j的最大长度
        memset(dp, 0x80, sizeof(dp));
        for(int i = 0; i <= 9; i++) dp[i][0] = 0;
        for(int i = 1; i <= 9; i++)
        {
            for(int j = 1; j <= target; j++)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= cost[i-1]) dp[i][j] = max(dp[i][j], dp[i][j-cost[i-1]] + 1);
            }
        }
        if(dp[9][target] <= 0) return "0";
        int ei = 9, ej = target;
        string ans;
        while(dp[ei][ej] > 0)
        {
            if(ej >= cost[ei-1] && dp[ei][ej] == dp[ei][ej-cost[ei-1]] + 1)
            {
                ans += ('0' + ei);
                ej -= cost[ei-1];
            }
            else ei--;
        }
        return ans;
    }
};

优化到一维同样可以恢复选择的信息,只需要每一次循环中从最大的数开始寻找可行解。

class Solution {
public:
    string largestNumber(vector<int>& cost, int target) {
        const int MAXT = 5000;
        int dp[MAXT+1];
        memset(dp, 0x80, sizeof(dp));
        dp[0] = 0;
        for(int i = 1; i <= 9; i++)
        {
            for(int j = cost[i-1]; j <= target; j++)
            {
                dp[j] = max(dp[j], dp[j-cost[i-1]] + 1);
            }
        }
        if(dp[target] <= 0) return "0";
        string ans;
        int ej = target;
        while(ej > 0)
        {
            for(int i = 9; i >= 1; i--)
            {
                if(ej >= cost[i-1] && dp[ej] == dp[ej-cost[i-1]] + 1)
                {
                    ans += ('0' + i);
                    ej -= cost[i-1];
                    break;
                }
            }
        }
        return ans;
    }
};

 

01背包问题

1. 494. 目标和 

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释: 

-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

一共有5种方法让最终目标和为3。

//思路

解决本题的重要突破点在于想到将+ - 两部分分开考虑,即选择一部分数取它们的和a,剩下的数取它们的相反数的和b,那么有

a+b = S, a-b = sum(nums)。从而,可以得到 a = (S + sum(nums)) / 2,问题也就转化成01背包问题:选择一些数,它们的和是a,每个数只能选一次,问有多少种取法。

注意这里 nums[i] 是非负整数,所以注意 dp[i][j] 的 j 遍历时从0开始, dp[0][0] 初始化为1,dp[i][0](i >= 1)不能确定,因为可能有一些 nums[i-1]本身就是0。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        long long sum = 0;
        for(int i : nums) sum += i;
        if(sum < S || (sum + S) % 2 == 1) return 0;
        sum = (sum + S) / 2;
        vector<vector<int>> dp(nums.size() + 1, vector<int>(sum + 1, 0));
        dp[0][0] = 1;
        for (int i = 1; i <= nums.size(); ++i)
        {
            for (int j = 0; j <= sum; ++j)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= nums[i-1]) dp[i][j] += dp[i-1][j-nums[i-1]];
            }
        }
        return dp.back()[sum];
    }
};

很容易看到,代码几乎与完全背包的(A)一模一样,分为两种情况,不取nums[i-1],和取nums[i-1]。但是由于是0-1背包,每个数只能取一次,所以选了第i-1个数之后,只能在[0, i-2]中找凑出 j - nums[i-1]的方案数目,所以取法是dp[i-1][j-nums[i]]。转移方程如下:

dp[i][j] = \left\{\begin{matrix} 1, i = 0, j = 0& \\dp[i-1][j], j <nums[i] \\dp[i-1][j] + dp[i-1][j - nums[i]], j \geq nums[i] & \end{matrix}\right.

01背包的空间优化:一维dp注意与完全背包的一维dp的不同之处,j 的遍历方向不同。这是因为01背包的dp[i][j]取决于上一行的dp[i-1][j-nums[i-1]],所以j要从右向左进行更新,更新的部分是dp[i][j],未更新的部分是dp[i-1][j],从右向左避免覆盖。而完全背包的dp[i][j]取决于dp[i][j-nums[i-1]],所以j要从左向右更新。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        long long sum = 0;
        for(int i : nums) sum += i;
        if(sum < S || (sum + S) % 2 == 1) return 0;
        sum = (sum + S) / 2;
        vector<int> dp(sum + 1, 0);
        dp[0] = 1;
        for(int i : nums)
        {
            for (int j = sum; j >= i; --j)
            {
                dp[j] += dp[j - i];
            }
        }
        return dp[sum];
    }
};

2. 879. 盈利计划

帮派里有 G 名成员,他们可能犯下各种各样的罪行。

第 i 种犯罪会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。

让我们把这些犯罪的任何子集称为盈利计划,该计划至少产生 P 的利润。

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

 

示例 1:

输入:G = 5, P = 3, group = [2,2], profit = [2,3]
输出:2
解释: 
至少产生 3 的利润,该帮派可以犯下罪 0 和罪 1 ,或仅犯下罪 1 。
总的来说,有两种方案。
示例 2:

输入:G = 10, P = 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 <= G <= 100
0 <= P <= 100
1 <= group[i] <= 100
0 <= profit[i] <= 100
1 <= group.length = profit.length <= 100

也是01背包计数问题,要注意的是这里要求的是利润大于K的数量,所以在二维背包问题上又增加了一维。但是,因为所有可组合的利润最大能达到100*100,如果定义dp数组是 方案*容量*利润的话,规模太大。因为题目只要求利润大于等于P的,而P的数量级是100,所以应该定义dp数组 方案*容量*P,把所有利润大于等于P的组合都算到P里。 

定义 dp[i][j][k]: 在[0...i-1]中选,背包容量是j,利润是k的组合数,注意所有大于k的组合也划到等于k里。

初始化要注意所有的k=0都有1种方案,就是都不选。

转移方程:

dp[i][j][k] = \left\{\begin{matrix} 1, k = 0& \\dp[i-1][j][k], j <group[i-1] \\dp[i-1][j][k] + dp[i-1][j - group[i-1]][max(0, k-profit[i-1])], j \geq group[i-1] & \end{matrix}\right.

class Solution {
public:
    int profitableSchemes(int G, int P, vector<int>& group, vector<int>& profit) {
        int base = 1e9 + 7;
        int n = group.size();
        long long dp[101][101][101] = {0};
        //dp[i][j][k]: 从 [0...i-1]里选,有j个人,利润是k的方案数
        for(int i = 0; i <= n; i++)
        {
            for(int j = 0; j <= G; j++)
            {
                dp[i][j][0] = 1;
            }
        }
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= G; j++)
            {
                for(int k = 0; k <= P; k++)
                {
                    dp[i][j][k] = dp[i-1][j][k];
                    if(j >= group[i-1])
                    {
                        dp[i][j][k] += dp[i-1][j-group[i]][max(0, k-profit[i-1])];
                        dp[i][j][k] %= base;
                    }
                }
            }
        }
        return dp[n][G][P];
    }
};

3. 416. 分割等和子集

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

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].
 

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

两种做法。

(1) 定义 dp[i][j]: 使用 nums[0...i-1], 能否组成j 。那么转移方程是:

 dp[i][j] = \left\{\begin{matrix} true, i = 0, j = 0& \\dp[i-1][j], j <nums[i] \\dp[i-1][j] \: || \: dp[i-1][j - nums[i]], j \geq nums[i] & \end{matrix}\right.

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum & 1) return false;
        sum /= 2;
        int n = nums.size();
        vector<vector<bool>> dp(n+1, vector<bool>(sum+1, false));
        //dp[i][j]: 使用nums[0...i-1]中的数,能否组成j
        dp[0][0] = true;
        for (int i = 1; i <= n; ++i)
        {
            for (int j = 0; j <= sum; ++j)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= nums[i-1]) dp[i][j] = dp[i][j] || dp[i-1][j - nums[i-1]];
            }
        }
        return dp[n][sum];
    }
};

优化成一维:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum & 1) return false;
        sum /= 2;
        vector<bool> dp(sum+1, false);
        dp[0] = true;
        for(int i : nums)
        {
            for(int j = sum; j >= i; j--)
            {
                dp[j] = dp[j] || dp[j - i];
            }
        }
        return dp[sum];
    }
};

(2) 定义dp[i][j]:使用 nums[0...i-1],能够组成的最大不超过 j 的数,最后只需要判断 dp[n][sum/2] == sum/2 即可。

转移方程是:

dp[i][j] = \left\{\begin{matrix} 0, i = 0, j = 0& \\dp[i-1][j], j <nums[i] \\max(dp[i][j], dp[i-1][j - nums[i]] + nums[i]), j \geq nums[i] & \end{matrix}\right.

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum & 1) return false;
        sum /= 2;
        int n = nums.size();
        vector<vector<int>> dp(n+1, vector<int>(sum+1, 0));
        //dp[i][j]: 使用nums[0...i-1]中的数,能组成最大不超过 sum 的数
        for (int i = 1; i <= n; ++i)
        {
            for (int j = 1; j <= sum; ++j)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= nums[i-1]) dp[i][j] = max(dp[i][j], dp[i-1][j-nums[i-1]] + nums[i-1]);
            }
        }
        return dp[n][sum] == sum;
    }
};

优化成一维:

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(sum & 1) return false;
        sum /= 2;
        vector<int> dp(sum+1, 0);
        for(int i : nums)
        {
            for(int j = sum; j >= i; j--)
            {
                dp[j] = max(dp[j], dp[j-i] + i);
            }
        }
        return dp[sum] == sum;
    }
};

4.1049. 最后一块石头的重量 II

有一堆石头,每块石头的重量都是正整数。

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

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

 

示例:

输入:[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],这就是最优值。
 

提示:

1 <= stones.length <= 30
1 <= stones[i] <= 1000

本质是说,分成两组,并且使两组的差尽量小。那么两组的和都应该越接近sum/2越好。也就转化成,选一些数,它们的和不超过sum/2,能得到的最大和,是01背包问题。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int n = stones.size(), sum = accumulate(stones.begin(), stones.end(), 0);
        vector<int> dp(sum/2+1, 0);
        for (int i = 0; i < n; ++i)
        {
            for (int j = sum/2; j >= stones[i]; --j)
            {
                dp[j] = max(dp[j], dp[j-stones[i]] + stones[i]);
            }
        }
        return abs(dp[sum/2] * 2 - sum);
    }
};

 

5. 474. 一和零

在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

注意:

给定 0 和 1 的数量都不会超过 100。
给定字符串数组的长度不会超过 600。
示例 1:

输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4

解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
示例 2:

输入: Array = {"10", "0", "1"}, m = 1, n = 1
输出: 2

解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。

相当于两个维度上的01背包问题,所以不进行空间优化的话dp数组应该是三维的。

(1) 定义 dp[i][j][k] : 考察strs[0...i-1],使用 j 个0 和 k 个 1 能组成的最大字符串数。转移方程:

 dp[i][j][k] = \left\{\begin{matrix} i = 0, j = 0, k = 0& \\dp[i-1][j][k], j <zerocnt[i] \: ||\: k<onecnt[i]\\max(dp[i][j][k], dp[i-1][j - zerocnt[i]][k-onecnt[i]] + 1), j \geq zerocnt[i] \: \&\&\: k \geq onecnt[i]& \end{matrix}\right.

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int size = strs.size();
        int dp[size+1][m+1][n+1];
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= size; ++i)
        {
            int zerocnt = 0, onecnt = 0;
            for(char c : strs[i-1])
            {
                if(c == '0') zerocnt++;
                else onecnt++;
            }
            for (int j = 0; j <= m; ++j)
            {
                for (int k = 0; k <= n; ++k)
                {
                    dp[i][j][k] = dp[i-1][j][k];
                    if(j >= zerocnt && k >= onecnt) dp[i][j][k] = max(dp[i][j][k], dp[i-1][j-zerocnt][k-onecnt] + 1);
                }
            }
        }
        return dp[size][m][n];
    }
};

(2)同样的,dp[i][j][k]只和i-1有关,优化一维:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int dp[101][101];
        memset(dp, 0, sizeof(dp));
        for(const auto& i : strs)
        {
            int onecnt = 0, zerocnt = 0;
            for(char c : i) 
            {
                if(c == '0') zerocnt++;
                else onecnt++;
            }
            for (int j = m; j >= zerocnt; --j)
            {
                for (int k = n; k >= onecnt; --k)
                {
                    dp[j][k] = max(dp[j][k], dp[j-zerocnt][k-onecnt] + 1);
                }
            }
        }
        return dp[m][n];
    }
};

二维费用背包问题还有:https://www.luogu.com.cn/problem/P1855#submit P1855 榨取kkksc03 

多重背包

1. 宝物筛选

小 F 有一个最大载重为 W的采集车,洞穴里总共有 n 种宝物,每种宝物的价值为 vi​,重量为wi​,每种宝物有 mi​ 件。小 F 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。

输入格式

第一行为一个整数 n 和 W,分别表示宝物种数和采集车的最大载重。

接下来 n 行每行三个整数 vi​,wi​,mi​。

输出格式

输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。

数据规模:n <= 100, W <= 1e4, \sum m_i \leq 1e5

背包容量W,每个物品价值v, 重量w,  个数k,需要找到使得背包价值最大的物品组合。

最简单的做法是,把k个物品展开,本质上还是01背包问题。当然不需要真的展开,k个物品可以选1个,选2个...选k个,直接在循环里处理。

转移方程:dp[i][j]表示从[0...i-1]物品里选,重量不超过j的最大价值

dp[i][j] = \max_{k=0}^{k_i}(dp[i-1][j], k*v[i] + dp[i-1][j-k*w[i]]), j \geq k*w[i]

#include <bits/stdc++.h>
using namespace std;

const int MAXW = 4e4 + 10;
const int MAXN = 110;

struct Node
{
    int v;
    int w;
    int k;
};

int main()
{
    int n, w;
    scanf("%d%d", &n, &w);
    Node all[MAXN];
    for(int i = 0; i < n; i++)
    {
        scanf("%d%d%d", &all[i].v, &all[i].w, &all[i].k);
    }
    int dp[MAXN][MAXW] = {0};
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= w; j++)
        {
            dp[i][j] = dp[i-1][j];
            for(int k = 1; k <= all[i-1].k; k++)
            {
                int totalw = k * all[i-1].w;
                if(j < totalw) break;
                dp[i][j] = max(dp[i][j], dp[i-1][j-totalw] + k * all[i-1].v);
            }
        }
    }
    printf("%d\n", dp[n][w]);
    return 0;
}

优化到一维:

dp[j] = \max_{k=0}^{k_i}(dp[j], k*v[i] + dp[j-k*w[i]]), j \geq k*w[i]

#include <bits/stdc++.h>
using namespace std;

const int MAXW = 4e4 + 10;
const int MAXN = 110;

struct Node
{
    int v;
    int w;
    int k;
};

int main()
{
    int n, w;
    scanf("%d%d", &n, &w);
    Node all[MAXN];
    for(int i = 0; i < n; i++)
    {
        scanf("%d%d%d", &all[i].v, &all[i].w, &all[i].k);
    }
    int dp[MAXW] = {0};
    for(int i = 0; i < n; i++)
    {
        for(int j = w; j >= all[i].w; j--)
        {
            for(int k = 1; k <= all[i].k; k++)
            {
                int totalw = k * all[i].w;
                if(j < totalw) break;
                dp[j] = max(dp[j], dp[j-k*all[i].w] + k*all[i].v);
            }
        }
    }
    printf("%d\n", dp[w]);
    return 0;
}

转化为01背包的时间复杂度是O(W*\sum k_i),因为本质上还是对所有物品考虑选或者不选。

\sum k_i比较大的时候,会超时。因为做了大量重复计算,例如,考虑了选择第1个物品的第1、2件,和考虑选择第1个物品的第2、3件,这两种选法是等效的,但是因为转换成01背包的时候把它们看成了不同的物品,也就是第i个物品贡献了ki个分组,每个分组都是1,导致了重复计算。

所以,需要减少可供选择的物品种类,才能降低复杂度。

假设第i件物品有ki个,那么它贡献的分组应该能组成<=ki的任意数,但是分组数要尽量少。这就是二进制优化的原理。例如ki = 6,它应该只贡献 1 2 3 三个分组,同时这三个分组一定可以组合出任意 <= 6 的可能。更一般的,第i个物品应该只贡献2^j, 0 <= j <= log_2(k_i+1)-1这些分组,如果ki+1不是2的幂,那么最后还剩下不能表示成2的幂的分组,要单独作为一组,保证由这些分组一定能组成 <= ki的任意数。

那么二进制分组的代码如下,在设定分组的数组大小时要注意分成的组数可能会比n大,但是一定不会超过\sum k_i

    //all:存放二进制分组的结果,最多是每个物品一组,一共有SUMK个物品,会分成SUMK组
    Node all[MAXSUMK];
    //count:最后一共分了多少组
    int count = 0;
    for(int i = 0; i < n; i++)
    {
        int vi, wi, ki;
        scanf("%d%d%d", &vi, &wi, &ki);
        //二进制分组
        int c = 1;
        while(ki >= c)
        {
            all[count++] = {vi*c, wi*c};
            ki -= c;
            c *= 2;
        }
        if(ki > 0) all[count++] = {vi*ki, wi*ki};
    }

得到了所有的二进制分组之后,就可以像01背包一样对每个分组选或不选了。

#include <bits/stdc++.h>
using namespace std;

const int MAXW = 4e4 + 10;
const int MAXSUMK = 1e5 + 10;

struct Node
{
    int v;
    int w;
};

int main()
{
    int n, w;
    scanf("%d%d", &n, &w);
    //all:存放二进制分组的结果,最多是每个物品一组,一共有MAXSUMK个物品,会分成MAXSUMK组
    Node all[MAXSUMK];
    //count:最后一共分了多少组
    int count = 0;
    for(int i = 0; i < n; i++)
    {
        int vi, wi, ki;
        scanf("%d%d%d", &vi, &wi, &ki);
        //二进制分组
        int c = 1;
        while(ki >= c)
        {
            all[count++] = {vi*c, wi*c};
            ki -= c;
            c *= 2;
        }
        if(ki > 0) all[count++] = {vi*ki, wi*ki};
    }
    int dp[MAXW] = {0};
    for(int i = 0; i < count; i++)
    {
        for(int j = w; j >= all[i].w; j--)
        {
            dp[j] = max(dp[j], dp[j-all[i].w] + all[i].v);
        }
    }
    printf("%d\n", dp[w]);
    return 0;
}

复杂度降低到了O(W*log_2\sum k_i)

2. POJ 1014 Dividing

POJ 1014也是一道多重背包问题:大意如下:

有6种物品,第i种物品(i >= 1)的重量是i,有ki个,问能否把这些物品划分为重量相等的两堆。

和01背包中划分等和子集问题很像,不过这里需要用二进制分组来转化成01背包问题。

#include <string>
#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;

const int MAXSUMK = 20000;

int main()
{
    string str;
    int index = 1;
    while(getline(cin, str))
    {
        int nums[7];
        sscanf(str.c_str(), "%d%d%d%d%d%d", &nums[1], &nums[2], &nums[3], &nums[4], &nums[5], &nums[6]);
        int sum = nums[1] + nums[2] * 2 + nums[3] * 3 + nums[4] * 4 + nums[5] * 5 + nums[6] * 6;
        if(sum == 0) break;
        printf("Collection #%d:\n", index++);
        if(sum & 1)
        {
            printf("Can't be divided.\n\n");
            continue;
        }
        sum /= 2;
        vector<bool> dp(sum+1, false);
        dp[0] = true;
        //二进制分组
        int all[MAXSUMK];
        int count = 0;
        for(int i = 1; i <= 6; i++)
        {
            int c = 1;
            while(nums[i] >= c)
            {
                nums[i] -= c;
                all[count++] = c * i;
                c *= 2;
            }
            if(nums[i] > 0) all[count++] = nums[i] * i;
        }
        for(int i = 0; i < count; i++)
        {
            for(int j = sum; j >= all[i]; j--)
            {
                dp[j] = dp[j] || dp[j-all[i]];
            }
        }
        if(dp[sum]) printf("Can be divided.\n\n");
        else printf("Can't be divided.\n\n");
    }
    return 0;
}

 3. 1363. 形成三的最大倍数

给你一个整数数组 digits,你可以通过按任意顺序连接其中某些数字来形成 3 的倍数,请你返回所能得到的最大的 3 的倍数。

由于答案可能不在整数数据类型范围内,请以字符串形式返回答案。

如果无法得到答案,请返回一个空字符串。

 

示例 1:

输入:digits = [8,1,9]
输出:"981"
示例 2:

输入:digits = [8,6,7,1,0]
输出:"8760"
示例 3:

输入:digits = [1]
输出:""
示例 4:

输入:digits = [0,0,0,0,0,0]
输出:"0"
 

提示:

1 <= digits.length <= 10^4
0 <= digits[i] <= 9
返回的结果不应包含不必要的前导零。

可以用多重背包的思路去做,也就是找一些分组,它们的和能被3整除(且大于0),并且长度最大。最后从dp数组里恢复选择的分组信息。时间复杂度比较高,因为最大和(背包容量)可以达到90000。看评论可以用贪心去做,待补。

class Solution {
public:
    string largestMultipleOfThree(vector<int>& digits) {
        int h[10] = {0};
        int maxsum = 0;
        for(int i : digits)
        {
            h[i]++;
            maxsum += i;
        }
        //数位最多的,并且和是3的倍数的分组
        vector<pair<int,int>> all;
        //{和,个数}
        for(int i = 0; i <= 9; i++)
        {
            int k = h[i], c = 1;
            while(k >= c)
            {
                k -= c;
                all.push_back({c*i, c});
                c *= 2;
            }
            if(k > 0) all.push_back({k*i, k});
        }
        int n = all.size();
        vector<vector<int>> dp(n+1, vector<int>(maxsum + 1, INT32_MIN / 2));
        dp[0][0] = 0;
        //dp[i][j]: 从all[0...i-1]里选,能组成j的分组的最大长度
        for(int i = 1; i <= n; i++)
        {
            for(int j = 0; j <= maxsum; j++)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= all[i-1].first) dp[i][j] = max(dp[i][j], dp[i-1][j-all[i-1].first] + all[i-1].second);
            }
        }
        int ei = n, ej = -1, maxlen = 0;
        for(int j = 1; j <= maxsum; j++)
        {
            if(j % 3 == 0)
            {
                if(dp[n][j] >= maxlen)
                {
                    maxlen = dp[n][j];
                    ej = j;
                }
            }
        }
        if(ej < 0)
        {
            if(dp[n][0] > 0) return "0";
            else return "";
        }
        vector<pair<int,int>> ans;
        //{数,个数}
        while(ei > 0 && ej >= 0)
        {
            if(dp[ei][ej] == dp[ei-1][ej]) ei--;
            else
            {
                ans.push_back({all[ei-1].first / all[ei-1].second, all[ei-1].second});
                ej -= all[ei-1].first;
                ei--;
            }
        }
        sort(ans.rbegin(), ans.rend());
        string res;
        for(const auto& p : ans) res += string(p.second, static_cast<char>(p.first + '0'));
        return res;
    }
};

混合背包

1.P1833 樱花

题目大意:

背包容量W,有n种物品,有一些物品有无限个,有一些物品只有一个,还有一些物品有大于一个的有限个,求背包容纳的最大价值。

混合背包问题只需要分情况,无限个的物品,套用完全背包模板,有限个的物品,如果大于1个就用二进制优化,转化成01背包。都可以用一维dp去解决。

#include <bits/stdc++.h>
using namespace std;

const int MAXW = 1001;
const int MAXN = 10001;
const int MAXSUMK = 1e6 + 5;

struct Node
{
    int w;
    int v;
    int k;
};

int main()
{
    int h1, m1, h2, m2, n;
    scanf("%d:%d %d:%d %d\n", &h1, &m1, &h2, &m2, &n);
    int w = h2 * 60 + m2 - h1 * 60 - m1;
    Node all[MAXSUMK];
    int count = 0;
    for(int i = 0; i < n; i++)
    {
        int wi, vi, ki;
        scanf("%d%d%d", &wi, &vi, &ki);
        //01背包或者完全背包
        if(ki == 0 || ki == 1) all[count++] = {wi, vi, ki};
        else
        {
            //多重背包,二进制优化
            int c = 1;
            while(ki >= c)
            {
                ki -= c;
                all[count++] = {wi*c, vi*c, 1};
                c *= 2;
            }
            if(ki > 0) all[count++] = {wi*ki, vi*ki, 1};
        }
    }
    int dp[MAXW] = {0};
    for(int i = 0; i < count; i++)
    {
        if(all[i].k == 0)
        {
            //完全背包模板
            for(int j = all[i].w; j <= w; j++) 
            {
                dp[j] = max(dp[j], dp[j-all[i].w] + all[i].v);
            }
        }
        else
        {
            //01背包模板
            for(int j = w; j >= all[i].w; j--)
            {
               dp[j] = max(dp[j], dp[j-all[i].w] + all[i].v);
            }
        }
    }
    printf("%d\n", dp[w]);
    return 0;
}

分组背包

1. P1757 通天之分组背包

题目大意:

背包容量W,有n件物品,每件物品重量wi, 价值vi,属于第 gi 组,同一组的物品只能取一件。求背包能容纳的最大价值。

分组背包问题,要求每一组的物品只能选1件,就要把每一组看成01背包中的一件物品,然后再在组内选择一件使得总价值最大。首先要想到合适的数据结构来存储物品,因为有分组的存在,所以属于第 gi 组的物品应该存放在 all[gi][cnti] 里,之后,把每一组看成一件物品,再在组内选择一件。核心代码:

    for(int i = 1; i <= maxg; i++) // 循环每一组
    {
        for(int j = w; j >= 0; j--) // 循环背包容量
        {
            for(int k = 0; k < count[i]; k++) // 循环该组的每一个物品
            {
                if(j >= all[i][k].w)
                {
                    dp[j] = max(dp[j], dp[j-all[i][k].w] + all[i][k].v);
                }
            }
        }
    }

要注意循环顺序,因为这里是每一组看成一件物品,所以和01背包一样,外层循环是每一组,内层循环是从总容量w逆序遍历,但是因为这里每一组里的重量并不相同,所以遍历到0,再对同一组里的每一个元素进行 dp[j] = max(dp[j], dp[ j - w[i][k] ] + v[i][k])的计算。

#include <bits/stdc++.h>
using namespace std;

const int MAXG = 1010;
const int MAXN = 1010;
const int MAXW = 1010;

struct Node
{
    int w;
    int v;
};

int main()
{
    int w, n;
    scanf("%d%d", &w, &n);
    Node all[MAXG][MAXN] = {0};
    int count[MAXG] = {0};
    int maxg = 0;
    for(int i = 0; i < n; i++)
    {
        int wi, vi, gi;
        scanf("%d%d%d", &wi, &vi, &gi);
        all[gi][count[gi]++] = {wi, vi};
        maxg = max(maxg, gi);
    }
    int dp[MAXW] = {0};
    for(int i = 1; i <= maxg; i++)
    {
        for(int j = w; j >= 0; j--)
        {
            for(int k = 0; k < count[i]; k++)
            {
                if(j >= all[i][k].w)
                {
                    dp[j] = max(dp[j], dp[j-all[i][k].w] + all[i][k].v);
                }
            }
        }
    }
    printf("%d\n", dp[w]);
    return 0;
}

有依赖的背包

1.P1064 金明的预算方案

题目大意:

背包容量W,有m件物品,重量wi, 价值vi, 分为主件和附件,一个主件最多有2个附件。如果选择了附件那么必须也要选择主件。求背包能容纳的最大价值。

因为选附件时必须要选主件,假设主件为A,附件为B C,那么相当于 A AB AC ABC 这四种里只能选择一种,可以转换为分组背包问题。

题目的数据处理比较繁琐,首先要找到属于同一组的,再对每一组的物品进行组合得到所有可供选择的物品。同时注意题目里的价值是 pi * wi。

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 3201;
const int MAXG = 65;

struct Node
{
    int w;
    int v;
    int q;
};

int main()
{
    int w, m;
    scanf("%d%d", &w, &m);
    //价格都是10的倍数,可以减小时间复杂度。
    w /= 10;
    Node groups[MAXG][3];
    int indexs[MAXG] = {0};
    //先找到每一组里包含哪些元素
    for(int i = 1; i <= m; i++)
    {
        int wi, vi, qi;
        scanf("%d%d%d", &wi, &vi, &qi);
        wi /= 10;
        if(qi == 0) groups[i][indexs[i]++] = {wi, vi * wi, 0};
        else groups[qi][indexs[qi]++] = {wi, vi * wi, qi};
    }
    //一个商品最多有2个附件,也就是一组内最多有A AB AC ABC 四种选择
    Node all[MAXG][4];
    int count[MAXG] = {0};
    for(int i = 1; i <= m; i++)
    {
        if(indexs[i] > 0)
        {
            int master = -1, slave1 = -1, slave2 = -1;
            for (int j = 0; j < indexs[i]; ++j)
            {
                if(groups[i][j].q == 0) master = j;
                else if(slave1 == -1) slave1 = j;
                else slave2 = j;
            }
            all[i][count[i]++] = {groups[i][master].w, groups[i][master].v , 0};
            if(slave1 > -1)
            {
                int totalw = groups[i][master].w + groups[i][slave1].w;
                int totalv = groups[i][master].v + groups[i][slave1].v ;
                all[i][count[i]++] = {totalw, totalv, 0};
            }
            if(slave2 > -1)
            {
                int totalw = groups[i][master].w + groups[i][slave2].w;
                int totalv = groups[i][master].v + groups[i][master].w + groups[i][slave2].v + groups[i][slave2].w;
                all[i][count[i]++] = {totalw, totalv, 0};
                totalw += groups[i][slave1].w;
                totalv += groups[i][slave1].v * groups[i][slave1].w;
                all[i][count[i]++] = {totalw, totalv, 0};
            }
        }
    }
    //分组背包模板
    int dp[MAXN] = {0};
    for(int i = 1; i <= m; i++)
    {
        for(int j = w; j >= 0; j--)
        {
            for(int k = 0; k < count[i]; k++)
            {
                if(j >= all[i][k].w)
                {
                    dp[j] = max(dp[j], dp[j-all[i][k].w] + all[i][k].v);
                }
            }
        }
    }
    printf("%d\n", dp[w] * 10);
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值