leetcode 經典動態規劃DP算法題目(思路、方法、code)

leetcode 經典動態規劃DP算法題目(思路、方法、code)

動態規劃最重要的在於設計DP數組,找到相應的動態轉移方程

70. 爬樓梯

假設你正在爬樓梯。需要 n 階你才能到達樓頂。

每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?

**注意:**給定 n 是一個正整數。

分析:這個問題,可以看青蛙跳臺階(遞歸與數學歸納),實際上就是斐波那契數列的變體

在上述中,採用了遞歸或者正向遍歷的方法,實際上動態規劃就是一種正向遍歷的方法。如果採用遞歸時,由於f(n)=f(n-1)+f(n-2),故直接遞歸即可,但是仔細思考,如果用該方法,計算f(n-1)時計算了f(n-2),而計算f(n)時又計算了f(n-2),因此存在大量的重複計算。故在此便考慮用一個數組存儲對應的函數值,這便是動態規劃思想。

令f(n)表示到達n階的樓頂的方法數,則f(n)=f(n-1)+f(n-2),正向計算即可。

class Solution {
public:
    int climbStairs(int n) 
    {
         vector<int> dp(n+1);
         dp[0]=1;dp[1]=1;
         for(int i=2;i<=n;i++)
            dp[i]=dp[i-1]+dp[i-2];
        return dp[n];
    }
};

198. 打家劫舍

你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。

給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。

示例 1:
輸入: [1,2,3,1]
輸出: 4
解釋: 偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4 。

示例 2:
輸入: [2,7,9,3,1]
輸出: 12
解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。
     偷竊到的最高金額 = 2 + 9 + 1 = 12

分析:由於相鄰的房間不能同時選擇,因此在第k個房間時,有兩種情況

  • 如果選取了第k個房間,說明第k-1個房間一定不能選,那說明這種情況的最大金額應該是前k-2個房間的最大金額加上第k個房間的金額
  • 如果沒有選取第k個房間,說明這種情況的最大金額實際上就是前k-1個房間的最大金額
  • 因此,如果令 dp[i]dp[i] 表示從0 到 $i $ 房間能盜竊的最高金額,動態轉移方程則爲 dp[i]=max(dp[i2]+a[i],dp[i1])dp[i]=max(dp[i-2]+a[i],dp[i-1]) ,其中 a[i]a[i]表示第i個房間的金額

根據此,可以遍歷,最終結果應該爲 dp[n1]dp[n-1]

class Solution {
public:
    int rob(vector<int>& nums) 
    {
        int length=nums.size();
        if(length==0) return 0;
        if(length==1) return nums[0];
        vector<int> dp(length+1);
        dp[0]=nums[0];
        dp[1]=max(nums[0],nums[1]);
        for(int i=2;i<length;i++)
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[length-1];
    }
};

213. 打家劫舍 II

你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都圍成一圈,這意味着第一個房屋和最後一個房屋是緊挨着的。同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。

給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。

示例 1:
輸入: [2,3,2]
輸出: 3
解釋: 你不能先偷竊 1 號房屋(金額 = 2),然後偷竊 3 號房屋(金額 = 2, 因爲他們是相鄰的。

示例 2:
輸入: [1,2,3,1]
輸出: 4
解釋: 你可以先偷竊 1 號房屋(金額 = 1),然後偷竊 3 號房屋(金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4

分析:這個題與上一個題的區別在於,房屋是環形的,這意味着,第一個房間和最後一個房間不能同時偷竊。這就意味着,**將原來的問題轉化爲兩個子問題:偷竊1 ~ n-1個房間的最大值以及偷竊 2~n 個房間的最大值。最終,取二者的最大值即爲解。**因此可以對上一個問題進行兩次dp即可。

class Solution {
public:
    int rob(vector<int>& nums) 
    {
        int length=nums.size();
        if(length==0) return 0;
        if(length==1) return nums[0];
        vector<int> dp(length+1);
        dp[0]=nums[0];
        dp[1]=max(nums[0],nums[1]);
        for(int i=2;i<length-1;i++) //先對前n-1進行DP
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        int result=dp[length-2]; //存儲其值
        dp[0]=0;dp[1]=nums[1];
        for(int i=2;i<length;i++)  //對後n-1進行DP
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        //取最大值即可
        result=max(result,dp[length-1]);
        return result;
    }
};

337. 打家劫舍 III

在上次打劫完一條街道之後和一圈房屋後,小偷又發現了一個新的可行竊的地區。這個地區只有一個入口,我們稱之爲“根”。 除了“根”之外,每棟房子有且只有一個“父“房子與之相連。一番偵察之後,聰明的小偷意識到“這個地方的所有房屋的排列類似於一棵二叉樹”。 如果兩個直接相連的房子在同一天晚上被打劫,房屋將自動報警。

計算在不觸動警報的情況下,小偷一晚能夠盜取的最高金額。

示例 1:
輸入: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \ 
     3   1

輸出: 7 
解釋: 小偷一晚能夠盜取的最高金額 = 3 + 3 + 1 = 7.

示例 2:
輸入: [3,4,5,1,3,null,1]

     3
    / \
   4   5
  / \   \ 
 1   3   1

輸出: 9
解釋: 小偷一晚能夠盜取的最高金額 = 4 + 5 = 9.

分析:每個節點可選擇偷或者不偷兩種狀態,根據題目意思,相連節點不能一起偷,因此父結點如果偷,則兩個字節點就不能偷,父結點如果不偷,則子節點可以偷。注意這裏說的是可以而不是一定,只需要父結點不偷的情況下兩個子節點能夠拿出最多的錢即可。因此有了遞歸的想法。

  • 當前節點選擇不偷:當前節點能偷到的最大錢數 = 左孩子能偷到的最大錢 + 右孩子能偷到的最大錢
  • 當前節點選擇偷:當前節點能偷到的最大錢數 = 左孩子選擇自己不偷時能得到的錢 + 右孩子選擇不偷時能得到的錢 + 當前節點的錢數
  • 考慮到在python中常用的返回多值的函數,在此處用了pair來存儲結果,其中pair的第一個表示當前節點不偷獲得的最大錢,pair的第二個表示當前節點偷的話獲得的最大錢,根據此,便可以將上述邏輯輕鬆地表達出來。
class Solution {
public:
    int rob(TreeNode* root) 
	{
		pair<int,int> result=rob_helper(root);
		return max(result.first,result.second);
    }
    pair<int,int> rob_helper(TreeNode* root)
    {
    	pair<int,int> root_result;
    	pair<int,int> root_left;
    	pair<int,int> root_right;
    	if(root==NULL) return root_result;	
    	else
    	{
    		root_left=rob_helper(root->left);
    		root_right=rob_helper(root->right);
		}
			
  root_result.first=max(root_left.first,root_left.second)+max(root_right.first,root_right.second);
		root_result.second=root_left.first+root_right.first+root->val;
		return root_result;
	}
};

53. 最大子序和

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

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

分析:

動態規劃法:動態規劃的關鍵,就是設置dp變量,找到動態轉移方程

對於該題而言,由於需要找連續子數組,因此設置 dpdp變量時,必須考慮如何設置才能夠將連續考慮在內。在此用 dp[i]dp[i] 表示以第 ii 個元素結尾的數組的最大和,則dp[i]=max(dp[i1]+nums[i],nums[i])dp[i]=max(dp[i-1]+nums[i],nums[i]) ,因此也可以理解爲如果 dp[i1]>0dp[i-1]>0, 則 dp[i]=dp[i1]+nums[i]dp[i]=dp[i-1]+nums[i] ,否則 dp[i]=nums[i]dp[i]=nums[i]

class Solution {
public:
    int maxSubArray(vector<int>& nums) 
    {
        int length=nums.size();
        if(length==0) return 0;
        if(length==1) return nums[0];
        vector<int> dp(length);
        dp[0]=nums[0];
        int result=dp[0];
        for(int i=1;i<length;i++)
        {
            dp[i]=dp[i-1]>0?dp[i-1]+nums[i]:nums[i];
            result=max(result,dp[i]);
        }
        return result;

    }
};

貪心法:

貪心的思想在於,每次儘可能地選取數字,但如果已經選取的數字和小於0了,則說明如果加上前面這些數字,只會令後面的連續子數組和變小,故重新開始選取數字。期間記錄每次選取的連續子數組的和並更新最大值。

class Solution {
public:
    int maxSubArray(vector<int>& nums)
    {
        int max_sum=INT_MIN;  //初始化
        int cur_sum=0;  //當前選取的數字的和
        for(int i=0;i<nums.size();i++)
        {
            cur_sum+=nums[i];
            max_sum=max(cur_sum,max_sum);
            if(cur_sum<0)  //說明前面的部分應當拋棄
            {
                cur_sum=0;
            }
        }
        return max_sum;
    }
};

322. 零錢兌換

給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。

示例 1:
輸入: coins = [1, 2, 5], amount = 11
輸出: 3 
解釋: 11 = 5 + 5 + 1
   
示例 2:
輸入: coins = [2], amount = 3
輸出: -1

分析:首先要理解,貪心會出問題,例如硬幣面額爲[2,7,10],想要14元,則貪心會10+2+2,但是實際上7+7即可,因此我們可以發現,對於部分面額(日常生活中的1,2,5,10)貪心可行,但是一些情況下是不可行的。

動態規劃法設置 dp[i]dp[i] 表示金額 i 的最優解,則dp[i]是由哪些dp[j] 相關呢?如果面值爲[1,2,5],則我們可以發現, dp[i]可以由dp[i-1],dp[i-2],dp[i-5]添加一枚硬幣獲得。因此,實際上,dp[i]是由任意一個可能添加一枚金幣得到 i 面額的dp[i-j] 轉移到達。因此可以用兩重循環完成dp[i]的遍歷,需要注意的是,所給的金錢可能無法組成i,此時應該記爲-1.

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        vector<int> dp; //dp[i]表示i面額的最優解
        for(int i=0;i<=amount;i++)
            dp.push_back(-1); //表示還沒有遍歷到
        dp[0]=0;
        for(int i=1;i<=amount;i++)
        {    for(int j=0;j<coins.size();j++)
            {
                if(i-coins[j]>=0&&dp[i-coins[j]]!=-1) //說明可以組成
                    dp[i]=(dp[i]==-1)?(dp[i-coins[j]]+1):min(dp[i],dp[i-coins[j]]+1);
            }
        }
        return dp[amount];
    }
};

120. 三角形最小路徑和

給定一個三角形,找出自頂向下的最小路徑和。每一步只能移動到下一行中相鄰的結點上。

例如,給定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

自頂向下的最小路徑和爲 11(即,2 + 3 + 5 + 1 = 11)。

說明:如果你可以只使用 O(n) 的額外空間(n 爲三角形的總行數)來解決這個問題,那麼你的算法會很加分

分析:直觀上,爲每一個節點設置一個狀態便可以較爲輕鬆地用動態規劃解決,dp[i][j]dp[i][j] 表示從起始點到達第i行第j列最小路徑和,則 dp[i][j]=min(dp[i1][j],dp[i1,j1])dp[i][j]=min(dp[i-1][j],dp[i-1,j-1]) (考慮自己將三角形畫圖時左側對齊,則每個點可以走向自己的下一個節點或者下右方節點),根據此,很容易解決問題。

這裏我用到了一個技巧,由於路徑節點的空間已經給定,所以設置dp數組時直接用原始數組進行了賦值,這樣的拷貝會降低錯誤率並且更快。(實際上,可以直接在給定數組上進行修改,甚至都不需要再開dp數組,但是由於傳入的是引用,爲了避免修改原始數組,在此我拷貝了新的數組)

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) 
    {
        vector<vector<int>> dp=triangle;
        if(dp.size()==0) return 0;
        for(int i=1;i<dp.size();i++)
            dp[i][0]=dp[i-1][0]+triangle[i][0]; //將第一列初始化,減少判斷時候的繁瑣
        for(int i=1;i<dp.size();i++)
            for(int j=1;j<dp[i].size();j++)
                if(j==i) dp[i][j]=dp[i-1][j-1]+triangle[i][j];
                else dp[i][j]=min(dp[i-1][j-1],dp[i-1][j])+triangle[i][j];
        int result=dp[dp.size()-1][0];
        for(int j=1;j<dp.size();j++)
            result=min(result,dp[dp.size()-1][j]);
        return result;
    }
};

僅需要 O(n)O(n) 空間的dp算法解決該問題:

主要思路在於,用dp[i]表示當前的行時,到達第i個位置的最小和,也就是把dp 數組設置爲動態更新的。每遍歷一行,都會更新 dp數組,這樣,在最後一行時,就會得到和上例一樣的dp數組,取最小值即可。但是這裏注意的是,**更新dp時候是從右向左更新,否則會導致左側更新影響右側的值。**可以畫圖感受一下

可以理解一下這個思路,沒有改動原始數組以及並沒有增加時間的情況下,將空間進行了壓縮

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) 
    {
        if(triangle.size()==0) return 0;
        vector<int> dp(triangle.size());
        dp[0]=triangle[0][0];
        for(int i=1;i<triangle.size();i++)
        for(int j=i;j>=0;j--)  //注意從右側向左側更新
        {
            if(j==0) dp[j]=dp[j]+triangle[i][j];  //第一列時候
            else if(j==i) dp[j]=dp[j-1]+triangle[i][j]; //最右側時候
            else dp[j]=min(dp[j],dp[j-1])+triangle[i][j];
        }
        int result=dp[0];
        for(int i=0;i<dp.size();i++)
            result=min(result,dp[i]);
        return result;
    }
};

300. 最長上升子序列

給定一個無序的整數數組,找到其中最長上升子序列的長度

示例:

輸入: [10,9,2,5,3,7,101,18]
輸出: 4 
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:

可能會有多種最長上升子序列的組合,你只需要輸出對應的長度即可。
你算法的時間複雜度應該爲 O(n2) 。
進階: 你能將算法的時間複雜度降低到 O(n log n)?

分析:

時間複雜度爲 O(n2)O(n^2)的方法:

設置DP數組,其中dp[i]表示以第i個元素結尾的最長上升子序列的長度,則思考dp[i]應該如何計算得到?

對於任意的 j<ij<i ,可知,如果在 jj 處的元素小於在第i處的元素值,則說明以j結尾的最長子序列,可以接上第i個元素,從而形成新的上升序列。故可知 如果 nums[j]<nums[i]nums[j]<nums[i],則$dp[i]=max(dp[j]+1,dp[i]) $

基於此,便可以解決問題了。複雜度爲 O(n2)O(n^2)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        if(nums.size()==0) return 0;
        vector<int> dp(nums.size());
        int result=0;
        for(int i=0;i<nums.size();i++)
        {   
            dp[i]=1; 
            for(int j=0;j<i;j++)
                if(nums[j]<nums[i]) 
                    dp[i]=max(dp[i],dp[j]+1);
            result=max(result,dp[i]);
        }    
        return result;
    }
};

時間複雜度爲 O(nlogn)O(nlogn) 的方法

思路主要在於對於任意一個固定的子序列長度,爲了達到儘可能地使得後面可以繼續添加,我們要儘可能使末尾元素最小。設置dp[i] ,表示長度爲i的子序列結尾的最小值。因此,遍歷一遍數組,便可以更新完dp[i],最終最後一個dp[i]的 ii 就是結果。

但是這樣實際上仍舊是一個時間複雜度爲O(n)的方法,只是每次在遍歷新元素時候,比較的對象從該元素前面的元素變成了dp數組的元素。

在此有一個數學可證的內容,即dp數組是一個升序的,因爲如果插入的新元素比較小,它一定會把dp數組中小於它的元素都更新了。 因爲dp數組是增序的,所以可以採用二分查找,從而將比較的O(n)縮減爲O(logn)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        if(nums.size()==0)
            return 0;
        vector<int> dp;
        dp.push_back(nums[0]);
        for(int i=1;i<nums.size();i++)  //從左到右遍歷
        {
            if(nums[i]>dp.back())  //說明可以組成更長的字串
                dp.push_back(nums[i]);
            else
            {
                //pos是找到第一個大於nums[i]的位置,
                int pos=lower_bound(dp.begin(),dp.end(),nums[i])-dp.begin();
                dp[pos]=nums[i];
            }
        }
        return dp.size(); //返回dp的長度即可
    }
};

64. 最小路徑和

給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和爲最小。

**說明:**每次只能向下或者向右移動一步。

示例:

輸入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
輸出: 7
解釋: 因爲路徑 13111 的總和最小。

分析:如果設 dp[i][j]dp[i][j]表示到達[i][j][i][j]位置的最小和,由於只能向下或者向右,因此DP方程很容易推出:

dp[i][j]=min(dp[i1][j],dp[i][j1])+grid[i][j]dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j] ,基於此進行遍歷更新即可,最後返回最尾部的元素

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) 
    {
        if(grid.size()==0) return 0;
        vector<vector<int>> dp=grid;
        for(int i=1;i<dp[0].size();i++)
            dp[0][i]=dp[0][i-1]+grid[0][i]; //第一行初始化
        for(int i=1;i<dp.size();i++)
            dp[i][0]=dp[i-1][0]+grid[i][0]; //第一列初始化
        
        for(int i=1;i<dp.size();i++)  //將其他區域通過DP方程賦值
        for(int j=1;j<dp[0].size();j++)
        {
            dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]; 
        }
        return dp.back().back();  //返回最後一個元素
    }
};

174. 地下城遊戲

一些惡魔抓住了公主(P)並將她關在了地下城的右下角。地下城是由 M x N 個房間組成的二維網格。我們英勇的騎士(K)最初被安置在左上角的房間裏,他必須穿過地下城並通過對抗惡魔來拯救公主。

騎士的初始健康點數爲一個正整數。如果他的健康點數在某一時刻降至 0 或以下,他會立即死亡。

有些房間由惡魔守衛,因此騎士在進入這些房間時會失去健康點數(若房間裏的值爲負整數,則表示騎士將損失健康點數);其他房間要麼是空的(房間裏的值爲 0),要麼包含增加騎士健康點數的魔法球(若房間裏的值爲正整數,則表示騎士將增加健康點數)。

爲了儘快到達公主,騎士決定每次只向右或向下移動一步。

編寫一個函數來計算確保騎士能夠拯救到公主所需的最低初始健康點數。

例如,考慮到如下佈局的地下城,如果騎士遵循最佳路徑 右 -> 右 -> 下 -> 下,則騎士的初始健康點數至少爲 7。

在這裏插入圖片描述

說明:

騎士的健康點數沒有上限。

任何房間都可能對騎士的健康點數造成威脅,也可能增加騎士的健康點數,包括騎士進入的左上角房間以及公主被監禁的右下角房間。

分析:這個題屬於hard的主要問題在於,按照之前的常規思路考慮,即從左上到右下,我們會發現,當前的血量與最低初始健康點數沒有直接關聯。

按照從右下到左上的方式,讓dp數組表示:dp[i][j]dp[i][j] 表示從 [i][j][i][j] 位置到終點所需要的最低健康血量。則根據該定義,就可以找到動態轉移方程: dp[i][j]=max{1,min{dp[i+1][j],dp[i][j+1]}dungeon[i][j]}dp[i][j]=max\{1,min\{dp[i+1][j],dp[i][j+1]\}-dungeon[i][j]\}

然後從右下到左上更新dp數組,dp[0][0]dp[0][0]就是解

class Solution {
public:
    int calculateMinimumHP(vector< vector<int> >& dungeon) 
	{
		 if(dungeon.size()==0) return 0; 
		 vector< vector<int> > dp=dungeon;
		 int row=dungeon.size(),col=dungeon[0].size();
		 //先把最右和最下進行初始化
		 dp[row-1][col-1]=max(1,1-dungeon[row-1][col-1]); 
		 for(int i=col-2;i>=0;i--)
		 	dp[row-1][i]=max(1,dp[row-1][i+1]-dungeon[row-1][i]);
		 for(int i=row-2;i>=0;i--)
		 	dp[i][col-1]=max(1,dp[i+1][col-1]-dungeon[i][col-1]);
		//根據動態轉移方程對DP數組進行更新 
        for(int i=row-2;i>=0;i--)
		 	for(int j=col-2;j>=0;j--)
		 	{
		 		int dp_min=min(dp[i+1][j],dp[i][j+1]);
		 		dp[i][j]=max(1,dp_min-dungeon[i][j]);
			 }
		return dp[0][0];		 
    }
};

32. 最長有效括號

給定一個只包含 ‘(’ 和 ‘)’ 的字符串,找出最長的包含有效括號的子串的長度。

示例 1:
輸入: "(()"
輸出: 2
解釋: 最長有效括號子串爲 "()"

示例 2:
輸入: ")()())"
輸出: 4
解釋: 最長有效括號子串爲 "()()"

分析:這個題目屬於DP問題中非常複雜的

採用動態規劃 令dp[i]表示以該字符起始,最遠到字符串結尾的最長的有效括號字串長度。在這裏從後向前進行處理(從前向後也可以的)

  • 設當前的座標爲index
  • 如果當前的位置是 ‘)’ ,則說明以該字符爲起始的字串必定非法,因此dp[index]=0
  • 如果當前的位置是’(’,則需要判斷
    • 如果右側相鄰處即str[index+1]是一個右括號,則 dp[index]=dp[index+2]+2dp[index]=dp[index+2]+2
    • 如果右側相鄰處是一個左括號,則需要找一下右邊是否還有右括號,即dp[index+1]所指的字串的結尾處是否是‘)’,如果不是的話說明沒辦法將index處的字符與後面的字符連起來,故de[index]=0;但是如果那個位置有右括號,就說明至少是dp[index+1]+2,並且這種情況還要考慮,這樣會將剛剛加上去的右括號右側的有效括號字串能夠連起來,也就是將index+1+dp[index+1]+1處的合法字符連了起來,即再加上dp[index+1+dp[index+1]+1]dp[index+1+dp[index+1]+1]
class Solution {
public:
    int longestValidParentheses(string s) 
    {
        int len=s.length();
        vector<int> dp(len+1);
        for(int i=len-1;i>=0;i--)
        {
            int sy=i+1+dp[i+1];  //找到i+1+dp[i+1]的位置
            if(s[i]=='('&&s[i+1]==')')  //相鄰爲右括號則與其組成合法
                dp[i]=2+dp[i+2];
            else if(s[i]=='('&&s[i+1]=='('&&s[sy]==')')
            {
                dp[i]=dp[i+1]+2;
                if(i+1+dp[i+1]+1<=len-1)
                    dp[i]+=dp[i+1+dp[i+1]+1];     
            }   
        }
        int ans=0;
        for(int i=0;i<len;i++)
            ans=max(ans,dp[i]);
        return ans;
    }
};

152. 乘積最大子數組

給你一個整數數組 nums ,請你找出數組中乘積最大的連續子數組(該子數組中至少包含一個數字),並返回該子數組所對應的乘積。

示例 1:
輸入: [2,3,-2,4]
輸出: 6
解釋: 子數組 [2,3] 有最大乘積 6。

示例 2:
輸入: [-2,0,-1]
輸出: 0
解釋: 結果不能爲 2, 因爲 [-2,-1] 不是子數組。

分析:該題的難點在於,由於存在負數元素,因此如果只是每一步選取最大值的話,會將負數忽略,但是兩個負數相乘會產生正數,故在此我的解決辦法是開設兩個數組,MAX和MIN。分別存儲從數組頭開始,包含當前元素的乘積最大的連續子數組。每次在遇到正數負數時要分開考慮:

  • 如果當前存儲的是正數,則 MAX[i]=max{MAX[i1],1}nums[i]MAX[i]=max\{MAX[i-1],1\}*nums[i]

    MIN[i]=min{MIN[i1],1}nums[i]MIN[i]=min\{MIN[i-1],1\}*nums[i]

  • 如果當前存儲的是負數,則MAX[i]=min{MIN[i1],1}nums[i];MAX[i]=min\{MIN[i-1],1\}*nums[i];

    MIN[i]=max{MAX[i1],1}nums[i]MIN[i]=max\{MAX[i-1],1\}*nums[i]

class Solution {
public:
    int maxProduct(vector<int>& nums) 
    {
        vector<int> MAX(nums.size());
        vector<int> MIN(nums.size());
        MAX[0]=MIN[0]=nums[0];
        for(int i=1;i<nums.size();i++)
        {
            if(nums[i]>=0)
            {
                MAX[i]=max(MAX[i-1],1)*nums[i];
                MIN[i]=min(MIN[i-1],1)*nums[i];
            }
            else
            {
                MAX[i]=min(MIN[i-1],1)*nums[i];
                MIN[i]=max(MAX[i-1],1)*nums[i];
            }
        }
        int result=MAX[0];
        for(int i=0;i<nums.size();i++)
            result=max(result,MAX[i]);
        return result;
    }
};

62. 不同路徑

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲“Finish”)。

在這裏插入圖片描述
問總共有多少條不同的路徑?

示例 1:

輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

分析:這是一個典型的排列組合問題,實際上問題的答案就是 CmnC_m^n

現用另一種思路,動態規劃求解

每一個位置只可以從上或者左到達,故如果用 dp[i][j]dp[i][j]表示到達 [i][j][i][j]處的路徑,則到達該位置的路徑數量等於到達上方的數量加上到達左方的路徑數量, dp[i][j]=dp[i1][j]+dp[i][j1]dp[i][j]=dp[i-1][j]+dp[i][j-1]

class Solution {
public:
    int uniquePaths(int m, int n) 
    {
        int dp[n][m];
        for(int i=0;i<m;i++)
            dp[0][i]=1;
        for(int i=1;i<n;i++)
            dp[i][0]=1;
        for(int i=1;i<n;i++)
        for(int j=1;j<m;j++)
            dp[i][j]=dp[i-1][j]+dp[i][j-1];
        return dp[n-1][m-1];    
    }
};

63. 不同路徑 II

現在考慮網格中有障礙物。那麼從左上角到右下角將會有多少條不同的路徑?

示例 1:
輸入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
輸出: 2
解釋:
3x3 網格的正中間有一個障礙物。
從左上角到右下角一共有 2 條不同的路徑:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

分析:在上一題基礎上,增加了障礙物。實際上,是類似的,如果一個位置的左側有障礙物,則它只能由上方到達,如果一個位置的上方有障礙物,則它只能由左側到達。如果上方左方沒有障礙物,則可以從上方或者左方到達。路徑的方法傳遞類似。

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) 
    {
        if(obstacleGrid.size()==0) return 0;
        int row=obstacleGrid.size();
        int col=obstacleGrid[0].size();
        int dp[row][col];
        for(int i=0;i<row;i++)
        for(int j=0;j<col;j++)
            dp[i][j]=0;
        dp[0][0]=1;
        for(int i=0;i<row;i++)
            for(int j=0;j<col;j++)
            {
                if(i-1>=0&&obstacleGrid[i-1][j]!=1) dp[i][j]+=dp[i-1][j];
                if(j-1>=0&&obstacleGrid[i][j-1]!=1) dp[i][j]+=dp[i][j-1];
                if(obstacleGrid[i][j]==1) dp[i][j]=0;
            }
        return dp[row-1][col-1];
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章