首页 > 试题广场 >

买卖股票的最好时机(四)

[编程题]买卖股票的最好时机(四)
  • 热度指数:2178 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 256M,其他语言512M
  • 算法知识视频讲解
假设你有一个数组,长度为,其中是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1. 你最多可以对该股票有笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费
数据范围:





示例1

输入

[8,9,3,5,1,3],3

输出

5

说明

第一天(股票价格=8)买进,第二天(股票价格=9)卖出,收益为1
第三天(股票价格=3)买进,第四天(股票价格=5)卖出,收益为2 
第五天(股票价格=1)买进,第六天(股票价格=3)卖出,收益为2 
总收益为5。
示例2

输入

[3,2,5,0,0,3,1,4],2

输出

7

说明

第二天(股票价格=2)买进,第三天(股票价格=5)卖出,收益为3
第五天(股票价格=0)买进,第八天(股票价格=4)卖出,收益为4
总收益为7
示例3

输入

[9,8,4,1],4

输出

0
import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param prices int整型一维数组 
     * @param k int整型 
     * @return int整型
     */
    public int maxProfit (int[] prices, int k) {
        // write code here
     int n = prices.length;
        if(n == 0) return 0;
        int[][][] dp = new int[k + 1][n][2];
        for(int i = 0; i <= k; i++){
            dp[i][0][1] = -prices[0];     // 首日买入均为负债情况
        }
        for(int day = 1; day < n; day++){
            for(int rest = k - 1; rest >= 0; rest--){
                // 要保证第day天手上无股票,可以在前一天持股的基础上卖掉也可以直接从前一天无股的状态转移过来
                dp[rest][day][0] = Math.max(dp[rest][day - 1][0], dp[rest][day - 1][1] + prices[day]);
                // 要保证第day天手上有股票,可以在前一天无股的基础上买入也可以直接从前一天有股的状态转移过来
                dp[rest][day][1] = Math.max(dp[rest][day - 1][1], dp[rest + 1][day - 1][0] - prices[day]);    // 开启一次新的交易会使得剩余交易数减1
            }
        }
        return dp[0][n - 1][0];
    
    }
}
发表于 2022-03-07 18:08:56 回复(0)

暴力递归

从左往右尝试,需要如下的几个可变参数(固定不变的参数不讨论):
  1. 当前是第几天day;
  2. 还剩下几次交易次数rest;
  3. 当前的收益profit;
  4. 之前是不是刚卖掉股票notBuy。
如此一来,我们有两个base case需要结算当前的收益,并更新最大收益:(1) 已经来到了最后一天;(2) 交易数已经用完。对于普遍的情况,我们需要考虑之前是不是卖掉了股票:(1) 如果刚卖掉过股票,此时可以选择买与不买;(2) 如果刚买过股票,此时可以选择卖与不卖。由此得到如下的暴力递归解法,其实这个解法已经可以通过大部分用例了(16/18)。
import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param prices int整型一维数组 
     * @param k int整型 
     * @return int整型
     */
    int maximumProfit = 0;
    public int maxProfit (int[] prices, int k) {
        // write code here
        recurrent(prices, 0, k, 0, true);
        return maximumProfit;
    }
    
    private void recurrent(int[] prices, int day, int rest, int profit, boolean notBuy) {
        if(day == prices.length || rest == 0){
            // 来到最后一天或交易数已经用完,结算收益
            maximumProfit = Math.max(maximumProfit, profit);
            return;
        }
        if(notBuy){
            // 还没买过可以买或者不买
            recurrent(prices, day + 1, rest, profit, notBuy);    // 不买
            recurrent(prices, day + 1, rest, profit - prices[day], !notBuy);      // 买
        }else{
            // 当前可以选择卖与不卖
            recurrent(prices, day + 1, rest, profit, notBuy);    // 不卖
            recurrent(prices, day + 1, rest - 1, profit + prices[day], !notBuy);    // 卖
        }
    }
}

动态规划

根据递归的逻辑,我们可以改出动态规划。
  1. base case:第一天如果买入股票,则收益为股价的相反数,即dp[x][0][1]=-prices[0]。
  2. 分析递归的参数列表:profit是要填入到dp表中的值,因此它不是一个可变的参数,可变参数一共有day,rest,notBuy三个,因此本质上是一个三维动态规划问题。
  3. 根据参数的取值范围,确定dp表的维度:day的取值范围是0~n-1,rest的取值范围是0~k,notBuy为布尔类型只有两个取值,这里把它规定为0和1。因此dp表的维度可以规定为(k+1)*n*2。
  4. 分析状态转移:暴力递归中的递归调用就对应于动态规划从dp表中取值。
最终可以改出如下的动态规划版本,相应的业务解释也写在了注释里
import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param prices int整型一维数组 
     * @param k int整型 
     * @return int整型
     */
    public int maxProfit (int[] prices, int k) {
        // write code here
        int n = prices.length;
        if(n == 0) return 0;
        int[][][] dp = new int[k + 1][n][2];
        for(int i = 0; i <= k; i++){
            dp[i][0][1] = -prices[0];     // 首日买入均为负债情况
        }
        for(int day = 1; day < n; day++){
            for(int rest = k - 1; rest >= 0; rest--){
                // 要保证第day天手上无股票,可以在前一天持股的基础上卖掉也可以直接从前一天无股的状态转移过来
                dp[rest][day][0] = Math.max(dp[rest][day - 1][0], dp[rest][day - 1][1] + prices[day]);
                // 要保证第day天手上有股票,可以在前一天无股的基础上买入也可以直接从前一天有股的状态转移过来
                dp[rest][day][1] = Math.max(dp[rest][day - 1][1], dp[rest + 1][day - 1][0] - prices[day]);    // 开启一次新的交易会使得剩余交易数减1
            }
        }
        return dp[0][n - 1][0];
    }
}

动态规划空间压缩

从动态规划的解法中我们还可以看到,dp[rest][day]数组中的取值仅依赖于dp[rest][day-1]这个数组中的取值,因此中间这个day的维度是可以被压缩掉的。
import java.util.*;


public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param prices int整型一维数组 
     * @param k int整型 
     * @return int整型
     */
    public int maxProfit (int[] prices, int k) {
        // write code here
        int n = prices.length;
        if(n == 0) return 0;
        int[][] dp = new int[k + 1][2];
        for(int i = 0; i <= k; i++){
            dp[i][1] = -prices[0];     // 首日买入均为负债情况
        }
        for(int day = 1; day < n; day++){
            for(int rest = k - 1; rest >= 0; rest--){
                // 要保证第day天手上无股票,可以在前一天持股的基础上卖掉也可以直接从前一天无股的状态转移过来
                dp[rest][0] = Math.max(dp[rest][0], dp[rest][1] + prices[day]);
                // 要保证第day天手上有股票,可以在前一天无股的基础上买入也可以直接从前一天有股的状态转移过来
                dp[rest][1] = Math.max(dp[rest][1], dp[rest + 1][0] - prices[day]);
            }
        }
        // 返回剩余交易数为0,且手中已经没有股票时的利润
        return dp[0][0];
    }
}
听过左神讲的从暴力递归到动态规划的解题思路,再经过一定量题目的积累,越来越感觉对动态规划的题目而言,重中之重应该是找到尝试的方案,然后不断优化整个算法过程(暴力递归->记忆化搜索->动态规划->斜率优化或空间压缩的动态规划),这样是最稳妥的。因为对于一道没有见过的新题而言,短时间内定义出对解题有帮助的状态,并找出状态转移方程是十分困难的。
编辑于 2021-12-21 10:14:14 回复(0)