跳至主要內容

动态规划

HeChuangJun约 7564 字大约 25 分钟

介绍

**动态规划(Dynamic Programming, DP)**是一种用于解决复杂问题的算法思想。它将一个复杂问题分解成更小的子问题,通过保存子问题的结果,避免重复计算,从而提高算法的效率。动态规划特别适用于具有重叠子问题和最优子结构性质的问题。

动态规划的两个关键性质:

  • 重叠子问题(Overlapping Subproblems):问题可以被分解成相同的子问题,这些子问题在求解过程中会被多次用到。通过保存子问题的解,可以避免重复计算。例如,斐波那契数列的计算中,fibonacci(n) = fibonacci(n-1) + fibonacci(n-2),其中 fibonacci(n-1) 和 fibonacci(n-2) 会被多次计算。
  • 最优子结构(Optimal Substructure):一个问题的最优解包含其子问题的最优解。这意味着可以通过解决子问题的最优解来构造原问题的最优解。例如,求解最短路径问题时,子路径的最优解可以帮助找到整体路径的最优解。

动态规划的基本步骤:
定义状态:明确用哪些变量(状态)表示原问题和子问题。例如,求解最长公共子序列时,状态可以定义为 dp[i][j] 表示字符串 A 的前 i 个字符和字符串 B 的前 j 个字符的最长公共子序列长度。
确定状态转移方程:确定如何通过子问题的解来得到原问题的解,即确定状态之间的关系。例如,对于最长公共子序列问题,如果 A[i-1] == B[j-1],则 dp[i][j] = dp[i-1][j-1] + 1;否则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
初始化:确定初始状态。例如,在求解斐波那契数列时,初始状态为 dp[0] = 0,dp[1] = 1。
计算结果:根据状态转移方程和初始状态,逐步计算出问题的最优解。

动态规划的应用:
最优化问题:如背包问题、最长递增子序列、最长公共子序列、最小编辑距离等。
计数问题:如计算不同路径、硬币组合问题等。
博弈问题:如石子游戏问题、打家劫舍问题等。

  • 动态规划算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
  • 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
  • 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)
  • 动态规划可以通过填表的方式来逐步推进,得到最优解.

背包问题

  • 背包问题:有一个背包,容量为4磅 , 现有如下物品
    • 1.要求达到的目标为装入的背包的总价值最大,并且重量不超出
    • 2,要求装入的物品不能重复
  • 思路分析和图解
    • 1.背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大
    • 2.其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)
    • 3.这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。
    • 4.每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果:
      • (1) v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是0
      • (2) 当w[i]> j 时:v[i][j]=v[i-1][j]   // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略
      • (3) 当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}  // 当 准备加入的新增的商品的容量小于等于当前背包的容量,
        v[i-1][j]:上一个单元格的装入的最大值,(还没装i的值)
        v[i] : 表示当前商品的价值,v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值(装了i的值)
物品重量价格
吉他(G)11500
音响(S)43000
电脑(L)32000
    public class KnapsackProblem {

        public static void main(String[] args) {
            int[] w = {1, 4, 3};//物品的重量
            int[] val = {1500, 3000, 2000}; //物品的价值 这里val[i] 就是前面讲的v[i]
            int m = 4; //背包的容量
            int n = val.length; //物品的个数
            
            //v[i][j] 表示在前i个物品中能够装入容量为j的背包中的最大价值
            int[][] v = new int[n+1][m+1];
            //记录放入商品的情况,定一个二维数组
            int[][] path = new int[n+1][m+1];
            
            //初始化第一行和第一列, 这里在本程序中,可以不去处理,因为默认就是0
            for(int i = 0; i < v.length; i++) {
                v[i][0] = 0; //将第一列设置为0
            }
            for(int i=0; i < v[0].length; i++) {
                v[0][i] = 0; //将第一行设置0
            }
            
            //根据前面得到公式来动态规划处理
            for(int i = 1; i < v.length; i++) { //不处理第一行 i是从1开始的
                for(int j=1; j < v[0].length; j++) {//不处理第一列, j是从1开始的
                    //公式
                    if(w[i-1]> j) { // 因为我们程序i 是从1开始的,因此原来公式中的 w[i] 修改成 w[i-1]
                        v[i][j]=v[i-1][j];
                    } else {
                        //说明:
                        //因为我们的i 从1开始的, 因此公式需要调整成
                        //v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
                        //为了记录商品存放到背包的情况,我们不能直接的使用上面的公式,需要使用if-else来体现公式
                        if(v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]) {
                            v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
                            //把当前的情况记录到path
                            path[i][j] = 1;
                        } else {
                            v[i][j] = v[i - 1][j];
                        }
                        
                    }
                }
            }
            
            //输出最后我们是放入的哪些商品
            //遍历path, 这样输出会把所有的放入情况都得到, 其实我们只需要最后的放入
    //		for(int i = 0; i < path.length; i++) {
    //			for(int j=0; j < path[i].length; j++) {
    //				if(path[i][j] == 1) {
    //					System.out.printf("第%d个商品放入到背包\n", i);
    //				}
    //			}
    //		}
            
            int i = path.length - 1; //行的最大下标
            int j = path[0].length - 1;  //列的最大下标
            while(i > 0 && j > 0 ) { //从path的最后开始找
                if(path[i][j] == 1) {
                    System.out.printf("第%d个商品放入到背包\n", i); 
                    j -= w[i-1]; //w[i-1]
                }
                i--;
            }
            
        }

    }

70爬楼梯

爬楼梯需要n阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法?

时间复杂度:循环执行 n 次,每次花费常数的时间代价,故渐进时间复杂度为 O(n)。
空间复杂度:这里只用了常数个变量作为辅助空间,故渐进空间复杂度为 O(1)。
到达第 n 阶的方法数量 f(n) 等于到达第 n-1 阶的方法数量 f(n-1) 和到达第 n-2 阶的方法数量 f(n-2) 之和,即 f(n) = f(n-1) + f(n-2)f(n-2),即到达第 n-2 阶的方法数。只需再爬2阶,方法数不变就可以到达n
f(n-1),即到达第 n-1 阶的方法数。只需再爬2阶,方法数不变就可以到达n
f(n),即到达第 n 阶的方法数。
class Solution {
  public int climbStairs(int n) {
    if(n == 1) return 1;
    int[] dp = new int[n+1];
    dp[1]=1;dp[2]=2;
    for (int i = 3; i <= n; ++i) {
      dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
  }
}

118杨辉三角

给定一个非负整数numRows生成「杨辉三角」的前 numRows 行。
1.gif

时间复杂度:O(numRows^2)。
空间复杂度:O(1)。不考虑返回值的空间占用。
class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        for (int i = 0; i < numRows; ++i) {
            List<Integer> row = new ArrayList<Integer>();
            for (int j = 0; j <= i; ++j) {
                if (j == 0 || j == i) {
                    row.add(1);
                } else {
                    row.add(ret.get(i - 1).get(j - 1) + ret.get(i - 1).get(j));
                }
            }
            ret.add(row);
        }
        return ret;
    }
}

198打家劫舍

小偷计划偷窃沿街的房屋内藏有的现金,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算不触动警报装置的情况下能够偷窃到的最高金额。
示例1:输入:[1,2,3,1] 输出:4 解释:偷窃1号和3号房屋。偷窃到的最高金额=1+3=4
示例2:输入:[2,7,9,3,1] 输出:12 解释:偷窃1号、3号、5号房屋。偷窃到的最高金额=2+9+1=12

时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。
空间复杂度:O(n)。
思路:如果偷当前房屋i则金额为i-2和i的和,如果不偷的话则金额为i-1,每次比较这2种情况就行
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int length = nums.length;
        if (length == 1) {
            return nums[0];
        }
        int[] dp = new int[length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < length; i++) {
            dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[length - 1];
    }
}

279完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
示例 2:输入:n = 13 输出:2 解释:13 = 4 + 9

f[i]=1+min|i|f[j2],=1

[ \min_{x \in \mathbb{R}} \left( x^2 - 2x + 1 \right) = \frac{2+\sqrt{8}}{2} ]

时间复杂度:O(n√n),其中 n 为给定的正整数。状态转移方程的时间复杂度为 O(√n),共需要计算 n 个状态,因此总时间复杂度为 O(n√n)。
空间复杂度:O(n)。我们需要 O(n) 的空间保存状态。
class Solution {
  public int numSquares(int n) {
    int[] dp = new int[n+1];
    // 初始化dp数组,假设所有值都未能被正确计算过
    Arrays.fill(dp, n+1);
    dp[0] = 0;
    for(int i=1;i<=n;i++){
        for(int j=1;j*j<=i;j++){
            dp[i] = Math.min(dp[i],dp[i-j*j] + 1);
        }
    }
    return dp[n];
  }
}

322零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。

时间复杂度:O(Sn),其中 S 是金额,n 是面额数。我们一共需要计算 O(S) 个状态,S 为题目所给的总金额。对于每个状态,每次需要枚举 n 个面额来转移状态,所以一共需要 O(Sn) 的时间复杂度。
空间复杂度:O(S)。数组 dp 需要开长度为总金额 S 的空间。
public class Solution {
  public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, max);
    dp[0] = 0;
    for (int i = 1; i <= amount; i++) {
        for (int j = 0; j < coins.length; j++) {
            if (coins[j] <= i) {
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
            }
        }
    }
    return dp[amount] > amount ? -1 : dp[amount];
  }
}

139单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出: false

时间复杂度:O(n^2) ,其中 n 为字符串 s 的长度。我们一共有 O(n) 个状态需要计算,每次计算需要枚举 O(n) 个分割点,哈希表判断一个字符串是否出现在给定的字符串列表需要 O(1) 的时间,因此总时间复杂度为 O(n 
^2)。
空间复杂度:O(n) ,其中 n 为字符串 s 的长度。我们需要 O(n) 的空间存放 dp 值以及哈希表亦需要 O(n) 的空间复杂度,因此总空间复杂度为 O(n)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 i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

300最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列
示例 1:输入:nums = [10,9,2,5,3,7,101,18] 输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:输入:nums = [0,1,0,3,2,3] 输出:4
示例 3:输入:nums = [7,7,7,7,7,7,7] 输出:1

时间复杂度:O(n^2),其中 n 为数组 nums 的长度。动态规划的状态数为 n,计算状态 dp[i] 时,需要 O(n) 的时间遍历 dp[0…i−1] 的所有状态,所以总时间复杂度为 O(n^2)。
空间复杂度:O(n),需要额外使用长度为 n 的 dp 数组。
class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];
        dp[0] = 1;
        int maxans = 1;
        for (int i = 1; i < nums.length; i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }
}

152乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
示例 1: 输入: nums = [2,3,-2,4] 输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:输入: nums = [-2,0,-1] 输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

记 nums 元素个数为 n。
时间复杂度:程序一次循环遍历了 nums,故渐进时间复杂度为 O(n)。
空间复杂度:优化后只使用常数个临时变量作为辅助空间,与 n 无关,故渐进空间复杂度为 O(1)class Solution {
    public int maxProduct(int[] nums) {
        int length = nums.length;
        long[] maxF = new long[length];//每个元素结尾的最大和最小乘积的子数组
        long[] minF = new long[length];
        for (int i = 0; i < length; i++) {
            maxF[i] = nums[i];
            minF[i] = nums[i];
        }
        for (int i = 1; i < length; ++i) {
          //maxF[i] 为 max(maxF[i-1] * nums[i], minF[i-1] * nums[i], nums[i]),用于计算可能的最大值。
          //minF[i] 为 min(maxF[i-1] * nums[i], minF[i-1] * nums[i], nums[i]),用于计算可能的最小值。
            maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
            minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
            if (minF[i] < (-1 << 31)) {
                minF[i] = nums[i];
            }
        }
        int ans = (int) maxF[0];
        for (int i = 1; i < length; ++i) {
            ans = Math.max(ans, (int) maxF[i]);
        }
        return ans;
    }
}

416分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:输入:nums = [1,5,11,5] 输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:输入:nums = [1,2,3,5] 输出:false
解释:数组不能分割成两个元素和相等的子集。

时间复杂度:O(n×target),其中 n 是数组的长度,target 是整个数组的元素和的一半。需要计算出所有的状态,每个状态在进行转移时的时间复杂度为 O(1)。
空间复杂度:O(target),其中 target 是整个数组的元素和的一半。空间复杂度取决于 dp 数组,在不进行空间优化的情况下,空间复杂度是 O(n×target),在进行空间优化的情况下,空间复杂度可以降到 O(target)class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        if (n < 2) {
            return false;
        }
        int sum = 0, maxNum = 0;
        for (int num : nums) {
            sum += num;
            maxNum = Math.max(maxNum, num);
        }
        if (sum % 2 != 0) {
            return false;
        }
        int target = sum / 2;
        if (maxNum > target) {
            return false;
        }
        //用于表示是否存在一个子集,其和等于数组索引 j/target
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            //正向遍历则在同一轮循环中,dp[j - num] 可能已经被本轮循环的某个元素更新过了,导致错误地使用了当前元素多次。
            //逆向遍历的原因是为了防止在同一轮迭代中重复使用某个元素。
            for (int j = target; j >= num; --j) {
                dp[j] |= dp[j - num];
                //表示:如果之前已经存在一个和为 j 的子集(dp[j] 为 true),
                //或者通过加入 num 后能找到一个和为 j 的子集(dp[j - num] 为 true),那么 dp[j] 就应该为 true。
            }
        }
        return dp[target];
    }
}

32最长有效括号

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:输入:s = "(()" 输出:2 解释:最长有效括号子串是 "()"
示例 2:输入:s = ")()())"输出:4 解释:最长有效括号子串是 "()()"
示例 3:输入:s = "" 输出:0

时间复杂度: O(n),其中 n 为字符串的长度。我们只需遍历整个字符串一次,即可将 dp 数组求出来。
空间复杂度: O(n)。我们需要一个大小为 n 的 dp 数组。
假设字符串:()()(())
dp[i - 1] 仅表示 s[i - 1] 结尾的有效括号子串的长度。这个长度仅由 s[i - dp[i - 1]] 到 s[i - 1] 的子串贡献,它并不包括更前面的部分。
dp[i - dp[i - 1] - 2] 表示在 s[i - dp[i - 1] - 2] 位置结束的有效括号子串的长度。
如果 s[i - dp[i - 1] - 1](,则 s[i] 和 s[i - dp[i - 1] - 1] 配对,这样就可以把 dp[i - 1] 前面的部分(即 dp[i - dp[i - 1] - 2])也包含进来,形成更长的有效括号子串。
class Solution {
    public int longestValidParentheses(String s) {
        int maxans = 0;
        int[] dp = new int[s.length()];//以第 i 个字符结尾的最长有效括号子串的长度。
        for (int i = 1; i < s.length(); i++) {
            if (s.charAt(i) == ')') {
                if (s.charAt(i - 1) == '(') {//对应i=1和3
                    dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;//2表示"()"的长度为2
                //i - dp[i - 1] > 0确保在访问 s.charAt(i - dp[i - 1] - 1) 时,不会数组越界
                //i-dp[i-1]-1求的是去掉成对括号后的前一个字符串,并判断是否等于'(',如果相等则+2
                } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {//对应i=7时
                  //意味着在 dp[i - 1] 之前还有一个有效的括号。
                  //总数=前一个最长有效括号子串长度+本次2+前前最长有效括号子串长度,因为(之前可能还有子串
                    dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
                }
                maxans = Math.max(maxans, dp[i]);
            }
        }
        return maxans;
    }
}

62不同路径

一个机器人位于一个 m x n 网格的左上角,每次只能向下或者向右移动一步试图达到网格的右下角。总共有多少条不同的路径?

时间复杂度:O(mn)。
空间复杂度:O(mn),即为存储所有状态需要的空间。注意到 f(i,j) 仅与第 i 行和第 i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)。此外,由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m 和 n 使得 m≤n,这样空间复杂度降低至 O(min(m,n))class Solution {
    public int uniquePaths(int m, int n) {
        int[][] f = new int[m][n];
        //第一列的所有格子(即 (i,0))的路径数量都被初始化为 1。
        //因为在网格的第一列,从起点 (0,0) 到达任意一个 (i,0) 的路径只有一种:一直向下移动。
        for (int i = 0; i < m; ++i) {
            f[i][0] = 1;
        }
        for (int j = 0; j < n; ++j) {
            f[0][j] = 1;
        }
        //到达 (i,j) 的路径数量 f[i][j] 是这两个格子的路径数量之和:f[i][j] = f[i - 1][j] + f[i][j - 1]。
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                f[i][j] = f[i - 1][j] + f[i][j - 1];
            }
        }
        return f[m - 1][n - 1];
    }
}

64最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。每次只能向下或者向右移动一步。
示例 1:输入:grid = [[1,3,1],[1,5,1],[4,2,1]]输出:7 解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:输入:grid = [[1,2,3],[4,5,6]] 输出:12

时间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。需要对整个网格遍历一次,计算 dp 的每个元素的值。
空间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。创建一个二维数组 dp,和网格大小相同。
空间复杂度可以优化,例如每次只存储上一行的 dp 值,则可以将空间复杂度优化到 O(n)class Solution {
    public int minPathSum(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        int rows = grid.length, columns = grid[0].length;
        int[][] dp = new int[rows][columns];
        dp[0][0] = grid[0][0];
        //这里填充 dp 的第一列(即 (i,0)),每个格子只能从它上面的格子 (i-1,0) 走过来。
        //因此,dp[i][0] 是上面格子的最小路径和加上当前格子的权重值。
        for (int i = 1; i < rows; i++) {
            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        for (int j = 1; j < columns; j++) {
            dp[0][j] = dp[0][j - 1] + grid[0][j];
        }
        //到达 (i,j) 的最小路径和 dp[i][j] 是上面或左边两个格子路径和中的较小值再加上当前格子的权重值:
        //dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]。
        for (int i = 1; i < rows; i++) {
            for (int j = 1; j < columns; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[rows - 1][columns - 1];
    }
}

5最长回文子串

给你一个字符串 s,找到 s 中最长的 回文 子串。
示例 1:输入:s = "babad"输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:输入:s = "cbbd" 输出:"bb"

时间复杂度:O(n^2),其中 n 是字符串的长度。动态规划的状态总数为 O(n^2),对于每个状态,我们需要转移的时间为 O(1)。
空间复杂度:O(n^2),即存储动态规划状态需要的空间。
public class Solution {
    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 的子串 s[i..j] 是否为回文串
        boolean[][] dp = new boolean[len][len];
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < len; i++) {
            dp[i][i] = true;
        }

        char[] charArray = s.toCharArray();
        // 递推开始
        // 先枚举子串长度
        for (int L = 2; L <= len; L++) {
            // 枚举左边界,左边界的上限设置可以宽松一些
            for (int i = 0; i < len; i++) {
                // 由 L 和 i 可以确定右边界,即 L = j - i + 1  得
                int j = L + i - 1;
                // 如果右边界越界,就可以退出当前循环
                if (j >= len) {
                    break;
                }

                if (charArray[i] != charArray[j]) {
                    dp[i][j] = false;
                } else {
                    //当子串长度为 2 或 3 (j - i < 3) 时,s[i..j] 一定是回文子串,因为两个相等的字符(例如 "aa")或者中间只有一个字符(例如 "aba")都构成回文。
                    //对于更长的子串,s[i..j] 是否为回文取决于 s[i+1..j-1] 是否为回文(即 dp[i + 1][j - 1] 的值)。
                    if (j - i < 3) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substring(begin, begin + maxLen);
    }
}

1143最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:输入:text1 = "abc", text2 = "abc" 输出:3 解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:输入:text1 = "abc", text2 = "def" 输出:0 解释:两个字符串没有公共子序列,返回 0 。

时间复杂度:O(mn),其中 m 和 n 分别是字符串 text1和 text2的长度。二维数组 dp 有 m+1 行和 n+1 列,需要对 dp 中的每个元素进行计算。
空间复杂度:O(mn),其中 m 和 n 分别是字符串 text 1和 text 2的长度。创建了 m+1 行 n+1 列的二维数组 dp。
   ""  a  c  e
""  0  0  0  0
 a  0  1  1  1
 b  0  1  1  1
 c  0  1  2  2
 d  0  1  2  2
 e  0  1  2  3
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            char c1 = text1.charAt(i - 1);
            for (int j = 1; j <= n; j++) {
                char c2 = text2.charAt(j - 1);
                if (c1 == c2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                  //如果 c1 和 c2 不相等,说明此时只能继承前一个子问题的最优解
                  //dp[i-1][j] 表示忽略 text1[i-1] 字符的情况。
                  //dp[i][j-1] 表示忽略 text2[j-1] 字符的情况。
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

72编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符

示例 1:输入:word1 = "horse", word2 = "ros" 输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:输入:word1 = "intention", word2 = "execution" 输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

时间复杂度 :O(mn),其中 m 为 word1 的长度,n 为 word2 的长度。
空间复杂度 :O(mn),我们需要大小为 O(mn)D 数组来记录状态值。
class Solution {
    public int minDistance(String word1, String word2) {
        int n = word1.length();
        int m = word2.length();

        // 有一个字符串为空串
        //如果其中一个字符串为空串,则最小编辑距离直接等于另一个字符串的长度,
        //即需要进行的插入或删除操作的次数为 n + m。
        if (n * m == 0) {
            return n + m;
        }

        // DP 数组
        int[][] D = new int[n + 1][m + 1];

        // 边界状态初始化
        //当 word2 为空串时,需要删除 word1 的全部字符,所以 D[i][0] = i。
        //当 word1 为空串时,需要插入 word2 的所有字符,所以 D[0][j] = j。
        for (int i = 0; i < n + 1; i++) {
            D[i][0] = i;
        }
        for (int j = 0; j < m + 1; j++) {
            D[0][j] = j;
        }

        // 计算所有 DP 值
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < m + 1; j++) {
                int left = D[i - 1][j] + 1;// 删除操作 将 word1[0..i-1] 的第 i 个字符删除,然后与 word2[0..j] 比较。
                int down = D[i][j - 1] + 1;// 插入操作 在 word1[0..i] 的结尾处插入一个字符,使得 word1[0..i] 和 word2[0..j-1] 匹配。
                int left_down = D[i - 1][j - 1];// 替换操作 将 word1[0..i-1] 和 word2[0..j-1] 进行替换,如果字符不同则需要一次替换操作,left_down 的值加 1。
                if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
                    left_down += 1;
                }
                //D[i][j] 取三者中的最小值,表示三种操作中编辑距离最小的情况。
                D[i][j] = Math.min(left, Math.min(down, left_down));
            }
        }
        return D[n][m];
    }
}