文章目录
- 动态规划理论基础
- 509. 斐波那契数 - 12.21
- 剑指 Offer 10- II. 青蛙跳台阶问题 - 12.3
- 70. 爬楼梯 - 12.21
- 518. 零钱兑换 II - 12.22
- 322. 零钱兑换 - 12.22
- 62. 不同路径 - 12.23
- 63. 不同路径 II - 12.23
- 42. 接雨水 - 2.10
- 84. 柱状图中最大的矩形 - 2.10
- 剑指 Offer 10- I. 斐波那契数列
- 剑指 Offer 10- II. 青蛙跳台阶问题
- 剑指 Offer 63. 股票的最大利润
- 剑指 Offer 42. 连续子数组的最大和
- 剑指 Offer 47. 礼物的最大价值
- 剑指 Offer 46. 把数字翻译成字符串
- 剑指 Offer 48. 最长不含重复字符的子字符串
- 剑指 Offer 19. 正则表达式匹配
- 剑指 Offer 49. 丑数
- 剑指 Offer 60. n个骰子的点数
- 198. 打家劫舍 - 3.5
- 300. 最长递增子序列 - 3.7
- 931. 下降路径最小和 - 3.7
- 1143. 最长公共子序列 - 3.9
- 583. 两个字符串的删除操作 - 3.9
- 712. 两个字符串的最小ASCII删除和 - 3.9
- 72. 编辑距离 - 3.10
动态规划理论基础
什么是动态规划?
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题
,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导
出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
对于动态规划问题,有如下五步曲:
509. 斐波那契数 - 12.21
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。
解法一:递归
class Solution {
public int fib(int n) {
int sum = 0;
if(n == 0) return 0;
if(n == 1) return 1;
if(n > 1){
sum = fib(n-1) + fib(n-2);
}
return sum;
}
}
解法二:动态规划
class Solution {
public int fib(int n) {
if(n <= 1) return n;
//定义dp数组
int[] dp = new int[n+1];
//初始化
dp[0] = 0;
dp[1] = 1;
for(int i=2; i<=n; i++){ //注意从2开始
dp[i] = dp[i-1] + dp[i-2]; //状态转移方程
}
return dp[n];
}
}
剑指 Offer 10- II. 青蛙跳台阶问题 - 12.3
解析:首先考虑n等于0、1、2时的特殊情况,f(0) = 0 f(1) = 1 f(2) = 2 其次,当n=3时,青蛙的第一跳有两种情况:跳1级台阶或者跳两级台阶,假如跳一级,那么 剩下的两级台阶就是f(2);假如跳两级,那么剩下的一级台阶就是f(1),因此f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2),以此类推…
class Solution {
public int numWays(int n) {
int former1 = 1;
int former2 = 2;
int sum = 0;
if(n == 0) return 1;
if(n == 1) return 1;
if(n == 2) return 2;
else{
//动态规划,当n = 3时,f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2)
for(int i=3; i<=n; i++){
sum = (former1 + former2) % 1000_000_007;
former1 = former2;
former2 = sum;
}
return sum;
}
}
}
70. 爬楼梯 - 12.21
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
解析:动态规划
从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。
首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。
那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
所以dp[i] = dp[i - 1] + dp[i - 2] 。
class Solution {
public int climbStairs(int n) {
if(n <= 2) return n;
//构建dp数组
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];
}
}
518. 零钱兑换 II - 12.22
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
解析:完全背包应用
1、确定dp数组以及下标的含义
dp[j]:凑成总金额j的货币组合数为dp[j]
2、确定递推公式
dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。
所以递推公式:dp[j] += dp[j - coins[i]];
3、dp数组如何初始化
首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。
从dp[i]的含义上来讲就是,凑成总金额0的货币组合数为1。
4、确定遍历顺序
外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)
5、举例推导dp数组
输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:
最后红色框dp[amount]为最终结果。
class Solution {
public int change(int amount, int[] coins) {
//dp数组
int[] dp = new int[amount + 1];
//初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
dp[0] = 1;
//先遍历硬币(物品)
for(int i=0; i<coins.length; i++){
//再遍历总金额(背包)
for(int j=coins[i]; j<=amount; j++){
//递推表达式
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
}
322. 零钱兑换 - 12.22
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
解析:完全背包应用
class Solution {
public int coinChange(int[] coins, int amount) {
//最大值
int max = Integer.MAX_VALUE;
//dp[j]:凑成总金额所需的最少的硬币个数
int[] dp = new int[amount+1];
//初始化为最大值
for(int i=0; i<dp.length; i++){
dp[i] = max;
}
//当金额为0时需要的硬币数目为0
dp[0] = 0;
//
for(int i=0; i<coins.length; i++){
//正序遍历:完全背包每个硬币可以选择多次
for(int j = coins[i]; j <= amount; j++){
//只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
if(dp[j-coins[i]] != max){
//选择硬币数目最小的情况
dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
}
}
}
//如果没有任何一种硬币组合能组成总金额,返回 -1;否则返回dp[amount]
return dp[amount] == max ? -1 : dp[amount];
}
}
62. 不同路径 - 12.23
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
解析:动态规划,二维数组dp
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
//第一行都赋予 1
for(int i=0; i<n; i++) dp[0][i] = 1;
//第一列都赋予 1
for(int i=0; i<m; i++) dp[i][0] = 1;
//从上到下,从左到右的顺序
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
63. 不同路径 II - 12.23
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物
。那么从左上角到右下角将会有多少条不同的路径?
解析:遇到障碍时,dp[i][j]保持0。
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length; //行
int n = obstacleGrid[0].length; //列
int[][] dp = new int[m][n];
for(int i=0; i<m; i++){
//如果遇到障碍就停止赋值
if(obstacleGrid[i][0] == 1) break;
dp[i][0] = 1;
}
for(int j=0; j<n; j++){
//如果遇到障碍就停止赋值
if(obstacleGrid[0][j] == 1) break;
dp[0][j] = 1;
}
for(int i=1; i<m; i++){
for(int j=1; j<n; j++){
//如果遇到障碍就 跳过推导
if(obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
42. 接雨水 - 2.10
解析:我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。这样就避免了重复计算,这就用到了动态规划。
当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。
即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);
从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);
//动态规划
class Solution {
public int trap(int[] height) {
int len = height.length; //长度
if(len == 0){
return 0;
}
//从左往右遍历,记录左边最高值
int[] left_max = new int[len];
left_max[0] = height[0];
for(int i=1; i<len; i++){
left_max[i] = Math.max(left_max[i-1],height[i]);
}
//从右往左遍历,记录右边最高值
int[] right_max = new int[len];
right_max[len-1] = height[len-1];
for(int j=len-2; j>=0; j--){
right_max[j] = Math.max(right_max[j+1],height[j]);
}
//计算接水量
int sum = 0;
for(int k=0; k<len; k++){
sum += Math.min(left_max[k],right_max[k]) - height[k];
}
return sum;
}
}
84. 柱状图中最大的矩形 - 2.10
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
解析: 动态规划,本题要记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。所以需要循环查找,也就是下面在寻找的过程中使用了while。
class Solution {
public int largestRectangleArea(int[] heights) {
int len = heights.length;
// 从左往右,记录左边第一个小于该柱子的下标
int[] minLeftIndex = new int[len];
minLeftIndex[0] = -1;
for(int i=1; i<len; i++){
int temp = i-1;
//注意这里是用while来查找
while(temp >= 0 && heights[temp] >= heights[i]) temp = minLeftIndex[temp];
minLeftIndex[i] = temp;
}
// 从右往左,记录右边第一个小于该柱子的下标
int[] minRightIndex = new int[len];
minRightIndex[len-1] = len;
for(int i=len-2; i>=0; i--){
int temp = i+1;
while(temp < len && heights[temp] >= heights[i]) temp = minRightIndex[temp];
minRightIndex[i] = temp;
}
// 求和
int res = 0;
for(int i=0; i<len; i++){
int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
res = Math.max(res, sum);
}
return res;
}
}
剑指 Offer 10- I. 斐波那契数列
解析:动态规划,构建dp数组,初始化然后推导出递推表达式。
class Solution {
public int fib(int n) {
if(n <= 1) return n;
int[] dp = new int[n+1]; //dp数组
dp[0] = 0; //初始化
dp[1] = 1;
for(int i=2; i<=n; i++){ //注意这里是 <=n
//递推表达式
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
}
return dp[n];
}
}
剑指 Offer 10- II. 青蛙跳台阶问题
解析:动态规划,当n = 3时,f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2);依次类推。
class Solution {
public int numWays(int n) {
if(n <= 1) return 1;
if(n == 2) return 2;
int sum = 0;
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
//动态规划,当n = 3时,f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2)
for(int i=3; i<=n; i++){
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
}
return dp[n];
}
}
剑指 Offer 63. 股票的最大利润
解析:动态规划,动态取买入最小值和卖出最大值
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if(len <= 1) return 0;
int minCost = Integer.MAX_VALUE;
int max = 0;
for(int price : prices){
minCost = Math.min(minCost, price); //取买入最小价格
max = Math.max(max, price-minCost); //取卖出最大值
}
return max;
}
}
剑指 Offer 42. 连续子数组的最大和
解析:
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
//dp[i]表示以元素 nums[i] 为结尾的连续子数组最大和。
int[] dp = new int[len+1];
dp[0] = nums[0];
//存储较大的 子数组最大和
int res = dp[0];
for(int i=1; i<=len-1; i++){
dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
}
剑指 Offer 47. 礼物的最大价值
解析:dp(i,j) 代表从棋盘的左上角开始,到达单元格 (i,j)(i,j) 时能拿到礼物的最大累计价值。
在这里插入图片描述
class Solution {
public int maxValue(int[][] grid) {
int rows = grid.length; //行
int cols = grid[0].length; //列
int[][] dp = new int[rows][cols];
dp[0][0] = grid[0][0];
//行赋值
for(int i=1; i<rows; i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
//列赋值
for(int j=1; j<cols; j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
//从上到下,从左到右的顺序
for(int i=1; i<rows; i++){
for(int j=1; j<cols; j++){
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[rows-1][cols-1];
}
}
剑指 Offer 46. 把数字翻译成字符串
解析:先将数字转为字符串,然后定义dp[i]数组,表示前i个数字一共有多少种不同的翻译方法。
class Solution {
public int translateNum(int num) {
//将数字转为字符串
String s = String.valueOf(num);
int n = s.length();
//dp[i]表示 前i个数字共有多少种不同的翻译
int[] dp = new int[n+1];
dp[0] = 1;
for(int i=1; i<=n; i++){
//单独翻译s[i]
dp[i] = dp[i-1];
if(i > 1){
int temp = (s.charAt(i-2) - '0')*10 + (s.charAt(i-1) - '0');
if(temp >= 10 && temp <= 25){
dp[i] = dp[i] + dp[i-2]; //组合翻译
}
}
}
return dp[n];
}
}
剑指 Offer 48. 最长不含重复字符的子字符串
解法一:滑动窗口
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s == null || s.length()<=0) return 0;
int leftIndex = -1; //定义左边界,注意这里是-1
int res = 0;
Map<Character, Integer> map = new HashMap<>();
char[] ch = s.toCharArray();
for(int i=0; i<s.length(); i++){
if(map.containsKey(ch[i])){ //当前数字 在之前遍历时出现过,就更新左边界
leftIndex = Math.max(leftIndex, map.get(ch[i]));
}
map.put(ch[i], i); //添加元素
res = Math.max(res, i-leftIndex); //计算当前最大长度
}
return res;
}
}
解法二:动态规划+哈希表
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
dic.put(s.charAt(j), j); // 更新哈希表
// dp[j - 1] -> dp[j]
tmp = tmp < j - i ? tmp + 1 : j - i;
// max(dp[j - 1], dp[j])
res = Math.max(res, tmp);
}
return res;
}
}
剑指 Offer 19. 正则表达式匹配
解析:
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length() + 1, n = p.length() + 1;
boolean[][] dp = new boolean[m][n];
dp[0][0] = true;
// 初始化首行
for(int j = 2; j < n; j += 2)
dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
// 状态转移
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
if(p.charAt(j - 1) == '*') {
if(dp[i][j - 2]) dp[i][j] = true; // 1.
else if(dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) dp[i][j] = true; // 2.
else if(dp[i - 1][j] && p.charAt(j - 2) == '.') dp[i][j] = true; // 3.
} else {
if(dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) dp[i][j] = true; // 1.
else if(dp[i - 1][j - 1] && p.charAt(j - 1) == '.') dp[i][j] = true; // 2.
}
}
}
return dp[m - 1][n - 1];
}
}
解法二:
class Solution {
public boolean isMatch(String s, String p){
if(s == null || p == null)
return false;
int len_s = s.length();
int len_p = p.length();
//存放状态,默认初始值都是 false。
boolean[][] dp = new boolean[len_s+1][len_p+1];
//初始化
dp[0][0] = true;
for(int j = 1; j <= len_p; j++){
if(p.charAt(j-1) == '*')
dp[0][j] = dp[0][j-2];
}
for(int i = 1; i <= len_s; i++){
for(int j = 1; j <= len_p; j++){
//如果不为‘*’且匹配
if(p.charAt(j-1)=='.'||p.charAt(j-1)==s.charAt(i-1))
dp[i][j] = dp[i-1][j-1];
//如果是 *
else if(p.charAt(j-1)=='*'){
//如果p[j]前面的字符p[j-1]与s[i]字符不匹配,则匹配0个
if(j!=1&&p.charAt(j-2)!='.'&&p.charAt(j-2)!=s.charAt(i-1)){
dp[i][j] = dp[i][j-2];
}else{
//否则有三种情况:
//匹配0个,匹配1个,匹配多个
dp[i][j] = dp[i][j-2] || dp[i][j-1]||dp[i-1][j];
}
}
}
}
return dp[len_s][len_p];
}
}
剑指 Offer 49. 丑数
解析:
class Solution {
public int nthUglyNumber(int n) {
if(n == 1) return 1;
int a = 0, b = 0, c = 0;
int[] dp = new int[n]; //dp[i]代表第 i + 1 个丑数;
dp[0] = 1;
for(int i=1; i<n; i++){
int n2 = dp[a] * 2;
int n3 = dp[b] * 3;
int n5 = dp[c] * 5;
dp[i] = Math.min(Math.min(n2, n3), n5); //取最小值
if(dp[i] == n2) a++; //更新索引
if(dp[i] == n3) b++;
if(dp[i] == n5) c++;
}
return dp[n-1];
}
}
剑指 Offer 60. n个骰子的点数
解析:
class Solution {
public double[] dicesProbability(int n) {
//因为最后的结果只与前一个动态转移数组有关,所以这里只需要设置一个一维的动态转移数组
//原本dp[i][j]表示的是前i个骰子的点数之和为j的概率,现在只需要最后的状态的数组,所以就只用一个一维数组dp[j]表示n个骰子下每个结果的概率。
//初始是1个骰子情况下的点数之和情况,就只有6个结果,所以用dp的初始化的size是6个
double[] dp = new double[6];
//只有一个数组
Arrays.fill(dp,1.0/6.0);
//从第2个骰子开始,这里n表示n个骰子,先从第二个的情况算起,然后再逐步求3个、4个···n个的情况
//i表示当总共i个骰子时的结果
for(int i=2;i<=n;i++){
//每次的点数之和范围会有点变化,点数之和的值最大是i*6,最小是i*1,i之前的结果值是不会出现的;
//比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6*i-(i-1),化简:5*i+1
//当有i个骰子时的点数之和的值数组先假定是temp
double[] temp = new double[5*i+1];
//从i-1个骰子的点数之和的值数组入手,计算i个骰子的点数之和数组的值
//先拿i-1个骰子的点数之和数组的第j个值,它所影响的是i个骰子时的temp[j+k]的值
for(int j=0;j<dp.length;j++){
//比如只有1个骰子时,dp[1]是代表当骰子点数之和为2时的概率,它会对当有2个骰子时的点数之和为3、4、5、6、7、8产生影响,因为当有一个骰子的值为2时,另一个骰子的值可以为1~6,产生的点数之和相应的就是3~8;比如dp[2]代表点数之和为3,它会对有2个骰子时的点数之和为4、5、6、7、8、9产生影响;所以k在这里就是对应着第i个骰子出现时可能出现六种情况,这里可能画一个K神那样的动态规划逆推的图就好理解很多
for(int k=0;k<6;k++){
//这里记得是加上dp数组值与1/6的乘积,1/6是第i个骰子投出某个值的概率
temp[j+k]+=dp[j]*(1.0/6.0);
}
}
//i个骰子的点数之和全都算出来后,要将temp数组移交给dp数组,dp数组就会代表i个骰子时的可能出现的点数之和的概率;用于计算i+1个骰子时的点数之和的概率
dp = temp;
}
return dp;
}
}
198. 打家劫舍 - 3.5
解析:
class Solution {
public int rob(int[] nums) {
if(nums.length == 0) return 0;
//当小偷到达i号房屋时,能偷窃到的最高金额是dp[i]
int[] dp = new int[nums.length+1];
dp[0] = nums[0];
if(nums.length < 2){ // 每次做数组判定时都需要做数组边界判定,防止越界
return nums[0];
}
dp[1] = Math.max(nums[0], nums[1]);
for(int i=2; i<nums.length; i++){
//假如要不偷,那么就有 dp[i] = dp[i-1]。
//假如要偷,那么意味着前面的那个房子不能偷,那么有 dp[i] = num[i] + dp[i-2].
//故关系式为 dp[i] = max{dp[i-1], dp[i-2] + num[i]}.
dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]);
}
return dp[nums.length-1];
}
}
300. 最长递增子序列 - 3.7
解析:
nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。
显然,可能形成很多种新的子序列,但是我们只选择最长的那一个,把最长子序列的长度作为 dp[5] 的值即可。
class Solution {
public int lengthOfLIS(int[] nums) {
//dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
int[] dp = new int[nums.length + 1];
//初始化为1
Arrays.fill(dp, 1);
for(int i=0; i<nums.length; i++){
for(int j=0; j<i; j++){
if(nums[i] > nums[j]){
//若发现当前元素nums[i]大于nums[j],则取(dp[i], dp[j]+1)的较大值
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
}
int res = 0;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]); //更新最大长度
}
return res;
}
}
931. 下降路径最小和 - 3.7
解析:
状态转移方程,对于nums[row][col]这个位置,其最小下降路径和为:
dp[row][col] = nums[row][col] + Math.min(nums[row+1][col-1], nums[row+1][col], nums[row+1][col+1])
class Solution {
/**
* 状态转移方程,对于nums[row][col]这个位置,其最小下降路径和为:
* dp[row][col] = nums[row][col] + Math.min(nums[row+1][col-1], nums[row+1][col], nums[row+1][col+1])
* base case为落到底时,注意检查col是否越界
* TC=O(N^2),SC=O(N^2)
*/
int[][] dp;
public int minFallingPathSum(int[][] matrix) {
int n = matrix.length; //行数
dp = new int[n+1][n+1];
for(int[] arr : dp){
// 初始化dp数组
Arrays.fill(arr, Integer.MAX_VALUE);
}
int min = Integer.MAX_VALUE;
for(int i = 0; i < n; i++){
// 逐个计算第一行的每个元素的最小下降路径和,取最小的一个
min = Math.min(min, matrix(matrix, 0, i));
}
return min;
}
int matrix(int[][] matrix, int row, int col){
int n = matrix.length;
// base case
// 列越界
if(col < 0 || col >= n) return Integer.MAX_VALUE;
// 落到底
if(row == n - 1) return matrix[row][col];
// 查询备忘录
if(dp[row][col] != Integer.MAX_VALUE) return dp[row][col];
int res = matrix[row][col] + Math.min(Math.min(matrix(matrix, row+1, col-1), matrix(matrix, row+1, col)), matrix(matrix, row+1, col+1));
// 保存结果到备忘录
dp[row][col] = res;
return res;
}
}
1143. 最长公共子序列 - 3.9
解析:动态规划
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
// 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
// base case: dp[0][..] = dp[..][0] = 0
int[][] dp = new int[len1+1][len2+1];
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
// 现在 i 和 j 从 1 开始,所以要减一
if(text1.charAt(i-1) == text2.charAt(j-1)){
// s1[i-1] 和 s2[j-1] 必然在 lcs 中
dp[i][j] = dp[i-1][j-1] + 1;
}else{
// s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[len1][len2];
}
}
583. 两个字符串的删除操作 - 3.9
解析:先算出两个字符串最长公共字串的长度,再推导最小删除次数。
class Solution {
public int minDistance(String word1, String word2) {
int len1 = word1.length();
int len2 = word2.length();
//算出最长公共字串的长度
int num = longestCommonSubsequence(word1, word2);
//推导删除的次数
return len1 - num + len2 - num;
}
//求最长公共字串的长度
public int longestCommonSubsequence(String text1, String text2) {
int len1 = text1.length();
int len2 = text2.length();
// 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
// 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
// base case: dp[0][..] = dp[..][0] = 0
int[][] dp = new int[len1+1][len2+1];
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
// 现在 i 和 j 从 1 开始,所以要减一
if(text1.charAt(i-1) == text2.charAt(j-1)){
// s1[i-1] 和 s2[j-1] 必然在 lcs 中
dp[i][j] = dp[i-1][j-1] + 1;
}else{
// s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[len1][len2];
}
}
712. 两个字符串的最小ASCII删除和 - 3.9
解法一:
class Solution {
public int minimumDeleteSum(String s1, String s2) {
int n = s1.length();
int m = s2.length();
int sum = 0;
//求字符串1的总值
for(int i = 0;i<n;i++){
sum += s1.charAt(i);
}
//求字符串2的总值
for(int j = 0;j<m;j++){
sum += s2.charAt(j);
}
//求最长公共子串的长度
int[][] dp = new int[n+1][m+1];
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
if(s1.charAt(i-1)==s2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+s1.charAt(i-1);
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
//字符串1 加 字符串2,再减去 两倍的最长公共子串
return sum - 2*dp[n][m];
}
}
解法二:
class Solution {
// 备忘录
int memo[][];
public int minimumDeleteSum(String s1, String s2) {
int m = s1.length(), n = s2.length();
// 备忘录值为 -1 代表未曾计算
memo = new int[m][n];
for (int[] row : memo)
Arrays.fill(row, -1);
return dp(s1, 0, s2, 0);
}
// 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串,
// 最小的 ASCII 码之和为 dp(s1, i, s2, j)。
int dp(String s1, int i, String s2, int j) {
int res = 0;
// base case
if (i == s1.length()) {
// 如果 s1 到头了,那么 s2 剩下的都得删除
for (; j < s2.length(); j++)
res += s2.charAt(j);
return res;
}
if (j == s2.length()) {
// 如果 s2 到头了,那么 s1 剩下的都得删除
for (; i < s1.length(); i++)
res += s1.charAt(i);
return res;
}
//查备忘录
if (memo[i][j] != -1) {
return memo[i][j];
}
if (s1.charAt(i) == s2.charAt(j)) {
// s1[i] 和 s2[j] 都是在 lcs 中的,不用删除
memo[i][j] = dp(s1, i + 1, s2, j + 1);
} else {
// s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个
memo[i][j] = Math.min(
s1.charAt(i) + dp(s1, i + 1, s2, j),
s2.charAt(j) + dp(s1, i, s2, j + 1)
);
}
return memo[i][j];
}
}
72. 编辑距离 - 3.10
解析:
class Solution {
public int minDistance(String s1, String s2) {
int m = s1.length(), n = s2.length();
// 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp[i-1][j-1]
int[][] dp = new int[m + 1][n + 1];
// base case
for (int i = 1; i <= m; i++)
dp[i][0] = i;
for (int j = 1; j <= n; j++)
dp[0][j] = j;
// 自底向上求解
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i-1) == s2.charAt(j-1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(
dp[i - 1][j] + 1, //删除
dp[i][j - 1] + 1, //插入
dp[i - 1][j - 1] + 1 //替换
);
}
}
}
// 储存着整个 s1 和 s2 的最小编辑距离
return dp[m][n];
}
int min(int a, int b, int c) {
return Math.min(a, Math.min(b, c));
}
}