题目:
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
/*
本题有两种方法:
方法一:回溯法
与回溯算法中的 第131题 分割回文串 比较相似,
整体思路,枚举分割后的所有子串,判断是否在字典里出现过
时间复杂度 O(2^n),因为每个单词有两个状态,切割,或者不切割
空间复杂度:O(n),算法递归系统调用栈的空间
普通的回溯法 超时了,使用记忆数组进行优化,虽然时间复杂度没有发生改变,但是测试用例都可以通过
方法二:
字典中单词的个数 即为 物品的个数,字典中的每个单词可以等价为 物品的重量
字符串 s 即可以等价为 背包
单词能否组成字符串 s ,就是问物品能否把背包装满。
时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是字符串 s 的长度)
空间复杂度:O(n)
动态规划五部曲:
1、确定 dp 数组以及下标的含义
dp[j] 字符串长度为 i 的话,dp[i] = true,表示可以拆分为一个或多个在字典中出现的单词
2、确定递推公式
如果确定 dp[j] 是 true,且[j, i]这个区间的子串出现在字典里,那么 dp[i] 一定是true.
所以递推公式是 if( [j,i] 这个区间的子串出现在字典里 && dp[j]=true ),那么 dp[i] = true
两个判断条件:
一个是[0,i]区间可以拆分为字典中的单词
一个是[i, j]区间可以拆分为字典中的单词
3、dp数组如何初始化
从递推公式中可以看出,dp[i]的状态依靠 dp[j] 是否为true,
那么 dp[0] 就是递归的根基,dp[0]一定要为 true,否则递归下去后面都是 false,就无意义了
那么 dp[0]=true 的意义是什么呢? 表示 如果字符串为空的话,说明出现在字典里
对于其他非 0 的下标,dp[i] 初始化为 false
4、确定遍历顺序
题目说的是可以拆分为 一个或者多个 在字典中出现的单词,所以是完全背包问题
对于完全背包,就需要考虑是求组合数还是排列数
如果是组合数就是 外层for遍历物品,内层for遍历背包容量
如果是排列数就是 外层for遍历背包,内层for遍历物品
本题是问是否出现,所以顺序不重要,因此是求组合数,因此 使用组合或者 排列 都可以的
但是由于本题的特殊性,是求子串,最好是先遍历背包,再遍历物品
因为:如果要是外层for循环遍历物品,内层for循环遍历背包的话,就需要把所有的子串先预先放在一个容器里,
因此:最终的遍历顺序为,先遍历背包,在遍历物品,正序遍历
*/
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);
// // 这一句,判断 哈希表 中是否出现过 word 这个词
// if(wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1))
// {
// // 这里注意是把两个条件写一块了,要么再多写一个 if 判断,
// // 因为如果 backtracking 为 false 并不需要返回 false,
// 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);
// }
// /************************方法二:记忆化递归,优化后不超时了*********************************/
// // 优化:使用数组保存一下递归过程中的计算结果,也就是使用 memory 数组保存每次计算的以 startIndex 起始的结果
// // 如果 memory[startIndex] 里已经被赋值了,直接用 memory[startIndex] 的结果
// private:
// vector<int> memory;
// bool backtracking(const string& s, const unordered_set<string>& wordSet, int startIndex)
// {
// // 终止条件
// if(startIndex == s.size())
// {
// return true;
// }
// // 如果 memory[stratIndex] 不是初始值,则直接使用 memory[startIndex] 的结果
// if(memory[startIndex] != -1) return memory[startIndex];
// for(int i = startIndex; i < s.size(); ++i)
// {
// // 切割字符串,需要学习!!!!!!!!加深记忆
// string word = s.substr(startIndex, i - startIndex + 1);
// // 这一句,判断 哈希表 中是否出现过 word 这个词
// if(wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1))
// {
// // 这里注意是把两个条件写一块了,要么再多写一个 if 判断,
// // 因为如果 backtracking 为 false 并不需要返回 false,
// memory[startIndex] = 1; // 记录以 startIndex 开始的子串是可以被拆为字典中的一个词
// return true;
// }
// }
// memory[startIndex] = 0; // 记录以 startIndex 开始的子串是不可以拆分为字典中的一个词的
// return false;
// }
// public:
// bool wordBreak(string s, vector<string>& wordDict)
// {
// // 将字典数组 存放在哈希表中,便于查找
// unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
// memory = vector<int>(s.size(), -1); // -1 表示初始化状态
// return backtracking(s, wordSet, 0);
// }
/***************************方法三:转化为 dp 的背包问题**********************************/
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 j = 1; j <= s.size(); j++)
{
// 后遍历物品
for(int i = 0; i < j; i++)
{
// substr 的两个参数,一个是 起始位置,一个是截取的个数
string word = s.substr(i, j - i);
// 两个判断条件:一个是[i, j]区间可以拆分为字典中的单词,
// 一个是[0,i]区间可以拆分为字典中的单词,满足这两个条件则表明[0, j]是可以是字典中的单词
if(wordSet.find(word) != wordSet.end() && dp[i])
{
dp[j] = true;
}
}
}
return dp[s.size()];
}
};