1 01背包
🚀 题目链接:https://www.acwing.com/problem/content/2/
🚀 代码
- 二维dp
// n为物品数量,maxV为背包容量,v[i]、w[i]分别为第i件物品的容量和价值 public int zeroOneKnapsack(int n, int maxV, int[] v, int[] w) { // 动态函数,表示将第i个物品放入容量为j的背包中的最大价值 int[][] dp = new int[n + 1][maxV + 1]; for (int i = 1;i <= n;i++) { for (int j = 1;j <= maxV;j++) { if (v[i - 1] <= j) { // 能放入,则获取放入和不放入的较大价值 dp[i][j] = Math.max( // 放入,这里是dp[i-1] // dp[i-1]表示在上一件物品放或没放的基础上放入当前物品,因为一件物品只能放一次 dp[i - 1][j - v[i - 1]] + w[i - 1], // 不放 dp[i - 1][j]); } else { // 放不下,最大价值就是放前一个物品时的价值 dp[i][j] = dp[i - 1][j]; } } } return dp[n][maxV]; }
- 一维dp
public int zeroOneKnapsack(int n, int maxV, int[] v, int[] w) { // 一维动态函数,表示将容量为j的背包中的最大价值 int[] dp = new int[maxV + 1]; for (int i = 1;i <= n;i++) { // 从后往前遍历,如果从前往后遍历会取到重复的物件 for (int j = maxV;j <= maxV && j >= v[i - 1];j--) { dp[j] = Math.max(dp[j], dp[j - v[i - 1]] + w[i - 1]); } } return dp[maxV]; }
1.1 分割等和子集
🚀 题目链接:https://leetcode.cn/problems/partition-equal-subset-sum/
🚀 代码
class Solution {
public boolean canPartition(int[] nums) {
// 统计数组和和,并求出最大值
int sum = 0, maxVal = 0;
for (int num : nums) {
sum += num;
maxVal = Math.max(maxVal, num);
}
// 和是奇数,肯定不能分割
if (sum % 2 != 0) {
return false;
}
int target = sum >> 1, n = nums.length;
// 最大值比和的一半还大,肯定不能分割
if (maxVal > target) {
return false;
}
// 01背包的做法
// dp(x,y),x表示数组元素的索引,y表示目标和
// dp(x,y)表示在[0,x]之间是否能选择若干个数组元素的和为y
boolean[][] dp = new boolean[nums.length][target + 1];
// 初始化dp
for (int i = 0;i < n;i++) {
// 目标和为0时(不可能达到0)
// 设置为true是要保证j刚好等于num时为true
dp[i][0] = true;
}
// 只有一个元素(第一个元素)时,如果目标和等于刚好等于它,肯定能分割
dp[0][nums[0]] = true;
for (int i = 1;i < n;i++) {
int num = nums[i];
for (int j = 1;j <= target;j++) {
if (j >= num) {
// 可以选择当前num,则不选与选的情况进行与运算,有一个true即可
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
} else {
// 不能选入当前num,则直接等于i-1时对应的dp
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
}
1.2 目标和
🚀 题目链接:https://leetcode.cn/problems/target-sum
🚀 代码
- 二维dp
class Solution { public int findTargetSumWays(int[] nums, int target) { // 统计数组全部元素和(元素都是非负数) int sum = 0; for (int num : nums) { sum += num; } int diff = sum - target; if (diff < 0 || diff % 2 != 0) { // sum和目标值的差为负数或奇数,则肯定不能算出target return 0; } // neg为添加符号的元素值和 int n = nums.length, neg = diff >> 1; // 01背包选方案做法,dp(x,y)表示在[0,x]范围内和为neg的元素方案数 int[][] dp = new int[n + 1][neg + 1]; // 初始化dp,默认一种方案 dp[0][0] = 1; for (int i = 1;i <= n;i++) { int num = nums[i - 1]; for (int j = 0;j <= neg;j++) { // 默认赋值为不选入num的方案数 dp[i][j] = dp[i - 1][j]; if (j >= num) { // 能选择当前num,则再加上选入num的方案数 dp[i][j] += dp[i - 1][j - num]; } } } return dp[n][neg]; } }
- 一维dp
class Solution { public int findTargetSumWays(int[] nums, int target) { // 统计数组全部元素和(元素都是非负数) int sum = 0; for (int num : nums) { sum += num; } int diff = sum - target; if (diff < 0 || diff % 2 != 0) { // sum和目标值的差为负数或奇数,则肯定不能算出target return 0; } // neg为添加符号的元素值和 int n = nums.length, neg = diff >> 1; // 01背包选方案做法,一维dp int[] dp = new int[neg + 1]; // 初始化dp,默认有一种方案 dp[0] = 1; for (int num : nums) { // 一维dp要反过来遍历 for (int j = neg;j >= 0;j--) { if (j >= num) { // 能选择当前num,则再加上选入num的方案数 dp[j] += dp[j - num]; } } } return dp[neg]; } }
2 完全背包
🚀 题目链接:https://www.acwing.com/problem/content/3/
🚀 代码
- 二维dp
public int completeKnapsack(int n, int maxV, int[] v, int[] w) { // 动态函数,表示将第i个物品放入容量为j的背包中的最大价值 int[][] dp = new int[n + 1][maxV + 1]; for (int i = 1;i <= n;i++) { for (int j = 1;j <= maxV;j++) { // 这里物品是否能放的条件不能放到for循环中 if (v[i - 1] <= j) { // 能放入,获取放入和不放入的较大价值 dp[i][j] = Math.max( // 放入,这里是dp[i],不是dp[i-1](01背包的话是dp[i-1]) // dp[i]表示在当前物品放或没放的基础上再放入当前物品,因为一件物品可以放多次 dp[i][j - v[i - 1]] + w[i - 1], // 不放 dp[i - 1][j]); } else { // 放不下,最大价值就是放前一个物品时的价值 dp[i][j] = dp[i - 1][j]; } } } return dp[n][maxV]; }
- 一维dp
public int completeKnapsack(int n, int maxV, int[] v, int[] w) { // 一维动态函数,表示将第i种物品放入容量为j的背包中的最大价值 int[] dp = new int[maxV + 1]; for (int i = 1;i <= n;i++) { // 01背包中内循环是倒序,这里是升序,因为可以重复放入物品 for (int j = 1;j <= maxV;j++) { if (j >= v[i - 1]) { dp[j] = Math.max(dp[j], dp[j - v[i - 1]] + w[i - 1]); } } } return dp[maxV]; }
3 多重背包
🚀 题目链接:https://www.acwing.com/problem/content/4/
🚀 代码
- 朴素解法①:在01背包中嵌套一层循环(一个种类中每一件物品逐渐合并的过程)
// c[i]表示第i件物品的数量 public int multipleKnapsack(int n, int maxV, int[] v, int[] w, int[] c) { // 动态函数 int[] dp = new int[maxV + 1]; // 将每一种物品根据数量合并,转化成01背包 for (int i = 1;i <= n;i++) { for (int j = maxV;j >= v[i - 1];j--) { // k表示物品数量,注意k*v不能大于背包当前的容量 for (int k = 1;k <= c[i - 1] && k * v[i - 1] <= j;k++) { dp[j] = Math.max(dp[j], dp[j - k * v[i - 1]] + k * w[i - 1]); } } } return dp[maxV]; }
- 朴素解法②:将每一种物品一件一件地进行拆分,再直接01背包
public int multipleKnapsack(int n, int maxV, int[] v, int[] w, int[] c) { // 动态函数 int[] dp = new int[maxV + 1]; List<Integer> mergeV = new ArrayList<>(); List<Integer> mergeW = new ArrayList<>(); // 拆分后的物品件数 int newN = 0; // 将第i种物品拆成c[i]件 for (int i = 0;i < n;i++) { for (int j = 0;j < c[i];j++) { mergeV.add(v[i]); mergeW.add(w[i]); newN++; } } // 直接01背包 for (int i = 1;i <= newN;i++) { for (int j = maxV;j >= mergeV.get(i - 1);j--) { dp[j] = Math.max(dp[j], dp[j - mergeV.get(i - 1)] + mergeW.get(i - 1)); } } return dp[maxV]; }
- 二进制优化:在朴素解法②的基础上优化拆分的算法(不是一件一件地拆分)
public int multipleKnapsack(int n, int maxV, int[] v, int[] w, int[] c) { // 动态函数 int[] dp = new int[maxV + 1]; List<Integer> mergeV = new ArrayList<>(); List<Integer> mergeW = new ArrayList<>(); // 拆分后的物品件数 int newN = 0; // 根据二进制分解拆分 for (int i = 0;i < n;i++) { for (int j = 1;j < c[i];j <<= 1) { mergeV.add(j * v[i]); mergeW.add(j * w[i]); newN++; // 拆分后数量减少 c[i] -= j; } if (c[i] != 0) { // 还有剩余数量/当前物品数量为1,直接合并 mergeV.add(c[i] * v[i]); mergeW.add(c[i] * w[i]); newN++; } } // 直接01背包 for (int i = 1;i <= newN;i++) { for (int j = maxV;j >= mergeV.get(i - 1);j--) { dp[j] = Math.max(dp[j], dp[j - mergeV.get(i - 1)] + mergeW.get(i - 1)); } } return dp[maxV]; }
4 混合背包
🚀 题目链接:https://www.acwing.com/problem/content/7/
🚀 代码
public int mixKnapsack(int n, int maxV, int[] v, int[] w, int[] c) {
// 动态函数
int[] dp = new int[maxV + 1];
List<Integer> mergeV = new ArrayList<>();
List<Integer> mergeW = new ArrayList<>();
int newN = 0;
// 修改物品的数量,-1改成1,0(无限)改成maxV/v(最多能放的物品件数)
for (int i = 0;i < n;i++) {
if (c[i] == -1) {
c[i] = 1;
} else if (c[i] == 0) {
c[i] = maxV / v[i];
}
}
// 后面直接套多重背包的二进制优化解法即可
for (int i = 0;i < n;i++) {
for (int j = 1;j < c[i];j <<= 1) {
mergeV.add(j * v[i]);
mergeW.add(j * w[i]);
newN++;
c[i] -= j;
}
if (c[i] != 0) {
mergeV.add(c[i] * v[i]);
mergeW.add(c[i] * w[i]);
newN++;
}
}
// 01背包
for (int i = 1;i <= newN;i++) {
for (int j = maxV;j >= mergeV.get(i - 1);j--) {
dp[j] = Math.max(dp[j], dp[j - mergeV.get(i - 1)] + mergeW.get(i - 1));
}
}
return dp[maxV];
}
5 二维费用背包
🚀 题目链接:https://www.acwing.com/problem/content/8/
🚀 代码
// m[i]表示第i件物品的重量
public int twoDimensionCostKnapsack(int n, int maxV, int maxM, int[] v, int[] m, int[] w) {
// 二维动态数组,记住容量为j,重量为k的背包能承受的最大价值
int[][] dp = new int[maxV + 1][maxM + 1];
// 三重循环
for (int i = 1;i <= n;i++) {
// 遍历容量
for (int j = maxV;j <= maxV && j >= v[i - 1];j--) {
// 遍历重量
for (int k = maxM;k <= maxM && k >= m[i - 1];k--) {
// 套01背包的模板
dp[j][k] = Math.max(dp[j][k], dp[j - v[i - 1]][k - m[i - 1]] + w[i - 1]);
}
}
}
return dp[maxV][maxM];
}
6 分组背包
🚀 题目链接:https://www.acwing.com/problem/content/9/
🚀 代码
// s[i]表示第i组中物品件数,v[i][j]、w[i][j]分别表示第i组中第j件物品的容量和价值
public int groupKnapsack(int n, int maxV, int[] s, int[][] v, int[][] w) {
// 一维dp,类似01背包
int[] dp = new int[maxV + 1];
// 三重循环,外循环遍历每一组
for (int i = 1;i <= n;i++) {
// 遍历背包容量,一维dp,需要倒序遍历
for (int j = maxV;j >= 1;j--) {
// 遍历每一组中每一件物品,在上一组的物品基础上选择
// 类似于01背包,一件物品只能选一次,但一组内最多选一件物品,所以需要再套一重循环
for (int k = 0;k < s[i - 1];k++) {
// 这里是否能放入的条件不能放到for循环中,可能该组的第一件物品就放不下,循环就直接结束
if (j >= v[i - 1][k]) {
dp[j] = Math.max(dp[j], dp[j - v[i - 1][k]] + w[i - 1][k]);
}
}
}
}
return dp[maxV];
}
7 求最优方案数
🚀 题目链接:https://www.acwing.com/problem/content/11/
🚀 代码
// 01背包基础上求最优方案数
public int getBestCnt(int n, int maxV, int[] v, int[] w) {
// 一维dp
int[] dp = new int[maxV + 1];
// 记住背包为i时的最优方案数
int[] bestCnt = new int[maxV + 1];
// 初始化方案数全为1:不放当前物品,直接继承上一件物品(这种方案肯定有,如果放了价值更大再修改)
Arrays.fill(bestCnt, 1);
// 01背包
for (int i = 1;i <= n;i++) {
for (int j = maxV;j <= maxV && j >= v[i - 1];j--) {
// 记住放入当前物品后的价值
int putVal = dp[j - v[i - 1]] + w[i - 1];
if (putVal > dp[j]) {
// 如果放入后价值比不放要大,那肯定放
dp[j] = putVal;
// 方案数更新为放入当前物品后剩余容量对应的方案数
bestCnt[j] = bestCnt[j - v[i - 1]];
} else if (putVal == dp[j]) {
// 如果放和不放的价值一样
// 则需要在原来(不放)的基础上加上放入当前物品后剩余容量对应的方案数
bestCnt[j] = (bestCnt[j] + bestCnt[j - v[i - 1]]) % (int) (1e9 + 7) ;
}
}
}
return bestCnt[maxV];
}
continue…