動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。
動態規劃算法一般基於一個遞推公式及一個或多個初始狀態。 當前子問題的解將由上一次子問題的解推出。算法
要解決一個給定的問題,咱們須要解決其不一樣部分(即解決子問題),再合併子問題的解以得出原問題的解。
一般許多子問題很是類似,爲此動態規劃法試圖只解決每一個子問題一次,從而減小計算量。
一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次須要同一個子問題解之時直接查表。
這種作法在重複子問題的數目關於輸入的規模呈指數增加時特別有用。
動態規劃有三個核心元素:
1.最優子結構
2.邊界
3.狀態轉移方程優化
咱們來看一到題目spa
有一座高度是10級臺階的樓梯,從下往上走,每跨一步只能向上1級或者2級臺階。求出一共有多少種走法。
好比,每次走1級臺階,一共走10步,這是其中一種走法。
再好比,每次走2級臺階,一共走5步,這是另外一種走法。code
可是這樣一個個算太麻煩了,咱們能夠只去思考最後一步怎麼走,以下圖blog
這樣走到第十個樓梯的走法 = 走到第八個樓梯 + 走到第九個樓梯
咱們用f(n)來表示 走到第n個樓梯的走法,因此就有了f(10) = f(9) + f(8)
而後f(9) = f(8) + f(7), f(8) = f(7) + f(6)......遞歸
這樣咱們就得出來一個遞歸式:
f(n) = f(n-1) + f(n-2);
還有兩個初始狀態:
f(1) = 1;
f(2) = 2; 圖片
這樣就得出了第一種解法ci
function getWays(n) { if (n < 1) return 0; if (n == 1) return 1; if (n == 2) return 2; return getWays(n-1) + getWays(n-2); }
這種方法的時間複雜度爲O(2^n)get
能夠看到這是一顆二叉樹,數的節點個數就是咱們遞歸方程須要計算的次數,
數的高度爲N,節點個數近似於2^n
因此時間複雜度近似於O(2^n)數學
可是這種方法能不能優化呢?
咱們會發現有些值被重複計算,以下圖
相同顏色表明着重複的部分,那麼咱們可不能夠把這些重複計算的值記錄下來呢?
這樣的優化就有了第二種方法
const map = new Map(); function getWays(n) { if (n < 1) return 0; if (n == 1) return 1; if (n == 2) return 2; if (map.has(n)) { return map.get(n); } const value = getWays(n-1) + getWays(n-2); map.set(n, value); return value; }
由於map裏最終會存放n-2個鍵值對,因此空間複雜度爲O(n),時間複雜度也爲O(n)
繼續想想這就是最優的解決方案了嗎?
咱們回到一開始的思路,咱們是假定前面的樓梯已經走完,只考慮最後一步,因此才得出來f(n) = f(n-1) + f(n-2)的遞歸式,這是一個置頂向下求解的式子
通常來講,按照正常的思路應該是一步一步往上走,應該是自底向上去求解才比較符合正常人的思惟,咱們來看看行不行的通
這是一開始走的一個和兩個樓梯的走法數,即以前說的初始狀態
這是進行了一次迭代得出了3個樓梯的走法,f(3)只依賴於f(1) 和 f(2)
繼續看下一步
這裏又進行了一次迭代得出了4個樓梯的走法,f(4)只依賴於f(2) 和 f(3)
咱們發現每次迭代只須要前兩次迭代的數據,不用像備忘錄同樣去保存全部子狀態的數據
function getWays(n) { if (n < 1) return 0; if (n == 1) return 1; if (n == 2) return 2; // a保存倒數第二個子狀態數據,b保存倒數第一個子狀態數據, temp 保存當前狀態的數據 let a = 1, b = 2; let temp = a + b; for (let i = 3; i <= n; i++) { temp = a + b; a = b; b = temp; } return temp; }
這是咱們能夠再看看當前的時間複雜度和空間複雜度
當前時間複雜度仍爲O(n),但空間複雜度降爲O(1)
這就是理想的結果
這只是動態規劃裏最簡單的題目之一,由於它只有一個變化維度當變化維度變成兩個、三個甚至更多時,會更加複雜,揹包問題就是比較典型的多維度問題,有興趣的能夠去網上看看《揹包九講》