2021/01/09 每日一题 最佳时间III(斐波那契数列和动态规划)

困难题我哭了,虽然有点思路,但是最后的代码还是超时了,记录下吧

LeetCode上最佳时机 III,之前做了最佳时机I最佳时机II,这道是困难题记录下解题思路。这是一个动态规划问题,之前写过的斐波那契数就是一个类似一位数组下的动态规划问题。

动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。

就像斐波那契数的公式f(n) = f(n-1) + f(n-2)一样,在n情况下的值,是前两次值的和。当我们求f(5)的值的时候就能拆分成下图所示,f(0)f(1)都是很好计算或者已知的值。通过将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案的过程就是动态规划的思想了。


将上面斐波那契的例子扩展成二维数组情况下,就能求解当前问题。
首先最多可交易2次,那么就有0/1/2次三种可交易的形式,假设现在传入的价格数组是prices = [7,1,5,3,2,4,8],可以构建出如下二维数组表格,最上排灰色字体对应价格数组的序号

建立数组

首先填充第一排,第一排是操作次数为0的情况,如果什么都不做,那么收益就是0
第一列填充,也是如果在第一天买卖,不管多少次,收益均为0,这两条都是可以直接获取的,可以作为二维数组的初始值来填充
第一排第一例填充0

接下来填充第二排数据,当天只操作1次的时候,并且还需要注意必须先买入后卖出,那么会有两种操作方式即:

  1. 当天不操作 当天不操作的情况就是继承前一天操作1次时候的收益
  2. 当天操作 当天操作的情况就是不操作的情况的收益 + 今天卖出的收益
    对于不操作的情况的收益,根据天数不同,需要判断的数据也不同,例如现在填写序号2的数据,那么就要对比在序号0、1的时候,操作为0的时候的收入,取其中最大的值

在这2种操作中间取最大值,就是今天操作1次的最大收益,根据上面的计算步骤可以填写如下表格

填充第二排

接下来填充第三排数据,此时需要操作2次,就是拆分为继承前一天的收益,即当天不操作或者前一天操作1次,今天再操作一次两种可能(这里不考虑一天操作2次的情况,因为必须卖出后买入,如果一天操作2次,那么当天就要买卖一次,获利0,就和第二种情况重合了)

根据动态规划的原理这里的6就是我们需要的结果,看官方例子给的结果也是6,达到目标

结合2排数据可以得到以下结论

当天的收益结果可以为以下情况

  1. 当天不操作继承前天的收益
  2. 当天操作一次+前几天操作一次的收益,然后取这些的最大值

可以得出以下代码片段

let maxp = 0
for(let n =j-1; n>=0; n--) {
// 对比几次的收益,获取最大值
// prices[j] 是今天的价格
// prices[n] 是第n天的价格
// dp[i-1][n] 是第n天的最大收益,指前一天操作一次的时候的收益
maxp = Math.max(prices[j]-prices[n]+dp[i-1][n],maxp)
}
// 和前今天一次都不操作的收益进行对比,求最大值
Math.max(dp[i][j-1],maxp)

这样我们了解了所有的原理之后就能得到如下完整代码

 var maxProfit = function(prices) {
    // 保存数组长度 
    let n = prices.length
    // 如果为空返回0
    if(n == 0) return 0
    // 第一个参数为行数,第二个参数为列数,快速定义二维数组,快速创建二维数组小技巧
    let dp = Array.from(Array(3),() => new Array(n));
    // 第一行填充0
    for (let i = 0; i < n; i++) {
      dp[0][i] = 0
    }
    // 第一列填充0
    for (let j = 0; j < 3; j++) {
      dp[j][0] = 0
    }
    console.log(dp);
    // 然后开始填充数组
    for(let j = 1; j < 3; j++) {
      // 从第二排开始,先填充操作一次的数据
      for(let i = 1; i < n; i++) {
        // 设置最大收益
        let maxp = 0
        // 第i行的第j个数据的最大值
        for(let n = i-1; n >= 0; n--){
          maxp = Math.max(prices[i]-prices[n]+dp[j-1][n],maxp)
        }
        // 和前一天的数据对比,填入最大值
        dp[j][i] = Math.max(dp[j][i-1],maxp)
      }
    }
    console.log(dp);
    return dp[2][n-1];
};

这个代码得到的结果和之前我们填写的表格一样,最后一排的最后一位就是需要的结果



上面的代码对于小数据还是能够处理的,但是传入LeetCode上提交的时候出现了超时的问题,主要还是出现在下面这个for循环上,多次嵌套遍历需要的时间太多了。

// 第i行的第j个数据的最大值
for(let n = i-1; n >= 0; n--){
  maxp = Math.max(prices[i]-prices[n]+dp[j-1][n],maxp)
}

下面开始优化
自己优化太水了,参考的Leetcode刷题 123.买卖股票的最佳时机 III Best Time to Buy and Sell Stock III
这里应该使用一个变量来保存之前天数的最大利润,就像回到最开始斐波那契数的公式f(n) = f(n-1) + f(n-2),如果保存了f(n-1)f(n-2)的数据的时候,就不用每次都去前面推算这两个数据了,可以大大减少计算量。
可以将代码优化如下。

 var maxProfit = function(prices) {
    // 保存数组长度 
    let n = prices.length
    // 如果为空返回0
    if(n == 0) return 0
    // 第一个参数为行数,第二个参数为列数,快速定义二维数组,快速创建二维数组小技巧
    let dp = Array.from(Array(3),() => new Array(n));
    // 第一行填充0
    for (let i = 0; i < n; i++) {
      dp[0][i] = 0
    }
    // 第一列填充0
    for (let j = 0; j < 3; j++) {
      dp[j][0] = 0
    }
    // 然后开始填充数组
    for(let j = 1; j < 3; j++) {
      // 设置最大收益
      let maxp = -prices[0]
      // 从第二排开始,先填充操作一次的数据
      for(let i = 1; i < n; i++) {
        // 和前一天的数据对比,填入最大值
        dp[j][i] = Math.max(dp[j][i-1],prices[i] + maxp)
        maxp = Math.max(maxp,dp[j-1][i]-prices[i])
      }
    }
    console.log(dp);
    return dp[2][n-1];
};

几个优化的点:

  1. let maxp = -prices[0]设置最大收益
  2. 用maxp保存前i-1天的最大收益,从而减少计算量

参考文章:

  1. 从斐波那契数列看递归和动态规划
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351