139. 单词拆分
题目描述
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出 s
。
注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
思路分析
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
动规五部曲 分析如下:
- 确定dp数组以及下标的含义
dp[i]:字符串长度为i的话,dp[i]为true,表示可有拆分为一个或多个在字典中出现的单词.为false表示无法拆分
- 确定递推公式
如果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]有没有意义呢? 由于题目说了 ‘给定一个非空字符串s’,所以测试数据中不会出现i为0情况,dp[0]为true完全是为了推导公式.
对于下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或者多个在字典中出现的单词.
- 确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
还要讨论两层for循环的前后循序。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
本题最终要求的是是否都出现过,所以对出现单词集合里的元素是组合还是排列,并不在意!
但是本题具有特殊性,因为是求子串,最好是遍历背包放在外循环,将遍历物品放在内循环.
如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的子串都预先放在一个容器里。
- 举例推导dp[i]
以输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:
参考代码
#include<bits/stdc++.h>
using namespace std;
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++){//物品肯定是背包中的一小块. 所以j<i,不能等于,否则物品为空无意义.
string word = s.substr(j,i-j); // i:5 j:4
if(wordSet.find(word)!=wordSet.end() && dp[j]==true){//如果当前子串属于物品,并且之前的串也是物品. 那么从0到i就是个大物品.
dp[i] = true;
}
}
}
return dp[s.size()];
}
198. 打家劫舍
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路分析
打家劫舍是dp的经典问题.动规五部曲 分析如下:
- 确定dp数组以及下标的含义
dp[i]:下标为i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
- 确定递推公式
决定dp[i]的因素就是第i间房间偷还是不偷.
- 如果偷第i间房间,那么i-1房间一定是不偷的.找出下标i-2以内的房屋,最多可以偷窃的金额dp[i-2]加上第i房间偷到的钱.
- 如果不偷第i房间,那么
dp[i] = dp[i-1]
然后dp[i]取最大值,即dp[i] = max(dp[i-2] + nums[i], dp[i-1])
- dp数组如何初始化
从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]
从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
- 确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
- 举例推导dp数组
参考代码
int rob(vector<int>& nums) {
if(nums.size()==0){
return 0;
}
if(nums.size()==1){
return nums[0];
}
vector<int> dp(nums.size(),0) ;
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
//就是简单的dp,一层循环就够...
for(int i = 2;i < nums.size(); i++){
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);//状态转移方程 dp[i]偷到的最大金额= 当前房子偷的金额 和 当前房子不偷的最大值.
}
return dp[nums.size() - 1] ;
}
213. 打家劫舍 II
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
思路分析
这道题目和 198.打家劫舍 (opens new window) 是差不多的,唯一区别就是成环了。
对于一个数组,成环的话主要有如下三种情况:
- 情况一:考虑不包含首尾元素
- 情况二:考虑包含首元素,不包含尾元素
- 情况三:考虑包含尾元素,不包含首元素
注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。
而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。(不理解的可以举一个例子)
分析到这里,本题其实比较简单了。 剩下的和 198.打家劫舍 (opens new window)就是一样的了。
参考代码
#include<bits/stdc++.h>
using namespace std;
int rob(vector<int>& nums) {
if(nums.size()==0){
return 0;
}
if(nums.size()==1) {
return nums[0];
}
//包含头不包含尾
int result1 = robRange(nums,0,nums.size()-2) ;
//包含尾不包含头
int result2 = robRange(nums,1,nums.size()-1);
return max(result1,result2);
}
//上个题中的逻辑
int robRange(vector<int>& nums,int begin,int end){
if(begin==end){//如果只有一个元素,则直接进行返回
return nums[begin];
}
vector<int> dp(nums.size(),0);//定义dp
//初始化
dp[begin] = nums[begin];
dp[begin+1] = max(nums[begin],nums[begin+1]);
for(int i = begin+2;i <= end;i++) {
dp[i] = max(dp[i-2]+nums[i],dp[i-1]);// 进行递推
}
return dp[end];
}
337. 打家劫舍 III
题目描述
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
思路分析
动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,下面我们以递归三部曲来分析本题
- 确定递归函数的参数和返回值
参数:当前节点
返回值:长度为2的dp数组:表示当前节点偷与不偷所得到的金钱
dp数组以及下标的含义:下标为0记录不偷该节点所得到的最大金钱,下标为1记录偷该节点所得到的最大金钱.
- 确定终止条件
当遇到空节点的时候,进行返回.
if (cur == NULL) return vector<int>{0, 0};
- 确定遍历顺序
首先明确要使用后序遍历,因为需要通过递归函数的返回值来做下一步计算.
通过递归左节点,得到左节点偷与不偷的金钱数.
通过递归右节点,得到右节点偷与不偷的金钱数.
- 确定单层递归逻辑
- 如果是偷当前节点,那么左右孩子就不能偷,
val1 = cur->val+left[0]+right[0]
- 如果不偷当前节点,那么左右孩子就可以偷,至于偷不偷一定是选择其中最大的.
val2 = max(left[0],left[1])+max(right[0],right[1])
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
- 举例推导dp数组
以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导)
最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱。
参考代码
#include<bits/stdc++.h>
using namespace std;
//Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0],result[1]);
}
vector<int> robTree(TreeNode* cur) {
//结束条件
if(cur==NULL) {
return vector<int> {0,0}; //另外一种方式定义 vector<int> v = {0,0}
}
vector<int> left = robTree(cur->left) ;//遍历左子树
vector<int> right = robTree(cur->right);//遍历右子树
//偷当前节点
int val1 = cur->val+left[0]+right[0] ;
//不偷当前节点
int val2 = max(left[0],left[1])+max(right[0],right[1]);
return {val2,val1};
}
如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发。
创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客