返回顶部

算法刷题:DP专题(9.16,持续更)

算法刷题系列上期:


力扣链接:
递归中的状态转移方程:

  1. 二叉树的最长同值路径
  2. 二叉树中的最大路径和

动态规划题目:

  1. 爬楼梯
  2. 三角形最小路径和
  3. 打家劫舍
  4. 最长重复子数组
  5. LCS/最长公共子序列
  6. LIS/最长递增子序列
  7. 最长有效括号
  8. 通配符匹配

目录


动态规划基础

递归 / 多阶段最优问题

多个阶段操作后的最优问题,如果可以转化为根据新阶段的操作结果 累加到对应旧阶段操作 累计 的结果,这是典型的递归思路,也是状态转移

子问题和状态

状态就是子问题的解

具体的说‘阶段’概念

数据容器 下一个迭代位置

  • 打家劫舍 的房子,就是数组;
  • LCSLRA 的 s1、s2 就是数组 / 串;
  • 最长有效括号 的载体也是数组 / 串;
  • 通配符匹配 的下一个阶段也是数组 / 串的 最新索引位置

等等...

状态和阶段的区别

  • 状态是从一元素中计算的结果;
  • 阶段是从一元素中计算;

状态转移方程

i - 阶段下 - 决策的定义域、f - 决策 / 问题处理 函数、f(i) - 状态

\[f(i) = \begin{cases} g(f(k)),\,\,条件1\\ g(f(t)),\,\,条件2\\ ....\\ \end{cases} \]

意义

  1. 递归树的剪枝
  2. 递归子问题/层层决策 的 全局最优解,避免贪心的鼠目寸光
  3. 空间换时间,提高时间复杂度

最后说说解题思路

  1. 找出 单阶段的 操作 和 条件判断
  2. 找出 原问题 / 子问题 / 状态
  3. 确认 单个阶段的操作(1个) 和 子问题 / 旧状态(1组) 的关系
  4. 列出 状态转移方程

递归中的状态转移方程

最长同值路径(medium)

列出状态转移方程

当前状态作为原问题 需解决的 / 递 的子问题、向上层贡献 / 归:

\(f(i)=1 + (i.l.val==i.val?f(i.l):0) + (i.r.val==i.val?f(i.r)?0)\)

当前状态作为 原问题本身,看看能不能全局最优:

\(f(i)=1 + max((i.l.val==i.val?f(i.l):0),\,\,(i.r.val==i.val?f(i.r)?0))\)

代码实现(3ms)

class Solution {
    int ans = 0;
    public int longestUnivaluePath(TreeNode root) {
        if(root == null) return 0;
        dfs(root);
        return ans - 1; // -1 是把 节点数 表示为 边数
    }
    int dfs(TreeNode root){
        if(root == null){
            return 0;
        }
        int lft = dfs(root.left);
        int rgt = dfs(root.right);

        int res = 1;
        // 假设作为根节点 / 全局最优
        if(root.left != null){
            if(root.left.val == root.val){
                res += lft;
            } else lft = 0;
        }
        if(root.right != null){
            if(root.right.val == root.val){
                res += rgt;
            } else rgt = 0;
        } if(ans < res) ans = res;
        // 假设作为向上层的贡献
        // 告诉上层,以我为起点的同值路径长度是多少
        return 1 + Math.max(lft, rgt);
    }
}

二叉树中的最大路径和(hard)

列出状态转移方程

作为原问题的子树、子问题的解(返回给顶层):

\(f(x)=x.val + max(0,\,\,f(x.left),\,\,f(x.right))\)

作为原问题,看当前局部 是否是 全局最大值

\(f(x)=x.val + max(0,\,\,f(x.left),\,\,f(x.right),\,\, f(x.left)+f(x.right))\)

代码实现 (0ms)

class Solution {
    int ans = -0x7fffffff; // 最大值记录
    public int maxPathSum(TreeNode root) {
        f(root);
        return ans;
    }
    int f(TreeNode root){
        if(root == null) return 0;
        int lft = f(root.left);
        int rgt = f(root.right);
        // 后序位置
        // 路径的几种情况
        int a = root.val;
        int b = root.val + lft;
        int c = root.val + rgt;
        int d = root.val + lft + rgt;
        // 作为根节点,尝试 更新 最大值的记录ans
        int curmax = Math.max(Math.max(a, b), Math.max(c, d));
        if(curmax > ans) ans = curmax;
        // 作为子节点 贡献上层
        int retmax = Math.max(Math.max(a, b), c);
        return retmax;
    }
}

基础题目

爬楼梯(easy)

思路分析

  1. 当前阶段? 这个位置,从 \((i-2)\)\((i-1)\) 的台阶 \(i\) 位置
  2. 一组阶段?如何从 \(0\) 位置 \((i-2)\) / \((i-1)\)
  3. 状态转移方程:略

代码实现:

class Solution {
    public int climbStairs(int n) {
        int [] f = new int [n + 1];
        f[0] = 1, f[1] = 1;
        for(int i = 2; i <= n; i++)
            f[i] = f[i - 1] + f[i - 2];
        return f[n];
    }
}

三角形最小路径和(medium)

思路分析

  1. 问题的子结构
    • 原问题:从尖端到三角形最下层的最短路径
    • 子问题 1:从尖端到最下层第 j 节点的最短路径
      • 子问题 1-1:从尖端到第 i 层第 j 节点的最短路径
  2. 定义域:\((i,\,j)\)
  3. 状态转移方程:

\[f(i, j)= \begin{cases} f(i-1, j) + ta[i][j],\,\,j = 0\\ min(f(i-1,j-1),f(i-1,j))\,+\,ta[i][j],\,\,0 < i < 上层边界\\ f(i-1, j-1) + ta[i][j],\,j=上层边界\\ \end{cases} \]

代码实现(3ms)

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        if(triangle == null || triangle.isEmpty()) return 0;
        int n = triangle.size();
        int [][] f = new int [n][n];
        // ift0
        f[0][0] = triangle.get(0).get(0);
        for(int i = 1; i < n; i++){
            for(int j = 0; j < i; j++){
                int lf = 0x3fffffff, rf = f[i - 1][j];
                if(j > 0) lf = f[i - 1][j - 1];
                // 最开始是 f[i][j] = lf < rf ? lf : rf; //
                f[i][j] = (lf < rf ? lf : rf) + triangle.get(i).get(j);
            } // f[i][j] = f[i - 1][j - 1]; 
            // 最开始忘记 triangle.get(i).get(j) 这部分了
            f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
        }
        int min = 0x3fffffff;
        for(int i = 0; i < n; i++){
            if(min > f[n - 1][i]){
                min = f[n - 1][i];
            }
        } return min;
    }
}

打家劫舍(medium)

思路分析

代码实现(0ms)

class Solution {
    public int rob(int[] u) {
        if(u.length == 0) return 0;
        if(u.length == 1) return u[0];

        int [] f = new int[u.length];
        f[0] = u[0], f[1] = Math.max(u[0], u[1]);
        for(int i = 2; i < f.length; i++)
            f[i] = f[i - 2] + u[i] > f[i - 1] ? f[i - 2] + u[i] : f[i - 1];
        return f[f.length - 1];
    }
}

最长重复子数组(medium)

思路分析

  1. 问题子结构:
    • 原问题:求 s1 前 n 项和 s2 前 m 项进行匹配,找到最长重复子串
    • 子问题:求 s1 前 i 项和 s2 前 m 项进行匹配,找到最长重复子串
  2. 定义域:\(i\) 表示 \(a\) 串的尾端位置,\(j\) 表示 \(b\) 串的尾端位置
  3. 状态值:\(f(i,j)\) 表示 a[i] 和 b[j] 匹配时,最后位置(\((i-1,j-1)\))已经匹配到的子串长度
  4. 状态前进:\((i-1, j-1) -> (i, j)\)
  5. 状态转移方程:

\[f(i, j)= \begin{cases} f(i-1,j-1)+1,\,\,a[i] = b[j]\\ 0,\,\,\,\,\,\,\,\,a[i] \neq b[j]\\ \end{cases} \]

代码实现(31ms)

class Solution {
    public int findLength(int [] s1, int [] s2) {
        int n1 = s1.length, n2 = s2.length;
        int [][] f = new int[n1][n2];
        // 初始值
        int ans = 0;
        for(int i = 0; i < n1; i++){
            if(s1[i] == s2[0]){
                f[i][0] = 1;
                if(ans < 1) ans = 1;
            }
        }
        for(int j = 0; j < n2; j++){
            if(s1[0] == s2[j]){
                f[0][j] = 1;
                if(ans < 1) ans = 1;
            }
        }
        // i, j 遍历 状态定义域
        for(int i = 1; i < n1; i++){
            for(int j = 1; j < n2; j++){
                // 状态转移
                f[i][j] = g(f[i - 1][j - 1], s1[i], s2[j]);
                // f[i][j] = f[i - 1][j - 1] + g(s1[i], s2[j]);
                if(f[i][j] > ans) ans = f[i][j];
            }
        }
        return ans;
    }
    int g(int std, int c1, int c2){
        return c1 == c2 ? std + 1 : 0;
    }
}

LCS / 最长公共子序列(medium)

思路分析

  1. 问题的子结构:
    • 原问题:s1 的前 n 项和 s2 的前 m 项已经匹配到的最长子序列
    • 子问题:s1 的 i 前缀和 s2 的 j 前缀已经匹配到的最长子序列
  2. 定义域:\((i,\,j)\)
  3. 进入下一阶段时的情况:
    • 如果 \(a[i]=b[j]\) ,那么当前阶段要在上一阶段状态的基础上
      符合条件,继承上个状态的积累,并加上 新的一个单位的积累
    • 如果 \(a[i]\neq b[j]\) ,那么要找到 \(a[i]、b[j]\) 不同时出现的情况的最大边界;
      不符合条件,继承 最近最多 状态的积累,只继承
  4. 状态转移方程:

\[f(i, j) = \begin{cases} f(i-1, j-1) + 1,\,\,\,\,a[i] = a[j]\\ max(f(i-1, j),\,f(i, j-1))\,,a[i] \neq b[j]\\ \end{cases} \]

代码实现(15ms)

class Solution {
    public int longestCommonSubsequence(String s1, String s2) {
        int n1 = s1.length(), n2 = s2.length();
        int [][] f = new int [n1 + 1][n2 + 1];
        for(int i = 1; i <= n1; i++){
            char c1 = s1.charAt(i - 1);
            for(int j = 1; j <= n2; j++){
                char c2 = s2.charAt(j - 1);
                if(c1 == c2) 
                    f[i][j] = f[i - 1][j - 1] + 1;
                else 
                    f[i][j] = Math.max(f[i][j - 1], f[i - 1][j]);
            }
        } return f[n1][n2];
    }
}

LIS / 最长递增子序列

思路分析

代码实现

class Solution {
    public int lengthOfLIS(int[] s) {
        if(s.length == 0) return 0;
        int maxans = 1;
        int [] f = new int [s.length + 1];
        f[1] = 1;
        for(int i = 2; i < f.length; i++){
            f[i] = 1;
            for(int j = 1; j < i; j++){
                // f[i] = Math.max(f[j], f[i]);是不行的
                // 反例:[1, 3, 6, 7, 9, ** 4 ** , 10]

                // 而且每个状态 / f[j] 维护的是以 s[j] 为结尾的最长递增序列
                // 所以 f[-1] 并不一定是全局的最长递增序列
                // 但是所有的最长递增子序列 一定在 f 中
                // 所以最后遍历一遍 f
                if(s[j - 1] < s[i - 1]){
                    f[i] = Math.max(f[j] + 1, f[i]);
                } // if(f[i] > maxans) maxans = f[i];
            }
        }
        for(int ans : f){
            if(ans > maxans) maxans = ans;
        } return maxans;
    }
}

最长有效括号(hard)

所求问题无后效性,就一定作为状态吗?

所求问题可能不好维护;

  • 比如求一段区间内符合条件的最长子串,如果这段区间里有很多段子串,随着区间的扩大,维护多个子串的代价很大
  • 所以不能因为所求问题是无后效性的,就将其作为状态维护起来,可能需要简化问题,让状态尽可能单一

思路分析

  1. 问题的子结构:
  • 所求问题:\([0,\,n)\) 区间上的最长子串
  • 简化后的原问题:\([0,\,n)\) 区间上,以 \(n-1\) 位置为尾端的子串的长度
    • 子问题:
  1. 定义域:\(i\,\epsilon\,[0,\,n)\)
  2. 状态转移方程:

\[f(i)= \begin{cases} 0,\,\,s[i]="("\\ s[i]=")" \begin{cases} f(i-2)+2,\,\,\,\,s[i-1] ="("\\ f(i-1)+f(i-f(i-1)-2)+2,\,\, \begin{cases} s[i-1]=")"\\ s[i-f(i-1)-1]="("\\ \end{cases} \end{cases} \end{cases} \]

代码实现(1ms击败100%)

class Solution {
    public int longestValidParentheses(String s) {
        if(s.length() < 2) return 0;
        char [] cs = s.toCharArray();
        int [] f = new int [cs.length];
        int ans = 0;
        for(int i = 1; i < cs.length; i++){
            // if(cs[i] == '(') 
            //     f[i] = 0;
            if(cs[i] == ')') {
                if(cs[i-1] == '(') 
                    f[i] = (i > 1 ? f[i-2] : 0) + 2;
                else { // cs[i-1] == ')'
                    int idx = i - f[i-1] - 1;
                    if(idx >= 0 && cs[idx] == '(')
                        f[i] = f[i - 1] + (idx > 0 ? f[idx - 1] : 0) + 2;
                        // f[i] = (idx > 0 ? f[idx - 1] : 0) + 2
                }
            }
            if(ans < f[i]) ans = f[i];
        } return ans;
    }
}

通配符匹配

空字符串情况

空字符串用 \(s[0]\) 表示
如果模板 \(p\) 和串 \(s\) 都为空,\(return\,\,f[0][0]\) 一定为 \(true\)
如果串为空,但模板不为空,模板只有 * 能匹配
遍历:

for j in range(p.length):
    if p[j - 1] is "*":
        f[0][j] = True
    else break

思路分析

  1. 问题的子结构:
    • 原问题:\(s[0,\,n)\)\(p[0,\,m)\) 是否匹配
    • 子问题:\(s\) 的子串 \([0,\,i)\)\(p\) 的子串 \(p[0,\,j)\) 是否匹配
  2. 定义域:\((n,m)\)
  3. 进入下一阶段时的情况:
  • \(s[i] = p[j]\,\,->f[i][j] = f[i-1][j-1]\)
  • \(s[i] \neq p[j]\)
    • \(p[i] = "?"\,\,->f[i][j] = f[i-1][j-1]\)
      符合 ? 或相等 条件 - 继承上个状态的积累
    • \(p[i] = "*"\,\,->f[i][j] = f[i][j-1]\,\,OR\,\,f[i-1][j]\)
      符合 * 条件 - 继承 最近最多 状态的积累,只继承
    • \(else\,\,False;\)
      不符合条件,False,清除积累
  1. 状态转移方程:

\[f(i,j)= \begin{cases} f(i-1,\,j-1) ,\,\,\,(s[i]=p[j])\,\,\bigvee\,\,\,(s[i]\neq p[j]\bigwedge p[j]="?")\\ f(i,\,\,j-1)\,\bigvee\,f(i-1,\,\,j),\,s[i]\neq p[j]\bigwedge p[j]="*"\\ False,\,\,\,\,else \end{cases} \]

代码实现(12ms击败88%)

class Solution {
    public boolean isMatch(String s, String p) {
        char [] sc = s.toCharArray();
        char [] pc = p.toCharArray();
        boolean [][] f = new boolean[sc.length + 1][pc.length + 1];
        f[0][0] = true;
        for(int j = 1; j <= pc.length; j++){
            if(pc[j - 1] == '*'){
                f[0][j] = true;
            } else break;
        }
        for(int i = 1; i <= sc.length; i++){
            for(int j = 1; j <= pc.length; j++){
                if(sc[i - 1] == pc[j - 1])
                    f[i][j] = f[i - 1][j - 1];
                else {
                    if(pc[j - 1] == '?')
                        f[i][j] = f[i - 1][j - 1];
                    else if(pc[j - 1] == '*')
                        f[i][j] = f[i][j - 1] || f[i - 1][j];
                }
            }
        } return f[sc.length][pc.length];
    }
}

背包DP题目

分割等和子集

posted @ 2023-09-16 20:20  你好,一多  阅读(7)  评论(0编辑  收藏  举报