LeetCode动态规划的解题思路
动态规划
动态规划,其实就是找规律,总结公式/方程。
动态规划,类似于数学归纳法。
关键的思想在「自底向上」和「空间换时间」。
动态规划,可以使用一维数组,有时也会用到二维数组。
应用场景
“动态规划”可以用于子序列、最大/小值问题、回文子串的求解。
一维数组dp[i] 的动态规划的几个步骤:
- 确定数组dp[i]的下标i以及dp[i]值的含义,比如经典的LeetCode70爬楼梯, 爬到第i层楼梯,有dp[i]种方法;
- 确定动态规划的状态转移方程(递推公式)。比如,爬楼梯的公式:dp[i] = dp[i-1] + dp[i-2];
- dp数组的初始化:初始化值,dp[0]的值是多少 , dp[1]的值又是多少;
- 确定遍历顺序:分析递推顺序应该是从前往后,还是从后往前。还有就是,要从哪一个下标开始遍历;
LeetCode70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
public int climbStairs(int n) {
if (n <= 0) {
return 0;
}
//dp数组的初始化
//爬到第i层楼梯,有dp[i]种方法;
int[] dp = new int[n + 2];
//dp数组的初始化
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
//确定动态规划的状态转移方程(递推公式)
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
LeetCode53. 最大子数组和
- 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
class Solution {
public int maxSubArray(int[] nums) {
int length=nums.length;
int[] dp=new int[length];
//初始值
dp[0]=nums[0];
int maxSum=nums[0];
for(int i=1;i<length;i++) {
//dp[i]表示以第 i 个数结尾的「连续子数组的最大和」
//由于子数组是以nums[i]结尾,
//如果dp[i-1]是正数,那么dp[i]最大值是dp[i-1]+nums[i]
//如果dp[i-1]是负数,那么dp[i]最大值是nums[i]
//状态转移方程如下:
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
//找出不同的数组元素结尾的最大值。
maxSum=Math.max(dp[i],maxSum);
}
return maxSum;
}
}
用二维数组的动态规划:
二维数组的动态规划,跟一维数组的动态规划,基本是一样的。
- 设定状态。
二维 dp 问题,可以使用二维数组 dp[i][j],第一维的下标i可以表示A事物的状态,第二维的下标j可以表示B事物的状态。
比如LeetCode122的买卖股票,题中有两个状态,一个是天数,一个是是否持有股票,
定义dp[i][0]表示第 i天交易完后手里没有股票的最大利润,
dp[i][1] 表示第 i天交易完后手里持有一支股票的最大利润。 - 思考状态转移方程(也就是公式)。
找规律,找出 dp[i][j]是怎么由dp[i-1][j]、 dp[i-1][j-1] 推导得到的 - 考虑初始值。
也就是 dp[0][0] 、 dp[0][1] 之类的初始值。 - 考虑输出。
求出 dp[len - 1][j] (也可能是其他的如dp[len - 1][j]) 的值。
二维数组动态规划:122. 买卖股票的最佳时机 II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
链接: https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/
解答:定义状态 dp[i][0] 表示第 i 天交易完后手里没有股票的最大利润,dp[i][1] 表示第 i 天交易完后手里持有一支股票的最大利润(i 从 0 开始)。
考虑 dp[i][0] 的转移方程,如果这一天交易完后手里没有股票,那么可能的转移状态为前一天已经没有股票,即 dp[i−1][0],或者前一天结束的时候手里持有一支股票,即 dp[i−1][1],这时候我们要将其卖出,并获得 prices[i]的收益。
dp[i][0]=max{dp[i−1][0],dp[i−1][1]+prices[i]}
对于dp[i][1],按照同样的方式考虑转移状态,那么可能的转移状态为前一天已经持有一支股票,即 dp[i−1][1],或者前一天结束时还没有股票,即 dp[i−1][0],这时候我们要将其买入,并减少 prices[i] 的收益。可以列出如下的转移方程:
dp[i][1]=max{dp[i−1][1],dp[i−1][0]−prices[i]}
public int maxProfit(int[] prices) {
if (prices==null) {
return 0;
}
int length = prices.length;
//定义状态 dp[i][0] 表示第 i 天交易完后手里没有股票的最大利润,
//dp[i][1] 表示第 i 天交易完后手里持有一支股票的最大利润(i 从 0 开始)。
int[][] dp = new int[length][2];
dp[0][0]=0;
dp[0][1]=-prices[0];
for (int i=1;i<length;i++) {
dp[i][0] = Math.max (dp[i-1][0], dp[i-1][1]+prices[i]);
dp[i][1] = Math.max (dp[i-1][1], dp[i-1][0]-prices[i]);
}
return dp[length-1][0];
}
LeetCode5 :最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
/**
* 对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。
*
* P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串。(s[i:j] 类似)
*
* (1)状态转移方程:
*
* P(i,j)=P(i+1,j−1)∧(Si==Sj)
* 也就是:只有 s[i+1:j−1] 是回文串,并且 s 的第 i 和 j 个字母相同时,s[i:j] 才会是回文串。
*
* (2)初始值:
* 对于长度为 1 的子串,它显然是个回文串;
* 也就是 P(i,i)=true;
*
* 对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
* 也就是P(i,i+1)= (Si==Si+1)
*
* 详情见: https://leetcode.cn/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/
*
* @param s
* @return
*/
public String longestPalindrome(String s) {
int length = s.length();
if (length < 2) {
return s;
}
int maxLength = 1;
int begin = 0;
// dp[i][j] 表示从字符数组的下标i开始到下标j的子序列 s[i,i+1,..j] 是否是回文串
boolean[][] dp = new boolean[length][length];
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < length; i++) {
dp[i][i] = true;
}
char[] charArray = s.toCharArray();
// 递推开始
// 先枚举子序列的长度
for (int subLength = 2; subLength <= length; subLength++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int left = 0; left < length; left++) {
// 由 subLength 和 left 可以确定右边界,即 right - left + 1 = subLength 得
int right = subLength + left - 1;
// 如果右边界越界,就可以退出当前循环
if (right >= length) {
break;
}
if (charArray[left] != charArray[right]) {
//子序列的左边界和右边界的值,如果不相同,就不可能是回文串
dp[left][right] = false;
} else {
//左边界和右边界相同时,子序列的长度为1或为2,都是回文串。
if (right - left < 3) {
dp[left][right] = true;
} else {
//状态转移方程,左边界和右边界相同时,
// 从left到right的子序列,跟从left + 1 到 right - 1的子序列,要么都是回文串,要么都不是回文串。
dp[left][right] = dp[left + 1][right - 1];
}
}
// 只要 dp[left][subLength] == true 成立,就表示子串 s[left..subLength] 是回文,此时记录回文长度和起始位置
if (dp[left][right] && right - left + 1 > maxLength) {
//找出最大子序列的长度
maxLength = right - left + 1;
begin = left;
}
}
}
return s.substring(begin, begin + maxLength);
}
常见题目
动态规划常见题:LeetCode70,LeetCode121,LeetCode122,LeetCode5
参考资料:
《labuladong的算法小抄》
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/solution/bao-li-mei-ju-dong-tai-gui-hua-chai-fen-si-xiang-b/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2018-06-22 spring集成mybatis配置多个数据源,通过aop自动切换