Leetcode-動態規劃

70. 爬樓梯 https://leetcode-cn.com/problems/climbing-stairs/html

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

每次你能夠爬 1 或 2 個臺階。你有多少種不一樣的方法能夠爬到樓頂呢?正則表達式

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

解:express

暴力,若是隻有一階,就只有一種方法;若是隻有二階,就只有兩種方法。那麼n階的方法數就等於n-1階和n-2階方法數之和。但直接遞歸,有太多重複計算,超時。O(2n)數組

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        return self.climbStairs(n-1) + self.climbStairs(n-2)

 

遞歸+記憶化,放數組裏緩存。把每一步的結果存儲在 memo 數組之中,每當函數再次被調用,就直接從 memo 數組返回結果緩存

class Solution:
    def climbStairs(self, n: int) -> int:
        memo = [0]*(n+1)  # memo[i]表示從第i階到第n階的方法數,出發點是第0階
        
        def helper(i, n, memo):  # 起始點i,終點n
            if i > n:  # 越界,最高起點直接在第n階
                return 0
            if i == n:   # 不用動,就這一種方法
                return 1
            
            if memo[i] > 0:
                return memo[i]  # 若是已經算過了,就不用再算了
            
            memo[i] = helper(i+1, n, memo) + helper(i+2, n, memo)
            
            return memo[i]
            
        return helper(0, n, memo)

    

動態規劃,遞推, f(n) = f(n-1) + f(n-2) ,f(1)=1 , f(2)=2。O(N)  其實也能夠不用開一個數組。app

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        
        f = [0]*n
        f[0], f[1] = 1, 2
        for i in range(2, n):
            f[i] = f[i-1]+ f[i-2]
        return f[n-1]
# O(1) 空間
class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        
        pre1 = 1
        pre2 = 2
        for i in range(2, n):
            cur = pre1 + pre2
            
            pre1 = pre2
            pre2 = cur
            
        return cur

   

特徵根,特徵方程爲 a2 - a - 1 = 0框架

import math
class Solution:
    def climbStairs(self, n: int) -> int:
        sqrt5 = math.sqrt(5)
        res = math.pow((1+sqrt5)/2, n+1) - math.pow((1-sqrt5)/2, n+1)
        return int(res/sqrt5)

  

Binets 矩陣乘法,O(logN)。函數

[fn, fn-1] = [[1, 1], [1, 0]]*[fn-1, fn-2] = Q*[fn-1, fn-2] = Qn-2[f2, f1]

 

120.三角形最小路徑和 https://leetcode-cn.com/problems/triangle/

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

說明:

若是你能夠只使用 O(n) 的額外空間(n 爲三角形的總行數)來解決這個問題,那麼你的算法會很加分。

解:

顯然簡單的貪心是不行的。

dfs,自頂向下,O(2n),顯然極可能超時。

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        self.res = float('inf')
        n = len(triangle)
        
        def dfs(level, col, pre_sum):  # 搜索到位置(level,col),以前路徑和爲pre_sum
            cur_sum = pre_sum + triangle[level][col]
            if level == n-1:
                self.res = min(self.res, cur_sum)  # 當前搜索到最後一行了,更新全局最小路徑
                return
            
            # 不然的話,進入下一行
            dfs(level+1, col, cur_sum)
            dfs(level+1, col+1, cur_sum)
            
        dfs(0, 0, 0)
        return self.res

  

dfs + 記憶化

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        import functools
        n = len(triangle)

        @functools.lru_cache(None)  # 緩存遞歸結果
        def helper(level, i, j):
            if level == n:
                return 0
            res = 0
            
            a = float("inf")
            b = float("inf")
            if 0 <= i <len(triangle[level]):
                a = helper(level+1, i, i+1) + triangle[level][i]  # 若是(level, i)不越界,可選
            if 0 <= j <len(triangle[level]):
                b = helper(level+1, j, j+1) + triangle[level][j]  # 若是(level, j)不越界,可選
            
            res += min(a, b) # 自底向上到 (level, i) (level, j)的最小路徑
            return res

        return helper(0, -1, 0) 

  

 

動態規劃,自底向上考慮,到節點[i][j]的最短路徑之和爲dp[i][j],狀態轉移方程爲 dp[i][j] = min( dp[i-1][j], dp[i-1][j-1] ) + triangle[i][j]

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        if not triangle:
            return 0
        n = len(triangle)
        dp = [[0]*(i+1) for i in range(n)]
        
        dp[0][0] = triangle[0][0]
        
        for i in range(1, n):
            for j in range(i+1):
                # 若是當前節點是第一列,或最後一列,那上層路徑只可能來自於一個節點
                if j == 0:
                    dp[i][j] = dp[i-1][j] + triangle[i][j] 
                elif 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]
        return min(dp[-1])

  

倒着循環的話用一維數組就能夠了,不停地覆蓋,並且狀態不會被污染,狀態上一層狀態更新都是來源於下一層的狀態。

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        if not triangle:
            return 0
        n = len(triangle)
        
        dp = triangle[-1]
        
        for i in range(n-2, -1, -1):
            for j in range(i+1):
                dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j]
        return dp[0]

  

152. 乘積最大子序列  https://leetcode-cn.com/problems/maximum-product-subarray/

給定一個整數數組 nums ,找出一個序列中乘積最大的連續子序列(該序列至少包含一個數)。

解:

若是不要求連續,直接把全部大於0的數拿出來就好了。

暴力,直接遞歸,搜索全部可能的子序列。超時。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if nums is None or len(nums) == 0:
            return 0
        
        res = float('-inf')
        n = len(nums)
        
        def dfs(start):
            nonlocal res
            if start == n:
                return
            for end in range(start, n):
                cur = 1
                for i in range(start, end+1):
                    cur *= nums[i]
                    if cur > res:
                        res = cur
            
            dfs(start+1)
                
        dfs(0)
        return res

  

 

動態規劃,同時維護一組最小的狀態。a[i]爲正,選擇最大的dp[i-1],a[i]爲負,選擇最小的m[i-1]。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if nums is None or len(nums) == 0:
            return 0
        n = len(nums)
        
        dp = [nums[0]]*n
        m = [nums[0]]*n
        
        for i in range(1, n):
            dp[i] = max(dp[i-1]*nums[i], nums[i], m[i-1]*nums[i])
            m[i] = min(dp[i-1]*nums[i], nums[i], m[i-1]*nums[i])
  
        return max(dp)
# 節省空間
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if nums is None or len(nums) == 0:
            return 0
        n = len(nums)
        
        dp = nums[0]
        m = nums[0]
        res = nums[0]
        
        for i in range(1, n):
            # max 和 min 同時更新
            dp, m = max(dp*nums[i], nums[i], m*nums[i]), min(dp*nums[i], nums[i], m*nums[i])
            res = max(res, dp)
        return res

 

 

121. 買賣股票的最佳時機 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。

若是你最多隻容許完成一筆交易(即買入和賣出一支股票),設計一個算法來計算你所能獲取的最大利潤。

注意你不能在買入股票前賣出股票。

解:

只能買賣一次,確定在最低點買進,最高點賣出,但這個關係必需要服從時序。從前向後維護一個價格最低點,若是當天價格減去最低點獲得的利潤比目前的最大利潤大,更新便可。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if prices is None or len(prices) <= 1:
            return 0
        
        lowest = prices[0]
        res = 0
        for i in range(1, len(prices)):
            lowest = min(lowest, prices[i])
            res = max(res, prices[i]-lowest)
            
        return res

  

dp解法,相似與最大連續子序列和的問題。第i天的最大利潤,要麼是從i-1天以前就持有到第i天,利潤爲第i-1天的最大利潤加上新一天的利潤;要麼直接就是i-1天買入i天賣出(由於只能買賣一次)。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if prices is None or len(prices) <= 1:
            return 0
        dp = [0]*len(prices)
        for i in range(1,len(prices)):
            dp[i] = max(dp[i-1] + prices[i]- prices[i-1], prices[i] - prices[i-1])
        return max(dp)
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if prices is None or len(prices) <= 1:
            return 0
        n = len(prices)
        dp = 0
        res = 0
        
        for i in range(1, n):
            dp = max(dp + prices[i] - prices[i-1], prices[i] - prices[i-1])
            res = max(res, dp)
        return res

 

通用dp框架

dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 爲天數,K 爲最多交易數、買進了就算開始交易,0表示不持股、1表示持股。dp[i][k][j]:第i天至多交易了k次{1, ..., K}、處於k狀態{未持股、持股}的最大利潤。
此問題共 n × K × 2 種狀態。

for 0 <= i < n:
  for 1 <= k <= K:
    for s in {0, 1}:
      dp[i][k][s] = max(buy, sell, rest)

 

初始狀態:

dp[-1][k][0] = 0  # 還沒開始,不持股,利潤爲0
dp[-1][k][1] = float('-inf')  # 還沒開始,不可能持有股票
dp[i][0][0] = 0   # 至多交易了0次,不持股,利潤爲0
dp[i][0][1] = float('-inf')  # 至多交易了0次,不可能持股

 

狀態轉移:

# 第i天至多交易了k次、不持股 from {第i-1天至多交易了k次、不持股、第i天不操做,  第i-1天至多交易了k次、持股、第i天賣出}:{rest,  sell}
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])

# 第i天至多交易了k次、持股 from {第i-1天至多交易了k次、持股、第i天不操做, 第i-1天至多交易了k-1次、不持股、第i天買進}:{rest, buy}
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

 

#121,至多一次交易

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], 0 - prices[i])

能夠發現全是k=1的狀態,不須要三維dp

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1],  - prices[i]) 

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        dp = [[0]*2 for _ in range(n)]   # dp[i][j],第i天狀態j{不持股、持股}最大利潤,K=1天天都是至多交易了1次
        for i in range(n):
            # 處理初始狀態
            if i-1 == -1:
                dp[i][0] = 0  # max(dp[-1][0], dp[-1][1]+prices[i])=max(0, -inf+prices[i])=0
                dp[i][1] = -prices[i]  # max(dp[-1][1], dp[-1][0]-prices[i])=max(-inf, 0-prices[i])=-prices[i]
            else:    
                dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
                dp[i][1] = max(dp[i-1][1], - prices[i])
        
        return dp[n-1][0]
# 能夠發現i時刻狀態轉移只和相鄰狀態有關,直接滾動更新,降空間複雜度
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        # 初始狀態
        # dp[-1][1][0]=0, dp[-1][1][1]='-inf'
        dp_i_0, dp_i_1 = 0, float('-inf')
        
        for i in range(n):
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i])
            dp_i_1 = max(dp_i_1, - prices[i])
        
        return dp_i_0

  

  

  

122. 買賣股票的最佳時機ii  https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你能夠儘量地完成更多的交易(屢次買賣一支股票)。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。

解:

貪心在以前已經討論過了。這裏只看用dp實現。第i天的最大利潤,要麼是第i-1天的最大利潤(i天跌了,不操做),要麼要麼是第i-1天的最大利潤加上新一天的利潤(i天漲了,i-1天買入i天賣掉)。

狀態轉移方程爲:dp[i] = dp[i-1]  if prices[i] <= prices[i-1] else dp[i-1] + prices[i] -prices[i-1],遍歷一次便可,O(n)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp = [0]*n
        for i in range(1, n):
            dp[i] = max(dp[i-1], dp[i-1] + prices[i] - prices[i-1])
        return max(dp)
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        res = 0
        for i in range(1, n):
            res = max(res, res + prices[i] - prices[i-1])
        return res

 

通用dp,K=inf。若是k爲正無窮,那麼能夠認爲k=k-1。狀態轉移框架爲

dp[i][k][0] = max(dp[i-1][k][0],  dp[i-1][k][1] + prices[i])

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

不須要k了

dp[i][0] = max(dp[i-1][0],  dp[i-1][1] + prices[i])

dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        dp = [[0]*2 for _ in range(n)]  # dp[i][j],第i天狀態j{不持股、持股}最大利潤
        for i in range(n):
            # 處理初始狀態
            if i-1 == -1:
                dp[i][0] = 0  # max(dp[-1][0], dp[-1][1]+prices[i])=max(0, -inf+prices[i])=0
                dp[i][1] = -prices[i]  # max(dp[-1][1], dp[-1][0]-prices[i])=max(-inf, 0-prices[i])=-prices[i]
            else:    
                dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
                dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
        
        return dp[n-1][0]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        dp_i_0, dp_i_1 = 0, float('-inf')
        
        for i in range(n):
            tmp = dp_i_0  # 不要污染上一時刻的數據
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i])
            dp_i_1 = max(dp_i_1, tmp - prices[i])  # python同時賦就行,這裏爲了便於理解
        
        return dp_i_0

  

 

309. 最佳買賣股票時機含冷凍期 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/

給定一個整數數組,其中第 i 個元素表明了第 i 天的股票價格 。​

設計一個算法計算出最大利潤。在知足如下約束條件下,你能夠儘量地完成更多的交易(屢次買賣一支股票):

你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。
賣出股票後,你沒法在次日買入股票 (即冷凍期爲 1 天)。

解:

K=inf,且每次賣出後要隔一天才能賣出。狀態轉移方程爲:

dp[i][k][0] = max(dp[i-1][k][0],  dp[i-1][k][1] + prices[i])

dp[i][k][1] = max(dp[i-1][k][1], dp[i-2][k-1][0] - prices[i]) = max(dp[i-1][k][1], dp[i-2][k][0] - prices[i])

把k拿掉

dp[i][0] = max(dp[i-1][0],  dp[i-1][1] + prices[i])

dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i]) 

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        dp = [[0]*2 for _ in range(n)] 
        
        for i in range(n):
            # 處理初始狀態
            if i == 0:
                dp[i][0] = 0 
                dp[i][1] = -prices[i]
            
            elif i == 1:
                dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
                dp[i][1] = max(dp[i-1][1], - prices[i])
            
            else:    
                dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
                dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
        
        return dp[n-1][0]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        
        dp_i_0, dp_i_1 = 0, float('-inf')
        dp_pre_0 = 0   # dp[i-2][0]
        
        for i in range(n):
            tmp = dp_i_0  
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i])
            dp_i_1 = max(dp_i_1, dp_pre_0 - prices[i])
            dp_pre_0 = tmp   # 當前時刻的dp[i-1][0]是下一時刻的dp[i-2][0]
        return dp_i_0

  

 

714.買賣股票的最佳時機含手續費 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/

給定一個整數數組 prices,其中第 i 個元素表明了第 i 天的股票價格 ;非負整數 fee 表明了交易股票的手續費用。

你能夠無限次地完成交易,可是你每次交易都須要付手續費。若是你已經購買了一個股票,在賣出它以前你就不能再繼續購買股票了。

返回得到利潤的最大值。

解:

k=inf,只要從利潤中把手續費減去便可。

dp[i][0] = max(dp[i-1][0],  dp[i-1][1] + prices[i] - fee)

dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        dp = [[0]*2 for _ in range(n)]  # dp[i][j],第i天狀態j{不持股、持股}最大利潤
        
        for i in range(n):
            # 處理初始狀態
            if i-1 == -1:
                dp[i][0] = 0  # max(dp[-1][0], dp[-1][1]+prices[i]-fee)=max(0, -inf+prices[i]-fee)=0
                dp[i][1] = -prices[i]  # max(dp[-1][1], dp[-1][0]-prices[i])=max(-inf, 0-prices[i])=-prices[i]
            else:    
                dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i] - fee)
                dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
        
        return dp[n-1][0]
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        if not prices:
            return 0
        
        n = len(prices)
        
        dp_i_0, dp_i_1 = 0, float('-inf')
        
        for i in range(n):
            tmp = dp_i_0
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i] - fee)
            dp_i_1 = max(dp_i_1, tmp - prices[i])
           
        return dp_i_0

  

 

123. 買賣股票的最佳時機iii https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/

給定一個數組,它的第 i 個元素是一支給定的股票在第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你最多能夠完成 兩筆 交易。

注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。

解:

K=2,這題k是不能直接消掉的,狀態轉移時要對k進行窮舉。

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) 
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        max_k = 2
        dp = [[[0]*2 for _ in range(max_k+1)] for _ in range(n)]
        
        for i in range(n):
            for k in range(max_k, 0, -1):
                if i == 0:
                    dp[i][k][0] = 0 
                    dp[i][k][1] = -prices[i]
                else:
                    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) 
                    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
        return dp[n-1][max_k][0]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        max_k = 2
        
        dp_i_1_0, dp_i_1_1 = 0, float('-inf')
        dp_i_2_0, dp_i_2_1 = 0, float('-inf')
        
        for i in range(n):
            dp_i_2_0 = max(dp_i_2_0, dp_i_2_1 + prices[i])
            dp_i_2_1 = max(dp_i_2_1, dp_i_1_0 - prices[i])
            dp_i_1_0 = max(dp_i_1_0, dp_i_1_1 + prices[i])
            dp_i_1_1 = max(dp_i_1_1, - prices[i])
        
        return dp_i_2_0

  

 

188. 買賣股票的最佳時機iv https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/

給定一個數組,它的第 i 個元素是一支給定的股票在第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你最多能夠完成 k 筆交易。

注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。

解:

股票買賣系列的基本點,把這題dp搞懂的話這個系列問題不大。狀態定義+轉移方程,三維的dp,最完整的狀況。直接作超時的話,考慮一下若是K > n/2就退化成任意交易次數,直接貪心。

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        max_k = k
        
        if 2 * max_k > n:  # 若是k超過n的一半,問題就退化成了任意交易次數的狀況,由於任何一筆交易都不能重疊
            return self.greedy(prices)
        
        dp = [[[0]*2 for _ in range(max_k+1)] for _ in range(n)]
        
        for i in range(n):
            for k in range(max_k, 0, -1):
                if i == 0:
                    dp[i][k][0] = 0 
                    dp[i][k][1] = -prices[i]
                else:
                    dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) 
                    dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
        return dp[n-1][max_k][0]
    
    def greedy(self, prices):
        """貪心,只要次日漲了,就前一天買入次日賣出"""
        p = 0
        for i in range(1, len(prices)):
            cur = prices[i] - prices[i-1]
            if cur > 0:
                p += cur
        return p if p else 0

  

 

300. 最長上升子序列 https://leetcode-cn.com/problems/longest-increasing-subsequence/

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

說明:

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

解:

子序列沒必要連續,但先後位置不能變。

暴力dfs,O(2n),超時

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        
        def helper(prev, curpos):
            """prev當前最大子序列的最後一個元素, curpos當前位置"""
            if curpos == n:
                return 0
            
            taken = 0
            if nums[curpos] > prev:  # 若是上升且加到子序列中的狀況  
                taken = 1 + helper(nums[curpos], curpos+1)
            nottaken = helper(prev, curpos+1)  # 不選當前位置元素,多是不上升,也多是上升但不選的回溯 
            
            return max(taken, nottaken)
              
        return helper(float('-inf'), 0)

  

dfs + 記憶化。memo[i][j] 表示使用 nums[i] 做爲上一個被認爲包含/不包含在子序列中時子序列可能的長度,其中 nums[j] 做爲當前被認爲包含/不包含在子序列中的元素。仍是超時。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        memo = [[-1]*n for _ in range(n+1)]   # memo[i][j] 子序列長度
        
        def helper(prevpos, curpos, memo):
            """prev當前最大子序列的最後一個元素索引, curpos當前元素索引"""
            if curpos == n:
                return 0
            
            if memo[prevpos+1][curpos] >= 0:   # 多一行prevpos用於處理curpos=0的狀況。
                return memo[prevpos+1][curpos]
            
        
            taken = 0
            if prevpos < 0 or nums[curpos] > nums[prevpos]:  # 若是上升且加到子序列中的狀況  
                taken = 1 + helper(curpos, curpos+1, memo)
            
            nottaken = helper(prevpos, curpos+1, memo)  # 不選當前位置元素,多是不上升,也多是上升但不選的回溯 
            
            memo[prevpos+1][curpos] = max(taken, nottaken)
            return memo[prevpos+1][curpos]
              
        return helper(-1, 0, memo)

  

動態規劃,這題是最優子結構,當選到某個位置元素進入子序列時,無論其後面的元素,其前面的最長子序列就已經肯定了。

dp[i]表示從頭開始到第i元素且把i元素選上的狀況下,最長上升子序列長度。最後要的結果爲max(dp)。dp[i] = max(dp[0], ..., dp[j], ..., dp[i-1]) +1 且a[i] 要大於a[j]。由於從上一個被選上的元素到當前被選上的i中間多是有跳過的。O(n2)

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        dp = [0] * n
        
        for i in range(n):
            max_prev = 0
            for j in range(i):
                if nums[i] > nums[j]:
                    max_prev = max(max_prev, dp[j])
            
            dp[i] = max_prev + 1
            
        return max(dp)

  

動態規劃 + 二分搜索,把上面方法中內層 j 循環替換成二分。始終維護一個數組LIS爲要求的上升子序列,對每個a[i],插入到LIS中(二分法找到第一個比a[i]大的數替換掉,由於這樣儘量多的讓後面符合條件的數進來,要縮一下上界。若是a[i]比LIS全部都大就直接append),最後LIS的長度即爲所求。O(nlogn)

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        n = len(nums)
        LIS = []
        
        for i in range(n):
            if not LIS or nums[i] > LIS[-1]:
                LIS.append(nums[i])

            else:
                index = self.binarySearch(LIS, nums[i])
                LIS[index] = nums[i]
            
        return len(LIS)
    
    def binarySearch(self, array, target):
        """返回第一個比target大的元素索引"""
        if not array:
            return
        low, high = 0, len(array) - 1
        
        while low <= high:
            mid = low + (high-low)//2
            if array[mid] < target:
                low = mid + 1
            elif array[mid] > target:
                high = mid - 1
            else:
                return mid
        return low

  

354. 俄羅斯套娃信封問題 https://leetcode-cn.com/problems/russian-doll-envelopes/

給定一些標記了寬度和高度的信封,寬度和高度以整數對形式 (w, h) 出現。當另外一個信封的寬度和高度都比這個信封大的時候,這個信封就能夠放進另外一個信封裏,如同俄羅斯套娃同樣。

請計算最多能有多少個信封能組成一組「俄羅斯套娃」信封(便可以把一個信封放到另外一個信封裏面)。

說明:
不容許旋轉信封。

示例:

輸入: envelopes = [[5,4],[6,4],[6,7],[2,3]]
輸出: 3
解釋: 最多信封的個數爲 3, 組合爲: [2,3] => [5,4] => [6,7]。

解:

這道題是最長上升子序列的升維,要先對寬度進行升序排列,而後對寬度相同的按高度降序排序。最後對高度數組進行最長上升子序列的求解


class Solution: def maxEnvelopes(self, envelopes: List[List[int]]) -> int: if not envelopes: return 0 n = len(envelopes) nums = sorted(envelopes, key=lambda x: [x[0], -x[1]]) dp = [1] * n for i in range(n): for j in range(i-1, -1, -1): if nums[i][1] > nums[j][1]: dp[i] = max(dp[i], dp[j] + 1) return max(dp)

  

用二分法來優化

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        if not envelopes:
            return 0
        n = len(envelopes)
        nums = sorted(envelopes, key=lambda x: [x[0], -x[1]])
        LIS = []
        
        for i in range(n):
            if not LIS or nums[i][1] > LIS[-1]:
                LIS.append(nums[i][1])
            else:
                index = self.binarySearch(LIS, nums[i][1])
                LIS[index] = nums[i][1]
        return len(LIS)
    
    def binarySearch(self, array, target):
        """返回第一個比target大的元素索引"""
        if not array:
            return
        low, high = 0, len(array) - 1
         
        while low <= high:
            mid = low + (high-low)//2
            if array[mid] < target:
                low = mid + 1
            elif array[mid] > target:
                high = mid - 1
            else:
                return mid
        return low

  

 

322. 零錢兌換 https://leetcode-cn.com/problems/coin-change/

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

解:

仔細想清楚的話,這題其實和斐波那契數列是同樣的,只不過臺階數變成了總金額。

暴力dfs,對每一種可能的組合都走一邊,取硬幣數最小的組合。指數級複雜度O(knk)。

import sys
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if amount == 0:
            return -1
        ans = sys.maxsize
        for coin in coins:
            if amount < coin:  # 金額不可達
                continue
            subProblem = self.coinChange(coins, amount-coin)
            
            if subProblem == -1:  # 子問題無解
                continue
            ans = min(ans, subProblem + 1)
        
        return -1 if ans == sys.maxsize else ans 

  

dfs+記憶化,先選大面額的硬幣,O(kn)

import sys
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if not coins:
            return -1
        coins.sort(reverse=True)
        memo = [-2] * (amount+1)   # memo[amount]表示湊到金額爲amount的最少硬幣數
        
        def helper(coins, amount, memo):
            if amount == 0:
                return 0
            if memo[amount] != -2:
                return memo[amount]
            
            ans = sys.maxsize
            for coin in coins:
                if amount < coin:  # 金額不可達
                    continue
                subProblem = helper(coins, amount-coin, memo)

                if subProblem == -1:  # 子問題無解
                    continue
                ans = min(ans, subProblem + 1)
            
            memo[amount] = -1 if ans == sys.maxsize else ans  # 記錄本輪答案
            return memo[amount]  
        
        return helper(coins, amount, memo)

  

動態規劃,f(amount) = 1 + min{ f(amount - ci) | i in [1, k] },O(kn)

import sys
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        if not coins:
            return 0
        dp = [sys.maxsize] * (amount+1)
        dp[0] = 0
        
        for i in range(1, amount+1):
            for j in range(len(coins)):
                if i < coins[j]:
                    continue
                dp[i] = min(dp[i], dp[i - coins[j]] + 1)
            
        return -1 if dp[amount] == sys.maxsize else dp[amount]

  

 

72. 編輯距離 https://leetcode-cn.com/problems/edit-distance/

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操做數 。

你能夠對一個單詞進行以下三種操做:

插入一個字符
刪除一個字符
替換一個字符

解:

狀態定義。dp[i],一維狀態是不夠的。dp[i][j] 表示單詞1的前i個字符,要替換到單詞2的前j個字符,最少須要多少步數。dp[m][n]即爲最後要求的解。

狀態轉移。

  若是w1[i]、w2[j]字符相同,不用作任何操做,dp[i][j] = dp[i-1][j-1]。

  不然,能夠有三種操做,dp[i][j] = 1 +  min( dp[i-1][j],  dp[i][j-1], dp[i-1][j-1] ) 依次爲w1插入,w2插入,w1w2替換

O(mn)

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        
        dp = [[0]*(n+1) for _ in range(m+1)]
        
        # 初始狀態
        for i in range(m+1):
            dp[i][0] = i  # w1前i個匹配到w2前0個,i次刪除
        for j in range(n+1):
            dp[0][j] = j  # w2前j個匹配到w1前0個,j次刪除
            
        for i in range(1, m+1):
            for j in range(1, n+1):
                if word1[i-1] == word2[j-1]:  # w1第i個字符和w2第j個字符若是相同
                    dp[i][j] = dp[i-1][j-1]   # 不用操做
                else:
                    dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])  # 三種操做,w1刪除/插入仍是w2插入/刪除不用細摳
                    
        return dp[m][n]

    

 

5. 最長迴文子串 https://leetcode-cn.com/problems/longest-palindromic-substring/

給定一個字符串 s,找到 s 中最長的迴文子串。你能夠假設 s 的最大長度爲 1000。

解:

暴力法,選出左右子串可能開始和結束的位置,並檢驗其是不是迴文。直接嵌套兩層循環便可,這裏強行寫成dfs再繼續練習。O(n3)

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
 
        res = ''
        n = len(s)
        
        def isValid(s):
            str_len = len(s)
            for i in range(str_len//2):
                if s[i] != s[str_len-1-i]:
                    return False
            return True
        
        def helper(start):
            nonlocal res
            if start == n:
                return
            
            for end in range(start, n):
                if isValid(s[start: end+1]):
                    res = s[start: end+1] if end-start+1 > len(res) else res
                helper(start+1)
                
        helper(0)
        return res
class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
 
        res = ''
        n = len(s)
        
        def isValid(s):
            str_len = len(s)
            for i in range(str_len//2):
                if s[i] != s[str_len-1-i]:
                    return False
            return True
        
        for i in range(n):
            for j in range(i, n):
                if isValid(s[i:j+1]):
                    res = s[i:j+1] if j-i+1 > len(res) else res
    
        return res

  

如何優化暴力解法,動態規劃。定義p(i, j) = True if s[i, j] 是 迴文 else False,那麼顯然P(i, j) = p(i+1, j-1) and (s[i] == s[j])。只有一個字符和兩個字符的狀況單獨考慮。要注意的是,遞推時i的取值由i+1決定,因此i要倒着推。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
 
        res = ''
        n = len(s)
        p = [[-1 for _ in range(n)] for _ in range(n)]
        
        
        for i in range(n-1, -1, -1):
            for j in range(i, n):
                p[i][j] = (j==i or j-i==1 or p[i+1][j-1]) and (s[i] == s[j])
                
                if p[i][j] and j-i+1 > len(res):
                    res = s[i:j+1]
    
        return res

 

因爲i是倒着推的,因此求第i行時只須要i+1行的信息,而這一行的p[j]的取值須要知道下一行的p[j-1]的信息,因此倒着推j的話能夠把空間降到一維。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
 
        res = ''
        n = len(s)
        p = [-1 for _ in range(n)]
        
        
        for i in range(n-1, -1, -1):
            for j in range(n-1, i-1, -1):
                p[j] = (j==i or j-i==1 or p[j-1]) and (s[i] == s[j])
                
                if p[j] and j-i+1 > len(res):
                    res = s[i:j+1]
        return res

   

中心擴散。每次循環選擇一箇中心(一個字符或兩個字符之間),進行左右擴展,判斷左右字符是否相等便可。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
        def expand(s, left, right):
            """從中心向外擴展到最長的迴文s[left]...s[right]"""
            while left >= 0 and right < len(s) and s[left]==s[right]:
                left -= 1
                right += 1
            return left+1, right-1  # 返回中心擴散獲得的最長迴文字符的起止點
                
        
        n = len(s)
        res = ''
        for i in range(n):
            l1, r1 = expand(s, i, i)
            l2, r2 = expand(s, i, i+1)
            if r1-l1+1 > len(res):
                res = s[l1: r1+1]
            if r2-l2+1 > len(res):
                res = s[l2: r2+1]
        return res

  

Manacher's Algorithm 馬拉車算法,能夠降到O(n)。

 

 

10. 正則表達式匹配 https://leetcode-cn.com/problems/regular-expression-matching/

給你一個字符串 s 和一個字符規律 p,請你來實現一個支持 '.' 和 '*' 的正則表達式匹配。

'.' 匹配任意單個字符
'*' 匹配零個或多個前面的那一個元素
所謂匹配,是要涵蓋 整個 字符串 s的,而不是部分字符串。

說明:

s 可能爲空,且只包含從 a-z 的小寫字母。
p 可能爲空,且只包含從 a-z 的小寫字母,以及字符 . 和 *。

解:

先看第一個元素能不能匹配,若是能夠匹配就遞歸地看後面能不能匹配。若是p[0]取值爲s[0]或者'.',第一個元素都是能夠匹配的。

而後p[1]若是是'*',那麼可讓前一個字符重複任意次數,到底重複多少次交給遞歸解決,當前的選擇就兩個:匹配0次,匹配1次,兩種操做有一種使得剩下的字符串可以匹配,那麼初始時匹配串和模式串就能夠被匹配。

若是有字符和'*'結合,那麼或者匹配該字符0次、p跳過該字符和'*',繼續匹配s;或者在p[0]和s[0]匹配的前提下,刪除匹配串的第一個字符,再用p去匹配。若是沒有'*',那麼就必須得p[0]和s[0]匹配上,而後再拿p[1:]去匹配s[1:]。

暴力遞歸,超時。

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        if not p:
            return not s
        
        # p[0]可否匹配上s[0],就兩種狀況p[0]==s[0],或者p[0]能夠任意
        first_match = bool(s) and p[0] in {s[0], '.'}
        
        if len(p) >= 2 and p[1] == '*':
            return self.isMatch(s, p[2:]) or (first_match and self.isMatch(s[1:], p)) 
        
        else:  # 若是p[1]不是'*'那就看p[0]有沒有直接匹配上,以及s[1:]和p[1:]是否匹配
            return first_match and self.isMatch(s[1:], p[1:])

  

dfs+記憶化,用memo[(i, j)]來表示s[i:] 和p[j:]是否匹配。

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        memo = dict()
        
        def helper(i, j):
            if (i, j) in memo:
                return memo[(i, j)]
            if j == len(p):  # p串到最後了
                return i == len(s) 
            
            first_match = i < len(s) and p[j] in {s[i], '.'}
            
            if j <= len(p)-2 and p[j+1] == '*':
                ans = helper(i, j+2) or (first_match and helper(i+1, j))
            else:
                ans = first_match and helper(i+1, j+1)
                
            memo[(i, j)] = ans
            return ans
        
        return helper(0, 0)

  

動態規劃,自底向上,dp[i][j] 表示s[i:] 和 p[j:]是否匹配。

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        dp = [[False] * (len(p)+1) for _ in range(len(s)+1)]
        
        dp[-1][-1] = True  # []匹配[]
        for i in range(len(s), -1, -1):
            for j in range(len(p)-1, -1, -1):
                first_match = i < len(s) and p[j] in {s[i], '.'}
                if j <= len(p)-2 and p[j+1] == '*':
                    dp[i][j] = dp[i][j+2] or (first_match and dp[i+1][j])
                else:
                    dp[i][j] = first_match and dp[i+1][j+1]
                    
        return dp[0][0]

  

 

32. 最長有效括號 https://leetcode-cn.com/problems/longest-valid-parentheses/

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

示例 1:

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

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

解:

暴力,兩層迭代遍歷全部可能的起止點,對每一個子串判斷一下是否有效,維護一個全局最長距離

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        
        def isValid(s):
            if not s:
                return False
            stack = []
            for i in range(len(s)):
                if s[i] == ')':
                    if not stack or stack[-1] != '(':
                        return False
                    stack.pop()
                else:
                    stack.append(s[i])
            if stack:
                return False
            return True
        
        res = 0 
        for i in range(len(s)):
            for j in range(i+1, len(s), 2):
                if isValid(s[i:j+1]):
                    res = max(res, j-i+1)
        return res

  

動態規劃,定義dp[i] 表示以第i個元素結尾的最長有效字符串的長度。顯然有效字符串必須以')'結尾,那麼以'('結尾的子字符串對應的dp數組值必定爲0。

若是s[i] = ')' 且s[i-1]='(',那麼dp[i] = dp[i-2] + 2,這個緣由比較明顯,s[i-1]和s[i]正好湊了一對有效括號長度爲2

若是s[i] = ')' 且s[i-1] = ')',dp[i] = dp[i-1] + dp[i-dp[i-1] - 2] + 2,若是倒數第二個')'是一個有效子串sb的一部分,那麼就越過這個子串再往前看當前的這個')'能不能成爲有效子串的結尾。若是sb前面正好是'(',越過sb以後就根第一種狀況一致了,dp[i-dp[i-1]-2] + 2,再加上sb的長度便可。

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        if not s:
            return 0
        n = len(s)
        dp = [0] * n
        res = 0
        for i in range(1, n):
            if s[i] == ')':
                if s[i-1] == '(':
                    dp[i] = dp[i-2] + 2 if i >= 2 else 2
                elif i-dp[i-1] > 0 and s[i-dp[i-1]-1] == '(':
                    dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2 if i-dp[i-1]>=2 else dp[i-1] + 2
                res = max(res, dp[i])
        return res

  

對於這種括號匹配問題,通常都是使用棧。先找到全部能夠匹配的索引號,而後找出最長連續數列!

例如:s = )(()()),咱們用棧能夠找到,位置 2 和位置 3 匹配,位置 4 和位置 5 匹配,位置 1 和位置 6 匹配,這個數組爲:2,3,4,5,1,6 這是經過棧找到的,按遞增排序獲得 1,2,3,4,5,6。找出該數組的最長連續數列的長度就是最長有效括號長度,因此時間複雜度來自排序:O(nlogn)。接下來思考,是否能夠省略排序的過程,在彈棧時候進行操做呢?

用棧在遍歷給定字符串的過程當中去判斷到目前爲止掃描的子字符串的有效性,同時能獲得最長有效字符串的長度。咱們首先將 -1 放入棧頂。

對於遇到的每一個‘(’ ,咱們將它的下標放入棧中。
對於遇到的每一個 ‘)’ ,咱們彈出棧頂的元素並將當前元素的下標與彈出元素下標做差,得出當前有效括號字符串的長度。經過這種方法,咱們繼續計算有效子字符串的長度,並最終返回最長有效子字符串的長度。時間複雜度爲O(n)

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        if not s:
            return 0
        n = len(s)
        res = 0
        stack = [-1]
        for i in range(n):
            if s[i] == '(':  # 若是遍歷到‘(’就把當前索引i壓棧
                stack.append(i)
            else:
                stack.pop()  # 若是遍歷到‘)’,彈出棧頂
                if not stack:  # 若是棧空,說明前面已經配好對了,當前i是那個多餘的‘)’,索引i壓棧
                    stack.append(i)
                else:
                    res = max(res, i - stack[-1])  # 棧不空的話i-peek即爲有效子串長度
        return res
相關文章
相關標籤/搜索