代码随想录算法训练营Day46 动态规划
代码随想录算法训练营
代码随想录算法训练营Day46 动态规划| ● 139.单词拆分 关于多重背包,你该了解这些! 背包问题总结篇!
139.单词拆分
题目链接:139.单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
- 输入: s = "leetcode", wordDict = ["leet", "code"]
- 输出: true
- 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
总体思路
看到这道题目的时候,大家应该回想起我们之前讲解回溯法专题的时候,讲过的一道题目回溯算法:分割回文串,就是枚举字符串的所有分割情况。
回溯算法:分割回文串:是枚举分割后的所有子串,判断是否回文。
本道是枚举分割所有字符串,判断是否在字典里出现过。
回溯法C++
class Solution {
private:
bool backtracking (const string& s, const unordered_set<string>& wordSet, int startIndex) {
if (startIndex >= s.size()) {
return true;
}
for (int i = startIndex; i < s.size(); i++) {
string word = s.substr(startIndex, i - startIndex + 1);
if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) {
return true;
}
}
return false;
}
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
return backtracking(s, wordSet, 0);
}
};
背包解法
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
动规五部曲分析如下:
- 确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。 - 确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。 - dp数组如何初始化
从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。
那么dp[0]有没有意义呢?
dp[0]表示如果字符串为空的话,说明出现在字典里。
但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。 - 确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
还要讨论两层for循环的前后顺序。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
我在这里做一个总结:
求组合数:动态规划:518.零钱兑换II 求排列数:动态规划:377. 组合总和 Ⅳ、动态规划:70. 爬楼梯进阶版(完全背包) 求最小数:动态规划:322. 零钱兑换、动态规划:279.完全平方数而本题其实我们求的是排列数,为什么呢。 拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。
"apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。
"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。
所以说,本题一定是 先遍历 背包,再遍历物品。 - 举例推导dp[i]
以输入: s = "leetcode", wordDict = ["leet", "code"]为例,dp状态如图:
dp[s.size()]就是最终结果。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_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); //substr(起始位置,截取的个数)
if (wordSet.find(word) != wordSet.end() && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.size()];
}
};
关于多重背包,你该了解这些!
多重背包,即有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 2 |
| 物品1 | 3 | 20 | 3 |
| 物品2 | 4 | 30 | 2 |
| 问背包能背的物品最大价值是多少? | |||
| 和如下情况有区别么? | |||
| 重量 | 价值 | 数量 | |
| -- | -- | --- | --- |
| 物品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 |
| 毫无区别,这就转成了一个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();
}
- 时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量
也有另一种实现方式,就是把每种商品遍历的个数放在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;
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();
}
- 时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量
从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。
当然还有那种二进制优化的方法,其实就是把每种物品的数量,打包成一个个独立的包。
和以上在循环遍历上有所不同,因为是分拆为各个包最后可以组成一个完整背包.
背包问题总结篇!
关于这几种常见的背包,其关系如下:
在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结。
背包递推公式
问能否能装满背包(或最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
- 动态规划:416.分割等和子集
- 动态规划:1049.最后一块石头的重量 II
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下: - 动态规划:494.目标和
- 动态规划:518. 零钱兑换 II
- 动态规划:377.组合总和Ⅳ
- 动态规划:70. 爬楼梯进阶版(完全背包)
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下: - 动态规划:474.一和零
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下: - 动态规划:322.零钱兑换
- 动态规划:279.完全平方数
背包
二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!
完全背包
纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关题目如下:
- 求组合数:动态规划:518.零钱兑换II
- 求排列数:动态规划:377. 组合总和 Ⅳ、动态规划:70. 爬楼梯进阶版(完全背包)
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下: - 求最小数:动态规划:322. 零钱兑换、动态规划:279.完全平方数
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。
总结
代码随想录算法训练营Day46 动态规划的更多相关文章
- 代码随想录算法训练营day01 | leetcode 704/27
前言 考研结束半个月了,自己也简单休整了一波,估了一下分,应该能进复试,但还是感觉不够托底.不管怎样,要把代码能力和八股捡起来了,正好看到卡哥有这个算法训练营,遂果断参加,为机试和日后求职打下一个 ...
- 代码随想录算法训练营day02 | leetcode 977/209/59
leetcode 977 分析1.0: 要求对平方后的int排序,而给定数组中元素可正可负,一开始有思维误区,觉得最小值一定在0左右徘徊,但数据可能并不包含0:遂继续思考,发现元素分布有三种情 ...
- 代码随想录算法训练营day22 | leetcode 235. 二叉搜索树的最近公共祖先 ● 701.二叉搜索树中的插入操作 ● 450.删除二叉搜索树中的节点
LeetCode 235. 二叉搜索树的最近公共祖先 分析1.0 二叉搜索树根节点元素值大小介于子树之间,所以只要找到第一个介于他俩之间的节点就行 class Solution { public T ...
- 代码随想录算法训练营day17 | leetcode ● 110.平衡二叉树 ● 257. 二叉树的所有路径 ● 404.左叶子之和
LeetCode 110.平衡二叉树 分析1.0 求左子树高度和右子树高度,若高度差>1,则返回false,所以我递归了两遍 class Solution { public boolean is ...
- 代码随想录算法训练营day13
基础知识 二叉树基础知识 二叉树多考察完全二叉树.满二叉树,可以分为链式存储和数组存储,父子兄弟访问方式也有所不同,遍历也分为了前中后序遍历和层次遍历 Java定义 public class Tree ...
- 代码随想录算法训练营day12 | leetcode 239. 滑动窗口最大值 347.前 K 个高频元素
基础知识 ArrayDeque deque = new ArrayDeque(); /* offerFirst(E e) 在数组前面添加元素,并返回是否添加成功 offerLast(E e) 在数组后 ...
- 代码随想录算法训练营day10 | leetcode 232.用栈实现队列 225. 用队列实现栈
基础知识 使用ArrayDeque 实现栈和队列 stack push pop peek isEmpty() size() queue offer poll peek isEmpty() size() ...
- 代码随想录算法训练营day06 | leetcode 242、349 、202、1
基础知识 哈希 常见的结构(不要忘记数组) 数组 set (集合) map(映射) 注意 哈希冲突 哈希函数 LeetCode 242 分析1.0 HashMap<Character, Inte ...
- 代码随想录算法训练营day03 | LeetCode 203/707/206
基础知识 数据结构初始化 // 链表节点定义 public class ListNode { // 结点的值 int val; // 下一个结点 ListNode next; // 节点的构造函数(无 ...
- 代码随想录算法训练营day24 | leetcode 77. 组合
基础知识 回溯法解决的问题都可以抽象为树形结构,集合的大小就构成了树的宽度,递归的深度构成的树的深度 void backtracking(参数) { if (终止条件) { 存放结果; return; ...
随机推荐
- Linux & 标准C语言学习 <DAY8_1>
一.进制转换 1.为什么要使用二进制.八进制.十六进制 因为目前的CPU只能识别高低两种电平,只能对二进制数据进行计算 二进制虽然能够直接被计算机识别,但是不方 ...
- Linux基础知识归纳
1.Linux:Linux is not Unix.主要用于企业的服务器端.Windows不开源(系统价格大概2000左右,安装软件也特别贵,例如Offers就6000左右等).基于内核的操作系统(r ...
- PHP微信三方平台-序章
一 微信三方平台准备工作 参数说明: 1.登录授权的发起页域名:提供登录授权公众号的域名地址主开发地址 2.测试公众号列表:未全网发布之前只能添加测试公众号 3.授权事件后的接收URL: 这个地址只要 ...
- VUE百度地图API调用(手机端、PC端、微信通用)
百度地图API-示例中心: https://lbsyun.baidu.com/jsdemo.htm#aCreateMap 1.引入百度地图(此处用到的是V2.0版本) 1> 建立一个js文件,例 ...
- SpringBoot的EnableCaching简述
Spring Boot中的EnableCaching简述 spring boot中自带有数据缓存机制,主要通过其org.springframework.cache包下的各种类来实现. EnableCa ...
- react中登录注册 使用验证码验证
后端接口 var express = require('express'); var router = express.Router(); var User = require('./../sql/c ...
- [J2EE]Spring MVC中访问静态资源[转]
1 文献来源 SpringMVC访问静态资源的三种方式(转) - 博客园 SpringMVC访问静态资源的三种方式 + 提供解决方案的原作者 2 前言 2.1 访问不到静态资源起因 如果web.xml ...
- 腾讯出品小程序自动化测试框架【Minium】系列(七)测试框架的设计和开发
前言 整个框架的开发及调通是在3月27日晚上22点完成,如下: 这篇文章真的是拖了太久了,久到我居然把代码部分完成后,彻底给忘了,这记性,真的是年纪大了! 框架的设计开发 1.框架搭建设计要素 日志& ...
- day13:迭代器&高阶函数(map,reduce,filter,sorted)
迭代器 1.迭代器的定义: 能被next调用,并不断返回下一个值的对象,叫做迭代器(对象) 2.迭代器的概念: 迭代器指的是迭代取值的工具,迭代是一个重复的过程, 每次重复都是基于上一次的结果而继续的 ...
- 基于Containerd容器引擎和kubeadm工具部署K8sv1.26.3
前文我了解了基于ubuntu2204部署containerd容器引擎以及containerd客户端工具的部署和使用相关话题,回顾请参考:https://www.cnblogs.com/qiuhom-1 ...