在Leetcode动态规划题解1——两要素和解题步骤中我已经对动态规划的概念、两要素以及解题步骤进行了详细的解释。在这里,主要分享一下我在leetcode上遇到的线性dp问题。
线性dp,顾名思义,就是在线性序列上进行动态规划,可能有一个或多个线性序列。因为问题是求解线性序列上的最优化问题,所以会有一些规律能够帮助我们更快地解题,主要体现在解题步骤上。
按照这个思路,我们再回过头来看看之前讲过的一题。
198. 打家劫舍。
题目描述:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
首先,确定状态。用i表示房屋号,dp[i]表示从第一间到第i间房屋,小偷能得到的最大金额。
其次,确定状态转移方程。dp[i]的值依赖于dp[i-1]和dp[i-2]。如果你已经偷过了i-1号房屋,为了不触动警报装置,你不能偷i号房屋;如果你没有偷过i-1号房屋,那你可以偷i号房屋。所以,dp[i]=max(dp[i-1], dp[i-2]+nums[i])。
最后,确定边界条件。dp[i]的值依赖于dp[i-1]和dp[i-2],所以我们应该给出dp[0]和dp[1]才能进行递推。当没有房子的时候,可以偷到的金额为0,dp[0]=0;当只有一间房子的时候,可以偷到的金额为1号房屋的金额,dp[1]=nums[i]。
java版本
class Solution {
public int rob(int[] nums) {
// 确定状态,与nums的长度有关
int n = nums.length;
int[] dp = new int[n + 1];
// 边界条件
dp[1] = nums[0];
for(int i = 2; i <= n; i++){
// dp[i]与dp[i-1]和dp[i-2]的递推关系
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
}
return dp[n];
}
}
如果在线性序列上进行动态规划,那么可以很快判断出其状态,状态转移方程和边界条件,能够帮助我们更快解题。
在之后的题目中,我将按照同样的思路解题,但为了节约时间,只简略说明思路。如果以后有时间,我再将思路详细补充上去。
213. 打家劫舍 II
题目描述:你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,今晚能够偷窃到的最高金额。
示例1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
首先,确定状态。上一道题是线性dp,房屋是线性序列。这道题的房屋是环形,所以要先分割为两段线性序列,将nums[0]到nums[n-2]视作一个线性序列,将nums[1]到nums[n-1]视作另一个线性序列。然后从两个线性序列的结果中,求最大的结果。同样的,用i表示房屋号,dp[i]表示从第一间到第i间房屋,小偷能得到的最大金额。
其次,确定状态转移方程。同上题,dp[i]的值依赖于dp[i-1]和dp[i-2]。如果你已经偷过了i-1号房屋,为了不触动警报装置,你不能偷i号房屋;如果你没有偷过i-1号房屋,那你可以偷i号房屋。所以,dp[i]=max(dp[i-1], dp[i-2]+nums[i])。因为每次只涉及三个状态,dp[i],dp[i-1]和dp[i-2],所以可以把dp数组简化为三个dp变量。
最后,确定边界条件。同上题,dp[i]的值依赖于dp[i-1]和dp[i-2],所以我们应该给出dp[0]和dp[1]才能进行递推。当没有房子的时候,可以偷到的金额为0,dp[0]=0;当只有一间房子的时候,可以偷到的金额为1号房屋的金额,dp[1]=nums[i]。
java版本
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1){
return nums[0];
}
if(n == 2){
return Math.max(nums[0], nums[1]);
}
return Math.max(subrob(nums, 0, n - 1), subrob(nums, 1, n));
}
private int subrob(int[] nums, int start, int end){
int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for(int i = start + 2; i < end; i++){
int temp = second;
second = Math.max(second, first + nums[i]);
first = temp;
}
return Math.max(first, second);
}
}
这道题需要将环形连接的房屋,分割成两段线性序列,之后就可以按照之前的思路解题。
121. 买卖股票的最佳时机
题目描述:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
首先,确定状态。线性dp肯定有一个维度是序列长度。此外,是否持有股票,也是一种状态。如果已买入股票,那之后不可以再买入,只能够卖出;如果未买入股票,那之后不可以卖出,只能够买入。原问题是,可以买入或卖出股票。之前提到过,只要问题的规模会发生改变,那么就代表了一种状态。第一个维度用i表示日期,第二个维度用0/1表示未/已持有股票。
其次,确定状态转移方程。
最后,确定边界条件。
java版本
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = - prices[0];
for(int i = 1; i < n; 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], - prices[i]);
}
return dp[n - 1][0];
}
}
参考:
动态规划解题套路框架
算法设计与分析基础(第3版) (豆瓣)
力扣上的 DP 问题分类汇总
基础线性dp