動態規劃怎麼用?

動態規劃應該用於最優化問題算法

最優化問題指的是,解決一個問題可能有多種可行的值來解決問題,可是咱們須要一個最優的(最大或者最小)值數組

動態規劃適用於子問題不是獨立的狀況,即各個子問題之間包含公共的子問題。動態規劃對每一個子問題只計算一次,保存其計算結果到"一張表",重複利用,從而優化執行。bash

分治法則是把一個大的問題劃分紅一些獨立的子問題,遞歸求解子問題的狀況;貪心算法則是會先選擇當時看起來是最優的選擇,而後再求解一個結果的子問題post

如何使用動態規劃

以斐波那契數列爲例。通常的求解方式爲遞歸,運行時間爲 \theta(2^{n/2}),空間爲O(1)優化

fib(n):
    if n<=2:f=1;
    else: f= fib(n-1)+fib(n-2);
    return f;
複製代碼

能夠簡要分析下這個執行過程:要去求解 fib(n),首先要知道fib(n-1)和fib(n-2),要計算fib(n-1)則須要知道fib(n-2)和fib(n-3),要計算fib(n-2)則須要知道fib(n-3)和fib(n-4),依此類推。
很明顯能夠看到:若是計算出了fib(n-1)的子問題和fib(n-2)的子問題存在依賴性,要計算fib(n-1)必然要計算fib(n-2);同時若是複用了fib(n-2)那麼當計算fib(n-1)就很是簡單,直接相加便可。這也就是複用子問題處理結果ui

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;
複製代碼

分析可知:memo的存在使得實際產生調用的只有 fib(1) .... fib(n),共n次,其他的直接從memo中獲取,使用常量的時間。可得運行時間爲\theta(n),空間爲O(n)。這種方式仍然能夠優化到使用常量的空間,由於實際上只須要記住最初的兩個值便可。spa

int fib(int n){
        if(n<=2){
            return 1;
        }
        int f1=1,f2=1;
        for(int i=3;i<=n;i++){
            int f=f1+f2;
            f1=f2;
            f2=f;
        }
        return f2;
    }
複製代碼

它的運行時間爲\theta(n),空間O(1);
這種計算方式,它其實是至關於進行了拓撲排序,即我只有先執行完fib(n-2),再執行fib(n-1),而後才執行fib(n) code

斐波那契拓撲排序
分析下來能夠發現:

  1. 斐波那契數列求值是能夠分解成多個子問題 fib(k),且一共有n個
  2. 子問題之間存在依賴關係:F_k=F_{k-1}+F_{k-2} ,每一個子問題的處理時間是O(1)
  3. 每一個子問題的處理是按照拓撲排序的順序進行,它的順序爲 k=1,...,n

最終去解決了原來的問題F_n,總耗時爲 子問題個數 * 每一個子問題的處理時間=O(n)cdn

拓撲排序

斐波那契數列中的拓撲排序,它本質上相似拓撲排序的DAG最短路徑blog

要獲取它的最短路徑,過程以下

d_{SS}=0,d_{SA}=1,d_{SB}=2, d_{SC}=3
d_{SD}=min\left(\begin{array}{ccc}
{ d_{SA}+w_{AD} }\\\\
{ d_{SB}+w_{BD} } \\\\
\end{array} \right)=4
d_{SE}=min\left( \begin{array}{ccc}
{ d_{SA}+w_{AE} }  \\\\
{ d_{SB}+w_{BE} }  \\\\
{ d_{SC}+w_{CE} }  \\\\
\end{array} \right) =3
d_{SF}=min\left( \begin{array}{ccc}
{ d_{SB}+w_{BF} }  \\\\
{ d_{SC}+w_{CF} }  \\\\
\end{array} \right) =5
d_{SG}=min\left( \begin{array}{ccc}
{ d_{SD}+w_{DG} }  \\\\
{ d_{SE}+w_{EG} }  \\\\
{ d_{SF}+w_{FG} }  \\\\
{ d_{SC}+w_{CG} }  \\\\
\end{array} \right) =5

每條邊都訪問了一遍,而後初始化了每一個頂點的值,它的運行時間爲O(V+E)

計算過程能夠看到:

  1. 最短路徑會被劃分紅多個子問題,好比d_{SG}d_{SD}d_{SE}d_{SF}d_{SC}
  2. 子問題之間是存在依賴關係,好比d_{SG}依賴於d_{SD}d_{SE}d_{SF}d_{SC}
  3. 整個處理的過程按照 SABCDEFG的拓撲順序進行處理

通常的圖拓撲排序爲

要求s到t的最短路徑,那麼一定會通過與t相鄰的一條邊,如圖示的u,那麼最短路徑 \delta(s,t)= min_{(u,t)\in E}(\delta(s,u)+w(u,t))

\delta(s,u)就是須要遞歸調用處理的部分

對於DAG:\delta(s,t)每一個子問題的處理時間爲 indegree(t)+O(1)

indegree(t):入度數也就是相似(u,t)邊的數量,須要去遍歷全部t的入邊

O(1):判斷是否是有入邊

總共的執行時間爲

\sum_{v\in V}(indegree(v)+O(1))=O(E+V)

當圖中有環的時候求最短路徑產生的問題

要求s到v的最短路徑 \delta(s,v),首選須要去求 \delta(s,a),而後是\delta(s,b),到b節點有兩條路徑:\delta(s,s)\delta(s,v),此時去memo中查\delta(s,v)是不存在的,又會這回查詢,致使了一個死循環

解決圖中有環的時候求最短路徑的問題

方式是去環,將原來的圖一層一層的展開。
假設從s到v須要的路徑爲k步,那麼能夠獲得 \delta_k(s,v)=min_{(b,v)\in E}(\delta_{k-1}(s,b)+w(b,v)),當k遞減到0的時候,其實也就是從s到s自己

所須要的展開層數爲:|V|-1 對於求最短路徑來說,最長不能超過|V|-1,不然就是成環,會形成循環的狀況(從0開始的計數),這就是爲何Bellman-Ford的外層循環是 |V|-1

每層的節點數爲全部的節點。那麼總共的節點數爲|V'|=|V|(|V|-1)+1=O(V^2),邊數是|E'|=|E|(|V|-2)+1=O(VE)。轉換後的圖是DAG圖,那麼實際上的時間爲O(V'+E')=O(VE)。這也就是從動態規劃的角度去看Bellman-Ford算法 節點的數目是1個源點,邊的數目是每多一層實際上就多了加了一遍全部的邊。

從斐波那契和最短路徑的例子看出,要使用最短路徑,須要確保子問題之間是互相依賴的,這樣可以重複利用子問題產生的結果,而要去重複利用子問題,那首要條件是找到子問題是什麼?而後在多個子問題之間選擇最優的結果,並按照拓撲排序的順序進行計算

使用動態規劃的通常步驟是什麼?

  1. 定義子問題 :通常來說子能夠從輸入條件來尋找,若是輸入條件少了一項,我解決這個問題的方式會發生改變嗎?若是不會,那麼它基本就是子問題

計算子問題的數量

  1. 思考:明確要去嘗試全部可能方式,選取最好的一個。選取中要思考
  • 獲取到的輸入項是否應該被選入子問題的結果之中?
  • 有什麼途徑可以使子問題擴展到原有的問題?
  • 子問題要計算原有的問題,增長了什麼變化的因素?

計算選擇的數量

  1. 關聯全部的子問題:根據思考獲得父子問題的關聯關係

計算單個子問題所須要處理的時間

  1. 重用子問題結果並記下新的結果

計算總耗時

最終解決原有的問題,它消耗的時間爲: 子問題的數量 * 每一個子問題處理所須要時間

總的來講就是:嘗試全部可能的子問題的結果,將最好的可能子結果存儲下來,而後重複利用已經解決的子問題,遞歸去解決全部的問題(思考+記憶+遞歸)

必定要用動態規劃嗎?

給定一個整數數組 nums ,找到一個具備最大和的連續子數組(子數組最少包含一個元素),返回其最大和。好比

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,爲 6。
複製代碼

乍看之下,要求連續最大和,首先得計算出子串的最大和,才能去計算原始數組的最大和,也就是說

  1. 子問題是:子數組的最大和
  2. 依賴關係:dp(i)=max(i,i+dp(i-1)),增長了一個新的元素擴展子問題,獲得原問題,而後從擴展的結果和原有的結果中取獲取一個最優解
  3. dp(i-1)是能夠重用的
public int maxSubArray(int[] nums) {
       int max=Integer.MIN_VALUE;
       Map<Integer,Integer> mem=new HashMap<>();
       for(int i=0;i<nums.length;i++){
       //按照必定順序執行
           int v=dp(i,nums,mem);
           if(v>max){
               max=v;
           }
       }
        return max;
    }
    
    public int dp(int i,int[] nums,Map<Integer,Integer> mem){
        Integer v=mem.get(i);
        if(v!=null){
        //重用子問題的結果
            return v;
        }
        int sum = nums[i];
        if(i==nums.length-1){
            mem.put(i,sum);
            return sum;
        }
        //先計算子問題
       int cSum = dp(i+1,nums,mem);
       int tempV = sum+cSum;
       //明確父子問題之間的關係,存儲新的子問題結果
       if(tempV<sum){
           mem.put(i,sum);
           return sum;
       }
        mem.put(i,tempV);
       return tempV;
    }
複製代碼

分析能夠看到,它的執行爲須要遍歷一遍整個的數組,而後要去計算的子問題包括 n-1,n-2,..,1,耗時爲 O(n+n-1)=O(2n)=O(n),同時須要一個O(n)的空間存儲。

public int maxSubArray(int[] nums) {
       int max=nums[0]; 
        int currMax=nums[0];
        for(int i=1;i<nums.length;i++){
            int temp=nums[i]+currMax;
            if(temp>nums[i]){
                currMax=temp;
            }else{
                currMax=nums[i];
            }
            if(max<currMax){
                max=currMax;
            }
       }
        return max;
    }
複製代碼

經過這種方式,使用的空間爲O(1),時間就是O(n)。 因而可知動態規劃自己只是一種解決問題的思想,並非說動態規劃獲得的最優解就是解決問題的最佳方案

相關文章
相關標籤/搜索