缓存迭代
思想。最优子结构
:子问题最优解->整个问题最优解,大的问题拆解为类似的子问题。无后效性
:前一个状态推导出下一个状态,后面状态不影响前面的状态,可以只关注当下与前置推演即可。重复子问题
:有子问题重叠计算的情况。由于有子问题重叠计算的情况,所以递归过程浪费了时间。而动态规划将中间结果
保存在数组中,以空间换时间,保证每个子问题只求解一次,提升了效能。也正是缓存迭代
思想的体现。一个字符串S,一个单词字典wordDict。例如:s=“leetCode”,wordDict={“leet”,“code”}。问:s整体能否刚好被拆分为wordDict中的字符子串的组合。其中,wordDict中字符子串本身无重复,但是组合过程中每个字符子串可以被重复使用。比如“code”可以用两次去组合出“codecode”。
方法一:递归解法。
空间复杂度 树高 n,O(n)。
时间复杂度 每个s有n+1种分法,要么分,要么不分,极端情况 O(2^n)
运行时长:超时
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return wordCheck(s, 0, new HashSet<String>(wordDict));
}
private boolean wordCheck(String s, int start, Set<String> wordDict){
if(start == s.length()){
return true;}
for(int end = start + 1; end <= s.length(); end++){
// 此处的重复子问题 导致很多子串重复对比 效能低下。
if(wordDict.contains(s.substring(start, end)) && wordCheck(s, end, wordDict)){
return true;
}
}
return false;
}
}
方法二:自顶向下(递归+备忘录),其实就是缓存中间结果,避免重复计算。
运行时长:5ms
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return wordCheckWithMemo(s, 0, new HashSet<String>(wordDict), new Boolean[s.length()]);
}
private boolean wordCheckWithMemo(String s, int start, Set<String> wordDict, Boolean[] memo){
if(start == s.length()){
return true;
}
if(memo[start] != null){
return memo[start];
}
for(int end = start + 1; end <= s.length(); end++){
if(wordDict.contains(s.substring(start, end)) && wordCheckWithMemo(s, end, wordDict, memo)){
return memo[start] = true;
}
}
return memo[start] = false;
}
}
方法三:自底向上(迭代推演)
运行时长:6ms
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for(int end = 1; end <=s.length(); end++){
for(int start = 0; start < end; start++){
if(dp[start] && wordDictSet.contains(s.substring(start, end))){
dp[end] = true;
break;
}
}
}
return dp[s.length()];
}
}
方法四:广度优先搜索(Using Breadth-First-Search)
运行时长:7ms
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet<String>(wordDict);
boolean[] visited = new boolean[s.length()];
Queue<Integer> queue = new LinkedList<Integer>();
queue.add(0);
while(!queue.isEmpty()){
int start = queue.remove();
if(visited[start]){
continue;
}
for(int end = start+1; end <= s.length(); end++){
if(wordDictSet.contains(s.substring(start, end))){
// 凡是能往下继续走的,都加到队列中。
queue.add(end);
// 走到终点则直接返回,其他逻辑分支流程无需再走,找到一条大路通罗马即可。
if(end == s.length()){
return true;
}
}
}
// 之前尝试过的逻辑无需再走。
visited[start] = true;
}
return false;
}
}
方法五:滑动窗口的感觉!广度优先搜索 中的步长迭代策略是小步迭代,而实际上我们可以每次走 word_len,因为这样避免了无意义的“跨步行为”,让我们每一次的迭代都是货真价实的前行!
运行时长:1ms
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<Integer> visitedCache = new HashSet<Integer>();
return wordBreak(s, wordDict, 0, visitedCache);
}
private boolean wordBreak(String s, List<String> wordDict, int start, Set<Integer> visitedCache){
if(start == s.length()){
// 达到终点就返回成功!一条大路通罗马即可!
return true;
}
if(visitedCache.contains(start)){
// 走过的错路就无需再走!
return false;
}
for(String eachWord : wordDict){
// public boolean startsWith(String prefix , int toffset),其中,prefix为需要匹配的子串,toffset为字符串中开始查找的位置。
if(s.startsWith(eachWord, start) && wordBreak(s, wordDict, start + eachWord.length(), visitedCache)){
return true;
}else{
visitedCache.add(start);
}
}
return false;
}
}
方法六:自底向下的动态规划 + 预处理步长,让每一步都有意义!
运行时长:3ms
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int[] mark = new int[s.length()+1];
mark[0]=1;
List<Integer>wordsLenDict=new ArrayList<Integer>();
for(String per_word:wordDict){
if(wordsLenDict.contains(per_word.length())==false){
wordsLenDict.add(per_word.length());
}
}
for (int l = 1; l <=s.length(); l++) {
for(int w_len:wordsLenDict){
int r=l-1+w_len;
if (mark[l-1]==1&&r<=s.length()&&wordDict.contains(s.substring(l-1,r)))
mark[l-1+w_len] = 1;
}
}
return mark[s.length()] == 1;
}
}
字符串s分割成回文子串的所有可能的组合。such as,Input: s = “aab”
Output: [[“a”,“a”,“b”],[“aa”,“b”]]
方法一:回溯法 暴力破解 尝试所有可能
运行时长:8ms
时间复杂度:当树高度为N,比如N=3(S="aaa")时候,节点个数为8个。O(N * 2^N),判别回文的时候,是 或者 否 都有两种情况,分割过程要覆盖所有可能。
空间复杂度:用N字符串s的长度,O(N)
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> eachRes = new ArrayList<>();
dfs(s, 0, res, eachRes);
return res;
}
// 回溯法 深度遍历 模拟入栈出栈 递归所有可能
private void dfs(String s, int stackHeight, List<List<String>> res, List<String> eachRes){
if(stackHeight == s.length()){
// 分割的所有子字符串 拼接成 s,则添加记录结果
res.add(new ArrayList<>(eachRes));
}
for(int newStackHeight = stackHeight; newStackHeight < s.length(); newStackHeight++){
if(isPalindrome(s, stackHeight, newStackHeight)){
// 不同长度的子字符串 入栈操作
eachRes.add(s.substring(stackHeight, newStackHeight+1));
// 深度递归 尝试放不同的新的子字符串
dfs(s, newStackHeight+1, res, eachRes);
// 栈顶的子字符串 出栈操作
eachRes.remove(eachRes.size() - 1);
}
}
}
// 判别回文
private boolean isPalindrome(String s, int start, int end){
while(start < end){
if(s.charAt(start++) != s.charAt(end--)){
return false;
}
}
return true;
}
}
错误方法二:动态规划(递归+备忘录)时间效能并没有提升!why?因为 dfs(s, newStackHeight+1, res, eachRes, memo); 深度递归的过程是由主到子,所以备忘录记录到缓存始终是“延迟的”、使用过的。
运行时长:最快8ms
时间复杂度:当树高度为N,比如N=3(S="aaa")时候,节点个数为8个。O(N * 2^N),判别回文的时候,是 或者 否 都有两种情况,分割过程要覆盖所有可能。
空间复杂度:用N字符串s的长度,O(N)
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> eachRes = new ArrayList<>();
Boolean[][] memo = new Boolean[s.length()][s.length()];
dfs(s, 0, res, eachRes, memo);
return res;
}
// 回溯法 深度遍历 模拟入栈出栈 递归所有可能
private void dfs(String s, int stackHeight, List<List<String>> res, List<String> eachRes, Boolean[][] memo