题目描述
给定一个非空字符串$s$和一个非空词典$wordDict$,判断$s$是否能被分割成一个或多个单词分隔的序列。
注意,词典中的词可以用多次,词典中没有重复的单词。
样例
Example 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: "leetcode" 可以被切割成 "leet code"
Example 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: "applepenapple" 可以被切割成 "apple pen apple"
Example 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
算法1 动态规划
Time $O(n^2)$, Space $O(n)$
用一个bool数组$dp[i]$表示$s[0:i]$是否能被break,例如$dp[1]$表示$s[0:1]$也就是$s[0]$单独一个字母是可以break的。
- 构造集合unordered_set, 因为题目给的“词典”是vector不是set, 查找起来不好用。
- 遍历数组$s$,判断有没有某个断点$j$刚好可以满足$s[j,i]$在词典中
- 返回$dp[s.size()]$,表示$s[0:s.size()]$是否为断点
Time $O(n^2)$:两层for loop
Space $O(n)$: unordered_set和vector
C++ 代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> dict(wordDict.begin(), wordDict.end()); //构建为set,方便查找
vector<bool> dp(s.size()+1, false); //dp表示字符之间的隔板,n个字符有n+1个隔板
dp[0] = true; //dp[0]是s[0]前面的隔板
for(int i=1; i<=s.size(); ++i){
for(int j=i; j>=0; --j){
if(dict.find(s.substr(j,i-j))!=dict.end() && dp[j]){
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
这个好理解
时间复杂度标错了
没有必要每次都substr,把wordict中的字符串反着存就可以了。
请仔细说说
之所以每次都需要substr是因为dp的时候j是逆序枚举的,比如”leet”,如果不substr的话j从3扫到0生成的字符串是”teel”。但是如果当初如果把wordict中的字符串反着插到哈希表里,那保存的也是”teel”。相当于扫描的时候是反的,存的也是反的,效果是一样的。
当然解决这个问题的方法有很多,y总课上还演示了倒序枚举和修改转移方式两种办法。
为什么j从大到小循环比从小到大循环,效率更高呢
因为j如果从前开始枚举的话,那么从头开始枚举,比如很长的一串字符串applepenapple,j从小到大枚举pplepenapple,plepenapple,lepenapple…直到枚举到apple才能返回true,从后往前枚举的话显然枚举不了几次就枚举到了,个人理解是因为前面的字符串大多都已经匹配了,dp的思路不就是前面的字符串符合+后面的字符串在字典中即可么,那么肯定是从后往前枚举字符串快
对。因为长字符串与字典中的单词匹配的概率很低。
substr的时间复杂度是o (n) 这样好像就三次方了。。
substr时间复杂度是O(len), 可以看做是常数
我面试的时候面试官说是O N 我应该相信谁
面试官貌似是对的,len的最大长度就是n