LeetCode買賣股票之一:基本套路(122)

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/09/08/17673535.html
-Advertisement-
Play Games

### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 關於《LeetCode買賣股票》系列 - 在LeetC ...


歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

關於《LeetCode買賣股票》系列

  • 在LeetCode上,有數道和買賣股票有關的題目,覆蓋了簡單、中等、困難,要求都是選擇低價時間買入、高價時間賣出,以求達到利潤最大化
  • 這類題型的特點就是:典型的動態規劃題型,掌握套路後,越做越開心,就算難度是困難的題目,也能從容面對
  • 於是,欣宸將此類題目聚集在一起,集中火力分析和解題,構成了《LeetCode買賣股票》系列,在該系列中,欣宸與您一同打好基礎,再將該類型題目逐個攻剋,在LeetCode世界中做一回股神

本篇概覽

  • 對之前的解題經歷做了認真回顧後,我這邊決定用第122題《買賣股票的最佳時機 II》作為系列的開篇,原因是此題在所有買賣股票的文章中最為典型:題目具備代表性,同時其他題目中奇怪的約束條件如凍結期、交易次數等,在122題中都不存在,寫出的狀態轉移方程可以作為其他題目的參考
  • 接下來開始做題吧,先看題目

題目信息

  • 題號:122
  • 難度:中等
  • 描述
  1. 給你一個整數數組 prices ,其中 prices[i] 表示某支股票第 i 天的價格。
  2. 在每一天,你可以決定是否購買和/或出售股票。你在任何時候 最多 只能持有 一股 股票。你也可以先購買,然後在 同一天 出售。
  3. 返回 你能獲得的 最大 利潤 。
  • 示例 1:
輸入:prices = [7,1,5,3,6,4]
輸出:7
解釋:在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5 - 1 = 4 。
     隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6 - 3 = 3 。
     總利潤為 4 + 3 = 7 。
  • 示例 2:
輸入:prices = [1,2,3,4,5]
輸出:4
解釋:在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5 - 1 = 4 。
     總利潤為 4 。
  • 示例 3:
輸入:prices = [7,6,4,3,1]
輸出:0
解釋:在這種情況下, 交易無法獲得正利潤,所以不參與交易可以獲得最大利潤,最大利潤為 0 。
  • 提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

核心問題分析

  • 解題的關鍵,是搞清楚兩個最核心的問題:
  1. 我們要的是什麼?
  2. 變化有哪些?

第一個問題:我們要的是什麼?

  • 認真審題後,我們要的東西可以這樣描述:第i天股市結束後手裡的最大利潤

第二個問題:有哪些變化?

  • 很容易發現,一共有兩種變化:和行為無關、和行為有關
  1. 和行為無關的變化:是時間和股價,只要知道是第幾天,也就知道了股價,所以只要聚焦時間變化即可
  2. 和行為有關的變化:股票持有情況,即持有不持有

確定dp定義

  • 弄清楚上述兩個問題後,dp定義也就呼之欲出了:
  1. dp數組的值就是我們想要的東西
  2. dp數組的維度就是變化,一共有兩個變化,所以一共有兩個維度
  • 於是,我們對dp數組的定義如下圖
    在這裡插入圖片描述
  • 上圖中,i的取值好理解,表示第幾天,至於j,我們規定它只有兩個值:0和1,0代表不持有股票,1代表持有股票
  • 下圖是個例子,很容易理解:第3天股市結束後,未持有股票時,手裡的最大利潤是123元
    在這裡插入圖片描述

狀態轉移方程分析

  • 要想寫出狀態轉移方程,首先要弄明白狀態是怎麼變化的,時間狀態自不必分析,它是客觀在變化的,我們要弄明白的是另一個狀態:股票持有狀態,嚴格來說要弄清楚兩點:
  1. 第i天股市結束後,如果手裡持有股票,這個股票是從哪來的?
  2. 第i天股市結束後,如果手裡沒有股票,為什麼手裡會沒有股票?
  • 只要弄清楚上述兩個問題,狀態轉移方程也就出來了,接下來逐個分析

手裡持有股票的原因

  • 第i天股市結束後,如果手裡持有股票,有兩種可能:
  1. 第i天之前持有股票,到了第i天啥也不做,此時:dp[i][1]=dp[i-1][1]
  2. 第i天之前不持有股票,在第i天購買了,此時:dp[i][1]=dp[i-1][0]-price[i],因為購買要花錢,所以用手裡的錢減去當天股價
  • 我們要的是最大利潤,所以應該取上述兩種情況的最大值
  • 現在可以寫出dp[i][1]的表達式了:dp[i][i]=Math.max(dp[i-1][1], dp[i-1][0]-price[i])
  • 一圖勝千言,看過下圖您就一定明白了
    在這裡插入圖片描述

手裡未持有股票的原因

  • 接下來繼續分析,第i天股市結束後如果手裡沒有股票,有兩種可能導致:
  1. 第i天之前未持有股票,到了第i天啥也不做,此時:dp[i][0]=dp[i-1][0]
  2. 第i天之前持有股票,在第i天賣出,此時:dp[i][0]=dp[i-1][1] + price[i],因為賣出股票會換來錢,所以這裡用手裡的錢加上當天股價
  • 我們要的是最大利潤,所以應該取上述兩種情況的最大值
  • 現在可以寫出dp[i][0]的表達式了:dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+price[i])
  • 一圖勝千言,看過下圖您就一定明白了
    在這裡插入圖片描述
  • 狀態轉移方程已經出來了,接下來按部就班寫好代碼提交即可

編碼

  • 有了上面的分析,相信此刻您也能流暢的完成編碼了,參考代碼如下
class Solution {
    public int maxProfit(int[] prices) {

        int[][] dp = new int[prices.length][2];

        // 第0天股市結束後,如果手裡沒有股票,那就是沒有購買過,此時最大利潤只能等於0
        // 初始化為0的代碼可以省去
        // dp[0][0] = 0;

        // 第0天股市結束後,如果手裡有股票,那就是當前購買的,此時最大利潤就是負數
        dp[0][1] = -prices[0];

        for (int i=1;i<prices.length;i++) {
        	// 第i天股市結束時,手裡沒有股票的原因有兩個:
        	// 1. 之前就沒有股票,第i天啥樣沒做
        	// 2. 之前有股票,第i天賣出
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            // 第i天股市結束時,手裡有股票的原因有兩個:
        	// 1. 之前就有股票,第i天啥樣沒做
        	// 2. 之前沒有股票,第i天買入
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        }

        // 第i天結束後,手裡不持有股票的最大利潤就是返回值
        return dp[prices.length-1][0];
    }
}
  • 提交代碼,如下所示,雖然AC了,但是速度很一般,超過26.27%的提交,顯然還有優化空間
    在這裡插入圖片描述

優化

  • 回顧上述代碼中,dp[i][0]和dp[i][1]都是通過dp[i-i][0]和dp[i-1][1]計算出來的,如此看來,這個dp二維數組似乎有些浪費,用下麵這四個變數足矣
  1. prevWithStock:前一天股市結束後,手裡有股票時的最大利潤
  2. prevWithoutStock:前一天股市結束後,手裡沒有股票時的最大利潤
  3. currentWithStock:當天股市結束後,手裡有股票時的最大利潤
  4. currentWithoutStock:當天股市結束後,手裡沒有股票時的最大利潤
  • 優化後的代碼如下
class Solution {
    public int maxProfit(int[] prices) {
        // 第0天股市結束後,如果手裡有股票,那就是當前購買的,此時最大利潤就是負數
        int prevWithStock = -prices[0];

        // 第0天股市結束後,如果手裡沒有股票,那就是沒有購買過,此時最大利潤只能等於0
        int prevWithoutStock = 0;

        // 當天股市結束後,如果手裡有股票時的最大利潤
        int currentWithStock;

        // 當天股市結束後,如果手裡沒有股票時的最大利潤
        int currentWithoutStock = 0;
        
        for (int i=1;i<prices.length;i++) {
            currentWithoutStock = Math.max(prevWithoutStock, prevWithStock + prices[i]);
            currentWithStock = Math.max(prevWithStock, prevWithoutStock - prices[i]);
            prevWithStock = currentWithStock;
            prevWithoutStock = currentWithoutStock;
        }

        // 第i天結束後,手裡不持有股票的最大利潤就是返回值
        return currentWithoutStock;
    }
}
  • 再次提交,稍微提升了一點
    在這裡插入圖片描述
  • 至此,買賣股票的基本套路,以及狀態轉移方程設計思路和實現,咱們已經學習到了,接下來的文章中,都會基於這個思路去設置狀態轉移方程
  • 當然了,此刻您應該還有個疑問:為何速度的排名如此之低?接下來咱們來看看落後的原因

為啥排名不高?

  • 這道題本身也有一些特殊:除了動態規劃,貪心演算法也能解
  • prices={1, 2, 3}為例,聰明的您應該看出來了,如果1買入,3賣出,得到的利潤等於2,屬於最大利潤
  • 題目有個約束:一天不能既買入又賣出,如果跳出這個約束,那就可以做到1買入2賣出,然後2買入3賣出,利潤還是2!
  • 至於能不能將3-1轉化成(3-2)+(2-1)呢?當然可以,減去2再加上2,對原題的結果毫無影響,卻可以改變代碼流程,如下所示,每當買入賣出能賺錢時,就將插件累加起來,這樣的計算中,相比前面的代碼,每次迴圈中的計算量明顯減少了
class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length<2) {
            return 0;
        }

        int total = 0;

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

            if (prices[i]>prices[i-1]) {
                total += prices[i] - prices[i-1];
            }

        }

        return total;
    }
}
  • 再次提交,這回超越了百分百
    在這裡插入圖片描述
  • 至此又得出一個結論:本題用動態規劃做並沒有錯,也不是動態規劃代碼沒寫好,而是有更高效的貪心演算法恰巧也能解決此問題
  • 經過本篇實戰,相信您對動態規劃以及股票買賣問題都有了更深的理解,接下來,繼續挑戰其他股票買賣問題,在LeetCode世界中向著股神前進

歡迎關註博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在上一篇文章中,我們介紹了彈性資料庫連接失效的背景,並探討了HikariCP連接池探活策略的相關內容。在本文中,我們將會繼續探討另一個線上常用的連接池——Druid,併為您介紹如何在使用Druid時實現最佳實踐的彈性資料庫連接池探活策略。 ...
  • 原文地址:https://www.mssqltips.com/sqlservertip/3598/troubleshooting-transactional-replication-latency-issues-in-sql-server/ 問題 我安裝了幾個SQL Server 2012實例的集群 ...
  • sidebar: auto # Android 調試橋 (adb) Android 調試橋 (adb) 是一種功能多樣的命令行工具,可讓您與設備進行通信。adb 命令可用於執行各種設備操作,例如安裝和調試應用。adb 提供對 Unix shell(可用來在設備上運行各種命令)的訪問許可權。它是一種客戶 ...
  • 在Service中使用系統dialog彈框,但是無法覆蓋全部,底部菜單依然可以被點擊,在某些場景下是不符合需求的 getDialog().getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR); 顯然是 dialog 的層級 ...
  • # 什麼是Promise (含如何判斷一個值是Promise) > 本文旨在對 Promise 的規範進行解釋, 便於讀者在學習 Promise 的過程中梳理 Promise 之間的操作關係, 不對具體的代碼實現和Promise用法進行解釋. > > 比如, 為什麼 [[MDN-await]](ht ...
  • 寫在前面:初次嘗試小程式,在不使用框架的情況下,如果遇到問題,可以儘量去參考官方文檔 1.scroll-view組件 scroll-view是一個可滑動的組塊.需要設置其中具體的height高度,並且在標簽中設置scroll-y="true"; 1 <scroll-view class="sceol ...
  • 一、小程式代碼構成 1.創建文件 在app.json文件中,pages中,直接寫上要添加的文件的名及路徑,然後保存即可(此方法在Mac上親測沒成功), Mac創建頁面的方式: pages文件右鍵,新建文件,然後輸入文件名 ![](https://img2023.cnblogs.com/blog/29 ...
  • 本文給大家介紹了什麼是"編程範式",選擇合適的編程範式可以提高代碼的可讀性、可維護性和可擴展性。 一、 什麼是編程範式? "編程範式"是一種編程思想的總稱,它是指在編寫程式時所採用的基本方法和規範。常見的編程範式有面向對象、函數式、邏輯式等。 選擇合適的編程範式可以提高代碼的可讀性、可維護性和可擴展 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...