代码随想录算法训练营 Day38 动态规划Ⅵ 完全背包应用 多重背包

动态规划

组合与排列

DP 求组合数是外层遍历物品,内层遍历背包
DP求排列数是外层遍历背包,内层遍历物品

多重背包

多重体现在多个 0-1 背包,一个物品是有限个的背包问题

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
多重背包和01背包是非常像的, 为什么和01背包像呢?
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

多重背包题目说明

重量 价值 数量
物品0 1 15 2
物品1 3 20 3
物品2 4 30 2
可以稍加转换为如下形式,拆解为若干 0-1 背包问题
重量 价值 数量
物品0 1 15 1
物品0 1 15 1
物品1 3 20 1
物品1 3 20 1
物品1 3 20 1
物品2 4 30 1
物品2 4 30 1

背包总结

在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结

递推公式总结

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

  • 动态规划:416.分割等和子集(opens new window)
  • 动态规划:1049.最后一块石头的重量 II(opens new window)

问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

  • 动态规划:494.目标和(opens new window)
  • 动态规划:518. 零钱兑换 II(opens new window)
  • 动态规划:377.组合总和Ⅳ(opens new window)
  • 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:

  • 动态规划:474.一和零(opens new window)

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:

  • 动态规划:322.零钱兑换(opens new window)
  • 动态规划:279.完全平方数

遍历顺序总结

01背包

在动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
和动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!

完全背包

说完01背包,再看看完全背包。
在动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包
如果求排列数就是外层for遍历背包,内层for循环遍历物品
相关题目如下:

  • 求组合数:动态规划:518.零钱兑换II(opens new window)
  • 求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:

  • 求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)

对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了

题目

322. 零钱兑换 - 力扣(LeetCode)
完全背包问题下,装满背包最少多少件物品
1. Dp 表示装满背包 j 大小物品,最少需要多少物品
2. 递推公式类似于 dp[j]=min(dp[j],dp[j-coin[i]] + value[i]),只不过 value[i]=1
不要这个物品和要做个物品取其中最少数量的
3. 题目中说明了 dp[0]=0,由于求最少物品,非零下标初始化为 INT_MAX
4. 求最少元素数量,不是排列有不是组合,因此两种遍历顺序都可以
5. Dp 打印

int coinChange(vector<int>& coins, int amount) {
	// 定义dp数组
	std::vector<int> dp(amount+1, 0);
	// 初始化dp数组
	for (int i = 1; i <= amount; ++i) dp[i] = INT_MAX;
	// 遍历dp数组 先遍历物品再遍历背包
	for (int i = 0; i < coins.size(); ++i) {
		for (int j = coins[i]; j <= amount; ++j) {
			// 跳过初始值 防止相加越界
			if (dp[j - coins[i]] != INT_MAX) dp[j] = std::min(dp[j - coins[i]] + 1, dp[j]);
		}
	}
	// 分支情况
	if (dp[amount] == INT_MAX) return -1;
	return dp[amount];
}


// 另一种遍历方式注意初始值
int coinChange(vector<int>& coins, int amount) {
	vector<int> dp(amount+1, INT_MAX);
	dp[0] = 0;
	// 先便利背包 再遍历物品 背包从1开始因为dp[0]=0存在
	for (int j = 1; j <= amount; ++j) {
		for (int i = 0; i < coins.size(); ++i) {
			// 当背包有空间且背包物品不是初始值
			if (j - coins[i] >= 0 && dp[j - coins[i]] != INT_MAX) dp[j] = min(dp[j], dp[j-coins[i]]+ 1);
		}
	}
	if (dp[amount] == INT_MAX) return -1;
	return dp[amount];
}

279. 完全平方数 - 力扣(LeetCode)
完全平方数,本质类似上一题,求完全平方数最少数量即最少数量填满背包
其中背包容量看作整数 n,物品可以看作从 0-n 之间数的平方即 nums[i] = i * i 属于完全平方数
因此两层遍历终止数量都是 n,题目中 10^4>=nums[i] >= 1 物品遍历从 1 开始
如果从 0 开始就会无限 0 相加,不用担心相加超过的情况,因此不需要判断也可以加上
不可能出现填充不满的情况,实在不行用 1 填充
1. Dp 表示填满 j 需要的最少数量物品
2. 递推公式类似上题 dp[j] = min(dp[j], dp[j-?]+1) 其中 ?=i*i
3. 初始化非零数为 dp[0]=0
4. 同样的遍历顺序都可以
5. 打印 dp 数组

// 先物品后背包
int numSquares(int n) {
	// 定义dp数组
	std::vector<int> dp(n+1, INT_MAX);
	dp[0] = 0;
	// dp遍历
	for (int i = 1; i <= n; ++i) {
		for (int j = i*i; j <= n; ++j) {
			if(dp[j - i*i] != INT_MAX) dp[j] = std::min(dp[j], dp[j-i*i] + 1);
		}
	}
	return dp[n];
}

// 先背包后物品
int numSquares(int n) {
	vector<int> dp(n+1, INT_MAX);
	dp[0] = 0;
	for (int j = 1; j <= n; j++) {
		for (int i = 1; i * i <= j; ++i) {
			dp[j] = min(dp[j], dp[j - i*i] + 1);
		}
	}
	return dp[n];
}

139. 单词拆分 - 力扣(LeetCode)
看作完全背包问题,问有物品,能否用这些物品恰好装满背包?
1. Dp 含义,如果长度 i 字符串可以被组成则 dp[i]=true
2. 区间 [i, i+k] 单词在物品中,并且 dp[i]=true,则 dp[i+k]=true
3. Dp 初始化 dp[0]=true 便于推导,题目中 dp[0] 无意义,其他位置 dp[i]=false
4. 题目求排列数,因为要从物品匹配背包,要保证顺序,求排列数,先遍历背包再遍历物品即可
5. 打印 dp

bool wordBreak(string s, vector<string>& wordDict) {
	// 创建快速查询库
	std::unordered_set<string> words(wordDict.begin(), wordDict.end());
	// 创建dp数组
	std::vector<bool> dp(s.size()+1, false);
	dp[0] = true;
	// 遍历dp数组 由于求的是排列数先遍历背包再遍历物品
	for (int j = 1; j <= s.size(); ++j) {
		for (int i = 0; i <= j; ++i) {
			// 当前区间字符 i___j__
			string word = s.substr(i, j-i);
			// 当前区间之前字符串可以拼接 找到合适的之后本字符串也可以拼接
			if (words.find(word) != words.end() && dp[i] == true) dp[j] = true;
		}
	}
	return dp[s.size()];
}

56. 携带矿石资源(第八期模拟笔试)
多重背包看作多个 0-1 背包拼起来
1. Dp 数组表示 j 容量能装的最大价值
2. 递推公式 dp[j] = std::max(dp[j], dp[j-weight[i]] + value[i]
转化为 k dp[j] = std::max(dp[j], dp[j - k * weight[i]] + k * value[i]
3. Dp 初始化为 0
4. 0-1 背包遍历顺序先遍历物品,再遍历背包,背包遍历从后往前遍历,最后遍历数量
5. 打印 dp

#include
#include

int main() {
    int c = 0, n = 0;
    std::cin >> c >> n;
    // 存储数据
    std::vector<int> weight(n, 0);
    std::vector<int> value(n, 0);
    std::vector<int> nums(n, 0);

    for (int i = 0; i < n; ++i) std::cin >> weight[i];
    for (int i = 0; i < n; ++i) std::cin >> value[i];
    for (int i = 0; i < n; ++i) std::cin >> nums[i];

    // 创建dp数组
    std::vector<int> dp(c+1, 0);

    // 遍历dp数组 遍历物品然后背包最后数量
    for (int i = 0; i < n; ++i) {
        for (int j = c; j >= weight[i]; --j) {
            // 多重背包增加一个k次遍历物品
            for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; ++k) {
                dp[j] = std::max(dp[j], dp[j - k * weight[i]] + k * value[i]);
            }
        }
    }

    // 打印
    std::cout << dp[c] << std::endl;
    return 0;
}

你可能感兴趣的:(算法,动态规划)