动态规划之股票的买卖

主要讲解的是以动态规划的方式来解决算法问题,虽然部分题目也可以使用其他更加快速方法解决,但本篇关注的是动态规划的思想

Best Time to Buy and Sell Stock

只能买卖一次,求最大利润

从前往后遍历数组,求第i天卖出可以获得的最大利润,即第i天的价格(卖出) - 第1~i-1天最低的价格(买入)

状态转移方程:

  1. max_profit[i] = max(price[i] - min_buy_price[i-1], max_profit[i-1])
  2. min_buy_price[i] = min(price[i], min_buy_price[i-1])
import unittest

class Solution:
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        if len(prices) < 2:
            return 0

        max_profit = 0 
        min_buy_price = prices[0]

        for price in prices:
            if price > min_buy_price:
                max_profit = max(max_profit, price - min_buy_price)
            else:
                min_buy_price = min(min_buy_price, price)
        
        return max_profit

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [7, 1, 5, 3, 6, 4]
        self.assertEqual(self.s.maxProfit(input), 5)
    
    def test_two(self):
        input = [7, 6, 4, 3, 1]
        self.assertEqual(self.s.maxProfit(input), 0)

if __name__ == "__main__":
    unittest.main()

Best Time to Buy and Sell Stock II

允许多次买卖,求最大利润

第i天有2种状态:拥有股票own[i],未拥有股票no_own[i],那么其状态方程可以表示为:

  1. 在当天过后手头拥有股票的条件下,有两种情况:①当天买入的;②当天没有操作,之前买入的;own[i] = max(own[i-1], no_own[i-1] - price[i])
  2. 在当天过后手头未拥有股票的条件下,有两种情况:①当天卖出的;②当天没有操作,之前卖出的;no_own[i] = max(own[i-1] + price[i], no_own[i-1])

状态转移方程:

  1. own[i] = max(own[i-1], no_own[i-1] - price[i])
  2. no_own[i] = max(own[i-1] + price[i], no_own[i-1])
import unittest

class Solution(object):
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """

        if len(prices) < 2:
            return 0

        own = [0] * len(prices)
        no_own = [0] * len(prices)
        own[0] = -prices[0]

        for i in range(1, len(prices)):
            own[i] = max(own[i-1], no_own[i-1] - prices[i])
            no_own[i] = max(no_own[i-1], own[i] + prices[i])

        return no_own[-1]

class TestSolution(unittest.TestCase):
    def test_one(self):
        prices = [7,1,5,3,6,4]
        s = Solution()
        print(s.maxProfit(prices))

    def test_two(self):
        prices = [1, 2, 3, 4, 5]
        s = Solution()
        print(s.maxProfit(prices))


if __name__ == "__main__":
    unittest.main()

Best Time to Buy and Sell Stock III

最多两次的任意买卖,求最大利润

第i天有2种状态,k表示这是第k次来回交易:拥有股票own[k][i],未拥有股票no_own[k][i],那么其状态方程可以表示为:

  1. 在当天过后手头拥有股票的条件下,有两种情况:①当天是第k次轮回买入的;②当天没有操作,之前第k次买入的;own[k][i] = max(own[k][i-1], no_own[k-1][i-1] - price[i])
  2. 在当天过后手头未拥有股票的条件下,有两种情况:①当天是第k次轮回卖出的,即在之前第k次轮回买入的基础上计算;②当天没有操作,之前第k次轮回卖出的;no_own[k][i] = max(own[k][i-1] + price[i], no_own[k][i-1])

状态转移方程:

  1. own[k][i] = max(own[k][i-1], no_own[k-1][i-1] - price[i])
  2. no_own[k][i] = max(own[k][i-1] + price[i], no_own[k][i-1])
import unittest
from pprint import pprint

class Solution:
    def maxProfit(self, prices):
        if len(prices) < 2:
            return 0

        length = len(prices)

        own = [[0]*(length) for i in range(3)]
        no_own = [[0]*(length) for i in range(3)]

        own[1][0] = own[2][0] = -prices[0]

        for k in range(1,3):
            for j in range(1, length):
                own[k][j] = max(own[k][j-1], no_own[k-1][j-1] - prices[j])
                no_own[k][j] = max(no_own[k][j-1], own[k][j-1] + prices[j])
        return no_own[-1][-1]

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [3, 3, 5, 0, 0, 3, 1, 4]
        self.assertEqual(self.s.maxProfit(input), 6)
    
    def test_two(self):
        input = [1, 2, 3, 4, 5]
        self.assertEqual(self.s.maxProfit(input), 4)

if __name__ == "__main__":
    unittest.main()

Best Time to Buy and Sell Stock IV

最多k次的任意买卖,求最大利润

同上,不过由于k可能较大,为了节省空间,我们利用k%2来节省空间,因为每第k次交易只与上一次交易有关。
仅仅就行上面的优化是不够的,因为k可能大,单当k>=len(prices)//2时,就转化为Best Time to Buy and Sell Stock II的问题了,这样减少了许多计算量。

import unittest

class Solution:
    def maxProfit(self, k, prices):
        if len(prices) < 2:
            return 0
        
        length = len(prices)

        if k >= length//2:
            own = [0] * length
            no_own = [0] * length
            own[0] = -prices[0]

            for i in range(1, length):
                own[i] = max(own[i-1], no_own[i-1] - prices[i])
                no_own[i] = max(no_own[i-1], own[i] + prices[i])

            return no_own[-1]

        own = [[0]*(length) for i in range(2)]
        no_own = [[0]*(length) for i in range(2)]

        own[0][0] = own[1][0] = -prices[0]

        for k in range(0, k):
            k = k % 2
            own[k][0] = -prices[0]
            for j in range(1, length):
                own[k][j] = max(own[k][j-1], no_own[k-1][j-1] - prices[j])
                no_own[k][j] = max(no_own[k][j-1], own[k][j-1] + prices[j])
        return max(no_own[0][-1], no_own[1][-1])

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [2, 4, 1]
        self.assertEqual(self.s.maxProfit(2,input), 2)
    
    def test_two(self):
        input = [3, 2, 6, 5, 0, 3]
        self.assertEqual(self.s.maxProfit(2, input), 7)

    def test_three(self):
        input = [1, 2]
        self.assertEqual(self.s.maxProfit(1, input), 1)
if __name__ == "__main__":
    unittest.main()

Best Time to Buy and Sell Stock with Cooldown

可以进行任意次交易,但卖出股票的第二天不允许进行交易

第i天有2种状态:拥有股票own[i],未拥有股票no_own[i],那么其状态方程可以表示为:

  1. 在当天过后手头拥有股票的条件下,有两种情况:①当天买入的,由于条件限制,因此只能在第i-1之前卖出;②当天没有操作,之前买入的;own[i] = max(own[i-1], no_own[i-2] - price[i])
  2. 在当天过后手头未拥有股票的条件下,有两种情况:①当天卖出的;②当天没有操作,之前卖出的;no_own[i] = max(own[i-1] + price[i], no_own[i-1])

状态转移方程:

  1. own[i] = max(own[i-1], no_own[i-2] - price[i])
  2. no_own[i] = max(own[i-1] + price[i], no_own[i-1])
import unittest

class Solution:
    def search(self, nums, target):
        if len(nums) == 0:
            return -1

        l = 0
        h = len(nums) - 1

        while l < h:
            m = l + (h-l)//2
            if nums[m] <= target:
                l = m + 1
            elif nums[m] >= target:
                h = m
        
        pos = -1 
        if nums[l] > target:
            pos = l

        return pos

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 3), 4)
    
    def test_two(self):
        input = [-1, 0, 3, 3, 5, 9, 12]
        self.assertEqual(self.s.search(input, 2), 2)

    def test_three(self):
        input = [0]
        self.assertEqual(self.s.search(input, 0), -1)

if __name__ == "__main__":
    unittest.main()

Best Time to Buy and Sell Stock with Transaction Fee

可以交易任意多笔,但每次买卖需要支付一定的费用fee

第i天有2种状态:拥有股票own[i],未拥有股票no_own[i],那么其状态方程可以表示为:

  1. 在当天过后手头拥有股票的条件下,有两种情况:①当天买入的;②当天没有操作,之前买入的;own[i] = max(own[i-1], no_own[i-1] - price[i])
  2. 在当天过后手头未拥有股票的条件下,有两种情况:①当天卖出的,此时需要支付交易费;②当天没有操作,之前卖出的;no_own[i] = max(own[i-1] + price[i] - fee, no_own[i-1])

状态转移方程:

  1. own[i] = max(own[i-1], no_own[i-1] - price[i])
  2. no_own[i] = max(own[i-1] + price[i] - fee, no_own[i-1])
import unittest

class Solution:
    def maxProfit(self, prices, fee):
        """
        :type prices: List[int]
        :type fee: int
        :rtype: int
        """

        length = len(prices)

        if len(prices) < 2:
            return 0

        last_own = -prices[0]
        last_no_own = 0

        for i in range(1, length):
            own = max(last_own, last_no_own - prices[i])
            last_no_own = max(last_no_own, last_own + prices[i] - fee)
            last_own = own
        
        return last_no_own

class TestSolution(unittest.TestCase):
    def setUp(self):
        self.s = Solution()

    def test_one(self):
        input = [1, 3, 2, 8, 4, 9]
        self.assertEqual(self.s.maxProfit(input, 2), 8)

    def test_two(self):
        pass

if __name__ == "__main__":
    unittest.main()

总结

一开始做这些题的时候,不知道从何下手,但经过这几道题的训练之后,对于大部分的动态规划还是能分析出来,只要状态方程推导出来以后,确定好初始值,就可以得到最终的结果。

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

推荐阅读更多精彩内容