如何從動態規劃的角度去看問題
演算法導論(MIT 6.006 第19講)
動態規劃的核心處理流程是什麼?
1: 定義子問題
計運算元問題的數量
2:猜測(嘗試所有可能的方式,獲取最好的)
計算選擇的數量
3: 關聯所有的子問題
計算單個子問題所需要處理的時間
4: 重用子問題結果並記下新的結果,或者使用DP的bottom-up方式(需要注意沒有環)
計算總耗時
5: 解決原有的問題
對結果進行組合等等會劃掉部分額外的時間
總的來說就是:嘗試所有可能的子問題的結果,將最好的可能子結果儲存下來,然後重複利用已經解決的子問題,遞迴去解決所有的問題(猜測+記憶+遞迴)
它消耗的時間為: 子問題的數量 * 每個子問題處理所需要時間
例1:斐波那契數列
使用遞迴的方式求斐波那契數列
fib(n):
if n<=2:f=1;
else: f= fib(n-1)+fib(n-2);
return f;
這種方式的執行時間為 ( ),空間為O(1)
T(n)=T(n-1)+T(n-2)+O(1) 2T(n-2)= ( )
Memoized DP 演算法求斐波那契數列
fib(n):
memo={}
if n in memo:return memo[n];
if n<=2:f=1;
else f=fib(n-1)+fib(n-2);
memo[n]=f;
return f;
這種方式的執行時間為 ,空間為O(n)
memo的存在使得實際產生呼叫的只有 fib(1) …. fib(n),共n次,區域的直接從memo中獲取,使用常量的時間
Bottom-up DP演算法求斐波那契數列
fib(n):
fib={}
for i in range(1,n+1):
if i<=2: f=1
else: f=fib[i-1]+fib[i-2]
fib[i]=f
return fib[n]
這種方式的執行時間為 ,空間可以只用O(1)
它可以看做是一種拓撲排序(針對DAG),對於使用空間其實只需要記住前兩個即可
例2:最短路徑
要求s到t的最短路徑,那麼必定會經過與t相鄰的一條邊,如圖示的u,那麼最短路徑 =
就是需要遞迴呼叫處理的部分
對於DAG: 每個子問題的處理時間為 indegree(t)+O(1)
indegree(t):入度數也就是類似(u,t)邊的數量,需要去遍歷所有t的入邊
O(1):判斷是不是有入邊
總共的執行時間為
當圖中有環的時候求最短路徑產生的問題
要求s到v的最短路徑
,首選需要去求
,然後是
,到b節點有兩條路徑:
和
,此時去memo中查
是不存在的,又會這回查詢,導致了一個死迴圈
解決圖中有環的時候求最短路徑的問題
方式是去環,將原來的圖一層一層的展開。
假設從s到v需要的路徑為k步,那麼可以得到
=
,當k遞減到0的時候,其實也就是從s到s本身
所需要的展開層數為:|V|-1
對於求最短路徑來講,最長不能超過|V|-1,否則就是成環,會造成迴圈的情況(從0開始的計數),這就是為什麼Bellman-Ford的外層迴圈是 |V|-1
每層的節點數為所有的節點。那麼總共的節點數為|V’|=|V|(|V|-1)+1=O(
),邊數是|E’|=|E|(|V|-2)+1=O(VE)。轉換後的圖是DAG圖,那麼實際上的時間為O(V’+E’)=O(VE)。這也就是從動態規劃的角度去看Bellman-Ford演算法
節點的數目是1個源點,邊的數目是每多一層實際上就多了加了一遍所有的邊。
斐波那契數列與最短路徑使用動態規劃處理步驟的對比
例子 | 斐波那契數列 | 最短路徑 |
---|---|---|
1:定義子問題 | 其中 | 其中 |
子問題數量 | n | |
2:猜測 | 什麼都沒做,完全是定義 | 節點v的入邊(如果存在的話) |
選擇的數量 | 1 | v的入邊數+1 |
3:關聯所有的子問題 |