【算法题】动态规划学习总结

129 阅读2分钟

推荐一下大佬的文章,受益匪浅一份给算法新人们的「动态规划」讲解

什么是动态规划,也就是核心?也可以是做题思路

  • 问题能划分为几个小问题
  • 问题的解只依赖前面问题的解

什么题适合 动态规划?

  • 这种找数组找满足条件的最优解的问题

类型1

leetcode 42 接雨水

  • 问题能划分为几个小问题?可以,从0到1时的雨水量、从0到2时的雨水量、从0到3时的雨水量......
  • 问题的解只依赖前面问题的解?是的

核心思路:

  • 对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 i 处能接的雨水量等于下标 i 处的水能到达的最大高度减去height[i]。
  • 也就是俩dp数组
public int trap(int[] height) {
    int n = height.length;
    // 显而易见,从0至少有 3 根柱子才能存住水
    if(n < 3) return 0; 
    int[] leftMax = new int[n];
    leftMax[0] = height[0];
    for(int i = 1; i < n; i++) {
        leftMax[i] = Math.max(leftMax[i - 1], height[i]);
    }

    int[] rightMax = new int[n];
    rightMax[n - 1] = height[n - 1];
    for(int i = n - 2; i >= 0; i--) {
        rightMax[i] = Math.max(rightMax[i + 1], height[i]);
    }
    int res = 0;
    for(int i = 0; i < n; i++) {
        // 核心逻辑
        res += Math.min(leftMax[i], rightMax[i]) - height[i];
    }
    return res;
}

leetcode 5 最长回文字符串

  • 问题能划分为几个小问题?可以,字符串从i到j时是回文,那么去掉头尾,i+1到j-1一定也是回文
  • 问题的解只依赖前面问题的解?是的 image.png

核心思路:

  • 动态规划,什么时候可以用动态规划呢?数组的某种情况下的最优解,
  • 如果用dp[i][j]=1表示字符串从i到j是否是回文
  • 该题,明显就是回文,回文去掉头尾,他一定也是回文,也就是dp[i+1][j-1]=1,
  • 也就是如果dp[i][j]=1,且s[i-1]=s[j+1],dp[i-1][j+1]=1
  • 这很明显就要有条件,j-1 >= i+1,也就是j-i >=2, 也就是有两种情况,
    • 1.j-i=2,也就是3个字符判断回文,只要s[j]=s[i],即可
    • 2.j-i > 2,递归找dp[i+1][j-1]
  • 如果j-i<2呢,
  • 很明显有j=i,j=i+1俩种情况,
    • 1.j=i,他一定是回文,
    • 2.j=i+1且 s[j]=s[i],他是回文
public String longestPalindrome(String s) {
    int len = s.length();
    if (len < 2) {
        return s;
    }
    int maxLen = 1;
    int begin = 0;
    // dp[i][j] 表示 s[i..j] 是否是回文串
    int[][] dp = new int[len][len];
    // 初始化:所有长度为 1 的子串都是回文串
    for (int i = 0; i < len; i++) { dp[i][i] = 1; }
    char[] charArray = s.toCharArray();
    // 递推开始
    // 先枚举子串长度
    for (int L = 2; L <= len; L++) {
        // 枚举左边界,左边界的上限设置可以宽松一些
        for (int i = 0; i < len; i++) {
            // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
            int j = L + i - 1;
            // 如果右边界越界,就可以退出当前循环
            if (j >= len) { break;}
            if (charArray[i] == charArray[j]) {
                if (j - i < 3) { dp[i][j] = 1; }
                else { dp[i][j] = dp[i + 1][j - 1]; }
            }
            // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
            if (dp[i][j] ==1 && j - i + 1 > maxLen) {
                maxLen = j - i + 1;
                begin = i;
            }
        }
    }
    return s.substring(begin, begin + maxLen);
}

leetcode 22. 括号生成

  • 问题能划分为几个小问题?可以,i=0时的括号生成方案、i=1时的括号生成方案、i=2时的括号生成方案......
  • 问题的解只依赖前面问题的解?是的,长度n时的解只依赖<n时的解,因此可以用动态规划

核心思路:

  • n时的括号生成方案
    • ( + p时括号生成的全部方案 + ) + q时括号生成的全部方案,其中p+q = n-1
  • 也就是说,第n个括号的位置方案
  • 不能重复,p和q的组合不能重复出现

java版本代码

image.png

// 动态规划
public static List<String> generateParenthesis(int n) {
    if (n==0) return new ArrayList<>();
    List<String> dp0 = new ArrayList<>();
    dp0.add("");
    List<String> dp1 = new ArrayList<>();
    dp1.add("()");
    if (n==1) return dp1;
    List<List<String>> dp = new ArrayList<>();
    //dp 0
    dp.add(dp0);
    //dp 1
    dp.add(dp1);
    for (int i =2;i<=n;i++){
        List<String> dpn = new ArrayList<>();
        dp.add(dpn);
        int p=0,q=i-p-1;
        while(q >=0){
            List<String> dpp = dp.get(p);
            List<String> dpq = dp.get(q);
            for (int x = 0;x < dpp.size();x++){
                for (int y=0;y<dpq.size();y++){
                    String s = "(" + dpp.get(x) + ")" + dpq.get(y);
                    dpn.add(s);
                }
            }
            p++;
            q=i-p-1;
        }
    }
    return dp.get(n);
}

leetcode 32. 最长有效括号

image.png

暴力解法:

滑动窗口,判断窗口内子串是否是连续的有效括号

// 暴力滑动窗口,从大到小 on3,超时
public static int longestValidParentheses1(String s) {
    if (s == null || s.length() == 0 || s.length() == 1) return 0;
    int maxLen = 2*(s.length()/2);
    while(maxLen > 0){
        int i = maxLen;
        for (int j =0;j + i - 1< s.length();j++){
            String str = s.substring(j,j+i);
            if(isValid(str)) return i;
        }
        maxLen -=2;
    }
    return maxLen;
}

private static boolean isValid(String str){
    Stack<Character> stack = new Stack<Character>();
    if (str.charAt(str.length()-1) == '(') return false;
    for(int i =0;i<str.length();i++){
        char c = str.charAt(i);
        if ('(' == c){
            stack.push(c);
        }else {
            if (stack.isEmpty()) return false;
            stack.pop();
        }
    }
    return stack.isEmpty();
}

动态规划

  • 问题能划分为几个小问题?可以,我们可以从s头到尾依次判断最长连续有效括号字符串长度
  • 问题的解只依赖前面问题的解?可以,长度n时的解只依赖<n时的解,因此可以用动态规划
  • 核心思路:
    • i=n且是)时,找到对应的前面的(下标,减dp[n-1],因为要去掉前面的连续有效括号字符串长度, 也就是n - dp[n-1] - 1位, 如果是(,dp[n] +2;
    • 然后加上内部的连续有效括号字符串长度,也就是这对 ()内部的值,dp[n] + dp[n-1]
    • 然后看看这对()前面是否有?也要加,dp[n] + dp[n - dp[n-1] - 1]
public int longestValidParentheses(String s){
    // 动态规划数组,判断下标i(包括i)前最长连续有效括号字符串
    // 如果是左括号,不改变其默认值0
    int[] dp = new int[s.length()];
    // 找最长,留一个标志
    int maxLen = 0;
    for(int i = 0;i < s.length();i++){
        char c = s.charAt(i);
        if (c=='(') continue;
        //右括号计算dp值
        // 1.找该右括号对应的左括号下标
        // 1.1.要减去前一位的dp值,因为如果前一位有dp值,说明前面有一些有效的括号了,要去再前一位找,也就是下标 i-dp[i-1] -1
        // 1.2 0的话,说明是左括号,匹配上了, 不可能是其他值,因为是其他值的话,前一位的dp早就加上了,所以这就只能是该有括号对应的左括号下标,
        // 如果他不是左括号,该右括号没有对应的,也是0
        if (i-1 >= 0 && i-dp[i-1] -1 >=0){
            if (s.charAt(i-dp[i-1] -1) == ')') continue;
            dp[i]  = 2;
            // 2.找该[i - dp[i-1] -1,i]子串内是否有连续有效括号字符串,也就是判断前一位是否有dp值?
            // 2.1 dp[i-1]=0,说明没有,+0
            // 2.2 dp[i-1]!=0,说明内部也有,+dp[i-1]
            dp[i] += dp[i-1];
            //3. 连续么,找前面一位是否能连上呢?
            // 也就是判断该子串左下标i - dp[i-1] -1前一位的dp值是否为0,
            // 如果是0,说明连不上,+0, 如果连上了,+dp值
            if (i-dp[i-1] -1 -1 >=0){
                dp[i] += dp[i-dp[i-1] -1 -1 ];
            }
            // 判断是否是最大长度
            if (dp[i] > maxLen) maxLen = dp[i];
        }
    }
    return maxLen;
}