LeetCode-动态规划(持续更新中)
本文主要对LeetCode中的动态规划类型题进行解析,讲解一些问题的解题思路和动态规划的基础解法。
1.动态规划基础解法
我将动态规划基础解法分为三个步骤,具体如下:
- 定义数组dp,通过dp数组记录最终的函数返回值;
- 确认递推公式,确认数组之间的递推关系,如dp[i] = dp[i-1] + 1;
- 定义边界条件,确认边界处的临界值,如 dp[0] = 1 等。
2.动态规划题目类型
2.1 一维动态规划
70.爬楼梯
解题思路:
1.定义dp[i] 表示爬i层台阶的方法数;
2.找递推公式, 第i层台阶只能从 i-1层或i-2层爬上,故dp[i] = dp[i-1] + dp[i-2];
3.定义边界 dp[1] = 1,dp[2] = 2;
public int climbStairs(int n) {
if(n<=2){
return n;
}
//定义数组 dp[i]表示爬i层台阶的方法数
int[] dp = new int[n+1];
// 定义递推关系: dp[i] = dp[i-1] + dp[i-2]
// 定义边界条件
dp[1] = 1;
dp[2] = 2;
for(int i=3;i<=n;i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
显然我们可以对这种解法进行空间优化,使用pre1、pre2、cur三个变量记录即可。
public int climbStairs(int n) {
if(n<=2){
return n;
}
int pre2=1,pre1=2,cur=0;
for(int i=3;i<=n;i++){
cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
return cur;
}
198.打家劫舍
解题思路:
1.定义dp[i] 表示第i间房屋所能偷的最大金额;
2.找递推公式, i位置的金额与前一个和前二个相关,故递推公式:dp[i] = Math.max(dp[i-2],dp[i-1]);;
3.定义边界 dp[0] = nums[0]; dp[1] = Math.max(nums[0],nums[1]);;
public int rob(int[] nums) {
if(nums.length == 1){
return nums[0];
}
if(nums.length == 2){
return Math.max(nums[0],nums[1]);
}
// 定义数组dp[i] 表示第i间房屋所能偷的最大金额
int[] dp = new int[nums.length];
// 递推公式:dp[i] = Math.max(dp[i-2],dp[i-1]);
// 边界:
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i=2;i<nums.length;i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.length-1];
}
优化:
public int rob(int[] nums) {
if(nums.length == 1){
return nums[0];
}
if(nums.length == 2){
return Math.max(nums[0],nums[1]);
}
int pre2 = nums[0],pre1 = Math.max(nums[0],nums[1]),cur =0;
for(int i=2;i<nums.length;i++){
cur = Math.max(pre1,pre2+nums[i]);
pre2 = pre1;
pre1 = cur;
}
return cur;
}
2.2 二维动态规划
基本问题:找出题目的动态转移方程。
64.最小路径和
解题思路:
1.首先定义二维数组dp[i][j] 表示从左上角到当前位置的最小路径和,定义返回值:dp[grid.length-1][grid[0].length-1];
2.找出数组之间关系的递推公式,上边界:dp[i][j] = grid[i][j] + dp[i][j-1]; 左边界:dp[i][j] = grid[i][j] + dp[i-1][j]; 内部:dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
3.确定边界条件:左上角的点:dp[i][j] = grid[i][j];
public int minPathSum(int[][] grid) {
// 定义数组 dp[i][j] 表示从左上角到i,j位置的最短路径
int[][] dp = new int[grid.length][grid[0].length];
// 定义数组递推关系 dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])
// 定义边界条件
for(int i=0;i<grid.length;i++) {
for(int j=0;j<grid[0].length;j++){
if(i == 0){
if(j == 0){
dp[i][j] = grid[i][j];
continue;
}
dp[i][j] = grid[i][j] + dp[i][j-1];
}else if(j == 0){
dp[i][j] = grid[i][j] + dp[i-1][j];
}else {
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
}
}
}
return dp[grid.length-1][grid[0].length-1];
}
542.01矩阵
解题思路:
1.定义数组dp[i[[j] 表示当前位置到0最近的距离;
2.递推公式:此题遍历分为两步骤,先从左上角往右下角遍历,再从右下角往左上角遍历;两次遍历过程会覆盖到所有的位置;公式与上题类似,不再赘述。
3.边界,需要保证初始数组中的每个值保证足够大;
public int[][] updateMatrix(int[][] mat) {
// 定义数组dp[i][j] 表示当前位置到周围0的最小距离
int m = mat.length;
int n = mat[0].length;
int[][] dp = new int[m][n];
// 遍历分为两步骤,先从左上角往右下角遍历,再从右下角往左上角遍历;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
dp[i][j] = Integer.MAX_VALUE-1;
if(mat[i][j] == 0){
dp[i][j] = 0;
}else {
if(i > 0){
dp[i][j] = Math.min(dp[i][j],dp[i-1][j]);
}
if(j > 0){
dp[i][j] = Math.min(dp[i][j],dp[i][j-1]);
}
}
}
}
for(int i=m-1;i>=0;i--){
for(int j=n-1;j>=0;j--){
if(mat[i][j] != 0){
if(i < m-1){
dp[i][j] = Math.min(dp[i][j],dp[i+1][j]);
}
if(j < n-1){
dp[i][j] = Math.min(dp[i][j],dp[i][j+1]);
}
}
}
}
return dp;
}
2.3 分割类问题
对于分割类型的动态规划题目,状态转移方程往往取决于满足分割条件处的位置。
139.单词拆分
此题状态转移方程 dp[i] 与 dp[i-len] 分割位置的数组有关。
public boolean wordBreak(String s, List<String> wordDict) {
// 遍历字符串
int n = s.length();
// 记录状态
boolean[] dp = new boolean[n+1];
dp[0] = true;
for(int i=1;i<=n;i++){
for(String word:wordDict){
int len = word.length();
if(i >= len && s.substring(i-len,i).equals(word)){
dp[i] = dp[i] || dp[i-len];
}
}
}
return dp[n];
}
279.完全平方数
此题的状态转移方程,dp[i] 与满足条件的 dp[i-j*j] 相关
public int numSquares(int n) {
// 定义数组dp[i]表示i数组最少组成的完全平方数;
int[] dp = new int[n+1];
dp[0] = 0;
for(int i=1;i<=n;i++){
dp[i] = Integer.MAX_VALUE;
}
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];
}
2.4 子序列问题
对于子序列问题的常见解法为:1.定义一个数组 dp[i] 表示 i 结尾的子序列的特性,处理好每个位置后,再统计一遍即可。
300.最长递增子序列
public int lengthOfLIS(int[] nums) {
// 定义数组 dp[i]表示 i位置的nums最长的递增子序列;
int[] dp = new int[nums.length];
// 边界条件
for (int i = 0; i < dp.length; i++) {
dp[i] = 1;
}
// 定义最大返回子序列长度
int max = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
// 统计一遍最大的dp[i]
max = Math.max(max,dp[i]);
}
return max;
}
583.两个字符串的删除操作
646.最长对数链
2.5 背包问题
背包问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。
核心代码:
1.0-1背包,外层遍历物品,内层逆向遍历价值或重量;
2.完全背包:外层遍历物品,内层正向遍历价值或重量。
2.5.1 0-1背包问题
如果限定每种物品只能选择 0 个或 1 个,则此背包问题称为0-1背包问题。
解法1:
- 定义数组dp[i][j] 表示i个物品,容量为j所能取到的最大价值。
- 递推关系,当前位置存在取或不取两种状态,所以取两种情况的最大价值 dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w]+v);
- 边界条件:默认为0即可。
public int maxBagValue(int[] weights,int[] values,int N,int W){
// 定义数组 dp[i][j] 表示 i个物品,j个容量所能取到的最大价值;
int[][] dp = new int[N+1][W+1];
for (int i = 1; i <= N; i++) {
int w = weights[i-1],v = values[i-1];
for (int j = 1; j <= W; j++) {
if (j >= w){
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w]+v);
}else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][W];
}
在此我们可以发现递推公式之间,dp[i][] 只与dp[i-1][]相关,故可对解法1的空间进行优化。
解法2如下:
- 定义数组dp[i] 表示容量为i的背包所能取得的最大价值;
- 递推公式:dp[i] = Math.max(dp[i],dp[i-w]+v).
- 边界无。
public int maxBagValue2(int[] weights,int[] values,int N,int W){
// 定义数组 dp[i] 表示 重量为W的背包,所能取到的最大价值;
int[] dp = new int[W+1];
for (int i = 1; i <= N; i++) {
int w = weights[i-1],v = values[i-1];
// 需要倒序往前推,防止dp[j-w]不会被覆盖;若按照从左往右的顺序进行正向遍历,则 dp[j-w] 的值在遍历到
// j 之前就已经被更新成物品 i 的值了
for (int j = W; j >= w; j--) {
dp[j] = Math.max(dp[j],dp[j-w]+v);
}
}
return dp[W];
}
同类题目:
416.分割等和子集
可以转换为是否可以获取数组总值一半的0-1背包问题
public boolean canPartition(int[] nums) {
// 等价于0-1背包问题
int sum =0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
if(sum % 2 != 0){
return false;
}
int target = sum/2;
// 定义数组dp[i] 表示数组是否能够取到目标值i
boolean[] dp = new boolean[target+1];
dp[0] = true;
for(int i=0;i<nums.length;i++){
for(int j=target;j>= nums[i];j--){
dp[j] = dp[j] || dp[j-nums[i]];
}
}
return dp[target];
}
2.5.2 完全背包问题
同上0-1背包,核心代码可看背包问题下方区别。
public int maxBagValue(int[] weights,int[] values,int N,int W){
// 定义数组 dp[i] 表示 重量为W的背包,所能取到的最大价值;
int[] dp = new int[W+1];
for (int i = 1; i <= N; i++) {
int w = weights[i-1],v = values[i-1];
for (int j = w; j < W; j++) {
dp[j] = Math.max(dp[j],dp[j-w]+v);
}
}
return dp[W];
}
322. 零钱兑换
外层遍历物品,内层正向遍历体积或价值。
public int coinChange(int[] coins, int amount) {
// 完全背包问题
if(amount == 0){
return 0;
}
// 定义数组 dp[i] 表示凑成i所需的最小硬币个数
int[] dp = new int[amount+1];
for(int i=1;i<dp.length;i++){
// 保证初始值足够大
dp[i] = amount+2;
}
dp[0] = 0;
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
dp[j] = Math.min(dp[j],dp[j-coins[i]] + 1);
}
}
return dp[amount] == amount+2?-1:dp[amount];
}
2.6 字符串编辑问题
72.编辑距离
1.此题我们需要维护一个二维数组dp[]i[j] 表示 word1 到 i 位置和 word2 到 j 位置所需要的最小编辑数;
2. 当 i 和 j 位置的字符相同时,dp[i][j] = dp[i-1][j-1]; 当 i 和 j 位置的字符不同时,修改的需要 dp[i][j] = dp[i-1][j-1] + 1; 插入 i 位置/删除 j 位置的是 dp[i][j-1] + 1,插入 j 位置/删除 i 位置的是 dp[i-1][j] + 1 。
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
for (int i = 0; i <= m; i++) {
for(int j = 0;j <= n;j++) {
if (i == 0){
dp[i][j] = j;
}else if (j == 0){
dp[i][j] = i;
}else{
dp[i][j] = Math.min(dp[i-1][j-1] + (word1.charAt(i-1) == word2.charAt(j-1)?0:1),Math.min(dp[i-1][j]+1,dp[i][j-1]+1));
}
}
}
return dp[m][n];
}
2.7 股票问题
121.买卖股票的最佳时机
此题的主要解题思路在于,记录买入的最小值,同时记录卖出的最大利润即可。
public int maxProfit(int[] prices) {
// sell表示卖出的最大值,buy表示买入的最小值
int sell=0,buy = Integer.MAX_VALUE;
for(int i=0;i<prices.length;i++){
buy = Math.min(buy,prices[i]);
sell = Math.max(sell,prices[i] - buy);
}
return sell;
}
188.买卖股票的最佳时机IV
此题的解题思路在于,维护两个数组buy[i] 表示在i时刻买入股票时取得的最大利润,sell[i] 表示在i时刻卖出的最大利润。同时区分K大于数组prices长度时,选择盈利即可。小于时正常处理。
public int maxProfit(int k, int[] prices) {
// 区分情况 k>prices.length 则赚钱就卖出
if (k > prices.length){
return profit(prices);
}
// 定义数组buy[j] 表示j时刻买入时的最大收入,sell[j] 表示j时刻卖出的最大收入
int[] buy = new int[k+1];
int[] sell = new int[k+1];
for (int i = 0; i <= k; i++) {
buy[i] = Integer.MIN_VALUE;
}
sell[0] = 0;
for(int i=0;i<prices.length;i++){
for(int j=1;j<=k;j++){
// j位置买入的最大价值
buy[j] = Math.max(buy[j],sell[j-1] - prices[i]);
// j位置卖出的最大价值,此处转移方程
sell[j] = Math.max(sell[j],buy[j] + prices[i]);
}
}
return sell[k];
}
public int profit(int[] prices){
int res = 0;
for(int i=1;i<prices.length;i++){
if(prices[i] > prices[i-1]){
res += prices[i] - prices[i-1];
}
}
return res;
}
714.买卖股票的最佳时机含手续费
此题关键点在于找到状态转移方程,找到当前位置最大利润与前一步的关系。
public int maxProfit(int[] prices, int fee) {
// 股票
int maxProfit = 0;
// 定义数组,buy[i] sell[i]位置的数组分别表示买入和卖出的最大价值
int[] buy = new int[prices.length];
int[] sell = new int[prices.length];
// 边界问题
buy[0] = -prices[0];
sell[0] = 0;
for(int i=1;i<prices.length;i++){
// 如果当前买入,最大值为前一个买入的最大值,或前一个位置卖出的最大值减当前位置买入
buy[i] = Math.max(buy[i-1],sell[i-1] - prices[i]);
// 建立状态机,如果当前卖出,取得最大值,前一个位置卖出的最大值或前一个买入的最大值+当前卖出
sell[i] = Math.max(sell[i-1],buy[i-1]+prices[i]-fee);
}
return sell[prices.length-1];
}
309、最佳买卖股票时机含冷冻期
此题需要四个数组表示状态转换,比较复杂,先不记录了…