動態規劃

動態規劃思路解析

在此記錄如何分析一個問題使用動態規劃的方法實現時間複雜度上的優化。

對比暴力算法,在下面問題上的時間複雜度從O(n^2)降低到了O(n)。並且空間複雜度進行了優化,從O(n)降低到了O(1)。

 

示例問題:最大子序和

給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。

示例:

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。

https://leetcode-cn.com/problems/maximum-subarray/

 

思路解析

思路參考了leetcode題解中的高手(https://leetcode-cn.com/u/liweiwei1419/)在這個問題下的答案

第 1 步:定義狀態
既然一個連續子數組一定要以一個數作爲結尾,那麼我們就將狀態定義成如下。

dp[i]:表示以 nums[i] 結尾的連續子數組的最大和。

那麼爲什麼這麼定義呢?這是因爲這樣定義狀態轉移方程容易得到。
怎麼想到這麼定義的呢?憑經驗,以前做過類似問題,例如「力扣」第 300 題:“最長上升子序列”,或者說是憑感覺。這兩道題都是動態規劃的經典問題,當做例題來學習未嘗不可,我學習動態規劃的時候,就是直接看別人的博客和題解的。
第 2 步:思考狀態轉移方程
根據狀態的定義,由於 nums[i] 一定會被選取,並且 dp[i] 所表示的連續子序列與 dp[i - 1] 所表示的連續子序列(有可能)就差一個 nums[i] 。

假設數組 nums 全是正數,那麼一定有 dp[i] = dp[i - 1] + nums[i],但是搞不好 dp[i - 1] 是負數也是有可能的。例如前幾個數都是負數,突然來了一個正數。

於是分類討論:

如果 dp[i - 1] >= 0,那麼可以把 nums[i] 直接接在 dp[i - 1] 表示的那個數組的後面。

如果 dp[i - 1] < 0,那麼加上前面的數反而越來越小了,於是“另起爐竈”,單獨的一個 nums[i],就是 dp[i]。

以上兩種情況的最大值就是 dp[i] 的值,寫出如下狀態轉移方程:

dp[i] = \begin{cases} dp[i - 1] + nums[i], & if \quad dp[i - 1] \ge 0 \\ nums[i], & if \quad dp[i - 1] < 0 \end{cases}
 

記爲“狀態轉移方程 1”。

狀態轉移方程還可以這樣寫,反正求的是最大值,也不用分類討論了,就這兩種情況,取最大即可,因此還可以寫出狀態轉移方程如下:

dp[i] = \max \{nums[i],\; dp[i - 1] + nums[i]\}
dp[i]=max{nums[i],dp[i−1]+nums[i]}

記爲“狀態轉移方程 2”。

動態規劃的問題經常要分類討論,這是因爲動態規劃的問題本來就有最優子結構的特徵,即大問題的最優解通常由小問題的最優解得到,那麼我們就需要通過分類討論,得到大問題的小問題究竟是哪些。

第 3 步:思考初始值
dp[0] 根據定義,一定以 nums[0] 結尾,因此 dp[0] = nums[0]。

第 4 步:思考輸出
這裏狀態的定義不是題目中的問題的定義,不能直接將最後一個狀態返回回去。

輸出應該是把所有的 dp[0]、dp[1]、……、dp[n - 1] 都看一遍,取最大值。 同樣的情況也適用於「力扣」第 300 題:“最長上升子序列”。我經常在這一步“摔跟頭”,請各位也留意。

 

參考代碼 

// 動態規劃法 --> 創建一個動態數組,將目標的中間結果保存在該數組中,最後再求出動態數組的目標值。
    // 優化:此時時間複雜度爲O(n),空間複雜度也爲O(n),可以降低空間複雜度進行優化,從O(n)變爲O(1)
    int maxSubArray(vector<int>& nums)
    {
        if (nums.size() < 1) return 0;

        vector<int>dp(nums.size(), 0); // dp[i]表示以nums[i]結尾的最大子序的和

        dp[0] = nums[0];
        int result = dp[0];

        for (int i = 1; i < nums.size(); i++) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
            result = max(dp[i], result);
        }

        return result;
    }
    // 執行時間8ms,內存消耗9.4MB
    int maxSubArray(vector<int>& nums)
    {
        if (nums.size() < 1) return 0;

        //因爲只需要知道dp的前一項,我們用int代替一維數組
        int dp(nums[0]);
        int result = dp;

        for (int i = 1; i < nums.size(); i++) {
            dp = max(dp + nums[i], nums[i]);
            result = max(dp, result);
        }

        return result;
    }

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章