宅家防疫期间leetcode上小刷了十余道线性动态规划算法题,是时候自己总结提炼一下DP思想了。
我把总结的题目主要分为两类序列匹配类和生活类。
第一类的代表热题:
leetcode300. 最长上升子序列
第二类的代表热题:
leetcode121. 买卖股票的最佳时机
以下为7道题的思路和代码实现
300.最长上升子序列
- 思路 :用一个数组来存放上升序列,关键在于最后输出的是序列长度,所以保证长度而不需要时刻记录真正的子序。
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length==0) return 0;
for(int i=1; i<nums.length; i++){
dp[1] = 1;
for(int j=i-1; j>1; j--){
if(dp[j] < dp[i]) dp[j] = dp[j-1]+1;
}
}
}
}
之所以最先写这道题,因为之前用的是贪心算法,二分搜索比较难想到
public static int lengthOfLIS(int[]nums) {
if(nums.length < 2) return nums.length;
//LIS数组存放最长子序列的数组,但并非时刻都存放的是最长子序列
int[] LIS = new int[nums.length];
LIS[0] = nums[0];//数组有负整数的情况
int end = 0;
for(int i=1; i<nums.length; i++){
//如果该位元素大于子序列最后一位,上升子序列变长1
if(nums[i]>LIS[end]){
end++; LIS[end]=nums[i];
}
//如果当前nums[i]小于子序列最后一位,则用二分法搜索子序列中比nums[i]大的最小数
else{
int left = 0,right =end;
while(left<right){
int pivot = left+(right-left)/2;
if( LIS[pivot]< nums[i]){
left = pivot+1;
}
else{
assert LIS[pivot] >= nums[i];
right = pivot;
}
}
LIS[right]=nums[i];
}
}
return end+1;
}
LeetCode53. 最大子序和
- 思路:子序列问题难点在于是不连续的序列。定义dp[i] 表⽰以 nums[i] 这个数结尾的最⻓递增⼦序列的⻓度。dp[i]依赖于dp[i-1]的结果,每次递推都要max记录,自底向上推算出最后结果啦。先用一个简单的例子来推:nums = [1,-2,3],则dp[0] = nums[0] = 1;
- step1:dp[1] = dp[0]+nums[1] = -1; 此时max =1>dp[1],所以当前max不变;
- step2: dp[ 2] = dp[1] + nums[2] = 1;此时 max<dp[2],所以max更新为dp[2]
- 如此这般,max最后就是最大的dp[i]啦
算法思路的动画
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
//nums[i] > 0,说明对结果有增益,dp[i]再加当前遍历值
//nums[i] <= 0,说明对结果无增益,dp[i]直接更新为当前遍历数字
dp[i] = Math.max(dp[i- 1] + nums[i], nums[i]);
if (max < dp[i]) { //关键步:取每次遍历的当前最大和
max = dp[i];
}
}
return max;
}
LeetCode1143. 最长公共子序列
- 思路:
这里推荐youtube上一个白板演示算法思路的小哥,他的视频讲解的非常细,既可以帮助透彻理解,又可以提高下英语,两全其美:【LeetCode算法详解】最长公共子序列 中间状态都存在二维数组中,dp[i][j] 表示 串1的前 i 位和串2的前 j 位的位置。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n1 = text1.length(); int n2 = text2.length();
int[][] dp = new int[n1+1][n2+1];
for(int i=0; i<n1; i++) { dp[i][0] = 0; }
for(int j =0; j<n2; j++){ dp[0][j] = 0; }
for(int i=1; i<=n1; i++){
for(int j =1; j<=n2; j++){
if(text1.charAt(i-1)==text2.charAt(j-1))
dp[i][j] = dp[i-1][j-1] + 1;
else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[n1][n2];
}
}
LeetCode72. 编辑距离
- 思路:这类解决两个字符串的动态规划问题,可以用最长上升子序列一样的方法,用两个指针 i,j 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。
dp[i][j]—word1的前 i 位转换成 word2的前 j 位需要的最小步数。
public int minDistance(String word1, String word2) {
int n1 = word1.length(); int n2 = word2.length();
int[][] dp = new int[n1+1][n2+1];
for(int i=1; i<=n1; i++) dp[i][0] = dp[i-1][0] +1;
for(int j=1; j<=n2; j++) dp[0][j] = dp[0][j-1] +1;
for(int i=1; i<=n1; i++){
for(int j=1; j<=n2; j++){
//word1和word2的该位字符相同,不需要改动。
if(word1.charAt(i-1)==word2.charAt(j-1))
dp[i][j] = dp[i-1][j-1];
//如果字符不同,则取该步之前的状态基础上做删除,修改,插入中的最小改动
else
dp[i][j] = Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1])+1;
}
}
return dp[n1][n2];
}
股票系列的问题推荐一个总结很到位的思路:一个方法团灭 6 道股票问题
- 思路:前 i 天所获利润以来于前 i- 天利润;第 i 天是如果 “买入股票”,就要从利润中减去 prices[i],如果“卖出股票”,就要给利润增加 prices[i]。此时最大利润就是这两种可能选择中较大的那个。
因此可有最优子结构:
dp[i][0]表示第i天不持股票时拥有的利润;dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
而dp[i][1]表示第i天持有股票所获利润 dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if(prices.length==0)return 0;
int[][] dp = new int[n][2]; //行表示第 i天,列表示是否买入当天股票
dp[0][0] = 0; //i = 0 时 dp[i-1] 是不合法的。
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];
}
}
+思路:和上一题的思路基本一致,区别在于交易次数从 1 变成 无限次
那么dp[i][1] 就等于dp[i-1][1]和dp[i-1][0]-prices[i]中更大的结果。因为前 i 天也可以有买入交易了,也就是买股票的第 i天所获利润和第 i-1 直接相关。
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0) return 0;
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i=1; i<prices.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[prices.length-1][0];
}
}
leetcode322. 零钱兑换
- 思路:最优化状态dp[i]表示金额 i 需要的最少硬币个数。要注意的问题在于初始化时每个dp[]数都要大于金额总数,是极端假设硬币都是 1的情况。
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
// 注意:因为要比较的是最小值,不可能是结果的初始化值就得赋一个最大值
Arrays.fill(dp, amount + 1);
dp[0] =0;
for(int i=1; i<=amount; i++){
for(int coin: coins){
//如果可包含coin,那么剩余钱是i−coins,要兑换的硬币数是 dp[i−coins]+1
if(coin <= i)
dp[i] = Math.min(dp[i],dp[i-coin]+1);
}
}
return dp[amount]<=amount ? dp[amount] : -1;
}
}