1. 程式人生 > >《演算法筆記》12. 用暴力遞迴解法推匯出動態規劃

《演算法筆記》12. 用暴力遞迴解法推匯出動態規劃

[TOC] # 1 暴力遞迴到動態規劃 > 本篇是演算法基礎筆記的最後一篇,前面所記錄的和該篇共同覆蓋了平時刷題常用的資料結構和演算法,之後就是通過刷題量來鞏固所學的內容。接下來我準備針對基礎資料結構和演算法做一些刷題記錄,主要是分類別刷leetcode上的題,傳送門為:https://github.com/Dairongpeng/leetcode 一起刷題吧^_^。 > 轉載註明出處,原始碼地址: https://github.com/Dairongpeng/algorithm-note ,歡迎star ==動態規劃最核心的是暴力遞迴的嘗試過程,一旦通過嘗試寫出來暴力遞迴的解法,那麼動態規劃在此基礎上就很好改了== > 1、暴力遞迴之所以暴力,是因為存在大量的重複計算。加入我們定義我們的快取結構,用來查該狀態有沒有計算過,那麼會加速我們的遞迴 > 2、在我們加入快取結構之後,消除了大量的重複計算,快取表就是我們的dp表。那麼這種去除重複計算的遞迴,就是最粗糙的動態規劃,也叫記憶化搜尋 > 3、如果我們把我們的dp表,從簡單到複雜列出來,那麼就是經典的動態規劃。我們無需考慮轉移方程怎麼寫,而是根據我們的遞迴來推導。看下面例子: ## 1.1 例一 : 機器人運動問題(2018阿里面試題目) > 認識暴力遞迴改動態規劃的過程 假設有排成一行的N個位置,記為1~N,N一定大於等於2。開始時機器人在其中的M位置上(M一定是1~N中的一個)。 如果機器人來到1位置,那麼下一步只能往右來到2位置; 如果機器人來到N位置,那麼下一步只能往左來到N-1的位置; 如果機器人來到中間位置,那麼下一步可以往左走或者往右走; 規定機器人必須走K步,最終能來到P位置(P也是1~N中的一個)的方法有多少種? 給定四個引數N,M,K,P。返回方法數 > 暴力遞迴ways1呼叫的walk函式,就是暴力遞迴過程,存在重複計算。waysCache方法對應的walkCache就是在純暴力遞迴的基礎上加了快取 ==假設我們的引數N=7,M=2,K=5,P=3。我們根據我們的遞迴加快取的過程來填我們的dp表:== 1、int[][] dp = new int[N+1][K+1];是我們的DP表的範圍 2、當rest = 0的時候,dp[cur][rest] = cur == P ? 1 : 0; 我們只有cur=P的時候為1,其他位置都為0。 3、當cur=1的時候,dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp); 我們的dp當前的值,依賴於cur的下一個位置,rest的上一個位置 4、當cur = N的時候,dp[cur][rest] =walkCache(N, N - 1, rest - 1, P,dp); 我們dp當前位置依賴於N-1位置,和rest - 1位置 5、當cur在任意中間位置時。dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P,dp) + walkCache(N, cur - 1, rest - 1, P, dp); dp的當前位置依賴於dp的cur+1和rest-1位置加上dp的cur-1和rest-1的位置 那麼我們可以得到我們的DP表為: ```text 0 1 2 3 4 5 K 座標 0 x x x x x x 1 0 0 1 0 3 0 2 0 1 0 3 0 9 3 1 0 2 0 6 0 4 0 1 0 3 0 10 5 0 0 1 0 5 0 6 0 0 0 1 0 5 7 0 0 0 0 1 0 cur 坐 標 ``` ==所以任何的動態規劃,都可以由暴力遞迴改出來。也就是所任意的動態規劃都來自於某個暴力遞迴。反之任何暴力遞迴不一定能改成動態規劃,jia'ru某暴力遞歸併沒有重複計算,沒有快取的必要== ==動態規劃實質就是把引數組合完成結構化的快取== ```Java package class12; public class Code01_RobotWalk { public static int ways1(int N, int M, int K, int P) { // 引數無效直接返回0 if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { return 0; } // 總共N個位置,從M點出發,還剩K步可以走,返回最終能達到P的方法數 return walk(N, M, K, P); } // N : 位置為1 ~ N,固定引數 // cur : 當前在cur位置,可變引數 // rest : 還剩res步沒有走,可變引數 // P : 最終目標位置是P,固定引數 // 該函式的含義:只能在1~N這些位置上移動,當前在cur位置,走完rest步之後,停在P位置的方法數作為返回值返回 public static int walk(int N, int cur, int rest, int P) { // 如果沒有剩餘步數了,當前的cur位置就是最後的位置 // 如果最後的位置停在P上,那麼之前做的移動是有效的 // 如果最後的位置沒在P上,那麼之前做的移動是無效的 if (rest == 0) { return cur == P ? 1 : 0; } // 如果還有rest步要走,而當前的cur位置在1位置上,那麼當前這步只能從1走向2 // 後續的過程就是,來到2位置上,還剩rest-1步要走 if (cur == 1) { return walk(N, 2, rest - 1, P); } // 如果還有rest步要走,而當前的cur位置在N位置上,那麼當前這步只能從N走向N-1 // 後續的過程就是,來到N-1位置上,還剩rest-1步要走 if (cur == N) { return walk(N, N - 1, rest - 1, P); } // 如果還有rest步要走,而當前的cur位置在中間位置上,那麼當前這步可以走向左,也可以走向右 // 走向左之後,後續的過程就是,來到cur-1位置上,還剩rest-1步要走 // 走向右之後,後續的過程就是,來到cur+1位置上,還剩rest-1步要走 // 走向左、走向右是截然不同的方法,所以總方法數要都算上 return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P); } public static int waysCache(int N, int M, int K, int P) { // 引數無效直接返回0 if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) { return 0; } // 我們準備一張快取的dp表 // 由於我們的cur範圍是1~N,我們準備N+1。 // rest範圍在1~K。我們準備K+1。 // 目的是把我們的可能結果都能裝得下 int[][] dp = new int[N+1][K+1]; // 設定這張表的初始值都為-1,代表都還沒用過 for(int row = 0; row <= N; row++) { for(int col = 0; col <= K; col++) { dp[row][col] = -1; } } return walkCache(N, M, K, P,dp); } //