日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

Python算法:动态规划

發布時間:2025/6/15 python 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Python算法:动态规划 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本節主要結合一些經典的動規問題介紹動態規劃的備忘錄法和迭代法這兩種實現方式,并對這兩種方式進行對比

[這篇文章實際寫作時間在這個系列文章之前,所以寫作風格可能略有不同,嘿嘿]

大家都知道,動態規劃算法一般都有下面兩種實現方式,前者我稱為遞歸版本,后者稱為迭代版本,根據前面的知識可知,這兩個版本是可以相互轉換的

1.直接自頂向下實現遞歸式,并將中間結果保存,這叫備忘錄法;

2.按照遞歸式自底向上地迭代,將結果保存在某個數據結構中求解。

編程有一個原則DRY=Don’t Repeat Yourself,就是說你的代碼不要重復來重復去的,這個原則同樣可以用于理解動態規劃,動態規劃除了滿足最優子結構,它還存在子問題重疊的性質,我們不能重復地去解決這些子問題,所以我們將子問題的解保存起來,類似緩存機制,之后遇到這個子問題時直接取出子問題的解。

舉個簡單的例子,斐波那契數列中的元素的計算,很簡單,我們寫下如下的代碼:

Python
123def fib(i):????if i<2: return 1????return fib(i-1)+fib(i-2)

好,來測試下,運行fib(10)得到結果69,不錯,速度也還行,換個大的數字,試試100,這時你會發現,這個程序執行不出結果了,為什么?遞歸太深了!要計算的子問題太多了!

所以,我們需要改進下,我們保存每次計算出來的子問題的解,用什么保存呢?用Python中的dict!那怎么實現保存子問題的解呢?用Python中的裝飾器!

如果不是很了解Python的裝飾器,可以快速看下這篇總結中關于裝飾器的解釋:Python Basics

修改剛才的程序,得到如下代碼,定義一個函數memo返回我們需要的裝飾器,這里用cache保存子問題的解,key是方法的參數,也就是數字n,值就是fib(n)返回的解。

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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(100),你會發現這次很快就得到了結果573147844013817084101,這就是動態規劃的威力,上面使用的是第一種帶備忘錄的遞歸實現方式。

帶備忘錄的遞歸方式的優點就是易于理解,易于實現,代碼簡潔干凈,運行速度也不錯,直接從需要求解的問題出發,而且只計算需要求解的子問題,沒有多余的計算。但是,它也有自己的缺點,因為是遞歸形式,所以有限的棧深度是它的硬傷,有些問題難免會出現棧溢出了。

于是,迭代版本的實現方式就誕生了!

迭代實現方式有2個好處:1.運行速度快,因為沒有用棧去實現,也避免了棧溢出的情況;2.迭代實現的話可以不使用dict來進行緩存,而是使用其他的特殊cache結構,例如多維數組等更為高效的數據結構。

那怎么把遞歸版本轉變成迭代版本呢?

這就是遞歸實現和迭代實現的重要區別:遞歸實現不需要去考慮計算順序,只要給出問題,然后自頂向下去解就行;而迭代實現需要考慮計算順序,并且順序很重要,算法在運行的過程中要保證當前要計算的問題中的子問題的解已經是求解好了的。

斐波那契數列的迭代版本很簡單,就是按順序來計算就行了,不解釋,關鍵是你可以看到我們就用了3個簡單變量就求解出來了,沒有使用任何高級的數據結構,節省了大量的空間。

Python
123456789def fib_iter(n):????if n<2: return 1????a,b=1,1????while n>=2:????????c=a+b????????a=b????????b=c????????n=n-1????return c

斐波那契數列的變種經常出現在上樓梯的走法問題中,每次只能走一個臺階或者兩個臺階,廣義上思考的話,動態規劃也就是一個連續決策問題,到底當前這一步是選擇它(走一步)還是不選擇它(走兩步)呢?

其他問題也可以很快地變相思考發現它們其實是一樣的,例如求二項式系數C(n,k),楊輝三角(求從源點到目標點有多少種走法)等等問題。

二項式系數C(n,k)表示從n個中選k個,假設我們現在處理n個中的第1個,考慮是否選擇它。如果選擇它的話,那么我們還需要從剩下的n-1個中選k-1個,即C(n-1,k-1);如果不選擇它的話,我們需要從剩下的n-1中選k個,即C(n-1,k)。所以,C(n,k)=C(n-1,k-1)+C(n-1,k)。

結合前面的裝飾器,我們很快便可以實現求二項式系數的遞歸實現代碼,其中的memo函數完全沒變,只是在函數cnk前面添加了@memo而已,就這么簡單!

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 cnk(n,k): ????if k==0: return 1 #the order of `if` should not change!!! ????if n==0: return 0 ????return cnk(n-1,k)+cnk(n-1,k-1)

它的迭代版本也比較簡單,這里使用了defaultdict,略高級的數據結構,和dict不同的是,當查找的key不存在對應的value時,會返回一個默認的值,這個很有用,下面的代碼可以看到。 如果不了解defaultdict的話可以看下Python中的高級數據結構

Python
12345678910from collections import defaultdictn,k=10,7C=defaultdict(int)for row in range(n+1):????C[row,0]=1????for col in range(1,k+1):????????C[row,col]=C[row-1,col-1]+C[row-1,col]print(C[n,k]) #120

楊輝三角大家都熟悉,在國外這個叫Pascal Triangle,它和二項式系數特別相似,看下圖,除了兩邊的數字之外,里面的任何一個數字都是由它上面相鄰的兩個元素相加得到,想想C(n,k)=C(n-1,k-1)+C(n-1,k)不也就是這個含義嗎?

所以說,順序對于迭代版本的動態規劃實現很重要,下面舉個實例,用動態規劃解決有向無環圖的單源最短路徑問題。假設有如下圖所示的圖,當然,我們看到的是這個有向無環圖經過了拓撲排序之后的結果,從a到f的最短路徑用灰色標明了。

好,怎么實現呢?

我們有兩種思考方式:

1.”去哪里?”:我們順向思維,首先假設從a點出發到所有其他點的距離都是無窮大,然后,按照拓撲排序的順序,從a點出發,接著更新a點能夠到達的其他的點的距離,那么就是b點和f點,b點的距離變成2,f點的距離變成9。因為這個有向無環圖是經過了拓撲排序的,所以按照拓撲順序訪問一遍所有的點(到了目標點就可以停止了)就能夠得到a點到所有已訪問到的點的最短距離,也就是說,當到達哪個點的時候,我們就找到了從a點到該點的最短距離,拓撲排序保證了后面的點不會指向前面的點,所以訪問到后面的點時不可能再更新它前面的點的最短距離!(這里的更新也就是前面第4節介紹過的relaxtion)這種思維方式的代碼實現就是迭代版本。

[這里涉及到了拓撲排序,前面第5節Traversal中介紹過了,這里為了方便沒看前面的童鞋理解,W直接使用的是經過拓撲排序之后的結果。]

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def topsort(W): ????return W def dag_sp(W, s, t): ????d = {u:float('inf') for u in W} # ????d[s] = 0 ????for u in topsort(W): ????????if u == t: break ????????for v in W[u]: ????????????d[v] = min(d[v], d[u] + W[u][v]) ????return d[t] #鄰接表 W={0:{1:2,5:9},1:{2:1,3:2,5:6},2:{3:7},3:{4:2,5:3},4:{5:4},5:{}} s,t=0,5 print(dag_sp(W,s,t)) #7

用圖來表示計算過程就是下面所示:

2.”從哪里來?”:我們逆向思維,目標是要到f,那從a點經過哪個點到f點會近些呢?只能是求解從a點出發能夠到達的那些點哪個距離f點更近,這里a點能夠到達b點和f點,f點到f點距離是0,但是a到f點的距離是9,可能不是最近的路,所以還要看b點到f點有多近,看b點到f點有多近就是求解從b點出發能夠到達的那些點哪個距離f點更近,所以又繞回來了,也就是遞歸下去,直到我們能夠回答從a點經過哪個點到f點會更近。這種思維方式的代碼實現就是遞歸版本。

這種情況下,不需要輸入是經過了拓撲排序的,所以你可以任意修改輸入W中節點的順序,結果都是一樣的,而上面采用迭代實現方式必須要是拓撲排序了的,從中你就可以看出迭代版本和遞歸版本的區別了。

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from functools import wraps def memo(func): ????cache={} ????@wraps(func) ????def wrap(*args): ????????if args not in cache: ????????????cache[args]=func(*args) ????????????# print('cache {0} = {1}'.format(args[0],cache[args])) ????????return cache[args] ????return wrap def rec_dag_sp(W, s, t): ????@memo ????def d(u): ????????if u == t: return 0 ????????return min(W[u][v]+d(v) for v in W[u]) ????return d(s) #鄰接表 W={0:{1:2,5:9},1:{2:1,3:2,5:6},2:{3:7},3:{4:2,5:3},4:{5:4},5:{}} s,t=0,5 print(rec_dag_sp(W,s,t)) #7

用圖來表示計算過程就如下圖所示:

[擴展內容:對DAG求單源最短路徑的動態規劃問題的總結,比較難理解,附上原文]

Although the basic algorithm is the same, there are many ways of finding the shortest path in a DAG, and, by extension, solving most DP problems. You could do it recursively, with memoization, or you could do it iteratively, with relaxation. For the recursion, you could start at the first node, try various “next steps,” and then recurse on the remainder, or (if you graph representation permits) you could look at the last node and try “previous steps” and recurse on the initial part. The former is usually much more natural, while the latter corresponds more closely to what happens in the iterative version.

Now, if you use the iterative version, you also have two choices: you can relax the edges out of each node (in topologically sorted order), or you can relax all edges into each node. The latter more obviously yields a correct result but requires access to nodes by following edges backward. This isn’t as far-fetched as it seems when you’re working with an implicit DAG in some nongraph problem. (For example, in the longest increasing subsequence problem, discussed later in this chapter, looking at all backward “edges” can be a useful perspective.)

Outward relaxation, called reaching, is exactly equivalent when you relax all edges. As explained, once you get to a node, all its in-edges will have been relaxed anyway. However, with reaching, you can do something that’s hard in the recursive version (or relaxing in-edges): pruning. If, for example, you’re only interested in finding all nodes that are within a distance r, you can skip any node that has distance estimate greater than r. You will still need to visit every node, but you can potentially ignore lots of edges during the relaxation. This won’t affect the asymptotic running time, though (Exercise 8-6).

Note that finding the shortest paths in a DAG is surprisingly similar to, for example, finding the longest path, or even counting the number of paths between two nodes in a DAG. The latter problem is exactly what we did with Pascal’s triangle earlier; the exact same approach would work for an arbitrary graph. These things aren’t quite as easy for general graphs, though. Finding shortest paths in a general graph is a bit harder (in fact, Chapter 9 is devoted to this topic), while finding the longest path is an unsolved problem (see Chapter 11 for more on this).

好,我們差不多搞清楚了動態規劃的本質以及兩種實現方式的優缺點,下面我們來實踐下,舉最常用的例子:矩陣鏈乘問題,內容較多,所以請點擊鏈接過去閱讀完了之后回來看總結!

OK,希望我把動態規劃講清楚了,總結下:動態規劃其實就是一個連續決策的過程,每次決策我們可能有多種選擇(二項式系數和0-1背包問題中我們只有兩個選擇,DAG圖的單源最短路徑中我們的選擇要看點的出邊或者入邊,矩陣鏈乘問題中就是矩陣鏈可以分開的位置總數…),我們每次選擇最好的那個作為我們的決策。所以,動態規劃的時間復雜度其實和這兩者有關,也就是子問題的個數以及子問題的選擇個數,一般情況下動態規劃算法的時間復雜度就是兩者的乘積。

動態規劃有兩種實現方式:一種是帶備忘錄的遞歸形式,這種方式直接從原問題出發,遇到子問題就去求解子問題并存儲子問題的解,下次遇到的時候直接取出來,問題求解的過程看起來就像是先自頂向下地展開問題,然后自下而上的進行決策;另一個實現方式是迭代方式,這種方式需要考慮如何給定一個子問題的求解方式,使得后面求解規模較大的問題是需要求解的子問題都已經求解好了,它的缺點就是可能有些子問題不要算但是它還是算了,而遞歸實現方式只會計算它需要求解的子問題。


練習1:來試試寫寫最長公共子序列吧,這篇文章中給出了Python版本的5種實現方式喲!

練習2:算法導論問題 15-4: Planning a company party 計劃一個公司聚會

Start example Professor Stewart is consulting for the president of a corporation that is planning a company party. The company has a hierarchical structure; that is, the supervisor relation forms a tree rooted at the president. The personnel office has ranked each employee with a conviviality rating, which is a real number. In order to make the party fun for all attendees, the president does not want both an employee and his or her immediate supervisor to attend.

Professor Stewart is given the tree that describes the structure of the corporation, using the left-child, right-sibling representation described in Section 10.4. Each node of the tree holds, in addition to the pointers, the name of an employee and that employee’s conviviality ranking. Describe an algorithm to make up a guest list that maximizes the sum of the conviviality ratings of the guests. Analyze the running time of your algorithm.

原問題可以轉換成:假設有一棵樹,用左孩子右兄弟的表示方式表示,樹的每個結點有個值,選了某個結點,就不能選擇它的父結點,求整棵樹選的節點值最大是多少。

假設如下:

dp[i][0]表示不選i結點時,i子樹的最大價值

dp[i][1]表示選i結點時,i子樹的最大價值

列出狀態方程

dp[i][0] = sum(max(dp[u][0], dp[u][1]))??(如果不選i結點,u為結點i的兒子)

dp[i][1] = sum(dp[u][0]) + val[i]??(如果選i結點,val[i]表示i結點的價值)

最后就是求max(dp[root][0], dp[root][1])

《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

總結

以上是生活随笔為你收集整理的Python算法:动态规划的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。