在动态规划的海洋中遨游(三)

本文介绍了动态规划算法的基本原理,包括最优化原理、适用情况和做题步骤。通过打家劫舍和买卖股票的经典问题,展示了动态规划的解题思路,强调了状态转移的重要性。文章适合中等水平的学习者,提供了详细的代码示例和解题策略。
摘要由CSDN通过智能技术生成

前言: \textcolor{Green}{前言:} 前言:

💞 好久没写题,有点生疏了。这也是给大家提一个醒,一定要一直坚持下去,哪怕每天只做一点点。💞

一、算法介绍

原理

思想:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。按顺序求解子阶段,前面子问题的解为后面子问题的求解提供信息。

如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来。

动态规划算法的基本思想:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

适用的情况

  1. 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,称该问题具有最优子结构,即满足最优化原理。
  2. 没有后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说:某状态以后的过程不会影响以前的状态,只与当前状态有关。
  3. 有重叠子问题:子问题之间是不独立的,一个子问题在下一个近阶段可能被多次遇到。(这条性质不是动态规划适用的必要条件但是具备这条性质那么动态规划相对于其他算法就具备一定的优势)。

做题步骤

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

二、算法实例

1. 打家劫舍(一)

题目来源: \textcolor{OrangeRed}{题目来源:} 题目来源:BM78 打家劫舍(一)
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等

👉题目描述

你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家;如果偷了第二家,那么就不能偷第一家和第三家。
给定一个整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

数据范围:数组长度满足 1 ≤ n ≤ 2 × 1 0 5 1≤n≤2×10^5 1n2×105,数组中每个值满足 1 ≤ n u m [ i ] ≤ 5000 1≤num[i]≤5000 1num[i]5000

示例1

输入:[1,2,3,4]
返回值:6
说明:最优方案是偷第 2,4 个房间   

示例2

输入:[1,3,6]
返回值:7
说明:最优方案是偷第 1,3个房间   

示例3

输入:[2,10,5]
返回值:10
说明:最优方案是偷第 2 个房间  

👉代码编写

最好的办法是通过动态规划来进行。
如果单纯选择奇数家或者偶数家进行偷取,也可能发生问题。例如为了更多的钱可能会连续选择两家不偷。

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int rob (int[] nums) {
        // write code here
        int len = nums.length;
        if (len <= 1) return nums[0];
        int[] dp = new int[len];
        dp[0] = nums[0];
        dp[1] = Math.max(dp[0], nums[1]);
        for (int i = 2; i < len; ++i) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return Math.max(dp[len - 1], dp[len - 2]);
    }
}

👉👉方法2
(借鉴的方法)

step 1:用dp[i]表示长度为i的数组,最多能偷取到多少钱,只要每次转移状态逐渐累加就可以得到整个数组能偷取的钱。
step 2:(初始状态) 如果数组长度为1,只有一家人,因此dp[1]=nums[0]。
step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,如果我们选择偷那么前一家必定不能偷,因此累加的上上级的最多收益,同理如果选择不偷他,那我们最多可以累加上一级的收益。因此转移方程为,dp[i]=max(dp[i−1],nums[i−1]+dp[i−2])。这里的i在dp中为数组长度,在nums中为下标。

import java.util.*;

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int rob (int[] nums) {
        //dp[i]表示长度为i的数组,最多能偷取多少钱
        int[] dp = new int[nums.length + 1];
        dp[1] = nums[0];
        for(int i = 2; i <= nums.length; i++)
            //对于每家可以选择不偷或者偷
            dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
        return dp[nums.length];
    }
}

👉 注意点

在第一个方法中
这个是对的。

dp[1] = Math.max(dp[0], nums[1]);

这个是错的,通不过倒数第二个案例。

dp[1] = nums[1];

2. 打家劫舍(二)

题目来源: \textcolor{blue}{题目来源: } 题目来源: BM79 打家劫舍(二)
等级: \textcolor{OrangeRed}{等级:} 等级: 中等

👉题目描述

你是一个经验丰富的小偷,准备偷沿湖的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家,如果偷了第二家,那么就不能偷第一家和第三家。沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。
给定一个长度为n的整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

数据范围:数组长度满足 1 ≤ n ≤ 2 × 1 0 5 1≤n≤2×10^5 1n2×105 ,数组中每个值满足 1 ≤ n u m s [ i ] ≤ 5000 1≤nums[i]≤5000 1nums[i]5000

示例1

输入:[1,2,3,4]
返回值:6
说明:最优方案是偷第 2 4 个房间      

示例2

输入:[1,3,6]
返回值:6
说明:由于 13 是相邻的,因此最优方案是偷第 3 个房间 

👉代码编写

和上一题类似。但是本题中主要是有环形,意思就是选择第一家就不能选择最后一家,选择最后一家就不能选择第一家。那么我们就可以分类来讨论。
偷第一家,那么最后一家就不能偷
不偷第一家,那么意思就是 d p [ 1 ] = 0 dp[1]=0 dp[1]=0,去选择偷最后一家。

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int rob (int[] nums) {
        // write code here
        int len = nums.length;
        if (len <= 1) return nums[0];
        int[] dp = new int[len + 1];
        dp[1] = nums[0];
        for (int i = 2; i < len; ++i) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        }
        int result = dp[len - 1];
        Arrays.fill(dp, 0);
        dp[1] = 0;
        for (int i = 2; i <= len; ++i) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        }
        return Math.max(dp[len], result);
    }
}

👉 注意点

注意环形,可以分开进行讨论。

3. 买卖股票的最好时机(一)

题目来源: \textcolor{blue}{题目来源: } 题目来源: BM80 买卖股票的最好时机(一)
等级:简单 \textcolor{OrangeRed}{等级:简单} 等级:简单

👉题目描述

假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天
2.如果不能获取到任何利润,请返回0
3.假设买入卖出均无手续费

数据范围: 0 ≤ n ≤ 1 0 5 0≤n≤10^5 0n105 , 0 ≤ v a l ≤ 1 0 4 0≤val≤10^4 0val104

要求:空间复杂度 O(1),时间复杂度 O(n)

示例1

输入:[8,9,2,5,4,7,1]
返回值:5
说明:在第3天(股票价格 = 2)的时候买入,在第6天(股票价格 = 7)的时候卖出,最大利润 = 7-2 = 5 ,不能选择在第2天买入,第3天卖出,这样就亏损7了;同时,你也不能在买入前卖出股票。            

示例2

输入:[2,4,1]
返回值:2

示例3

输入:[3,2,1]
返回值:0

👉代码编写

  1. 状态定义:dp[i][j]:下标为 i 这一天结束的时候,手上持股状态为 j 时,我们持有的现金数。
    j = 0,表示当前不持股; j = 1,表示当前持股。
    dp[i][0]:第 i 天不持股到该天最大收益
    dp[i][1]:第 i 天持股到该天最大收益

  2. 推导状态转移方程:

    • dp[i][0]:当天不持股,有以下两种情况:
      昨天不持股,今天什么都不做;
      昨天持股,今天卖出股票(现金数增加),
      状态转移方程: d p [ i ] [ 0 ] = M a t h . m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]) dp[i][0]=Math.max(dp[i1][0],dp[i1][1]+prices[i]);
    • dp[i][1]:当天持股,有以下两种情况:
      昨天持股,今天什么都不做(现金数与昨天一样);
      昨天不持股,今天买入股票(注意:只允许交易一次,因此手上的现金数就是当天的股价的相反数)
      状态转移方程: d p [ i ] [ 1 ] = M a t h . m a x ( d p [ i − 1 ] [ 1 ] , − p r i c e s [ i ] ) dp[i][1] = Math.max(dp[i - 1][1], -prices[i]) dp[i][1]=Math.max(dp[i1][1],prices[i]);

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 
     * @param prices int整型一维数组 
     * @return int整型
     */
    public int maxProfit (int[] prices) {
        // write code here
        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];
    }
}

👉 注意点

虽然是个简单题,但是还是需要思考一下,状态方程和转移方程是如何。

4. 买卖股票的最好时机(二)

题目来源: \textcolor{blue}{题目来源: } 题目来源:BM81 买卖股票的最好时机(二)
等级:中等 \textcolor{OrangeRed}{等级:中等} 等级:中等

👉题目描述

假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益

  1. 你可以多次买卖该只股票,但是再次购买前必须卖出之前的股票
  2. 如果不能获取收益,请返回0
  3. 假设买入卖出均无手续费

数据范围: 1 ≤ n ≤ 1 × 1 0 5 1≤n≤1×10^5 1n1×105 1 ≤ p r i c e s [ i ] ≤ 1 0 4 1≤prices[i]≤10^4 1prices[i]104

要求:空间复杂度 O(n),时间复杂度 O(n)
进阶:空间复杂度 O(1),时间复杂度 O(n)

示例1

输入:[8,9,2,5,4,7,1]
返回值:7
说明:
在第1天(股票价格=8)买入,第2天(股票价格=9)卖出,获利9-8=1
在第3天(股票价格=2)买入,第4天(股票价格=5)卖出,获利5-2=3
在第5天(股票价格=4)买入,第6天(股票价格=7)卖出,获利7-4=3
总获利1+3+3=7,返回7     

示例2

输入:[5,4,3,2,1]
返回值:0
说明:由于每天股票都在跌,因此不进行任何交易最优。最大收益为0。      

示例3

输入:[1,2,3,4,5]
返回值:4
说明:第一天买进,最后一天卖出最优。中间的当天买进当天卖出不影响最终结果。最大收益为4。              

备注:

总天数不大于200000。保证股票每一天的价格在[1,100]范围内。

👉代码编写

和上一题类似,不过该题不需要控制购买次数。

  1. 状态定义:dp[i][i]:下标为 i 这一天结束的时候,手上持股状态为 j 时,我们持有的现金数。
    j = 0,表示当前不持股; j = 1,表示当前持股。
    dp[i][0]:第 i 天不持股到该天最大收益
    dp[i][1]:第 i 天持股到该天最大收益
  2. 状态转移:
    • dp[i][0]: 当天不持股,表示前面卖了或者没有买。或者当天将股票卖出了。
      状态转移方程 d p [ i ] [ 0 ] = M a t h . m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + p r i c e [ i ] ) dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + price[i]) dp[i][0]=Math.max(dp[i1][0],dp[i1][1]+price[i])
    • dp[i][1]:当天持股,表示前面买入的股票还没有卖。或者当天买入了股票。
      状态转移方程 d p [ i ] [ 1 ] = M a t h . m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e [ i ] ) dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - price[i]) dp[i][1]=Math.max(dp[i1][1],dp[i1][0]price[i])

👉👉方法1

import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     * 计算最大收益
     * @param prices int整型一维数组 股票每一天的价格
     * @return int整型
     */
    public int maxProfit (int[] prices) {
        // write code here
        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], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
}

总结归纳

前方仍需努力。关于买卖股票这一个题还有一个困难题型,这里没有提出,有需求的小伙伴可以自行千万解决BM82 买卖股票的最好时机(三)。这一个题和(一)是类似的,但是加入了条件,只能进行两次购买票操作。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秦 羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值