天天算法之动态规划

动态规划

动态规划

  • 动态规划
      • 思想
      • 应用前提
      • 实践
        • 139.Word Break
        • 131.Palindrome Partitioning
        • 132. Palindrome Partitioning II
        • 55. Jump Game
        • 435. Non-overlapping Intervals
        • 241. Different Ways to Add Parentheses
            • 方案一:15ms
            • 方案二:10ms
            • 方案三:7ms
            • 方案四:1ms
        • 464. Can I Win:
            • 方案一:TimeOut
            • 方案二:968 ms
            • 方案三:TimeOut
        • 553. Optimal Division
      • leetCode国际版类型题
      • Reference

思想

  • 缓存迭代思想。

应用前提

  • 问题具有最优子结构:子问题最优解->整个问题最优解,大的问题拆解为类似的子问题。
  • 无后效性:前一个状态推导出下一个状态,后面状态不影响前面的状态,可以只关注当下与前置推演即可。
  • 重复子问题:有子问题重叠计算的情况。由于有子问题重叠计算的情况,所以递归过程浪费了时间。而动态规划将中间结果保存在数组中,以空间换时间,保证每个子问题只求解一次,提升了效能。也正是缓存迭代思想的体现。

实践

139.Word Break

一个字符串S,一个单词字典wordDict。例如:s=“leetCode”,wordDict={“leet”,“code”}。问:s整体能否刚好被拆分为wordDict中的字符子串的组合。其中,wordDict中字符子串本身无重复,但是组合过程中每个字符子串可以被重复使用。比如“code”可以用两次去组合出“codecode”。

方法一:递归解法。
  空间复杂度 树高 n,O(n)。
  时间复杂度 每个s有n+1种分法,要么分,要么不分,极端情况 O2^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;
	}
}
131.Palindrome Partitioning

字符串s分割成回文子串的所有可能的组合。such as,Input: s = “aab”
Output: [[“a”,“a”,“b”],[“aa”,“b”]]

方法一:回溯法 暴力破解 尝试所有可能
运行时长:8ms
时间复杂度:当树高度为N,比如N=3S="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=3S="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

你可能感兴趣的:(Algorithm,小小的天,天天JAVA,动态规划,算法,leetcode)