动态规划总结-以Leecode股票问题(买股票的最佳时机)为例

前言

前一段时间以背包问题总结了一波动态规划的内容,最近这段时间又刷了Leecode中的股票问题,对动态规划又有了一些更深入的理解,这里再来总结一波,希望可以便人便己。


一、前面的总结

这次博客在前面进行总结,后面在进行举例分析。来个总分(总分总)类型。

“动态规划算法,本质上算是一种搜索算法,只不过其自带剪枝功能.”。(不知道是哪位大神说的了).。 深奥而精辟,本质的东西总是抽象而难于理解的。从稍微具体的角度来看,它就是重复利用以前计算过的子单元,用空间换时间的一种方式。

在这里插入图片描述

稍微刷过一点题的人应该都了解上面这张表,没错。这就是DP中用来存储的内存表。

当我们需要填充第(i,j)位置的时候,参考的是以前已经计算完的子单元状态。(比如说可能是上图中的被红色包围起来的蓝色部分。 )。 这里牵扯到两个问题:

(1)、当我们研究的是(i,j)状态的空间,(即准备填充(i,j)时),已经遍历的子状态是什么样的,在上图中如何分布?

答: 这个不确定。 需要根据具体的算法来,比较简单的可能是上图中的情况(如背包问题、下文中的一些股票问题)。 比较常见的还有可能是下面两种情况。
在这里插入图片描述

在这里插入图片描述
其中后向结构比较典型的有背包问题的另一种解法, 对角线结构比较经典的有最长公共子序列。

 
不同的算法,不同的状态定义,可能会有不同的结构。 这个要是具体的情况而定。
  
(2)、 如何通过已有的DP状态,算出当前的dp[i][j]?

答: 这个问题也是不确定的。 关键的问题是状态转移方程。 通过状态转移方程,就可以知道如何通过子状态计算出当前状态。
当然,状态转移方程也不是好推的,需要对题目的意思进行分析、思考。

Leecode中股票的系列问题中有一种思考方式-状态转移法(当然,这个不是适用于所有DP问题),下文中会进行说明讲解。

 
 

二、股票问题(买股票的最佳时机I-IV)

 

2.1 买股票的最佳时机I

2.1.1 题目描述

在这里插入图片描述

2.1.2 解法

class Solution {
public:
    vector<int> prices;
    int maxProfit(vector<int>& prices) {
       
        int minPrice = INT_MAX;
        int maxPro = 0;

        for(auto tmpPrice : prices){
            if(minPrice <= tmpPrice){
                maxPro = max(maxPro,tmpPrice-minPrice);
            }else{
                minPrice = tmpPrice;
            }
        }
        return maxPro;
    }
};

代码说明:这里的代码很简单,就是维护了目前为止的最小值,然后遍历数组,依次和最小值比较,要么更新最小值,要么计算受益。 这题的解法思路有点单调栈的影子,不过并没有在栈底完全保存递增的元素,而是保存了一个最小值。 有关单调栈,刷题的时候遇到的也挺多的,以后抽时间也总结一波。

 

2.2 买股票的最佳时机II

2.2.1 题目描述

在这里插入图片描述

2.2.2 解法

这题和第一题不一样的地方在于,这里可以买多次股票(前者只能买卖一次)。 感觉上可以使用动态规划,第i天的操作,可以利用之前的结果。但是这里有个小问题,就是你也不清楚,第i天是可以买还是可以卖(不知道第i天是否持有一个股票),所以这就有点尴尬了…

其实可以稍微变通一点,前i天的买和卖的结果都记录下来,然后利用状态转换,即下图所示。

在这里插入图片描述

左边和右边的意思其实是一样的的,不过左边是动作转换图,右边是状态转换图。相对来说状态转换图用到的更多些,也更好理解一些。

所以,我们可以令
dp[i][0] 保存 为第i天股票数为0的最大受益;
dp[i][1] 保存为第i天股票书为1的最大受益。

所以很容易就可以得出状态转移方程:
在这里插入图片描述

在这里插入图片描述

最终的结果就是 dp[n][0] (最终的股票数目为0)。

 
基本的代码如下:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        
        int length = prices.size();
        vector<vector<int>> dp(2,vector<int>(length+1,0));

        dp[0][0] = 0;
        dp[1][0] = INT_MIN;

        for(int i = 1;i<=length;++i){

            dp[0][i] = max(dp[0][i-1],dp[1][i-1]+prices[i-1]);

            dp[1][i] = max(dp[1][i-1],dp[0][i-1]-prices[i-1]);

        }
        return dp[0][length];
    
    }
};

注意:代码中dp[0][i] 代表为第i天股票数为0的最大受益; 没太大影响。

 
 

2.3 买股票的最佳时机III

2.3.1 题目描述

在这里插入图片描述
 
 

2.3.2 解法

题目没啥大区别,只是把用户最多可以完成的交易增加到了两笔。 如果你深入理解了上个题目,这个题目应该很快就可以触类旁通出来,不就是多增加了一笔交易嘛,在增加一个状态嘛。 状态转换可以如下所示:

在这里插入图片描述

 
其中0、1代表用户持有的股票数量。 上图中不同的线代表状态之间转换的动作。 注意上面状态转换之间必须要符合一定的因果规则,即第一次卖出只能在第一次买入之后。

还有一幅从leecode上扒下来的图以供参考。
在这里插入图片描述

根据上图,我们可以定义以下状态:

dp[i][0][1] :代表第i天剩余0份股票的第一次卖出所带来的最大利润。
dp[i][0][2] :代表第i天剩余0份股票的第二次卖出所带来的最大利润。

dp[i][1][1] :代表第i天剩余1份股票的第一次买入所带来的最大利润。
dp[i][1][2] :代表第i天剩余1份股票的第二次买入所带来的最大利润。

然后,可以分析一波可以得到以下的动态转移方程:

在这里插入图片描述

然后,就是把算法化成代码实现了。

class Solution {
public:
    
    int maxProfit(vector<int>& prices) {

        int length = prices.size();
        vector<vector<vector<long long >>> dp(length+1,vector<vector<long long>>(2,vector<long long>(3,0)));

        for(int i = 0;i<length+1;++i){
            dp[i][0][0] = 0;
        }

        dp[0][0][1] = INT_MIN;
        dp[0][0][2] = INT_MIN;
        
        dp[0][1][1] = INT_MIN;
        dp[0][1][2] = INT_MIN;

        for(int i = 1;i<=length;++i){
            
            dp[i][0][1] = max(dp[i-1][0][1],dp[i-1][1][1]+prices[i-1]);
            
            dp[i][0][2] = max(dp[i-1][0][2],dp[i-1][1][2]+prices[i-1]);

            dp[i][1][1] = max(dp[i-1][1][1],dp[i-1][0][0]-prices[i-1]);
            dp[i][1][2] = max(dp[i-1][1][2],dp[i-1][0][1]-prices[i-1]);

        }

        long long maxAns = max(dp[length][0][0],dp[length][0][1]);
        return max(maxAns,dp[length][0][2]);
    }
};

2.4 买股票的最佳时机IV

2.4.1 题目描述

在这里插入图片描述

2.4.2 解法

 
你品,你细品,老坛酸菜,还是原来的味道。

只是,把上题目中的3变成了k, 原理还可以用原来的。

这里就不赘述具体的了。直接上代码。

class Solution {
public:
    
    int maxProfit(int k, vector<int>& prices) {

        int length = prices.size();
        vector<vector<vector<long long >>> dp(length+1,vector<vector<long long>>(2,vector<long long>(k+1,0)));

        for(int i = 0;i<length+1;++i){
            dp[i][0][0] = 0;
        }

		for(int i = 0;i<k;++i){
			dp[0][0][i+1] = INT_MIN;
			dp[0][1][i+1] = INT_MIN;
		}

        for(int i = 1;i<=length;++i){
            
			for(int j = 1;j<=k;++j){
				dp[i][0][j] = max(dp[i-1][0][j],dp[i-1][1][j]+prices[i-1]);
				
				dp[i][1][j] = max(dp[i-1][1][j],dp[i-1][0][j-1]-prices[i-1]);

			}
        }
		
		long long maxAns = 0;
		for(int i = 0;i<=k;++i){
			maxAns = max(maxAns,dp[length][0][i]);
		}
        
        return maxAns;
    }
};

 

三、 后面的概括

有始有终,最后再说点。

股票问题除了上面介绍的系列之外,leecode上还有几个,比如说带冷冻期的、要交手续费的。 最终大都可以使用动态规划解决。这里就不赘述了。

然后,

说点啥呢 …

哎,刷题,遇到动态规划,找到套路,ok,没毛病,一气呵成。 找不到方法,哎,难…


参考

【1】、买股票的最佳时机
【2】、买股票的最佳时机II
【3】、买股票的最佳时机III
【4】、买股票的最佳时机IV

LeetCode是一个在线的编程题库,提供了各种各样的算法和数据结构问题供开发者练习和提高编程能力。其中,提到了三个与数组相关的问题。 第一个引用是关于合并两个有序数组的问题,即将两个有序的整数数组合并成一个有序数组。具体的解法可以使用双指针法,分别指向两个数组的末尾,然后比较两个指针指向的元素大小,将较大的元素放入结果数组的末尾,然后将指针向前移动。这个过程重复直到其中一个数组遍历完毕,然后将剩下的元素依次放入结果数组中。 第二个引用是关于移除元素的问题,即移除数组中指定的元素,并返回新数组的长度。可以使用双指针法,左指针维护非指定元素的末尾位置,右指针遍历数组。当右指针指向的元素不等于指定元素时,将右指针指向的元素赋值给左指针指向的位置,然后将左指针和右指针都向前移动一位。直到右指针遍历完整个数组,最后返回左指针的值,即为新数组的长度。 第三个引用也是关于移动零元素的问题,即将数组中的零元素移动到数组末尾,同时保持非零元素的相对顺序不变。可以使用双指针法,左指针维护非零元素的末尾位置,右指针遍历数组。当右指针指向的元素不等于零时,将右指针指向的元素与左指针指向的元素交换位置,然后将左指针和右指针都向前移动一位。直到右指针遍历完整个数组,即完成了零元素的移动。 这些问题都可以使用双指针法解决,利用双指针在数组上进行遍历和操作,实现对数组的操作和处理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [LeeCode每日一题–合并两个有序数组](https://download.csdn.net/download/weixin_38645669/14856034)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [LeeCode 数组题目](https://blog.csdn.net/weixin_43763903/article/details/114675786)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值