这一期到了背包问题的最后一期,主要讲解一道leetcodee题,和对多重背包的一些简单介绍,由于leetcode没有对于多重背包的具体问题,且面试基本不会问到多重背包所以只是作为科普。
139. 单词拆分 - 力扣(LeetCode)https://leetcode.cn/problems/word-break/给定一个字符串要求将其拆分,拆分后的各部分单词都可以在给我们的字符串字典里找到,也就是说该字符串是由该字符串字典的某些单词拼接而成。字符串字典里每个单词都可以重复使用。
这道题是分割字符串问题,也可以用回溯算法来求解。这里不做这方面的讲解
无论是回溯算法还是完全背包的模型解决问题,都涉及到一个很重要的问题,如果不能想通,则问题将无法得到解决,我们在下面分析时候再做详细讲解。
动规分析
dp数组的含义:dp【i】指的是对于该字符串截止到第i个字符是否能由字符串字典里的字符串组成,如果能则dp【i】=true,否则则为false。
递推公式:dp【i】是否为真主要由两部分推测得来,一部分是我们要判断的这个字符串中由i到j的这个字符串是否在字典里出现过,另一部分是我们要判断的这个字符串它的前面如果有字符串那么它前面的字符串也必须是true,也就是说也必须由字符串字典的某些字符串构成,满足两者关系同时存在时,才能判定从头到现在的位置,为true。这一点很重要无论是回溯算法,还是用动态规划都要想出来,否则题目无法解出。
dp数组的初始化:根据递推公式得,由于dp【i】的推理依赖于前一个状态,所以dp【0】必须要初始化为true,否则最后不可能为true,其余的部分初始化为false即可。
遍历顺序:完全背包中的遍历顺序大多数都有考究的,不像01背包的一维数组实现,基本都是一样的遍历方法。这道题从排列组合的角度上思考,是要依靠排列的思想来写代码遍历,因为我们字符串字典里的字符串组成给定的字符串,是一定要有顺序的,顺序不对是无法拼接的。所以是先遍历背包再遍历物品。从直接看先遍历背包还是物品的角度来讲,如果先遍历物品再遍历背包,那么物品从前向后遍历只能遍历一次,无法达到物品可以重复使用的效果!这样推算下来肯定是有错误的,所以要先遍历背包。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
set<string>wordset(wordDict.begin(),wordDict.end());
vector<bool>dp(s.size()+1,false);
dp[0]=true;
for(int i=1;i<=s.size();i++){
for(int j=0;j<=i;j++){
string word=s.substr(j,i-j);
if(wordset.find(word)!=wordset.end()&&dp[j]==true)
dp[i]=true;
}
}
return dp[s.size()];
}
};
多重背包是和01背包有相像之处的模型。给我们的物品,价值不同且每件物品都有有限个,也就是说物品件数不一定是1,但也不是无限的,可能是2,3,4个,求怎样装使背包容量之内获得价值最高。那么为什么说它和01背包有相似之处呢?这是由于我们可以将物品个数大于1的物品展开,比如有一物品价值15有两个,我们可以展开为价值15有一个,和价值15有一个。这样不就展开为01背包了吗。根据这样的思路得到如下代码。
void test_multi_pack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
vector<int> nums = {2, 3, 2};
int bagWeight = 10;
for (int i = 0; i < nums.size(); i++) {
while (nums[i] > 1) { // nums[i]保留到1,把其他物品都展开
weight.push_back(weight[i]);
value.push_back(value[i]);
nums[i]--;
}
}
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
for (int j = 0; j <= bagWeight; j++) {
cout << dp[j] << " ";
}
cout << endl;
}
cout << dp[bagWeight] << endl;
}
int main() {
test_multi_pack();
}
如果不展开物品的话,还有另一种方法。就是在背包里重复遍历该物品。
void test_multi_pack() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
vector<int> nums = {2, 3, 2};
int bagWeight = 10;
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
// 以上为01背包,然后加一个遍历个数
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
// 打印一下dp数组
for (int j = 0; j <= bagWeight; j++) {
cout << dp[j] << " ";
}
cout << endl;
}
cout << dp[bagWeight] << endl;
}
int main() {
test_multi_pack();
}
我认为还是第一种方法比较好理解,对于第二种方法的递推公式,理解上可能有一点别扭。
以上即全部内容,对背包问题感兴趣的朋友,可以去看我的其他文章,还有对于背包基础和背包各应用的具体讲解。