数据结构与算法:动态规划中用观察优化枚举
前言
在动态规划里,有时候光写到严格位置依赖的话依然会超时,这时候可能就需要一点技巧来对动态规划进行优化。例如,多重背包问题中的二进制分组优化和单调队列优化都是动态规划优化的一种。
一、观察并优化转移方程
观察并优化转移方程基本上就是最简单的一种优化方式,主要就是通过观察对依赖的枚举,然后将枚举的循环优化成O(1)级别的方法。
1.买卖股票的最佳时机
class Solution {
public:int maxProfit(vector<int>& prices) {int n=prices.size();int ans=0;for(int i=1,Min=prices[0];i<n;i++){Min=min(Min,prices[i]);//维护左侧最小值 -> 买入ans=max(ans,prices[i]-Min);//看看此时卖出能否收益更大}return ans;}
};
其实要提的是题目四,正好就一起把股票问题都写了吧。
这个题其实就很简单了,就是每次维护左侧最小值,即买入时间,然后每次都判断在此时买入能否把收益更新得更大即可。
2.买卖股票的最佳时机 II
class Solution {
public:int maxProfit(vector<int>& prices) {//抓“上坡”int n=prices.size();int ans=0;for(int i=1;i<n;i++){if(prices[i-1]<prices[i]){ans+=prices[i]-prices[i-1];}}return ans;}
};
这个题也不难其实,就是抓“上坡”,即若下一天价值会增加就选择今天买入后明天卖出。
3.买卖股票的最佳时机 III
class Solution {
public:int maxProfit(vector<int>& prices) {//return way1(prices);//return way2(prices);//return way3(prices);return way4(prices);}//未优化枚举的解int way1(vector<int>&prices){int n=prices.size();//定义dp1[i]:0~i上发生且只发生一次交易,不一定必在i卖出vector<int>dp1(n);for(int i=1,Min=prices[0];i<n;i++){Min=min(Min,prices[i]);//左侧最小值dp1[i]=max(dp1[i-1],prices[i]-Min);//不卖和卖}//定义dp2[i]:0~i上发生两次交易且第二次必在i卖出vector<int>dp2(n);int ans=0;for(int i=1;i<n;i++){for(int j=0;j<=i;j++)//枚举第二次买入时机{dp2[i]=max(dp2[i],dp1[j]+prices[i]-prices[j]);}ans=max(ans,dp2[i]);}return ans;}//优化过枚举的解int way2(vector<int>&prices){int n=prices.size();//观察求dp2的枚举过程,可以发现,每一步都有加prices[i]//所以改为求dp1[j]+prices[j]的最大值,最后再加prices[i]//求dp1vector<int>dp1(n);for(int i=1,Min=prices[0];i<n;i++){Min=min(Min,prices[i]);//左侧最小值dp1[i]=max(dp1[i-1],prices[i]-Min);//不卖和卖}//定义best[i]:0~i范围上,dp1[i]+prices[i]最大值vector<int>best(n);//初始化best[0]=dp1[0]-prices[0];for(int i=1;i<n;i++){best[i]=max(best[i-1],dp1[i]-prices[i]);}//求dp2vector<int>dp2(n);int ans=0;for(int i=1;i<n;i++){dp2[i]=best[i]+prices[i];ans=max(ans,dp2[i]);}return ans;}//进一步优化循环的解int way3(vector<int>&prices){int n=prices.size();//因为三个for循环都是从1~n-1,所以可以合并vector<int>dp1(n);vector<int>best(n);vector<int>dp2(n);//初始化best[0]=-prices[0];int ans=0;for(int i=1,Min=prices[0];i<n;i++){Min=min(Min,prices[i]);dp1[i]=max(dp1[i-1],prices[i]-Min);best[i]=max(best[i-1],dp1[i]-prices[i]);dp2[i]=best[i]+prices[i];ans=max(ans,dp2[i]);}return ans;}//进行空间压缩的解int way4(vector<int>&prices){int n=prices.size();int dp1=0;int best=-prices[0];int ans=0;for(int i=1,Min=prices[0];i<n;i++){Min=min(Min,prices[i]);dp1=max(dp1,prices[i]-Min);best=max(best,dp1-prices[i]);ans=max(ans,best+prices[i]);}return ans;}
};
因为要求是只能交易两笔,所以考虑设置dp1和dp2,分别定义为0~i只发生一次交易和发生两次交易且第二次必须在i位置的最大收益。那么dp1的计算方法就是维护左侧最小值,然后来到i位置分卖和不卖两种情况即可。dp2的求法就是每来到i位置时要去左侧枚举第二次的买入时机,看看能不能把自己更新得更大,所以依赖关系就是dp1[j],即在0~j范围上完成一次交易,然后加上j位置买入i位置卖出的收益。最后,用ans记录在每个位置完成两次交易的最大收益。
因为这个方法时间复杂度太高了,所以要对其进行优化。进一步观察dp2的枚举过程可以发现,每一个依赖都有加i位置的价格。所以就可以考虑提出来最后统一加,那么依赖关系就转化成了求dp1[j]+prices[j]的最大值。所以可以定义best[i]为0~i范围上dp1[i]+prices[i]的最大值。那么有了best数组,求dp2的时候就直接再best的基础上加上prices[i]即可。
再观察可以发现,因为这三个循环都是从1到n-1的,所以可以考虑合并到一起求。
再观察依赖关系可以发现,可以进行空间压缩,那么就是用简单几个变量往下滚动更新。