九章算法知识点与题目总结(不定时更新...)

------------------------------------------------

本笔记主要记录在刷九章题目中的思路、问题,以及不能bug-free的原因。

 

Time Complexity in Coding Interview

• O(1) 极少

• O(logn) 几乎都是二分法

• O(√n) 几乎是分解质因数

• O(n) 高频

• O(nlogn) 一般都可能要排序

• O(n 2 ) 数组,枚举,动态规划

• O(n 3 ) 数组,枚举,动态规划

• O(2 n ) 与组合有关的搜索

• O(n!) 与排列有关的搜索

------------------排列组合问题---------------------

  • 适用范围:几乎所有搜索问题,但具体根据题目有改动(比如什么时候去除重复的)
  • 特别注意的点:什么时候输出、那些情况跳过。
  • 解决重复例子的方法:sort一下再判断下上次取的位置和这次的位置的数一不一样。if(i != pos && nums[i] == nums[i - 1]) continue;

有一些题目,是要求所有方案,这种题目90%是搜索问题,而这90的搜索问题中90%是用递归,另外10%是用非递归写递归。而这些搜索问题一般都是排列组合。

 

  

1.---------Permutaions


题意:找一个数组里元素里的全排列,数组中没有重复元素

思路:递归,在一个集合里找数组成排列,那么就需要一个set来记录当前集合里有没有这个数

一刷:WA:原因:没有考虑使用set。

二刷:AC

public class Solution {
    /*
     * @param nums: A list of integers.
     * @return: A list of permutations.
     */
    public List<List<Integer>> permute(int[] nums) {
        // write your code here
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> permutation = new ArrayList<>();
        if (nums.length == 0 || nums == null) {
            return res;
        }
        Set<Integer> set = new HashSet<>();
        Helper(nums, res, permutation, set);
        return res;
    }
    private void Helper(int[] nums, List<List<Integer>> res, 
                        List<Integer> permutation, Set<Integer> set) 
    {
        if (permutation.size() == nums.length) {
            res.add(new ArrayList<Integer>(permutation));
            return;
        } 
        for (int i = 0; i < nums.length; i++) {
            if (set.contains(nums[i])) continue;
            set.add(nums[i]);
            permutation.add(nums[i]);
            Helper(nums, res, permutation, set);
            set.remove(nums[i]);
            permutation.remove(permutation.size() - 1);
        }
    }                       
                        
}
View Code

 

2.---------SubSets ||


题意:找一个数组的全部子集,这个数组终有重复元素,但是答案里不能有重复的子集。

思路:递归。每次选取几个数,所以需要记录选取数的位置。

一刷:AC

public class Solution {
    /*
     * @param nums: A set of numbers.
     * @return: A list of lists. All valid subsets.
     */
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // write your code here
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> set = new ArrayList<>();
        if (nums.length == 0 || nums == null) {
            return res;
        }
        Arrays.sort(nums);
        Helper(res, set, nums, 0);
        return res;
    }
    private void Helper(List<List<Integer>> res, List<Integer> set,
                        int[] nums, int pos)
    {
        res.add(new ArrayList<Integer>(set));
        for (int i = pos; i < nums.length; i++) {
            if (i != pos && nums[i] == nums[i - 1]) {
                continue;
            }
            set.add(nums[i]);
            Helper(res, set, nums, i + 1);
            set.remove(set.size() - 1);
        }
    }
}
View Code

 

3.----------Combination Sum || 

题意:就是给一串数C, 以及一个target T,从这串数中找到能使其和为给定的T的组合。combinations sum to T。在Combination Sum I 中允许可以重复选取每个数,但是在Combination Sum II里每个数只能选一次。就是每个组合里每个数只能选一次。

思路:仍然用回溯,需要注意的是怎么排除重复的,就是在每次向下传递的时候应该设定为 i + 1 的位置。

一刷:WA : 原因:模版不熟练;而且在传递的时候将 i + 1  写成了 pos + 1;

二刷:超时:参考了九章的代码才发现中间还需要一个判断,if (target < candidates[i]) break,这样就后面就可以不用再加入比较了,相当于一个剪枝优化。

 

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> results = new ArrayList<>();
        if (candidates.length == 0 || candidates == null) 
            return results;
        List<Integer> combination = new ArrayList<Integer>();
        Arrays.sort(candidates);
        Helper(candidates, results, combination, target, 0);
        return results;
    }
    private void Helper(int[] candidates,
                       List<List<Integer>> results,
                       List<Integer> combination,
                       int target,
                       int pos) {
        if (target == 0) {
            results.add(new ArrayList<Integer>(combination));
            return;
        }
        for (int i = pos; i < candidates.length; i++) {
            if (i != pos && candidates[i] == candidates[i - 1])
                continue;
            if (target < candidates[i])
                break;
            combination.add(candidates[i]);
            Helper(candidates, results, combination, target - candidates[i], i + 1);
            combination.remove(combination.size() - 1);
        }
    }
}
View Code

 

4.----------Palindrome Partitioning  (这题就是组合排列问题的一个很好的变形)

题意:将字符串分割为都是回文串的子串。

一刷:WA:原因:如果子串不是回文串时,应该继续本次循环,而不是退出循环,回溯。(因为也许可以继续划分到后面然后这个子串是回文,但是当前的不是,所以应该结束本次循环,继续划分)。  if (!isPalindrome(sub)) continue; 并且当次划分的子串开始位置应该是上次划分的位置,结束位置就是i + 1,所以 sub = s.substring(startIndex, i + 1) 

二刷:AC

public class Solution {
    /*
     * @param s: A string
     * @return: A list of lists of string
     */
    public List<List<String>> partition(String s) {
        // write your code here
        List<List<String>> res = new ArrayList<>();
        List<String> partition = new ArrayList<>();
        if (s.length() == 0 || s == null) {
            return res;
        }
        Helper(s, res, partition, 0);
        return res;
    }
    private void Helper (String s, List<List<String>> res,
                         List<String> partition, int startIndex)
    {
        if (startIndex == s.length()) {
            res.add(new ArrayList<String>(partition));
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            String sub = s.substring(startIndex ,i + 1);
            if (!isPalindrome(sub)) continue;
            partition.add(sub);
            Helper(s, res, partition, i + 1);
            partition.remove(partition.size() - 1);
        }
    }
    private boolean isPalindrome(String s) {
        if (s.length() == 0) return true;
        int left = 0, right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
View Code

 

5.-----------------Permutation Sequence  

题意:给n个数的第k个排列组合值。

一刷:用排列组合模版做,递归回溯超时,需要改成非递归。(这就是递归问题改成非递归)

二刷:参考被人的做法AC。涉及到一些数学知识。具体思路可见http://bangbingsyb.blogspot.com/2014/11/leetcode-permutation-sequence.html

class Solution {
    public String getPermutation(int n, int k) {
        StringBuilder sb = new StringBuilder();
        List<Integer> nums = new ArrayList<>();
        for (int i = 1; i <= n; i++)
            nums.add(i);
        int factor = 1;
        for (int i = 1; i < n; i++) {
            factor *= i;
        }
        k = k - 1;
        for (int i = 0; i < n; i++) {
            int index = k / factor;
            k = k % factor;
            sb.append(nums.get(index).toString());
            nums.remove(index);
            if (i < n - 1)
                factor = factor / (n - 1 - i);
        }
        return sb.toString();
    }
}
View Code

 

 

------------------二分查找-----------------

1.需要注意的点:
  • 循环结束的条件:start + 1 < end : 当循环进行到数组里只有两个数时,即当start与end相邻时。里面就不用加1和减1
  • 使用 mid = start + (end - start) / 2 而不是 mid = (start + end) / 2,因为后者可能会导致溢出(细节)
  • 然后比较A[mid] == , <, > : 如果是要找last position, 当A[mid] == target时,执行start = mid,而不是直接返回;如果找first position ,则要执行end = mid,且后面判断的时候先判断A[start]
  • 结束循环最后还需比较A[start]和A[end]哪个是target,如果需要的是找最后一个出现的target,则需要先判断A[end];如果需要找第一个出现的target,则首先需要判断A[start]是不是target
 
====>> 任何二分法的问题都可以归为找first position和last position
 
2.看到O(logn)复杂度,一般考虑用二分法
  • 如果问题用O(n)可以解决,但是面试官不满意,则要找O(logn)的方法
 
3.旋转数组的问题:(旋转数组的特点:有两个上升区间(画图),特殊情况一个上升区间)
  • 没有重复元素时:可以用二分法 : 怎样划分区间 (二分法实质上是对区间的划分,每次去掉一部分空间)
  • 有重复元素:不能用二分法,因为有重复元素时实现的复杂度最坏就是O(n),证明:黑盒测试=> 假设前n-1次取出来的都是相同的,则不能得到任何有用的信息,第n次取到了最小的,才能知道最小的元素的位置。所以存在重复元素时的时间复杂度最小都是o(n),即不存在o(logn)的解法。

 

二分的三个境界:

• 第一境界 二分法模板

     时间复杂度小练习

     递归与非递归的权衡

     二分的三大痛点

     通用的二分法模板

• 第二境界:二分位置 之 圈圈叉叉 Binary Search on Index - OOXX

     找到满足某个条件的第一个位置或者最后一个位置

• 第三境界:二分位置 之 保留一半 Binary Search on Index - Half half

     保留有解的一半,或者去掉无解的一半(二分的核心思想,保证了logn的复杂度)

 

 

 

 6.-------------- Search in Sorted Array

 一刷:AC

class Solution {
    public int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int start = 0, end = nums.length - 1;
        int mid;
        while (start + 1 > end) {
            mid = start + (end - start) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > nums[start]) {
                if (target >= nums[start] && target <= nums[mid]) {
                    end = mid;
                } else {
                    start = mid;
                }
            } else {
                if (target >= nums[mid] && target <= nums[end]) {
                    start = mid;
                } else {
                    end = mid;
                }
            }
        }
        if (nums[start] == target) {
            return start;
        } else if (nums[end] == target) {
            return end;
        }
        return -1;
    }
}
View Code

 

---------补一道easy题,但是超容易出错!--------

leetcode 633. Sum of Square Numbers

Given a non-negative integer c, your task is to decide whether there're two integers a and b such that a2 + b2 = c.

坑:需要将类型都设置为long!!!

class Solution {
    // Binary Search
    public boolean judgeSquareSum(int c) {
        for (long a = 0; a * a <= c; a++) {
            if (find(0, c - a * a))
                return true;
        }
        return false;
    }
    private boolean find(long start, long end) {
        long target = end;
        while (start + 1 < end) {
            long mid = start + (end - start) / 2;
            if (mid * mid == target) return true;
            else if (mid * mid < target) start = mid;
            else end = mid;
        }
        if (start * start == target || end * end == target)
            return true;
        else
            return false;
    }
}
View Code

 

 7.-------------- Russian Doll Envelopes  (二分的应用)

题意:就是像套俄罗斯娃娃那样套信封。 要求宽度与长度都大于另一个信封的才能套进另一个信封。

一刷:WA 完全不知道怎么用二分来解决。

二刷:原来这个就是 Longest Increasing Subsequence的二维形式。本来最长递增子序列这题是dp的解法,但是还可以二分来解决!(LIS的follow 就是需要O(nlogn)的复杂度)

 思路:LIS:先建立一个空的dp数组,然后开始遍历原数组,对于每一个遍历到的数字,我们用二分查找法在dp数组找第一个不小于它的数字,如果这个数字不存在,那么直接在dp数组后面加上遍历到的数字,如果存在,则将这个数字更新为当前遍历到的数字,最后返回dp数字的长度即可。

class Solution {
    //version1 : dp
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0 || nums == null)
            return 0;
        int[] dp = new int[nums.length];
        int res = 0;
        for (int i = 0; i < nums.length; i++)
            dp[i] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            res = Math.max(dp[i], res);
        }
        return res;
    }
    //version2 : binary search
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0 || nums == null)
            return 0;
        int[] dp = new int[nums.length];
        int len = 0;
        for (int i = 0; i < nums.length; i++) {
            int index = Arrays.binarySearch(dp, 0, len, nums[i]);
            if (index < 0)
                index = -index - 1;
            dp[index] = nums[i];
            if (index == len)
                len++;
        }
        return len;
    }
}
View Code

有了这个,原题 Russian Doll Envelopes  的思路就清楚了:对信封的一个维度进行排序,对另一个维度计算LIS即可。

class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        if (envelopes.length == 0 || envelopes == null)
            return 0;
        Arrays.sort(envelopes, new Comparator<int []>() {
            public int compare(int[] arr1, int[] arr2) {
                if (arr1[0] == arr2[0])
                    return arr2[1] - arr1[1];
                else
                    return arr1[0] - arr2[0];
            }
        });
        int[] dp = new int[envelopes.length];
        int len = 0;
        for (int[] envelope : envelopes) {
            int index = Arrays.binarySearch(dp, 0, len, envelope[1]);
            if (index < 0)
                index = -index - 1;
            dp[index] = envelope[1];
            if (index == len)
                len++;
        }
        return len;
    }
}
View Code

 总结:这两题原本是dp的题目,但是二分在这里起到了优化的作用。

 
 
--------------------二叉树与分治法------------------- 
 
一、二叉树的前序遍历的三种方法:
1.非递归(用栈)
2.递归
3.分治
=> 树的绝大部分都可以用分治的方法解决!
 
二、分治算法:
1.quick sort => 先整体有序,再局部有序 (不需要额外空间)
2.merge sort => 先局部有序,再整体有序(先分再治)(需要额外空间)
 
三、树的题目的时间复杂度分析
所有的二叉树题目几乎都可以用分治法去做。
先分析每个节点所需的复杂度,然后看每层的 。
 
四、DFS模版
两种方法:
1.trversal : 就是普通的葱根节点开始的遍历
2.divide and conquer: 分治,分为左右子树,然后合并。
 
五、BFS模版:
1.Binart Tree level order traversal:
  • 2 Queues
  • 1 Queue and dummy node : => 在每一层结尾的位置加入yi
  • 1 Queue => 就是需要再取一下size,即当前队列中存在的元素个数,即这一层的个数)
 
 8.---------------Binary Tree Traversal  (非递归版要熟练)
前序很简单
while (!stack.isEmpty()) {
    TreeNode node = stack.pop();
    visit(node);
    if (node.right != null) {   //先压入右节点,即会后访问右节点
        stack.push(node.right);
    }
    if (node.left != null) {
        stack.push(node.left);
    }
}
View Code
中序:思路:利用stack,利用cur记录当前遍历到的点。大while循环一进去是个小while循环,作用是cur一直向左走,一边走一边存入stack,直到最左的点,循环退出。此时栈顶就是中序应该第一个遍历的点,所以pop出来存入res,然后再向右走,然后又左边走到底pop,右走...这样,最后res里存的就是[左,中,右]的顺序。
public class Solution {
    /*
     * @param root: A Tree
     * @return: Inorder in ArrayList which contains node values.
     */
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        Stack<TreeNode> stack = new Stack<TreeNode>();
        if (root == null) {
            return res;
        }
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();
            res.add(cur.val);
            cur = cur.right;
        }
        return res;
    }
}
View Code
后序:思路:后序遍历[左,右,根],需要保证的是左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点。那就使用一个pre和一个cur指针来控制。注意每次循环一次后pre=cur。
public class Solution {
    /*
     * @param root: A Tree
     * @return: Postorder in ArrayList which contains node values.
     */
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if (root == null) {
            return res;
        }
        TreeNode cur = root;
        TreeNode pre = null;
        stack.push(root);
        while (!stack.isEmpty()) {
            cur = stack.peek();  //返回栈顶对象而不移除它
            //如果上一次遍历的节点是空,或它的左儿子是当前节点,或它的右儿子是当前节点,那么应该继续遍历当前节点的左、右儿子
            if (pre == null || pre.left == cur || pre.right == cur) {
                if (cur.left != null) {
                    stack.push(cur.left);
                }else if (cur.right != null) {
                    stack.push(cur.right);
                }
            } else if (cur.left == pre) {  //如果上一次遍历的节点是当前节点的左儿子,那么这次应该遍历当前节点的右儿子
                if (cur.right != null) {
                    stack.push(cur.right);
                }
            } else {  //如果上一次遍历的节点是当前节点的右儿子,则现在应该遍历根节点
                res.add(cur.val);
                stack.pop();
            }
            pre = cur;
        }
        return res;
    }
}
View Code

 

 9.---------------Binary Tree Maximum Path Sum

 一刷: WA,知道应该用分治,但是想不出具体算法。

二刷:AC。使用一个resultType类,来维护一个单路径,和最大路径。

public class Solution {
    /*
     * @param root: The root of binary tree.
     * @return: An integer
     */
    
    private class ResultType {
        private int SinglePath; //从任意点为root向下走到任意点的最大路径,这条路可以不包含任意点
        private int MaxPath;    //树中从任意点到任意点的最大路径,这条路至少包含一个节点
        ResultType(int s, int m) {
            SinglePath = s;
            MaxPath = m;
        }
    }
    private ResultType Helper(TreeNode root) {
        //分治法
        if (root == null) {
            return new ResultType(0, Integer.MIN_VALUE);
        }
        //
        ResultType left = Helper(root.left);
        ResultType right = Helper(root.right);
        //
        int SinglePath = Math.max(left.SinglePath, right.SinglePath) + root.val;
        SinglePath = Math.max(SinglePath, 0);  //如果这条路径的值为负数,则不包括任何一个节点
        
        int MaxPath = Math.max(left.MaxPath, right.MaxPath);
        MaxPath = Math.max(MaxPath, left.SinglePath + right.SinglePath + root.val);
        
        return new ResultType(SinglePath, MaxPath);
    }
    public int maxPathSum(TreeNode root) {
        ResultType result = Helper(root);
        return result.MaxPath;
    }
}
View Code

 

  总结:树的题目,一般的解法有遍历(DFS、BFS)、分治。

 
 
 --------------动态规划-------------
 
1.动态规划因为有重复计算的子问题,就需要记忆化搜索。
  • 实现可以带哈希表的递归或者循环。面试一般提倡使用循环。
  • 自底向上的实现
  • 自顶向下的实现

2.如何想到用动归:(两个条件都需满足)

  • Maximum/Minimum;Yes/No;Count(*) 
  • 给你的数据不能更改位置。

 

3.面试中动归的题的占比
  • Matrix DP (10%) :
  • Sequence DP(40%)
  • Two Sequences DP (40%)
  • Backpack (10%)
 

 

10.----------------Triangle (记忆化搜索)

一刷:AC。注意这题的实现方式有三种:带hash的递归、自底向上的循环、自顶向下的循环。

public class Solution {
    /*
     * @param triangle: a list of lists of integers
     * @return: An integer, minimum path sum
     */
    //method1: recursive search,总耗时: 2566 ms
    // private int[][] minSum;
    // public int minimumTotal(int[][] triangle) {
    //     // write your code here
    //     if (triangle == null || triangle.length == 0)
    //         return -1;
    //     if (triangle[0] == null || triangle.length == 0)
    //         return -1;
            
    //     int n = triangle.length;        
    //     minSum = new int[n][n];
    //     for (int i = 0; i < n; i++) {
    //         for (int j = 0; j < n; j++) {
    //             minSum[i][j] = Integer.MAX_VALUE;
    //         }
    //     }
    //     return Helper(triangle, 0, 0);
    // }
    // private int Helper(int[][] triangle, int x, int y) {
    //     if (x >= triangle.length) return 0;
    //     if (minSum[x][y] != Integer.MAX_VALUE)
    //         return minSum[x][y];
    //     int leftSum = Helper(triangle, x + 1, y);
    //     int rightSum = Helper(triangle, x + 1, y + 1);
    //     minSum[x][y] = Math.min(leftSum, rightSum) + triangle[x][y];
    //     return minSum[x][y];
    // }
    
    // //method2: bottom-up,总耗时: 2451 ms
    // public int minimumTotal(int[][] triangle) {
    //     if (triangle == null || triangle.length == 0)
    //         return -1;
    //     if (triangle[0] == null || triangle.length == 0)
    //         return -1;
        
    //     int n = triangle.length;
    //     int[][] dp = new int[n][n];
    //     for (int i = 0; i < n; i++) {
    //         dp[n - 1][i] = triangle[n - 1][i];
    //     }
    //     for (int i = n - 2; i >= 0; i--) {
    //         for (int j = 0 ; j <= i; j++) {
    //             dp[i][j] = Math.min(dp[i + 1][j + 1], dp[i + 1][j]) + triangle[i][j];
    //         }
    //     }
    //     return dp[0][0];
    // }
    
    //method3: top-down
    public int minimumTotal(int[][] triangle) {
        if (triangle == null || triangle.length == 0) {
            return -1;
        }
        if (triangle[0] == null || triangle[0].length == 0) {
            return -1;
        }
        
        // state: f[x][y] = minimum path value from 0,0 to x,y
        int n = triangle.length;
        int[][] f = new int[n][n];
        
        // initialize 
        f[0][0] = triangle[0][0];
        for (int i = 1; i < n; i++) {
            f[i][0] = f[i - 1][0] + triangle[i][0];
            f[i][i] = f[i - 1][i - 1] + triangle[i][i];
        }
        
        // top down
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < i; j++) {
                f[i][j] = Math.min(f[i - 1][j], f[i - 1][j - 1]) + triangle[i][j];
            }
        }
        
        // answer
        int best = f[n - 1][0];
        for (int i = 1; i < n; i++) {
            best = Math.min(best, f[n - 1][i]);
        }
        return best;
    }
}
View Code

 

11.---------------不同的路径 II (matrix DP)
题意:矩阵中有障碍物
一刷:AC
class Solution {
public:
    /*
     * @param obstacleGrid: A list of lists of integers
     * @return: An integer
     */
    int uniquePathsWithObstacles(vector<vector<int>> &obstacleGrid) {
        int m = obstacleGrid.size();
        int n = obstacleGrid[0].size();
        if (m == 0 || n == 0) return 0;
        int dp[101][101];
        dp[0][0] = 1;
        for (int i = 1 ; i < m; i++) {
            if (obstacleGrid[i][0] == 1) dp[i][0] = 0;
            else   dp[i][0] = 1;
        }
        for (int j = 1; j < n; j++) {
            if (obstacleGrid[0][j] == 1) dp[0][j] = 0;
            else dp[0][j] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] == 1) dp[i][j] = 0;
                else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

    }
};
View Code

 

12.--------------Jamp Game || 跳跃游戏  (Sequence DP)

题意:最初位置在数组的第一个位置,数组中每个位置的值表示在那个位置能够跳跃的最大长度,求跳到最后一个位置的最少跳跃次数

一刷:AC

class Solution {
public:
    /*
     * @param A: A list of integers
     * @return: An integer
     */
    int jump(vector<int> &A) {
        int n = A.size();
        if (n == 0) return 0;
        int dp[n];
        dp[0] = 0;
        for (int i = 1; i < n; i++) dp[i] = INT_MAX;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (A[j] >= (i - j)) {
                    dp[i] = min(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[n - 1];
    }
};
View Code
 
13.--------------分割回文串 (经典Sequence DP)
 
题意:就是将字符串分成一些子串,每个串都是回文串,求最少的分割次数。
思路:找子问题:定义状态:dp[i]“前i个”字符组成的子串满足条件所需要的最少切分次数。。状态转移:就是遍历i之前的所有j,看看能不能在j这里切一刀,能够使得substr(j,i)是回文。则dp[i]=min(dp[i],dp[j]+1)
一刷:WA:原因:数组的初始化有问题。
      • 注意这题有个初始化的技巧:有时候动归要设置dp[n]的数组,有时候要设置成dp[n + 1],为什么呢?还有将每个初始化为dp[i]=i-1:因为这道题目里我们的状态定义是dp[i]表示“前i个”字符组成的子串满足条件所需要的最少切分次数。所以最长的应该是dp[len],则矩阵长度应为dp[n + 1],那么dp[0]应该为什么呢?其实这里我们都可以初始化为最大的。前1个字符最多需要分割0次,前2个字符最多需要分割1次,即dp[i]=i-1,则dp[0]=-1;
class Solution {
public:
    /**
     * @param s a string
     * @return an integer
     */
    int minCut(string s) {
        int n = s.length();
        if (n == 0) return 0;
        int dp[n + 1]; //dp[i]表示第i个位置的串最少的分割次数
        bool isPalin[n][n]; //将判断是否是回文串的操作存储下来,就可以将时间复杂度从O(n3)降到O(n2)
        
        //isPalin初始化
        for (int i = 0; i < n; i++) {
            isPalin[i][i] = true;
            if (i + 1 < n) {
                isPalin[i][i + 1] = (s[i] == s[i + 1]);
            }
        }
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i + 2; j < n; j++) {
                isPalin[i][j] = isPalin[i + 1][j - 1] && (s[i] == s[j]);
            }
        }
        
        dp[0] = -1;
        for (int i = 1; i < s.length() + 1; i++) {
            dp[i] = i - 1;
        }
        for (int i = 1; i < s.length() + 1; i++) {
            for (int j = 0; j < i; j++) { //假设在j这里切一刀
                if (isPalin[j][i - 1]) {
                    dp[i] = min(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[n];
    }
};
View Code

 

14.--------------单词拆分 Word Segmentation (Sequence DP)
 
题意:判断能否切分成几个单词,这几个单词都在词典里。
 
一刷:TLE:92% cases passed 超时,但是判断了子串是否超过字典的最大字符串长度;
class Solution {
public:
    /*
     * @param s: A string
     * @param dict: A dictionary of words dict
     * @return: A boolean
     */
    bool wordBreak(string &s, unordered_set<string> &dict) {
        bool canSeg[s.length() + 1];
        int maxWordLen = 0;
        for (auto it = dict.begin(); it != dict.end(); it++) {
            if ((*it).length() > maxWordLen) maxWordLen = (*it).length();
        }
        canSeg[0] = true;
        for (int i = 1; i < s.length() + 1; i++) {
            canSeg[i] = false;
            for (int j = 0; j < i; j++) {
                if ((i - j) > maxWordLen) continue;
                if (!canSeg[j]) continue;
                string sub = s.substr(j, i - j);
                if (dict.find(sub) != dict.end()) {
                    canSeg[i] = true;
                    break;
                }
            }
        }
        return canSeg[s.length()];
    }
};
View Code

 

=>Tips: 动态规划的注意点:初始化要想清楚是什么;还有循环的边界是什么?需要在具体题目里自己体会。

 
 15.-------------最长上升子序列 
 一刷:AC 。注意初始化为每个dp[i]=1
class Solution {
public:
    /*
     * @param nums: An integer array
     * @return: The length of LIS (longest increasing subsequence)
     */
    int longestIncreasingSubsequence(vector<int> &nums) {
        int n = nums.size();
        if (n == 0) return 0;
        int dp[nums.size()];
        for (int i = 0; i < n; i++) {
            dp[i] = 1;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        int max = 0;
        for (int i = 0; i < n; i++) {
            if (dp[i] > max) max = dp[i];
        }
        return max;
    }
};
View Code

 16.-------------最长回文子序列

一刷:35 / 83 test cases passed.l 思路错了,这个回文串应该考虑[i,j]区间的串,而不是for i for j的思路。应该设dp[i][j]表示[i,j]区间内的最长子串,考虑下一步的状态转移:i 向左,j向右。则可知应该分s[i]==s[j]来考虑,详见代码:

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = n - 1; i >= 0; --i) {
            dp[i][i] = 1;
            for (int j = i + 1; j < n; ++j) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
};
View Code

 

-------Two Sequence问题------- 

state: f[i][j]代表了第一个sequence的前i个数字 /字符 配上第二个sequence的前j个...

function: f[i][j] = 研究第i个和第j个的匹配关系 intialize: f[i][0] 和 f[0][i]

answer: f[s1.length()][s2.length()] 

 

 17.--------最长公共子序列sequence、最长公共子串(子串substring必须是连续的)
 注意要先初始化:dp[i][0] = 0 ; dp[0][j] = 0;
 
class Solution {
public:
    /*
     * @param A: A string
     * @param B: A string
     * @return: The length of longest common subsequence of A and B
     */
    int longestCommonSubsequence(string &A, string &B) {
        int n = A.size();
        int m = B.size();
        if (n == 0 || m == 0) return 0;
        int dp[m + 1][n + 1];
        for (int i = 0; i < n; i++) dp[i][0] = 0;
        for (int j = 0; j < m; j++) dp[0][j] = 0;
        
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < m + 1; j++) {
                //注意这里A[i-1]表示的是第i个
                if (A[i - 1] == B[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        } 
        return dp[n][m];
    }
};
View Code
class Solution {
public:    
    /**
     * @param A, B: Two string.
     * @return: the length of the longest common substring.
     */
    int longestCommonSubstring(string &A, string &B) {
        // write your code here
        int res=0;
        if(A.length()==0 || B.length()==0) {
            return res;
        }
        int len1=A.length();
        int len2=B.length();
        int dp[len1+1][len2+1];
        for(int i=0;i<=len1;i++){
            dp[i][0]=0;
        }
        for(int j=0;j<=len2;j++){
            dp[0][j]=0;
        }
        for(int i=1;i<=len1;i++){
            for(int j=1;j<=len2;j++){
                if(A[i-1]==B[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j]=0;
                }
                res=max(res,dp[i][j]);
            }
        }
        return res;
    }
};
View Code

 

18.---------最小编辑距离

 题意:求两个字符串的最小编辑距离,有替换、插入、删除三种操作
状态方程:
 

state: f[i][j]a的前i个字符“配上”b的前j个字符 最少要用几次编辑使得他们相等

function:

f[i][j] = MIN(f[i-1][j-1], f[i-1][j]+1, f[i][j-1]+1) // a[i] == b [j]

= MIN(f[i-1][j] + 1, f[i][j-1] + 1, f[i-1][j-1] + 1)  // a[i] != b[j]

intialize: f[i][0] = i, f[0][j] = j

answer: f[a.length()][b.length()]

class Solution {
public:
    int minDistance(string word1, string word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        if (len1 == 0) return len2;
        if (len2 == 0) return len1;
        int dp[len1 + 1][len2 + 1];
        //initialize
        for (int i = 0; i <= len1; i++) dp[i][0] = i;
        for (int j = 0; j <= len2; j++) dp[0][j] = j;
        //
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (word1[i - 1] == word2[j - 1]) 
                    dp[i][j] = dp[i - 1][j - 1];
                else 
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                dp[i][j] = min(dp[i][j], min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
            }
        }
        return dp[len1][len2];
    }
};
View Code

 

 -------背包问题------- 

模版1):

n个整数a[1..n],装m的背包
state: f[i][j] “前i”个数,取出一些能否组成和为j

function: f[i][j] = f[i-1][j - a[i]] or f[i-1][j]

intialize: f[X][0] = true; f[0][1..m] = false

answer: 能够使得f[n][X]最大的X(0<=X<=m) 

 2)

n个物品,背包为m,体积a[1..n],价值v[1..n]

state: f[i][j]表示前i个物品中,取出“若干”物品

后,体积“正好”为j的最大价值。
function: f[i][j] = max{f[i-1][j], f[i-1][j - a[i]] +

v[i]}
intialize: f[X][0] = 0, f[0][1..m] =-oo 

answer: f[n][1..m]中最大值 


 

 19.------------Backpack I II

1) 其实这种二维的可以简写为一维的,后面熟练了可以写成一维的,开始时写二维的体会思想

lass Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
    int backPack(int m, vector<int> A) {
        // write your code here
        int n = A.size();
        bool dp[n + 1][m + 1];
        for (int i = 0; i <= n; i++) dp[i][0] = true;
        for (int j = 1; j <= m; j++) dp[0][j] = false;
        dp[0][0] = true;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                if (j >= A[i - 1]) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        for (int i = m; i >= 0; i--) {
            if (dp[n][i]) return i;
        }
    }
};
View Code

 

2)

int backPackII(int m, vector<int> &A, vector<int> &V) {
        // write your code here
        //dp[i][j]表示前i个物品,取出若干个能够成体积正好为j的最大价值
        int dp[A.size() + 1][m + 1];
        for (int i = 0; i < A.size() + 1; i++) dp[i][0] = 0;
        dp[0][0] = 0;
        for (int j = 1; j < m + 1; j++) dp[0][j] = 0;
        for (int i = 1; i < A.size() + 1; i++) {
            for (int j = 0; j < m + 1; j++) {
                if (j >= A[i - 1]) {
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - A[i - 1]] + V[i - 1]);
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        int res = INT_MIN;
        for (int i = 1; i <= m; i++) {
            if (dp[A.size()][i] > res) res = dp[A.size()][i];
        }
        return res;
    }
View Code

 

20.-----------k Sum (三维动规)
题意:k sum,k个数,构成和为target的sum方案数。一般这个问方案数的可能想到用搜索,但是这里也可以用背包模型解,三维动规。
状态:dp[i][j][t]表示前i个数取j个能否构成和为t 
 
 
 
21. -----------787. Cheapest Flights Within K Stops  (图的动态规划)
 
22.------------808. Soup Servings (DP + DFS)
 
 
 
 
 
 
 
posted @ 2017-10-03 11:53  shawshawwan  阅读(5759)  评论(0编辑  收藏  举报