(歡迎轉載,但請注明出處并附帶鏈接)
算法好久沒復習了,今天看見一妹子在辦公室刷Leetcode,頓時我也來了興趣,也準備刷著玩。仔細想想以前學算法時,或多或少都有些遺漏或不足,借著這次機會都補上。
先從動態規劃下手吧,記得當初在這個算法上掙扎了很久。本文先理論后實踐
動態規劃(DP)
該算法的核心價值就兩部分:描述狀態 和 推導狀態的演變過程。其余基礎概念我不介紹了,網上大牛寫的好文多得是。在這就說說自己的感受。
這個核心價值可以解決很多的DP問題,感覺更像是一種泛式( 這里想表達:這是一個一般性的解決方案,如果具體到某一個問題上,都有改進的空間)。很多東西學到最后都是一種思想,只是在某一具體問題上表現形式不同。
到這理論結束,下面來實戰,就圍繞這個核心價值轉
- 例1 LeetCode 198. House Robber
根據之前所講,只要找到描述狀態和推導狀態演變過程就能解決該問題,那么住這里描述狀態是什么呢? 這里其實很簡單,只有一種狀態(本人采用數組來記錄),因為根據題目的description只需記錄搶到當前屋子的最大值就ok,描述狀態如下:
res[i]:搶到第 i個屋子時的目前利益最大值
下面就要找推導狀態的演變過程。演變過程用文字描述起來太累也不清晰,采用數學公式來說明,如下(圖片是自己做的,找個了在線公式編輯器):
當i=0時,只有一個房子,為了最大值,怎么可能不搶
當i=1時,就要比較下哪個屋子更值錢,然后再搶
當i>=2時,就要根據題目要求進行下判斷了,分為搶和不搶,其中 f(i-1) 就代表不搶,所以最大利益和上一項相同,而另一個就代表搶。在搶和不搶中找出最大值
代碼如下(python實現):
class Solution(object):
def rob(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if not nums: return 0
# create the statement
res = []
res = range( len(nums) )
# i = 0
res[0] = nums[0]
# i = 1
if len(nums) > 1: res[1] = max( nums[0], nums[1] )
# i >= 2
for i in range( 2, len(nums) ):
res[i] = max( res[i-1], res[i-2] + nums[i] )
return res[-1]
上面代碼還有改進空間,正如本人之前說的"這是一個一般性的解決方案,如果具體到某一個問題上,都有改進的空間"
- 例2 LeetCode 309. Best Time to Buy and Sell Stock with Cooldown
來,先找描述狀態,從題目中能發現3個狀態(本人用3個數組來記錄),如下:
buy[i]: 截至到第 i 天的最大值,且第 i 天執行的是buy操作
sell[i]: 截至到第 i 天的最大值,且第 i 天執行的是sell操作
rest[i]: 截至到第 i 天的最大值,且第 i 天執行的是rest操作
下一步就是 推導狀態的演變過程。根據題目的邏輯很輕松就能用如下3個演變過程:
設price為第i 天的價格
// buy操作的第i天只有兩種可能(**買**和**不買**)
// 不買就是buy[i-1];買就必須從rest[i-1]狀態切換過來,還要再減去當天的價格
buy[i] = max( rest[i-1] - price, buy[i-1] )
// sell操作也是兩種可能(**賣**和**不賣**)
// 不賣就是sell[i-1], 賣就從buy[i-1]的狀態切換過來,再加上當天價格
sell[i] = max( buy[i-1] + price, sell[i-1] )
// 這個演變過程被簡化過了
// 其原型是max(sell[i-1], buy[i-1], rest[i-1]), 因為rest操作什么也不做,所以就從3個狀態中找最大值
// 但是根據題意,不能出現{buy, rest, buy}這種不合理得排序,就刪除了buy[i-1]
rest[i] = max( sell[i-1], rest[i-1] )
python代碼如下:
注意初始化buy[0]和sell[0],也挺簡單的,就不詳述了
class Solution(object):
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
if len(prices) < 2: return 0
buy = [ 0 for _ in range( len(prices) )]
sell = [ 0 for _ in range( len(prices) )]
rest = [ 0 for _ in range( len(prices) )]
buy[0] = -prices[0] # After buy, you should have -prices[0] profit. Be positive!
sell[0] = -sys.maxint - 1 # you can sell before buy, set sell to MIN
for i in range( 1, len(prices) ):
buy[i] = max(rest[i-1]-prices[i], buy[i-1])
sell[i] = max(buy[i-1]+prices[i], sell[i-1])
rest[i] = max(sell[i-1], rest[i-1])
return max( sell[-1], rest[-1] )
5/25/2017 更新
- 例3 LeetCode 486. Predict the Winner
這次描述狀態并不是那么直接,分析一下得到如下:
dp(i,j):累加 player1從第 i 個位置到第 j 個位置選擇的數值(即每次選值之和),請注意這個累加值并不是整個數組每項疊加,而是根據題意得出來的(player1選擇一個,player2再選擇一個,一直重復下去。然后將player1每次選值相加得到dp(i,j) )
其中用一個二維數組表示dp(i,j),即dp[ i ][ j ]。看待dp[i][j]時,不要把它想成一個下標表示的值,而是把它看成從 i 到 j 的按題意邏輯得到的累加值,這樣比較好理解下面的代碼,這點很重要?。?!
在用數學公式表達下:
dp(i,j) = max( sum(i,j-1) - dp(i,j-1) + nums[j],
sum(i+1,j) - dp(i+1,j) + nums[i] )
簡化后,得到:
dp(i,j) = max( sum(i,j) - dp(i,j-1) , sum(i,j) - dp(i+1,j) )
下面找推導狀態的演變過程,其實上面那個式子就可以說是演變過程,但對于解題來說很不理想,所以這次從題目分析。
題目想問player1能不能贏,也就是問player1最后的數值是不是大于player2的數值,即player1 - player2 > 0
下面進行一些數學推導(字跡不好看,請注意看思路:-) ):
到此描述狀態和推到演變過程都結束了
下面開始上代碼(依舊python,代碼之后還有對一些迷茫地方的講解):
class Solution(object):
def PredictTheWinner(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
# store the maximum score player1 picking from i to j
dp = [ [0 for _ in range(len(nums))] for _ in range(len(nums))]
# initializing
for i in range( len(nums) ): dp[i][i] = nums[i]
for i in range( 1, len(nums) ):
for j in range( 0, len(nums)-i ):
dp[j][j+i] = max( nums[j+i] - dp[j][j+i-1], nums[j] - dp[j+1][j+i] )
return dp[0][-1] >= 0
- 可能有人會問“初始化那一行在做什么?”。首先,在任何dp問題中,我們都需要初始化某些值(就是那種能從題目中得到的確定值,在動態規劃開始之前它們就已經存在了),在這道題目中能確定的只有dp[0][0] = nums[0], dp[1][1] = nums[1], dp[2][2] = nums[2]...等等。dp[0][0]就是開頭說的dp(0,0),即從第0個開始到第0個結束所能得到的累加值,這里只有nums[0]這一個值,后面幾個dp[1][1],dp[2][2]同理
- 還有人可能問那個雙重循環在干什么?。我們要把全部的case列舉出來,放張圖舉個例子(長度為6的數組):
今天就說到這了
6/5/2017 更新
- 例4 377. Combination Sum IV
先看描述狀態,這里很明顯需要一個數組來記錄當前共有多少種組合,即:
dp[i]: 代表數值 i 一共有多少種組合方式
下面看推導狀態的演變過程,這個過程用個舉例來說明:
現在有一個array = [1,2,3]當做已知條件,target為4,請問當前的array可以提供多少種組合組成target?可以列出所有組合:
4 = dp[3] + 1
4 = dp[2] + 2
4 = dp[1] + 3
只有以上3種方式組成數字4,所以其組合總數為:
dp[4] = dp[3] + dp[2] + dp[1]
dp[4] = dp[4-1] + dp[4-2] + dp[4-3]
然后推導出公式
dp[target] = dp[ target - nums[0] ] + dp[target-nums[1]] + ……+ dp[target - nums[ nums.length() - 1] ]
dp[target] = sum( dp[ target - nums[i] ] ), 0 <= i < nums.length() 且 target >= nums[i]
上面最后一步就是推導狀態的演變過程
接著上代碼(python):
class Solution(object):
def combinationSum4(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
dp = [ 0 for _ in range(target+1)]
dp[0] = 1
for i in range(target+1):
for j in range( len(nums) ):
if i >= nums[j]:
dp[i] += dp[ i - nums[j] ]
return dp[target]
這道題挺簡單的,敢興趣的可以自己做優化,歡迎交流!
6/6/2017 更新
- 例5 64. Minimum Path Sum
先找描述狀態,基本上一眼就能看出來:
dp[i][j]: 記錄當前最小步數
推導狀態的演變過程,這個也很簡單,從題目中就能知道是個2選1問題,要么move down要么move right,從它倆找出最小值, 其公式為:
dp[i][j] = min ( dp[i-1][j], dp[i][j-1] ) + grid[i][j]
python代碼:
class Solution(object):
def minPathSum(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
if not grid: return
m = len(grid)
n = len(grid[0])
dp = [ [ 0 for _ in range(n)] for _ in range(m) ]
dp[0][0] = grid[0][0]
for i in range(1,n): dp[0][i] = dp[0][i-1] + grid[0][i]
for i in range(1,m): dp[i][0] = dp[i-1][0] + grid[i][0]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
不過上面這個算法實在太笨了,說下優化吧。
從邏輯上就能知道 i 要把整個二維數組走一遍,并且是按順序一行一行的走,所以我們就用一個一維數組替代二維數組,當這個循環結束時,我們只關心最后一個位置(即右下角的位置),其他的記不記錄都不無所謂,優化后的代碼如下:
class Solution(object):
def minPathSum(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
if not grid: return
m, n = len(grid), len(grid[0])
dp = [ 0 for _ in range(n) ]
dp[0] = grid[0][0]
for i in range(1, n): dp[i] = dp[i-1] + grid[0][i]
for i in range(1, m):
dp[0] += grid[i][0]
for j in range(1, n):
dp[j] = min( dp[j-1], dp[j] ) + grid[i][j]
return dp[-1]
空間復雜度降為 O(n), 比之前那個快了不少~~
- 例6 62. Unique Paths
這題簡直和上一題是好基友,不廢話了,先找描述狀態
dp[]: 走到當前位置共有幾種組合
下面是推導狀態的演變過程,如果明白上面那題,這題基本上瞬間解決,直接上公式了:
dp[i][j] = dp[i][j-1] + dp[i-1][j],其中 i,j >= 1
python代碼:
class Solution(object):
def uniquePaths(self, m, n):
"""
:type m: int
:type n: int
:rtype: int
"""
dp = [ [0 for _ in range(n)] for _ in range(m) ]
for i in range(n): dp[0][i] = 1
for i in range(m): dp[i][0] = 1
for i in range(1,m):
for j in range(1,n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
所有的代碼都有優化空間,做的題遠遠比寫在簡書上的多,不過有些題解釋起來太麻煩,就沒寫進來,以后偶爾還會更! 不過明天起準備看下其他算法了。
上面這些代碼都有優化的空間,各位改著玩吧