单词拆分
给定一个非空字符串s和一个包含非空单词的列表wordDict,判定s是否可以被空格拆分为一个或多个在字典中出现的单词。
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
思路:背包问题 动态规划
单词就是物品,字符串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]表示如果字符串为空的话,说明出现在字典里。
但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
- 确定遍历顺序
遍历背包放在外循环,将遍历物品放在内循环。内循环从前到后。
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()];
}
};
复杂度分析
- 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)
- 空间复杂度:O(n)
单词拆分II
给定一个非空字符串s和一个包含非空单词列表的字典wordDict,在字符串中增加空格来构建一个句子,使得句子中所有的单词都在词典中。返回所有这些可能的句子。
说明:
- 分隔时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例一
输入:
s = “catsanddog”
wordDict = [“cat”, “cats”, “and”, “sand”, “dog”]
输出:
[
“cats and dog”,
“cat sand dog”
]
示例二
输入:
s = “catsandog”
wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出:
[]
思路:dfs
s = “aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”
wordDict = [“a”,“aa”,“aaa”,“aaaa”,“aaaaa”,“aaaaaa”,“aaaaaaa”,“aaaaaaaa”,“aaaaaaaaa”,“aaaaaaaaaa”]
对于字符串s,如果某个前缀是单词列表中的单词,则拆分出该单词,然后对s的剩余部分继续拆分,比如aaaaa,拆分出头部的a之后,aaaa也同样存在于wordDict当中。如果可以将整个字符串s拆分成单词列表中的单词,则得到一个句子。
在对s的剩余部分拆分得到一个句子之后,将拆分出的第一个单词(即s的前缀)添加到句子的头部,即可得到一个完整的句子。
使用哈希表存储字符串s的每个下标和从该下标开始的部分可以组成的句子列表,在回溯过程中如果遇到已经访问过的下标,则可以直接从哈希表得到结果,而不需要重复计算。如果到某个下标发现无法匹配,则哈希表中,该下标对应的是空列表,因此可以对不能拆分的情况进行剪枝优化。
理解dfs的关键步骤有二:
其一是理解每一层dfs做了什么
其二是每层之间,都产生了何种关联,如何实现的关联(具体来说,dfs在哪个位置,先实现了什么再进入的dfs,实现dfs之后又做了什么)
class Solution
{
public:
unordered_map<int, vector<string>> res; //dp数组
unordered_set<string> words;
vector<string> wordBreak(string s, vector<string>& wordDict)
{
for (string w: wordDict) //转换成集合,查找快
words.insert(w);
dfs(s, 0);
return res[0];
}
void dfs(string s, int idx)
{
if (res.count(idx) == 0) //记忆化的效果。算过了就不用再计算了
{
if (idx == s.size())
{
res[idx] = {""};
return ;
}
res[idx] = vector<string>{};
for (int i = idx; i < s.size(); i++)
{
int cur_word_len = i - idx + 1;
string word = s.substr(idx, cur_word_len);
if (words.count(word))
{
dfs(s, i + 1);
for (string suffix: res[i + 1]) //所有可能的后缀
{
if (suffix.size() == 0)
res[idx].push_back(word);
else
res[idx].push_back(word + ' ' + suffix); //word + 后缀
}
}
}
}
}
};
整数拆分
给定一个正整数n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
思路
整数拆分I
给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。
返回该 最大总和 。
输入:nums = [1,4,3,2]
输出:4
解释:所有可能的分法(忽略元素顺序)为:
- (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3
- (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3
- (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4
所以最大总和为 4
输入:nums = [6,2,6,5,1,2]
输出:9
解释:最优的分法为 (2, 1), (2, 5), (6, 6). min(2, 1) + min(2, 5) + min(6, 6) = 1 + 2 + 6 = 9
思路:贪心算法:
先对数组排序。
由于每两个数,我们只能选择当前小的一个进行累加。
因此我们猜想应该从第一个位置进行选择,然后隔一步选择下一个数。这样形成的序列的求和值最大。
public:
int arrayPairSum(vector<int>& nums) {
int sum = 0;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i += 2) {
sum += nums[i];
}
return sum;
}