天天算法之动态规划

动态规划

思想

  • 缓存迭代思想。

应用前提

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

实践

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){
        if(stackHeight == s.length()){
            // 分割的所有子字符串 拼接成 s,则添加记录结果
            res.add(new ArrayList<>(eachRes));
        }
        for(int newStackHeight = stackHeight; newStackHeight < s.length(); newStackHeight++){
            if(isPalindromeWithMemo(s, stackHeight, newStackHeight, memo)){
                // 不同长度的子字符串 入栈操作
                eachRes.add(s.substring(stackHeight, newStackHeight+1));
                // 深度递归 尝试放不同的新的子字符串
                dfs(s, newStackHeight+1, res, eachRes, memo);
                // 栈顶的子字符串 出栈操作 
                eachRes.remove(eachRes.size() - 1);
            }
        }
    }
    
    // 判别回文
    private boolean isPalindromeWithMemo(String s, int start, int end, Boolean[][] memo){
        if(memo[start][end] != null){
            return memo[start][end];
        }
        while(start < end){
            if(s.charAt(start) != s.charAt(end)){
                return memo[start][end] = false;
            }
            start++;
            end--;
        }
        return memo[start][end] = true;
    }
}

方法三:动态规划
运行时长:最快8ms
时间复杂度:当树高度为N,比如N=3S="aaa")时候,节点个数为8个。O(N * 2^N),判别回文的时候,是 或者 否 都有两种情况,分割过程要覆盖所有可能。
空间复杂度:用N字符串s的长度,O(N^2)
备注:该题目中,在测试用例来看,时间性能并没有变好,反而空间复杂度变高。
class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> res = new ArrayList<>();
        List<String> stack = new ArrayList();
        boolean[][] dp = new boolean[s.length()][s.length()];
        dfs(s, res, stack, 0, dp);
        return res;
    }
    
    private void dfs(String s, List<List<String>> res, List<String> stack, int stackHeight, boolean[][] dp){
        if(stackHeight == s.length()){
            res.add(new ArrayList(stack));
            return;
        }
        for(int nextStackHeight = stackHeight; nextStackHeight < s.length(); nextStackHeight++){
            if((s.charAt(stackHeight) == s.charAt(nextStackHeight)) && ((nextStackHeight < stackHeight + 3) || dp[stackHeight+1][nextStackHeight-1])){
                dp[stackHeight][nextStackHeight] = true;
                stack.add(s.substring(stackHeight, nextStackHeight + 1));
                dfs(s, res, stack, nextStackHeight + 1, dp);
                stack.remove(stack.size() - 1);
            }
        }
    }
}
132. Palindrome Partitioning II

找到切刀次数最少的分割方式。比如:下面的demo切一刀就可以!

Input: s = “aab”
Output: 1 (一刀切!)
Explanation: The palindrome partitioning [“aa”,“b”] could be produced using 1 cut.

方法一:遍历所有记录下最小的切割方式 Time Limit Exceeded
class Solution {
    public int minCut(String s) {
        if(s.length() == 1){
            return 0;
        }
        List<String> stack = new ArrayList<String>(s.length());
        boolean[][] dp = new boolean[s.length()][s.length()];
        int[] minCutTimes = new int[1];
        minCutTimes[0] = Integer.MAX_VALUE;
        dfs(s, stack, 0, dp, minCutTimes);
        return minCutTimes[0];
    }
    
    private void dfs(String s, List<String> stack, int stackHeight, boolean[][] dp, int[] minCutTimes){
        if(stackHeight == s.length()){
            int curCutTimes = stack.size() - 1;
            if(minCutTimes[0] > curCutTimes){
                 minCutTimes[0] = curCutTimes;
            }
            return;
        }
        for(int nextStackHeight = stackHeight; nextStackHeight<s.length(); nextStackHeight++){
            if(s.charAt(stackHeight) == s.charAt(nextStackHeight) && ((stackHeight + 3 > nextStackHeight) || dp[stackHeight + 1][nextStackHeight - 1])){
                dp[stackHeight][nextStackHeight] = true;
                stack.add(s.substring(stackHeight, nextStackHeight + 1));
                dfs(s, stack, nextStackHeight + 1, dp, minCutTimes);
                stack.remove(stack.size() - 1);
            }
        }
    }
}

方法二:动态规划 备忘录
class Solution {
    // 最少刀数把字符串切割成多个回文子串
    public int minCut(String s) {
        // 备忘录
        int[] mem = new int[s.length()];
        // 设定备忘录初始值为-1
        for(int i = 0; i<s.length(); i++){
            mem[i] = -1;
        }
        return solve(s, 0, mem);
    }
    private int solve(String s, int start, int[] mem){
        // 边界判断
        if(s.length() == start){
            return 0;
        }
        // 备忘录缓存
        if(mem[start] != -1){
            return mem[start];
        }
        // 初始设定切 len - 1 刀(最多刀数可能),其中 len 表示字符串长度。
        int ans = s.length() - 1;
        for(int end=start;end<s.length();end++){
            int startIndex = start, endIndex = end;
            while(startIndex <= endIndex && s.charAt(startIndex) == s.charAt(endIndex)){
                startIndex++;
                endIndex--;
            }
            if(startIndex > endIndex){
                // 1. 如果抵达边界,当前这一刀就不用切了,因为后续无字符子串。
                if(end == s.length() - 1){
                    // 问题分割为 s的start到end的最少刀数 与 s的end+1到s.length()-1的最少刀数。
                    // 递归逻辑是真实思考步骤的逆序,也就是最先判断和缓存的实际是s的最后两个字符的比较。
                    
                    //
                    // ans = Math.min(ans, solve(s, end + 1, mem));
                    // ans = Math.min(ans, 0);
                    ans = 0;
                }else{
                    // 2. 否则这一刀切下去。
                    ans = Math.min(ans, 1 + solve(s, end + 1, mem));
                }
                mem[start] = ans;
            }
        }
        return mem[start];
    }
}
55. Jump Game
从index 0 跳到 最后的index:
2表示当前步骤最多可以跳2步。

Input: nums = [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Input: nums = [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.
class Solution {
    public boolean canJump(int[] nums) {
        int[] dp = new int[nums.length];
	    for(int i=0;i<nums.length;i++){
		    dp[i]=-1;
	    }
        return canJump(nums, 0, dp) == 1;
    }
    private int canJump(int[] nums, int start, int[] dp){
        if(start == nums.length - 1){
            return dp[start] = 1;
        }
        if(start > nums.length - 1){
            return 1;
        }
        if(dp[start] != -1){
		    return dp[start];
	    }
        if(nums[start] == 0){
            return dp[start] = 0;
        }
        for(int i=1; i<=nums[start]; i++){
            int nextStart = start + i;
            if(canJump(nums, nextStart, dp) == 1){
                return dp[start] = 1;
            }
        }
        return dp[start] = 0;
    }
}
435. Non-overlapping Intervals
让子区域块之间没有重叠,移除最小的块的个数。
Input: intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1
Explanation: [1,3] can be removed and the rest of the intervals are non-overlapping.

Input: intervals = [[1,2],[2,3]]
Output: 0
Explanation: You don't need to remove any of the intervals since they're already non-overlapping.
解法一:超时
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        int[] erase = new int[intervals.length];
        return minEraseNum(intervals, 0, erase);
    }
    
    private int minEraseNum(int[][] intervals, int start, int[] erase){
        if(noOverlap(intervals, erase)){
            return getCountSum(erase, intervals);
        }
        
        if(start == intervals.length){
            return intervals.length;
        }
        
        int res = intervals.length;
        for(int i = start; i < erase.length; i++){
            int noEraseIMin = minEraseNum(intervals, i + 1, erase);
            erase[i] = 1;
            int eraseIMin = minEraseNum(intervals, i + 1, erase);
            erase[i] = 0;
            int eachRes = Math.min(noEraseIMin, eraseIMin);
            res = Math.min(eachRes, res);
        }
        return res;
    }
    
    private boolean noOverlap(int[][] intervals, int[] erase){
        for(int i=0;i<intervals.length;i++){
            if(erase[i] == 1){
                continue;
            }
            for(int j=i+1;j<intervals.length;j++){
                if(erase[j] == 1){
                    continue;
                }
                if(isOverlap(intervals[i], intervals[j])){
                    return false;
                }
            }
        }
        return true;
    }
    
    private int getCountSum(int[] erase, int[][] intervals){
        int res = 0;
        for(int i=0;i<intervals.length;i++){
            if(erase[i] == 1){
                res++;
            }
        }
        return res;
    }

    private boolean isOverlap(int[] a, int[] b){
        return !(a[1] <= b[0] || b[1] <= a[0]);
    }
}

解法二:重复情况无法解决,错误!
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        int[][] dp = new int[intervals.length][intervals.length];
        int[] count = new int[intervals.length];
        int sum = 0;
        for(int i = 0; i < intervals.length; i++){
            for(int j = i + 1; j < intervals.length; j++){
                if(isOverlap(intervals[i], intervals[j])){
                    dp[i][j] = 1;
                    dp[j][i] = 1;
                    sum += 2;
                }
            }
        }
        
        for(int i=0;i<intervals.length;i++){
            for(int j=0;j<intervals.length;j++){
                count[i]+=dp[i][j];
            }
        }
        
        
        // print(dp);
        // print(count);
        
        int res = 0;
        do{
            
            int index = getMaxCountIndex(count);
            if(count[index] == 0){
                return res;
            }
            res++;
            // System.out.println(index);
            // System.out.println();
            // print(dp);
            
            sum -= count[index] * 2;
            eraseCount(count, dp, index);
        }while(sum > 0);
        return res;
    }
    private int getMaxCountIndex(int[] count){
        int value = count[0];
        int index = 0;
        for(int i=1;i<count.length;i++){
            if(count[i] > value){
                value = count[i];
                index = i;
            }
        }
        return index;
    }
    
    private void eraseCount(int[] count, int[][] dp, int index){
        for(int i=0; i<count.length; i++){
            if(dp[i][index] == 1){
                dp[i][index] = 0;
                dp[index][i] = 0;
                count[i] -= 1;
            }
        }
    }
    
    private boolean isOverlap(int[] a, int[] b){
        return !(a[1] <= b[0] || b[1] <= a[0]);
    }
    
    void print(int[][] a){
        for(int i=0;i<a.length;i++){
            for(int j=0;j<a[0].length;j++){
                System.out.print(a[i][j]);
            }
            System.out.println();
        }
    }
    
    void print(int[] count){
        for(int i=0;i<count.length;i++){
            System.out.print(count[i]);
        }
    }
}

解法三:
题目分析:题目中给出的[[1,2],[2,3],[3,4],[1,3]],就像一个一个的木板,长度不等,起始位置不等,现在要求去掉最少的木板个数,让木板之间没有重叠的部分。没有重叠的部分,并不一定要求木板之间没有间隙,是可以分散开来的。
  1. 可以按照木板的尾巴的位置标记进行排序,然后比较第一个木板的尾巴与下一个木板的头部有没有重叠,如果有重叠,就剔除掉,换下一个。
  2. 如果没有重叠,则可以将前一个木板的尾巴顺延至下一个木板,然后继续看与后续木板有没有重叠。

策略:右对齐,一块一块往前面拼接,留下短的,移走长的。
class Solution {
    public int eraseOverlapIntervals(int[][] plank) {
        if(plank.length == 1){
            // 一个木板直接返回,无需移除。
            return 0;
        }
        Arrays.sort(plank, Comparator.comparingInt(x->x[1]));
        int currentPlankIndex = 0;
        int headIndex = 0, tailIndex = 1;
        int removePlankCount = 0;
        for(int i = currentPlankIndex + 1; i < plank.length; i++){
            if(plank[currentPlankIndex][tailIndex] > plank[i][headIndex]){
                // 如果当前木板的尾巴比下一个木板的头部大,说明木板之间存在重叠,则移除。
                removePlankCount++;
            }else{
                currentPlankIndex = i;
            }
        }
        return removePlankCount;
    }
}

解法四:贪心算法
贪心算法思想:(根据题意选取一种量度标准,做出在当前看来是最好的选择!)
  总是做出当前看来最好的选择。不从整体最优上考虑,算法得到的是在某种局部意义上的最优解。所以贪心算法不要回溯。
  贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。
  
	不能保证解是最佳的。因为贪心算法总是从局部出发,并没从整体考虑。
	贪心算法一般用来解决求最大或最小解。
	贪心算法只能确定某些问题的可行性范围。

	回溯是深度的递归遍历所有可能。
  动态规划是空间换时间,迭代计算缓存思想优化重叠子问题。
举例:
  例如,平时购物找零钱时,为使找回的零钱的硬币数最少,不要求找零钱的所有方案,而是从最大面值的币种开始,按递减的顺序考虑各面额,先尽量用大面值的面额,当不足大面值时才去考虑下一个较小面值,这就是贪心算法。
  其中,先尽量用大面值的面额 就是一种局部最优的策略。

策略:左对齐,一块一块往后面拼接,留下短的,移走长的。
class Plank{
        int head;
        int tail;
        public Plank(int head, int tail){
            this.head = head;
            this.tail = tail;
        }
        public boolean isOverLap(Plank plank){
            return !(this.tail <= plank.head || this.head >= plank.tail);
        }
}

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        PriorityQueue<Plank> priorityQueue = new PriorityQueue<>(new Comparator<>(){
            public int compare(Plank plankA, Plank plankB){
                return (plankA.head != plankB.head ? plankA.head - plankB.head : plankA.tail - plankB.tail);
            }
        });
        
        for(int i = 0; i < intervals.length; i++){
            priorityQueue.add(new Plank(intervals[i][0], intervals[i][1]));
        }
        
        Stack<Plank> stack = new Stack<Plank>();
        while(!priorityQueue.isEmpty()){
            Plank curPlank = priorityQueue.poll();
            if(stack.isEmpty() || !stack.peek().isOverLap(curPlank)){
                stack.add(curPlank);
            }else if(curPlank.tail <= stack.peek().tail){
                stack.pop();
                stack.add(curPlank);
            }
        }
        return intervals.length - stack.size();
    }
}
241. Different Ways to Add Parentheses
题目:
任意选择计算子集的顺序。

Input: expression = "2-1-1"
Output: [0,2]
Explanation:
((2-1)-1) = 0 
(2-(1-1)) = 2

Input: expression = "2*3-4*5"
Output: [-34,-14,-10,-10,10]
Explanation:
(2*(3-(4*5))) = -34 
((2*3)-(4*5)) = -14 
((2*(3-4))*5) = -10 
(2*((3-4)*5)) = -10 
(((2*3)-4)*5) = 10
方案一:15ms
  • 思路:2-1-1-1-1,把符号和数字提取出来。把所有的运算看成是符号下标的全排列组合。注意,由于是通过括号的方式实现运算次序的变化,所以此处需要考虑个别排列组合无法达成的情况

  • 正序情况,通过括号可以正常实现。

  • 逆序情况,上述符号下标为:1,3,5,7。当排列组合1 7 3 5时,无法通过括号实现。即新加入的符号的下标,如果小于栈顶的下标(比如:7之后加3),则要判别3到7过程中的元素是否都在栈里面(是否可以通过括号方式触达),3 5 7,此时5没有入栈(括号不连续),所以此种情况要从全排列中剔除掉。同理,5 7 3 1是可以实现的,此种排列对应的表达式为:2-(1-((1-1)-1))。

    class Solution {
        public List<Integer> diffWaysToCompute(String expression) {
            List<Integer> symbolIndexsList = new ArrayList<>();
            List<Integer> numsList = new ArrayList<>();
            
            splitSymbolIndexAndNum(expression, symbolIndexsList, numsList);
    
            // list 转 array
            int[] symbolIndexs = Arrays.stream((Integer[])symbolIndexsList.toArray(new Integer[symbolIndexsList.size()])).mapToInt(Integer::valueOf).toArray();
            int[] nums = Arrays.stream((Integer[])numsList.toArray(new Integer[numsList.size()])).mapToInt(Integer::valueOf).toArray();
            
            List<Integer> result = new ArrayList<>();
    
            if(nums.length == 1){
                result.add(nums[0]);
                return result;
            }
    
            List<Integer> stack = new ArrayList<>();
            List<List<Integer>> allCombine = new ArrayList<>();
            boolean[] symbolUsed = new boolean[symbolIndexs.length];
            getAllCombineNew(symbolIndexs, stack, allCombine, symbolUsed);
            
            // 并查集 记录哪些值需要被覆盖更新
            for(List<Integer> eachCalWay : allCombine){
                int eachRes = getEachRes(expression, eachCalWay, getCopyNums(nums), getUnion(nums.length), symbolIndexs);
                result.add(eachRes);
            }
            return result;
        }
        
        private int[] getCopyNums(int[] nums){
            int[] copyNums = new int[nums.length];
            for(int i=0;i<nums.length;i++){
                copyNums[i] = nums[i];
            }
            return copyNums;
        }
        
        private int getEachRes(String expression, List<Integer> eachCalWay, int[] nums, int[] union, int[] symbolIndexs){
            int res = 0;
            for(int i=0;i<eachCalWay.size();i++){
                int symbolPos = eachCalWay.get(i);
                char symbol = expression.charAt(symbolPos);
                int calIndexPos = getCalIndexPos(symbolPos, symbolIndexs);
                res = calAndSet(symbol, nums, calIndexPos, union);
            }
            return res;
        }
        
        // 获取符号在 expression 中的位置。
        private int getCalIndexPos(int symbolPos, int[] symbolIndexs){
            for(int i=0;i<symbolIndexs.length;i++){
                if(symbolPos == symbolIndexs[i]){
                    return i*2+1;
                }
            }
            return 0;
        }
        
        // 四则运算。
        private int calAndSet(char symbol, int[] num, int symbolPos, int[] union){
            int res = 0;
            switch(symbol){
                case '+':res = num[symbolPos-1] + num[symbolPos+1];break;
                case '-':res = num[symbolPos-1] - num[symbolPos+1];break;
                case '*':res = num[symbolPos-1] * num[symbolPos+1];break;
                case '/':res = num[symbolPos-1] / num[symbolPos+1];break;
            }
            letThemSameParent(union, symbolPos-1, symbolPos+1);
            setAllSameParent(num, symbolPos-1, union, res);
            return res;
        }
        
        // 得到所有的组合
        // private void getAllCombine(int[] symbol, List<Integer> stack, List<List<Integer>> allCombine, boolean[] used){
        //     if(stack.size() == symbol.length){
        //         allCombine.add(new ArrayList<>(stack));
        //         // System.out.println(stack.toString());
        //         return;
        //     }
        //     for(int i=0;i<symbol.length;i++){
        //         if(!Boolean.TRUE.equals(used[i])){
        //             stack.add(symbol[i]);
        //             used[i] = true;
        //             getAllCombine(symbol, stack, allCombine, used);
        //             stack.remove(stack.size()-1);
        //             used[i] = false;
        //         }
        //     }
        // }
    
        // 得到所有的组合 排除 312 的情况,因为括号无法实现 312 的次序。4213 也无法实现,所以剔除掉 倒序非相邻的情况。
        private void getAllCombineNew(int[] symbolIndexs, List<Integer> stack, List<List<Integer>> allCombine, boolean[] used){
            if(stack.size() == symbolIndexs.length){
                allCombine.add(new ArrayList<>(stack));
                return;
            }
            for(int i=0;i<symbolIndexs.length;i++){
                if(!Boolean.TRUE.equals(used[i])){
                    if(check(stack, symbolIndexs[i], symbolIndexs)){
                        stack.add(symbolIndexs[i]);
                        used[i] = true;
                        getAllCombineNew(symbolIndexs, stack, allCombine, used);
                        stack.remove(stack.size()-1);
                        used[i] = false;
                    }
                }
            }
        }
    
        // 实际上是遍历奇数的所有可能的全排列组合。
        // 判断加入栈中的新的 wantAddedIndex:
        // 1. 比 栈顶元素 popStackIndex大,则返回 true。
        // 2. 比 栈顶元素 popStackIndex小,则是倒序。
        //      2.1 如果新加入的元素 到 栈顶的元素,之间的元素都在栈里面,返回true。
        //      2.2 如果不相邻返回false。
        private boolean check(List<Integer> stack, int wantAddedIndex, int[] symbolIndexs){
            if(stack.size() == 0 || stack.size() == symbolIndexs.length - 1){
                return true;
            }
            
            int popStackIndex = stack.get(stack.size() - 1);
            int bottomStackIndex = stack.get(0);
            
            // 正序
            if(wantAddedIndex > popStackIndex){
                return true;
            }
        
            int popStackIndexPos = 0;
            for(int i=0;i<symbolIndexs.length;i++){
                if(popStackIndex == symbolIndexs[i]){
                    popStackIndexPos = i;
                    break;
                }
            }
            
            int wantAddedIndexPos = 0;
            for(int i=0;i<symbolIndexs.length;i++){
                if(wantAddedIndex == symbolIndexs[i]){
                    wantAddedIndexPos = i;
                    break;
                }
            }
            
            // 逆序:想要加入的元素,比当前栈顶元素小。
            // 连续性质的考核:新加入的元素 到 栈顶的元素,之间的元素都在栈里面。说明该逆序情况是可以触达的。
            if(wantAddedIndexPos + 1 < symbolIndexs.length){
                while(symbolIndexs[++wantAddedIndexPos] != popStackIndex){
                    if(!stack.contains(symbolIndexs[wantAddedIndexPos])){
                        return false;
                    }
                }
                return true;
            }
            return false;
        }
        
        // 字符数组,偶数是数字,奇数是符号。字符和数字进行分离获取。
        private void splitSymbolIndexAndNum(String expression, List<Integer> symbolIndexsList, List<Integer> numsList){
            int symbolIndex = 0, numsIndex = 0, lastSymbolIndex = -1;
            for(int i=0;i<expression.length();i++){
                if(isSymbol(expression.charAt(i))){
                    symbolIndexsList.add(i);                
                    numsList.add(Integer.parseInt(expression.substring(lastSymbolIndex + 1, i)));
                    // -1 占位符号的地方
                    numsList.add(-1);
                    lastSymbolIndex = i;
                }
            }
            if(lastSymbolIndex+1 < expression.length()){
                numsList.add(Integer.parseInt(expression.substring(lastSymbolIndex + 1, expression.length())));
            }
        }
        
        // 符号字符判别。
        private boolean isSymbol(char s){
            switch(s){
                case '+':
                case '-':
                case '*':
                case '/':return true;
            }
            return false;
        }
        
        // 并查集:初始化。
        private int[] getUnion(int len){
            int[] union = new int[len];
            for(int parentIndex=0;parentIndex<len;parentIndex++){
                  union[parentIndex] = parentIndex;          
            }
            return union;
        }
        
        // parentIndex 的原因是 父节点只有一个。childrenIndex 是一对多关系,无法用数组刻画。
        private int getUnionParentIndex(int[] union, int curIndex){
            int parentIndex = curIndex;
            // 查找父节点
            while(union[parentIndex] != parentIndex){
                parentIndex = union[parentIndex];
            }
            // 优化路径,压缩指向同一个源头。
            while(union[curIndex] != curIndex){
                curIndex = union[curIndex];
                union[curIndex] = parentIndex;
            }
            return parentIndex;
        }
    
        // 并查集:同源判别。
        private boolean sameParent(int[] union, int indexA, int indexB){
            if(indexA == indexB){
                return true;
            }
            int parentIndexA = getUnionParentIndex(union, indexA);
            int parentIndexB = getUnionParentIndex(union, indexB);
            return parentIndexA == parentIndexB;
        }
    
        // 借助并查集,更新覆盖同源变更。
        private void setAllSameParent(int[] num, int index, int[] union, int newValue){
            for(int i=0; i<num.length; i++){
                if(sameParent(union, index, i)){
                    num[i] = newValue;
                }
            }
        }
        
        // 并差集:同源指定。
        private void letThemSameParent(int[] union, int indexA, int indexB){
            int parentIndex = getUnionParentIndex(union, indexA);
            union[indexB] = parentIndex;
        }
    }
    
方案二:10ms
  • 递归法,由于最优子结构的特性,可以考虑进行问题划分:

    class Solution {
        public List<Integer> diffWaysToCompute(String expression) {
            return partDiffWaysToCompute(expression, 0, expression.length());
        }
        
        private List<Integer> partDiffWaysToCompute(String expression, int start, int end){
            // if(start == end){
            //     return Arrays.asList(expression.charAt(start) - '0');
            // }
            List<Integer> res = new ArrayList<>();
            for(int i = start; i < end; i++){
                char curChar = expression.charAt(i);
                // 如果当前字符是 符号,则可以进行两侧的分割。-》()符号()
                if(curChar < '0' || curChar > '9'){
                    List<Integer> leftPartRes = partDiffWaysToCompute(expression, start, i);
                    List<Integer> rightPartRes = partDiffWaysToCompute(expression, i+1, end);
                    // 穷举求和
                    for(Integer eachLeftPartRes : leftPartRes){
                        for(Integer eachRightPartRes : rightPartRes){
                            switch(curChar){
                                case '+':
                                    res.add(eachLeftPartRes + eachRightPartRes);break;
                                case '-':
                                    res.add(eachLeftPartRes - eachRightPartRes);break;
                                case '*':
                                    res.add(eachLeftPartRes * eachRightPartRes);break;
                                case '/':
                                    res.add(eachLeftPartRes / eachRightPartRes);break;
                            }
                        }
                    }
                }
            }
            return res.size() > 0 ? res : Arrays.asList(Integer.valueOf(expression.substring(start, end)));
        }
    }
    
方案三:7ms
  • 加个缓存备忘录

    class Solution {
        public List<Integer> diffWaysToCompute(String expression) {
            Map<String, List<Integer>> memo = new HashMap<>();
            return partDiffWaysToCompute(expression, 0, expression.length(), memo);
        }
        
        private List<Integer> partDiffWaysToCompute(String expression, int start, int end, Map<String, List<Integer>> memo){
            // if(start == end){
            //     return Arrays.asList(expression.charAt(start) - '0');
            // }
            String key = getKey(start, end);
            if(memo.containsKey(key)){
                return memo.get(key);
            }
            List<Integer> res = new ArrayList<>();
            for(int i = start; i < end; i++){
                char curChar = expression.charAt(i);
                // 如果当前字符是 符号,则可以进行两侧的分割。-》()符号()
                if(curChar < '0' || curChar > '9'){
                    List<Integer> leftPartRes = partDiffWaysToCompute(expression, start, i, memo);
                    List<Integer> rightPartRes = partDiffWaysToCompute(expression, i+1, end, memo);
                    // 穷举求和
                    for(Integer eachLeftPartRes : leftPartRes){
                        for(Integer eachRightPartRes : rightPartRes){
                            switch(curChar){
                                case '+':
                                    res.add(eachLeftPartRes + eachRightPartRes);break;
                                case '-':
                                    res.add(eachLeftPartRes - eachRightPartRes);break;
                                case '*':
                                    res.add(eachLeftPartRes * eachRightPartRes);break;
                                case '/':
                                    res.add(eachLeftPartRes / eachRightPartRes);break;
                            }
                        }
                    }
                }
            }
            res = res.size() > 0 ? res : Arrays.asList(Integer.valueOf(expression.substring(start, end)));
            memo.put(key, res);
            return res;
        }
        
        private String getKey(int start, int end){
            return "" + start + '_' + end;
        }
    }
    
方案四:1ms
  • 备忘录的确有缓存优化效果,但是耗费时间的实际是字符串的charAt操作,所以直接substring进行截断操作,减法思想,让运算愈发精简。

    class Solution {
        public List<Integer> diffWaysToCompute(String expression) {
            Set<Character> symbolSet = new HashSet<>(Arrays.asList('*', '+', '-'));
            Map<String, List<Integer>> memo = new HashMap<>();
            return partDiffWaysToCompute(symbolSet, expression, memo);
        }
        
        private List<Integer> partDiffWaysToCompute(Set<Character> symbolSet, String expression, Map<String, List<Integer>> memo){
            // if(start == end){
            //     return Arrays.asList(expression.charAt(start) - '0');
            // }
            // String key = getKey(start, end);
            if(memo.containsKey(expression)){
                return memo.get(expression);
            }
            List<Integer> res = new ArrayList<>();
            for(int i = 0; i < expression.length(); i++){
                char curChar = expression.charAt(i);
                // 如果当前字符是 符号,则可以进行两侧的分割。-》()符号()
                // if(curChar < '0' || curChar > '9'){
                if(symbolSet.contains(curChar)){
                    List<Integer> leftPartRes = partDiffWaysToCompute(symbolSet, expression.substring(0, i), memo);
                    List<Integer> rightPartRes = partDiffWaysToCompute(symbolSet, expression.substring(i + 1), memo);
                    // 穷举求和
                    for(Integer eachLeftPartRes : leftPartRes){
                        for(Integer eachRightPartRes : rightPartRes){
                            switch(curChar){
                                case '+':
                                    res.add(eachLeftPartRes + eachRightPartRes);break;
                                case '-':
                                    res.add(eachLeftPartRes - eachRightPartRes);break;
                                case '*':
                                    res.add(eachLeftPartRes * eachRightPartRes);break;
                                case '/':
                                    res.add(eachLeftPartRes / eachRightPartRes);break;
                            }
                        }
                    }
                }
            }
            res = res.size() > 0 ? res : Arrays.asList(Integer.valueOf(expression));
            memo.put(expression, res);
            return res;
        }
        
        // private String getKey(int start, int end){
        //     return "" + start + '_' + end;
        // }
    }
    
464. Can I Win:
  • 两个人,轮流取数(比如:1到10,不可重复使用,可以任意随心取,两者取得的数放到一个篮子里),如果第一个人拿到数后先凑够了(大于等于)预期(比如:11),则第一个人获胜,否则第二个人获胜。问是否第一个人能够先触达预期取得胜利。(是一个博弈的过程,每个人都希望自己可以取胜)

    Input: maxChoosableInteger = 10, desiredTotal = 11
    Output: false
    Explanation:
    No matter which integer the first player choose, the first player will lose.
    The first player can choose an integer from 1 up to 10.
    If the first player choose 1, the second player can only choose integers from 2 up to 10.
    The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal.
    Same with other integers chosen by the first player, the second player will always win.
    
方案一:TimeOut
  • 思路:递归回溯。

    class Solution {
        public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
            int sum = (1 + maxChoosableInteger) * maxChoosableInteger << 1;
            if(sum < desiredTotal){
                return false;
            }
            Set<Integer> allChoosedNum = new HashSet<>();
            return canWin(maxChoosableInteger, desiredTotal, allChoosedNum);
        }
        
        private boolean canWin(int maxChoosableInteger, int desiredTotal, Set<Integer> allChoosedNum){
            for(int i = maxChoosableInteger; i > 0; i--){
                if(!allChoosedNum.contains(i)){
                    if(i >= desiredTotal){
                        // System.out.println("allChoosedNum:"+allChoosedNum.toString());
                        return true;
                    }
                    allChoosedNum.add(i);
                    // 对手没有赢的过程中,会去试错。对手成功返回,当前选手需要继续重试,所以该方法内也需要回溯。
                    if(!canWin(maxChoosableInteger, desiredTotal - i, allChoosedNum)){
                        // 此处也需要进行回溯,因为递归是深度遍历所有的情况,直到找到一种可能情况,如果不回溯会导致其他可能路径的错乱,最终影响整体的结果。
                        allChoosedNum.remove(i);
                        return true;
                    }
                    allChoosedNum.remove(i);
                }
            }
            return false;
        }
    }
    
方案二:968 ms
  • 思路:递归回溯+备忘录+位移操作+掩码信息标识(记录当前数字是否被使用过)

  • 注意:先缓存当前结果,再递归!注意思考DFS的路径推演变化。

    class Solution {
        public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
            // 位移操作。
            int sum = (1 + maxChoosableInteger) * maxChoosableInteger >> 1;
            // 累加求和 也 无法触达 desiredTotal
            if(sum < desiredTotal){
                return false;
            }
            // 备忘录
            Map<Integer, Boolean> memo = new HashMap<>();
            // Integer作为 掩码信息标识(记录当前数字是否被使用过)
            return canWin(maxChoosableInteger, desiredTotal, 0, memo);
        }
        
        private boolean canWin(int maxChoosableInteger, Integer desiredTotal, Integer maskInfoSign, Map<Integer, Boolean> memo){
            for(int i = maxChoosableInteger; i > 0; i--){
                int used = (1 << i) & maskInfoSign;
                // 使用过则越过
                if(used != 0){
                    continue;
                }
                if(i >= desiredTotal){
                    // System.out.println("allChoosedNum:"+allChoosedNum.toString());
                    return true;
                }
                maskInfoSign |= 1 << i;
                if(!memo.containsKey(maskInfoSign)){
                    // 备忘要备忘所有可能,包括错误路径,防止无意义的重复计算,缓存优化重复子问题(对和错的路径都有)。
                    memo.put(maskInfoSign, !canWin(maxChoosableInteger, desiredTotal - i, maskInfoSign, memo));
                  
                  	// 此种写法则会超时:put依赖递归过程的值,递归过程需要做缓存,所以要先put,再递归。
                  	// boolean res = !canWin(maxChoosableInteger, desiredTotal - i, usedBitRecord, memo);
                		// if(!memo.containsKey(usedBitRecord)){
                    	// memo.put(usedBitRecord, res);
                		// }
                }
                if(memo.get(maskInfoSign)){
                    return true;
                }
                maskInfoSign &= ~(1 << i);
            }
            return false;
        }
    }
    
方案三:TimeOut
  • 思路:递归+备忘录+标记数组

    class Solution {
        public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
            int sum = maxChoosableInteger * (maxChoosableInteger + 1) >> 1;
            if(sum < desiredTotal){
                return false;
            }
            int[] used = new int[maxChoosableInteger + 1];
            Map<String, Boolean> memo = new HashMap<>();
            return canWin(maxChoosableInteger, desiredTotal, used, memo);
        }
        
        private boolean canWin(int maxChoosableInteger, int desiredTotal, int[] used, Map<String, Boolean> memo){
            String key = Arrays.toString(used);
            if(memo.containsKey(key)){
                return memo.get(key);
            }
            
            for(int i = maxChoosableInteger; i > 0; --i){
                if(used[i] == 1){
                    continue;
                }
                used[i] = 1;
                if(i >= desiredTotal || !canWin(maxChoosableInteger, desiredTotal - i, used, memo)){
                    memo.put(key, true);
                    used[i] = 0;
                    return true;
                }
                used[i] = 0;
            }
            memo.put(key, false);
            return false;
        }
    }
    
553. Optimal Division
  • 题目:通过括号控制运算优先级,让最终运算结果最大。即最优除法。

    Input: nums = [1000,100,10,2]
    Output: "1000/(100/10/2)"
    Explanation:
    1000/(100/10/2) = 1000/((100/10)/2) = 200
    However, the bold parenthesis in "1000/((100/10)/2)" are redundant, since they don't influence the operation priority. So you should return "1000/(100/10/2)".
    Other cases:
    1000/(100/10)/2 = 50
    1000/(100/(10/2)) = 50
    1000/100/10/2 = 0.5
    1000/100/(10/2) = 2
    
  • 解法一:

    // 全排列
    //     public void getAllCombineDivision(int[] nums, List<List<Integer>> allCombine, List<Integer> stack, boolean[] used){
    //         if(stack.size() == nums.length){
    //             allCombine.add(new ArrayList<>(stack));
    //         }
            
    //         for(int i=0; i<nums.length; i++){
    //             if(!used[i]){
    //                 stack.add(nums[i]);
    //                 used[i] = true;
    //                 getAllCombineDivision(nums, allCombine, stack, used);
    //                 stack.remove(stack.size() - 1);
    //                 used[i] = false;
    //             }
    //         }
    //     }
    
    // 每个都是两部分,最大分子 和 最小分母。
    思路:a/(b/c/d/....)是最终的结果。进行拼接就好。
    
    class Solution {
        public String optimalDivision(int[] nums) {
            if(nums.length == 1){
                return "" + nums[0];
            }
            if(nums.length == 2){
                return "" + nums[0] + "/" + nums[1];
            }
            String res = "" + nums[0] + "/(";
            for(int i = 1; i<nums.length - 1; i++){
                res += "" + nums[i] + "/";
            }
            res += "" + nums[nums.length - 1] + ")";
            return res;
        }
    }
    

leetCode国际版类型题

  • \139. Word Break:https://leetcode.com/problems/word-break/
  • \131. Palindrome Partitioning:https://leetcode.com/problems/palindrome-partitioning/
  • \132. Palindrome Partitioning II:https://leetcode.com/problems/palindrome-partitioning-ii/
  • \55. Jump Game:https://leetcode.com/problems/jump-game/
  • \435. Non-overlapping Intervals:https://leetcode.com/problems/non-overlapping-intervals/
  • \241. Different Ways to Add Parentheses:https://leetcode.com/problems/different-ways-to-add-parentheses/
  • \464. Can I Win:https://leetcode.com/problems/can-i-win/
  • \553. Optimal Division:https://leetcode.com/problems/optimal-division/

Reference

  • https://zhuanlan.zhihu.com/p/126124250(经典算法系列:动态规划)
  • https://baike.baidu.com/item/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95/5411800(贪心算法)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值