重疊子問題
的狀況的最優解
時有效。子問題
。結果都逐漸被計算並被保存
,從簡單的問題
直到整個問題
都被解決。保存遞歸時
的結果,於是不會在解決一樣的問題時花費時間 · · · · · ·動態規劃只能應用於有最優 子結構
的問題。最優子結構的意思是局部最優解能決定全局最優解
(對有些問題這個要求並不能徹底知足,故有時須要引入必定的近似)。算法
簡單地說,問題可以分解成子問題來解決
。數組
通俗一點來說,動態規劃和其它遍歷算法(如深/廣度優先搜索)都是將原問題拆成多個子問題而後求解
,他們之間最本質的區別是,動態規劃保存子問題的解,避免重複計算
。markdown
解決動態規劃問題的關鍵是找到狀態轉移方程
,這樣咱們能夠通計算和儲存子問題的解來求解最終問題
。spa
同時,咱們也能夠對動態規劃進行空間壓縮
,起到節省空間消耗的效果。code
在一些狀況下,動態規劃能夠當作是帶有狀態記錄(memoization)的優先搜索
。orm
動態規劃是自下而上的
,即先解決子問題,再解決父問題;遞歸
而用帶有狀態記錄的優先搜索
是自上而下
的,即從父問題搜索到子問題,若重複搜索到同一個子問題則進行狀態記錄,防止重複計算。字符串
若是題目需求的是最終狀態,那麼使用動態搜索比較方便;it
若是題目須要輸出全部的路徑,那麼使用帶有狀態記錄的優先搜索會比較方便。io
對問題求解的時候,老是作出在當前看來是最好的作法
適用貪心算法的場景:問題可以分解成子問題來解決,子問題的最優解能遞推到最終問題的最優解
。這種子問題最優解成爲最優子結構
回溯法(backtracking)是優先搜索的一種特殊狀況,又稱爲試探法
,經常使用於須要記錄節點狀態
的深度優先搜索。一般來講,排列、組合、選擇類問題使用回溯法比較方便。 顧名思義,回溯法的核心是回溯
。在搜索到某一節點的時候,若是咱們發現目前的節點(及其子節點)並非需求目標時
,咱們回退到原來的節點
繼續搜索,而且把在目前節點修改的狀態 還原
。
這樣的好處是咱們能夠始終只對圖的總狀態進行修改,而非每次遍歷時新建一個圖來儲存 狀態。
在具體的寫法上,它與普通的深度優先搜索同樣,都有 [修改當前節點狀態]→[遞歸子節 點]
的步驟,只是多了回溯的步驟,變成了 [修改當前節點狀態]→[遞歸子節點]→[回改當前節點 狀態]
。
回溯法。有兩個小訣竅,一是按引用傳狀態
,二是全部的狀態修 改在遞歸完成後回改
。 回溯法修改通常有兩種狀況,一種是修改最後一位輸出,好比排列組合;一種是修改訪問標 記,好比矩陣裏搜字符串。
重複計算
來得到最優解假設你正在爬樓梯。須要 n 階你才能到達樓頂。
每次你能夠爬 1 或 2 個臺階。你有多少種不一樣的方法能夠爬到樓頂呢?
注意:給定 n 是一個正整數。
這是十分經典的斐波那契數列題。定義一個數組 dp,dp[i] 表示走到第 i 階的方法數,走到第 i 階的 方法數即爲走到第 i-1 階的方法數加上走到第 i-2 階的方法數。這樣咱們就獲得了狀態轉移方程 dp[i] = dp[i-1] + dp[i-2]。注意邊界條件的處理。
const climbStairs = function(n) {
if(n <= 2) return n;
const dp = [1, 2];
for(let i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2]
}
return dp[n-1];
}
// O(n) 空間複雜度
複製代碼
動態規劃進行空間壓縮 dp[i] 只與 dp[i-1] 和 dp[i-2] 有關,所以能夠只用兩個變量來存儲 dp[i-1] 和 dp[i-2]
const climbStairs = function(n){
if(n <= 2) return n;
let pre1 = 1, pre2 = 2, cur;
for(let i = 2; i < n; i++) {
cur = pre1 + pre2;
pre1 = pre2;
pre2 = cur;
}
return cur;
}
複製代碼
遞歸寫法:(能夠無視,空間複雜度最高)
const climbStairs = function(n){
if(n <= 2) return n;
return climbStairs(n - 1) + climbStairs(n-2);
}
複製代碼
超時