動態規劃(DP)的整理-Python描述

今天整理了一下關于動態規劃的內容,道理都知道,但是python來描述的方面參考較少,整理如下,希望對你有所幫助,實驗代碼均經過測試。

請先好好閱讀如下內容--什么是動態規劃?

摘錄于《算法圖解》

這里寫圖片描述

以上的都建議自己手推一下,然后知道怎么回事,核心的部分是142頁核心公式,待會代碼會重現這個過程,推薦沒有算法基礎的小伙伴看這本書《算法圖解》很有意思的書,講的很清晰,入門足夠

更深入的請閱讀python算法-動態規劃寫的不錯,可以參考

為什么要使用動態規劃?

From:動態規劃是什么,意義在哪里?!!!!

? 首先我們要知道為什么要使用(Dynamic programming)dp,我們在選擇dp算法的時候,往往是在決策問題上,而且是在如果不使用dp,直接暴力效率會很低的情況下選擇使用dp.

那么問題來了,什么時候會選擇使用dp呢,一般情況下,我們能將問題抽象出來,并且問題滿足無后效性,滿足最優子結構,并且能明確的找出狀態轉移方程的話,dp無疑是很好的選擇。

  • 無后效性通俗的說就是只要我們得出了當前狀態,而不用管這個狀態怎么來的,也就是說之前的狀態已經用不著了,如果我們抽象出的狀態有后效性,很簡單,我們只用把這個值加入到狀態的表示中。
  • 最優子結構(自下而上):在決策問題中,如果,當前問題可以拆分為多個子問題,并且依賴于這些子問題,那么我們稱為此問題符合子結構,而若當前狀態可以由某個階段的某個或某些狀態直接得到,那么就符合最優子結構
  • 重疊子問題(自上而下):動態規劃算法總是充分利用重疊子問題,通過每個子問題只解一次,把解保存在一個需要時就可以查看的表中,每次查表的時間為常數,如備忘錄的遞歸方法。斐波那契數列的遞歸就是個很好的例子
  • 狀態轉移:這個概念比較簡單,在抽象出上述兩點的的狀態表示后,每種狀態之間轉移時值或者參數的變化。

小結

  • 動態規劃: 動態規劃表面上很難,其實存在很簡單的套路:當求解的問題滿足以下兩個條件時, 就應該使用動態規劃:
    • 主問題的答案 包含了 可分解的子問題答案 (也就是說,問題可以被遞歸的思想求解)
    • 遞歸求解時, 很多子問題的答案會被多次重復利用
  • 動態規劃的本質思想就是遞歸, 但如果直接應用遞歸方法, 子問題的答案會被重復計算產生浪費, 同時遞歸更加耗費棧內存, 所以通常用一個二維矩陣(表格)來表示不同子問題的答案, 以實現更加高效的求解。
  • ?

Talk is cheap ,Show me the code

翻閱很多資料,貌似python描述的比較少,這里總結一下,用前面的圖解中的偽代碼重構下

背包問題

多謝rubik_wong--0/1背包問題,代碼參考如下

# 這里使用了圖解中的吉他,音箱,電腦,手機做的測試,數據保持一致
w = [0, 1, 4, 3, 1]   #n個物體的重量(w[0]無用)
p = [0, 1500, 3000, 2000, 2000]   #n個物體的價值(p[0]無用)
n = len(w) - 1   #計算n的個數
m = 4   #背包的載重量

x = []   #裝入背包的物體,元素為True時,對應物體被裝入(x[0]無用)
v = 0
#optp[i][j]表示在前i個物體中,能夠裝入載重量為j的背包中的物體的最大價值
optp = [[0 for col in range(m + 1)] for raw in range(n + 1)]
#optp 相當于做了一個n*m的全零矩陣的趕腳,n行為物件,m列為自背包載重量

def knapsack_dynamic(w, p, n, m, x):
    #計算optp[i][j]
    for i in range(1, n + 1):       # 物品一件件來
        for j in range(1, m + 1):   # j為子背包的載重量,尋找能夠承載物品的子背包
            if (j >= w[i]):         # 當物品的重量小于背包能夠承受的載重量的時候,才考慮能不能放進去
                optp[i][j] = max(optp[i - 1][j], optp[i - 1][j - w[i]] + p[i])    # optp[i - 1][j]是上一個單元的值, optp[i - 1][j - w[i]]為剩余空間的價值
            else:
                optp[i][j] = optp[i - 1][j]
    
    #遞推裝入背包的物體,尋找跳變的地方,從最后結果開始逆推
    j = m
    for i in range(n, 0, -1):
        if optp[i][j] > optp[i - 1][j]:
            x.append(i)
            j = j - w[i]  
    
    #返回最大價值,即表格中最后一行最后一列的值
    v = optp[n][m]
    return v
 
print '最大值為:' + str(knapsack_dynamic(w, p, n, m, x))
print '物品的索引:',x

#最大值為:4000
#物品的索引: [4, 3]

優化背包問題的遞歸方法

參考自:麻省理工的 背包算法 python

def MaxVal2(memo , w, v, index, last):  
    """ 
    得到最大價值 
    w為widght 
    v為value 
    index為索引 
    last為剩余重量 
    """  
  
    global numCount  
    numCount = numCount + 1  
  
    try:  
        #以往是否計算過分支,如果計算過,直接返回分支的結果  
        return memo[(index , last)]  
    except:  
        #最底部  
        if index == 0:  
            #是否可以裝入  
            if w[index] <= last:  
                return v[index]  
            else:  
                return 0  
  
        #尋找可以裝入的分支  
        without_l = MaxVal2(memo , w, v, index - 1, last)  
  
        #如果當前的分支大于約束  
        #返回歷史查找的最大值  
        if w[index] > last:  
            return without_l  
        else:  
            #當前分支加入背包,剪掉背包剩余重量,繼續尋找  
            with_l = v[index] + MaxVal2(memo , w, v , index - 1, last - w[index])  
  
        #比較最大值  
        maxvalue = max(with_l , without_l)  
        #存儲  
        memo[(index , last)] = maxvalue  
        return maxvalue  
  
w = [0, 1, 4, 3, 1]   # 東西的重量 
v = [0, 1500, 3000, 2000, 2000]       # 東西的價值
  
numCount = 0  
memo = {} 
n = len(w) - 1
m = 4
print MaxVal2(memo , w, v, n, m) , "caculate count : ", numCount  


# 4000 caculate count :  20

優化斐波那契數列的遞歸方法

多謝Python科學實驗----動態規劃,也就是對應上面的重疊子問題的方法,備忘錄的遞歸方法

#Dynamic Method Experiment
import matplotlib.pyplot as plt
count=0;
#blank
def f(n):
    global count
    count=count+1
    if n==1:
        return 1
    elif n==0:
        return 1
    else:
        return f(n-1)+f(n-2)

    
# function calls count
def calc_f(n):
    global count
    count=0
    f(n)
    return count

#using memorization
mem={}

def mem_f(n):
    global count,mem
    count=count+1
    if n in mem:
        return mem[n]
    else:
        if n==1:
            result=1
        elif n==0:
            result=1
        else:
            result=mem_f(n-1)+mem_f(n-2)
        mem[n]=result
        return result
    

def mem_calc_f(n):
    global count
    global mem
    mem={}
    count=0
    mem_f(n)
    return count  


x=range(1,15)
y=[]
y2=[]
for i in x:
    
    c=mem_calc_f(i)
    y.append(c)
    
    c2=calc_f(i)
    y2.append(c2)
    print "規模為%d時計算了%d次 i=%d時,val=%d"%(i,c,i,mem_f(i))
    print "規模為%d時計算了%d次 i=%d時,val=%d"%(i,c2,i,f(i))
plt.plot(x,y)
plt.plot(x,y2)
plt.show()

它的基本思想就是記錄已經計算過的值,避免重復計算。

如果使用裝飾器的寫法,則會優雅很多

from functools import wraps
 
def memo(func):
    cache={}
    @wraps(func)
    def wrap(*args):
        if args not in cache:
            cache[args]=func(*args)
        return cache[args]
    return wrap
 
@memo
def fib(i):
    if i<2: return 1
    return fib(i-1)+fib(i-2)

fib(2)

一些利用DP的筆試題

CPU雙核問題

網易筆試—動態規劃: 題目的大概意思:一種雙核CPU的兩個核能夠同時的處理任務,現在有n個已知數據量的任務需要交給CPU處理,假設已知CPU的每個核1秒可以處理1kb,每個核同時只能處理一項任務。n個任務可以按照任意順序放入CPU進行處理,現在需要設計一個方案讓CPU處理完這批任務所需的時間最少,求這個最小的時間。

輸入包括兩行:
第一行為整數n(1 ≤ n ≤ 50)
第二行為n個整數length[i](1024 ≤ length[i] ≤ 4194304),表示每個任務的長度為length[i]kb,每個數均為1024的倍數。
輸出一個整數,表示最少需要處理的時間。
問題實質是動態規劃問題,把數組分成兩部分,使得兩部分的和相差最小。
如何將數組分成兩部分使得兩部分的和的差最小?參考博客http://www.tuicool.com/articles/ZF73Af
思路:
差值最小就是說兩部分的和最接近,而且各部分的和與總和的一半也是最接近的。假設用sum1表示第一部分的和,sum2表示第二部分的和,SUM表示所有數的和,那么sum1+sum2=SUM。假設sum1<sum2 那么SUM/2-sum1 = sum2-SUM/2;
所以我們就有目標了,使得sum1<=SUM/2的條件下盡可能的大。也就是說從n個數中選出某些數,使得這些數的和盡可能的接近或者等于所有數的和的一般。這其實就是簡單的背包問題了:
背包容量是SUM/2. 每個物體的體積是數的大小,然后盡可能的裝滿背包。

w = [0, 3072, 3072, 7168, 3072, 1024]  # 假設進入處理的的任務大小
w = map(lambda x:x/1024,w)  # 轉化下
p = w  # 這題的價值和任務重量一致
n = sum(w)/2 +1 # 背包承重為總任務的一半

optp = [[0 for j in range(n+1)] for i in range(len(w))]

for i in range(1,len(p)):
    for j in range(1,n+1):
        if j >= p[i]:
            optp[i][j] = max(optp[i-1][j],p[i]+optp[i-1][j-w[i]])
        else:
            optp[i][j] = optp[i-1][j]

            
print optp[-1][-1]
print optp
    
# 背包矩陣入下所示,第一列和第一行無效占位符
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
 [0, 0, 0, 3, 3, 3, 3, 3, 3, 3], 
 [0, 0, 0, 3, 3, 3, 6, 6, 6, 6], 
 [0, 0, 0, 3, 3, 3, 6, 7, 7, 7], 
 [0, 0, 0, 3, 3, 3, 6, 7, 7, 9], 
 [0, 1, 1, 3, 4, 4, 6, 7, 8, 9]]
 

LIS問題

longest increasing subsequence問題,

# 講DP基本都會講到的一個問題LIS:longest increasing subsequence
# http://www.deeplearn.me/216.html
lis = [2 ,1, 5, 3, 6 ,4 ,8 ,9, 7]

d = [1]*len(lis)
res = 1
for i in range(len(lis)):
    for j in range(i):
        if lis[j] <= lis[i] and d[i] < d[j]+1:
            d[i] = d[j]+1
        if d[j] >  res:
            res = d[j]
print res

LCS問題

一個非常好的圖解教程:動態規劃 最長公共子序列 過程圖解

# 根據圖解教程寫的偽代碼,其實最后評論里面的代碼就是我添加上去的

s1 = [1,3,4,5,6,7,7,8]
s2 = [3,5,7,4,8,6,7,8,2]

d = [[0]*(len(s2)+1) for i in range(len(s1)+1) ]

for i in range(1,len(s1)+1):
    for j in range(1,len(s2)+1):
        if s1[i-1] == s2[j-1]:
            d[i][j] = d[i-1][j-1]+1
        else:
            d[i][j] = max(d[i-1][j],d[i][j-1])
            
            
print "max LCS number:",d[-1][-1]

給定一個有n個正整數的數組A和一個整數sum

? 給定一個有n個正整數的數組A和一個整數sum,求選擇數組A中部分數字和為sum的方案數。
當兩種選取方案有一個數字的下標不一樣,我們就認為是不同的組成方案。

輸入描述:

輸入為兩行:

第一行為兩個正整數n(1 ≤ n ≤ 1000),sum(1 ≤ sum ≤ 1000)

第二行為n個正整數A[i](32位整數),以空格隔開。

輸出描述:

輸出所求的方案數

示例1

輸入

5 15
5 5 10 2 3

輸出

4
#動態規劃算法。dp[i][j]代表用前i個數字湊到j最多有多少種方案。 
#dp[i][j]=dp[i-1][j];   //不用第i個數字能湊到j的最多情況 
#dp[i][j]+=dp[i-1][j-value[i]];用了i時,只需要看原來湊到j-value[i]的最多情況即可。并累加 

num_ = 5
sum_ = 10
line = [5 ,5 ,10 ,2 ,3] 

optp = [[1]+[0]*sum_ for i in range(num_+1)]  # 第一列為1的原因是和為0的時候只有一種取法,就是什么都不取

for i in range(1,num_+1):
    for j in range(1,sum_+1):
        if j - line[i-1] >=0:
            optp[i][j] = optp[i-1][j] + optp[i-1][j-line[i-1]]
        else:
            optp[i][j] = optp[i-1][j]

 

print optp

    0   1  2  3  4  5  6  7  8  9 10
0  [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 
5   [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], 
5   [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1], 
10  [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2], 
2   [1, 0, 1, 0, 0, 2, 0, 2, 0, 0, 2], 
3   [1, 0, 1, 1, 0, 3, 0, 2, 2, 0, 4]]

# 轉化為背包問題后,開始推,(注意第一行和第一列是預至位)比如說第一個數字是5,那么從構成和為1,怎么取?當然沒得取,直到構成和為5的時候,開始執行,如果用這個5,那么還剩下5-5=0的和,0的和取法只有1,而如果不用5,取法只有0,所以為1,之后重復推,再說第二個5,直到和為5之前,都是0取法,到了5之后,兩種取法,一種是要不要這個新的5,如果要這個新的5,那么剩下的和即5-5=0,一種取法,如果這個新的5不取,那以前能取到和為5就上次循環中的一種取法,所以合起來兩種取法

一個數組有 N 個元素,求連續子數組的最大和

一個數組有 N 個元素,求連續子數組的最大和。 例如:[-1,2,1],和最大的連續子數組為[2,1],其和為 3

輸入描述:

輸入為兩行。
第一行一個整數n(1 <= n <= 100000),表示一共有n個元素
第二行為n個數,即每個元素,每個整數都在32位int范圍內。以空格分隔。

輸出描述:

所有連續子數組中和最大的值。

示例1

輸入

3
-1 2 1

輸出

3
# 采用動態規劃的方法
# 設dp[i]表示以第 i個元素為結尾的連續子數組的最大和,則遞推方程式為 dp[i]=max{dp[i-1]+a[i], a[i]};

num = raw_input("")
line = raw_input("")
line = map(lambda x:int(x),line.split(" "))
num = int(num)

d =[0]*(num-1)
d.insert(0,line[0])

for i in range(1,num):
    
    d[i] = max(d[i-1]+line[i],line[i])

print max(d)

X*Y的網格迷宮

有一個X*Y的網格,小團要在此網格上從左上角到右下角,只能走格點且只能向右或向下走。請設計一個算法,計算小團有多少種走法。給定兩個正整數int x,int y,請返回小團的走法數目。

輸入描述:

輸入包括一行,逗號隔開的兩個正整數x和y,取值范圍[1,10]。

輸出描述:

輸出包括一行,為走法的數目。

示例1

輸入

3 2

輸出

10
# 動態規劃,使用遞推方程d[i][j] = d[i-1][j] + d[i][j-1]
# 因為可能從兩個方向走到同一個點,所以從上到下為一種走法,從左到右是另一種走法
# 注意題目給的是x*y方格,所以是(x+1)*(y+1)個點


line = map(int, raw_input("").split(" "))
x = line[0]
y = line[1]

d = [[0]*(y+2) for i in range(x+2)]

for i in range(1,x+2):
    for j in range(1,y+2):
        if i==j and i==1:
            d[i][j] = 1
        else:
            d[i][j] = d[i-1][j] + d[i][j-1]

print d[-1][-1]

暗黑字符串

一個只包含'A'、'B'和'C'的字符串,如果存在某一段長度為3的連續子串中恰好'A'、'B'和'C'各有一個,那么這個字符串就是純凈的,否則這個字符串就是暗黑的。例如:

BAACAACCBAAA 連續子串"CBA"中包含了'A','B','C'各一個,所以是純凈的字符串

AABBCCAABB 不存在一個長度為3的連續子串包含'A','B','C',所以是暗黑的字符串

你的任務就是計算出長度為n的字符串(只包含'A'、'B'和'C'),有多少個是暗黑的字符串。

輸入描述:

輸入一個整數n,表示字符串長度(1 ≤ n ≤ 30)

輸出描述:

輸出一個整數表示有多少個暗黑字符串

示例1

輸入

3

輸出

21

思路解析

#方式二,這么low的方式是我根據上面的解析寫的。遞歸所以速度慢
num = int(raw_input(""))

def dark(num):
    if num == 1:
        return 3
    elif num==2:
        return 9
    else:
        return 2*dark(num-1) + dark(num-2)
    
print dark(num)
# 方式一:別人家的代碼
n = int(raw_input())
dp = [0]*31
dp[0] = 3
dp[1] = 9
for i in xrange(2, n):
    dp[i] = 2*dp[i-1]+dp[i-2]
     
print dp[n-1]

最后

紙上得來終覺淺,這句話放在什么時候都一樣,自己覺得動態規劃比較了解了,其實了解個屁,需要重新打打基礎!以后再過來更新理解。

致謝

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容