目录
0-1背包类
经典0-1背包
思路1:二维数组动态规划
- dp数组的定义:
dp[i][j]
表示背包的容量为j
,对于前i
个物品来说的最佳策略,也就是最大价值。 - dp数组的推导:对于物品
i
来说,只有两种情况也就是装入背包和不装入背包:- 不装入背包:那么当前的最佳策略就是前一个物品的最佳策略,也就是
dp[i-1][j]
,这里仍然是j
,因为我们并没有消耗背包容量。 - 装入背包:那么我们需要给背包创造出来能够装入物品
i
的容量,也就是j-weights[i]
,我们定位到前一个物品对于背包容量j-weight[i]
的最佳策略的位置,我们从这里出发计算装入物品i
的最佳策略,也就是dp[i-1][j-weights[i]]
。 - 综合上述情况:我们通过
max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i])
就能得到前i
个物品的最佳策略。
- 不装入背包:那么当前的最佳策略就是前一个物品的最佳策略,也就是
思路1:cpp代码
// 二维dp数组实现
#include <iostream>
#include <vector>
using namespace std;
int main(){
int m, n; // 种类,行李空间
cin >> m >> n;
vector<int> weights(m);
vector<int> values(m);
for(int i = 0; i < m; i++){
cin >> weights[i];
}
for(int i = 0; i < m; i++){
cin >> values[i];
}
// dp数组初始化
vector<vector<int>> dp(m, vector<int>(n+1, 0));
for(int j = weights[0]; j <= n; j++){
dp[0][j] = values[0];
}
for(int i = 1; i < m; i++){
for(int j = 0; j <= n; j++){
if(j < weights[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i]);
}
}
cout << dp[m-1][n] << endl;
}
思路2:一维数组动态规划
在上面的二维数组中,我们当前层的值是由上一层决定的,那么一维数组这里就可以简单的看作是在每一层的基础上做覆盖来进行下一层的计算。
- dp数组的定义:
dp[j]
表示容量为j
的背包能装的最大价值。 - dp数组的推导:因为我们这里是直接覆盖计算,所以就不用考虑上面的
i
,即是dp[j]=max(dp[j], dp[j-weight[i]] + values[i])
。其中,dp[j]
是不装物品 i 的情况,dp[j-weight[i]] + values[i]
是装物品 i 的情况。 - dp数组初始化:当
j
为0的时候即背包容量为0,dp[j]
当然等于0。当j>0
时,因为我们在递推时,要和自己比较大小,所以这里我们应该初始化成一个最小值,即也是0。 - 遍历顺序:先遍历物品,再倒序遍历背包。如果先遍历背包再遍历物品,那么背包中永远装的只有一个物品。如果正序遍历背包,就会有重复放入。
思路2:cpp代码
// 一维dp数组实现
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 读取 M 和 N
int M, N;
cin >> M >> N;
vector<int> costs(M);
vector<int> values(M);
for (int i = 0; i < M; i++) {
cin >> costs[i];
}
for (int j = 0; j < M; j++) {
cin >> values[j];
}
// 创建一个动态规划数组dp,初始值为0
vector<int> dp(N + 1, 0);
// 外层循环遍历每个类型的研究材料
for (int i = 0; i < M; ++i) {
// 内层循环从 N 空间逐渐减少到当前研究材料所占空间
for (int j = N; j >= costs[i]; --j) {
// 考虑当前研究材料选择和不选择的情况,选择最大值
dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);
}
}
// 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值
cout << dp[N] << endl;
return 0;
}
分割等和子集(leetcode416)
题目🔗
给你一个 只包含正整数 的 非空 数组 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
思路1:暴力回溯(超时)
乍一看题目就会想到回溯,但是回溯终究会超时,它的时间复杂度是指数级别的。这里还是贴一下回溯的代码。
思路1:cpp代码(超时)
class Solution {
public:
int flag = false;
void backtrace(const vector<int>& nums, int idx, int subSum, int sum){
if(subSum == sum - subSum){
// 每次递归都要先判断subSum == sum - subSum
flag = true;
}
// 递归终止条件(可以不用写,因为for循环也会判断)
if(idx >= nums.size()){
return;
}
for(int i = idx; i < nums.size(); ++i){
subSum += nums[i];
backtrace(nums, i + 1, subSum, sum);
subSum -= nums[i]; // 回溯
}
}
bool canPartition(vector<int>& nums) {
// 简单思路:首先计算数组的和,然后回溯选取比较
int sum = 0;
for(int i = 0; i < nums.size(); ++i){
sum += nums[i];
}
backtrace(nums, 0, 0, sum);
return flag;
}
};
思路2:动态规划-01背包
该题实际上可以抽象成一个容量为sum/2
的背包(假设nums
数组的和为sum
),我们需要判断用nums
中的“物品”是否能够装满这个背包。
那么这题的“物品”的重量和价值都相等,即物品 i 的重量和价值都是nums[i]
。
下面采用一维dp数组实现动态规划:
- dp数组的含义:
dp[j]
代表容量为j
的背包能装的最大值。 - dp的递推公式:
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
。其中,dp[j]
是不装物品 i 的情况,dp[j - nums[i]] + nums[i]
是装入物品 i 的情况。 - dp数组初始化:全部初始化为0。
思路2:cpp代码
class Solution {
public:
bool canPartition(vector<int>& nums) {
// 首先计算nums的和
int sum = 0;
for(int i = 0; i < nums.size(); ++i){
sum += nums[i];
}
// sums/2即是题目要求的背包容量
if(sum % 2 != 0) return false;
sum /= 2;
vector<int> dp(sum + 1, 0);
for(int i = 0; i < nums.size(); ++i){
// 先遍历物品,再遍历背包容量
for(int j = sum; j >= nums[i]; --j){
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[sum] == sum;
}
};
最后一块石头的重量 II(leetcode1049)
有一堆石头,用整数数组 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
思路:动态规划-01背包
这题的突破口在于:将石头分为重量尽可能接近的两堆,这样他们互相碰撞到最后的重量就是最小的可能重量。
那么这题就跟上题分割等和子集很像了,他也是要将总体重量和sum/2
,然后将nums
中的物品放入容量为j=sum/2
的背包中。唯一的区别是,我们装不满也无所谓,只需要计算出最终碰撞后的重量即可。
下面采用一维dp数组实现动态规划:
- dp数组的含义:
dp[j]
代表容量为j
的背包能装的最大值。 - dp的递推公式:
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
。其中,dp[j]
是不装物品 i 的情况,dp[j -stones[i]] + stones[i]
是装入物品 i 的情况。 - dp数组初始化:全部初始化为0。
cpp代码
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int i = 0; i < stones.size(); ++i){
sum += stones[i];
}
// 背包容量大小
int target = sum / 2;
vector<int> dp(target + 1, 0);
for(int i = 0; i < stones.size(); ++i){
for(int j = dp.size() - 1; j >= stones[i]; --j){
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
int ans = sum - dp[target] - dp[target];
return ans;
}
};
0-1背包+互斥条件约束
商店里有 n n n个商品,分别编号为 [ 1 , n ] [1,n] [1,n],每个商品都有一个价值 v a l i val_i vali和体积 w i w_i wi,米小游有一个有一个 m m m容量的背包,他能够装得下任意多个体积之和不超过 m m m的商品。
米小游认为有些东西一起购买会带来灾难,比如可莉的角色立牌和蹦蹦炸弹的小手办,所以他设定了 k k k组互斥关系,每组关系给定两个数字 a , b a,b a,b,表示编号为 a a a的商品和编号为 b b b的商品不能同时购买。
米小游希望装下的物品的价值之和最大,请你帮帮他求出最大价值。
输入描述
第一行输入三个整数
n
,
m
,
k
(
1
≤
n
≤
15
;
1
≤
m
≤
1
0
9
;
0
≤
k
≤
15
)
n,m,k(1\le n\le 15;1\le m\le 10^9;0\le k\le 15)
n,m,k(1≤n≤15;1≤m≤109;0≤k≤15)表示商品数量、背包容量和互斥关系数量。
接下来 n n n行,每行输入两个整数 w i , v a l i ( 1 ≤ w i , v a l i ≤ 1 0 9 ) w_i,val_i(1\le w_i,val_i\le 10^9) wi,vali(1≤wi,vali≤109)表示每个物品的体积和价值。
接下来 k k k行,每行输入两个整数 a , b ( 1 ≤ a , b ≤ n , a ≠ b ) a,b(1\le a,b\le n,a≠b) a,b(1≤a,b≤n,a=b),描述一组互斥关系。
输出描述
在一行上输出一个整数表示答案。
示例 1
输入
3 100 2
15 19
20 30
15 19
1 2
2 3
输出
38
说明
根据两组互斥关系,买了 2 就不能买 1 和 3,所以我们可以购买物品 1 和物品 3,这样达到最大价值 。
示例 2
输入
3 20 2
15 19
20 30
15 19
1 2
2 3
输出
30
说明
背包容量只有20,只能装下一个物品,选择第二个物品,价值30。
思路1:暴力枚举+回溯
思路1:cpp代码
#include <iostream>
#include <vector>
#define MAX_NK 15
using namespace std;
int n, m, k; // 商品数量、背包容量、互斥关系数量
vector<int> weights(MAX_NK), values(MAX_NK);
vector<vector<bool>> conf(MAX_NK, vector<bool>(MAX_NK, false)); // 互斥关系数组
int ans = 0;
bool check(int cur_item, vector<int>& taken){
for(int item : taken){
// item和cur_item互斥,返回false
if(conf[item][cur_item]) return false;
// item和cur_item不互斥,返回true
}
return true;
}
void backtracking(int cur_item, int cur_value, int cur_weight, vector<int>& taken){
// 终止条件是商品已枚举完
if(cur_item >= n){
// 更新ans
ans = max(ans, cur_value);
return;
}
// 不拿当前物品,向后枚举
backtracking(cur_item + 1, cur_value, cur_weight, taken);
// 拿当前物品,并向后枚举
// 首先判断:cur_item是否和taken中的物品相斥&&当前背包容量是否能装下cur_item
if(check(cur_item, taken) && cur_weight + weights[cur_item] <= m){
taken.push_back(cur_item);
backtracking(cur_item + 1, cur_value + values[cur_item], cur_weight + weights[cur_item], taken);
taken.pop_back(); // 回溯
}
}
int main(){
cin >> n >> m >> k;
for(int i = 0; i < n; ++i){
cin >> weights[i] >> values[i];
}
int x, y;
for(int i = 0; i < k; ++i){
cin >> x >> y;
conf[x-1][y-1] = conf[y-1][x-1] = true;
}
vector<int> taken; // 记录拿了的物品的index
backtracking(0, 0, 0, taken);
cout << ans << endl;
}