1. 问题描述
给定一个非空字符串 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
注意你可以重复使用字典中的单词。
2. 递归法解题思路
这一题稍一分析感觉不算太难,只需要从头开始,拿出每一个字典里的单词进行匹配,当前不匹配就切换下一个单词。匹配的话就把原串中匹配的部分去除,剩下的字符串递归地调用函数进行匹配即可。直到整个字符串全部都匹配就返回true。
于是就有了以下的代码:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int len1 = s.length();
bool result = false;
for (auto str : wordDict) {
int len2 = str.length();
if (len2 > len1)
continue;
bool flag = true;
for (int i = 0; i < len2; i++) {
if (s[i] != str[i]) {
flag = false;
break;
}
}
if (flag == false)
continue;
if (len1 == len2)
return true;
string sSon = s.substr(len2, len1 - len2);
if (wordBreak(sSon, wordDict) == true) {
result = true;
break;
}
}
return result;
}
};
但是这种方法提交后有些输入的情况下会超时,leetcode上提醒超时的用例为:
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"]
很显然这个例子的递归深度会很深,同时字典中每个单词在每个循环中都会递归地调用函数,并且适配只出现在递归到最末尾。显然复杂度是很高的。
3. 动态规划解题思路
既然可以用递归来解决问题,说明是存在相同的子问题的,那么这个问题就是有可能用动态规划来解决的。动态规划通过将子问题的结果记录下来,可以降低递归法的复杂度。就类似于fabnacci数列存下前两项的值,而不用每次通过递归去求前两项的值。
由于输入是一个字符串,所有用dp来解属于1维dp问题。举个简单的例子:
输入:“catsandog”,wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:“false”
一维dp首先申请一个一维dp数组。初始化最简单的情况显然是dp[0] = 0,索引0表示为空串,里面有0个字符。dp[1],则表示字符串只有第一个字符“c”,在字典中查不到,所以dp[1] = 0。dp[2]则表示子串“ca”也不能在字典中找到,dp[2] = 0。dp[3]为“cat”,所以dp[3] = 1。依次类推,当计算到dp[4]的时候,需要判断两种情况:(1)子串“cats”能否在字典中找到,如果能找到则dp[4] = 1(2)因为dp[3] = 1,子串“s”如果能在字典中找到,那么dp[4] = 1。依次往后:
dp[0] = 0;
dp[1] = 0;
dp[2] = 0;
dp[3] = 1; //查找到cat
dp[4] = 1; //查找到cats
dp[5] = 0; //查不到catsa,且查找不到sa和a
dp[6] = 0; //查不到catsan,san,an
dp[7] = 1; //dp[3] = 1查到sand或dp[4] = 1查找到and
dp[8] = 0;
dp[9] = 0;
最后返回dp[9],即该字符串无法找到。
在这个查找过程中,最好记录下来字典中所有可使用的单词的长度,一旦dp的第i项长度超过了最大长度,其本身就不能在字典中找到,只能是查找其往前一个单词长度的dp[i - len]处是否是1,如果满足且从i-len到i - 1位置的子串能在字典中找到,那么dp[i]就能被拆分。如果不记录单词的长度的话,就只能盲目找到之前所有dp为1的项,浪费资源。
最终代码如下:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int length = s.length();
vector<bool> dp(length + 1, 0);
//set用来存字典中所有word的长度
int maxLen = 0;
set<int> storeLen;
for (auto word : wordDict) {
int len = word.length();
if (len > maxLen)
maxLen = len;
storeLen.insert(len);
}
//初始化
dp[0] = 0;
for (int i = 1; i <= length; i++) {
//只有i的长度比maxLen小才需要判断从0到i的字符串是否能找到
if (i <= maxLen) {
string tmp = s.substr(0, i);
for (auto word : wordDict) {
if (tmp == word) {
dp[i] = 1;
break;
}
}
}
//如果已经找到就可以计算dp的下一项
if (dp[i] == 1)
continue;
//否则就查找往前一个单词长度处的dp是否为1,为1且这个单词匹配则前i个字符匹配
for (auto len : storeLen) {
if (i - len <= 0)
continue;
if (dp[i - len] == 1) {
string tmp = s.substr(i - len, len);
for (auto word : wordDict) {
if (tmp == word && dp[i - len]) {
dp[i] = 1;
break;
}
}
if (dp[i] == 1)
break;
}
}
}
return dp[length];
}
};