[算法入门笔记] 18. 动态规划
動態規劃往往是有套路的,但套路是建立在熟練的基礎上的~
文章目錄
- 0 建議
- 1 機器人達到指定位置的方法數
- 1.1 暴力遞歸
- 1.2 記憶化搜索
- 1.3 動態規劃
- 2 換錢的最少貨幣數
- 2.1 暴力遞歸
- 2.2 記憶化搜索
- 2.3 動態規劃
- 3 紙牌博弈問題
- 3.1 暴力遞歸
- 3.2 動態規劃
- 4 高維動態規劃
- 4.1 中國象棋馬的跳法
- 2.1.1 暴力遞歸
- 2.1.2 動態規劃
- 2.2 生存問題
- 2.2.1 暴力遞歸
- 4.2.2 動態規劃
- 5 空間壓縮技巧
- 壓縮技巧1
- 壓縮技巧2
- 壓縮技巧3
- 壓縮技巧4
- 壓縮技巧5
- 矩陣的最小路徑和
- 暴力遞歸
- 動態規劃
- 動態規劃+空間壓縮技巧
0 建議
動態規劃流程
緩存結構如何優化成表結構
1 機器人達到指定位置的方法數
[問題]
假設有排成一行的N個位置,記為 [ 1 , N ] ( N ≥ 2 ) [1,N](N\ge2) [1,N](N≥2)。開始時機器人在其中的M位置上(M一定是 [ 1 , N ] [1,N] [1,N]中的一個),機器人可以往左走或者往右走。
如果機器人來到1位置,那么下一步只能往右來到2位置;
如果機器人來到N位置,那么下一步只能往左來到N-1位置。
規定機器人必須走K步,最終能來到Р位置(P也一定是 [ 1 , N ] [1,N] [1,N]中的一個)的方法有多少種。給定四個參數N、M、K、P,返回方法數。
[示例]
上面的參數代表所有位置為1 2 3 4 5。機器人最開始在2位置上,必須經過3步,最后到達3位置。走的方法只有如下3種:
- 從2到1,從1到2,從2到3
- 從2到3,從3到2,從2到3
- 從2到3,從3到4,從4到3
所以返回方法數3。
N=3。M=1,K=3,P=3上面的參數代表所有位置為1 2 3。機器人最開始在1位置上,必須經過3步,最后到達3位置。怎么走也不可能,所以返回方法數0。
題目特點分析
本題是經典的從左向右嘗試模型,考慮base case,然后有兩種狀態,向左走和向右走
1.1 暴力遞歸
遞歸函數參數有cur,表示當前處在的位置和rest,表示剩余步數,返回多少走法
如果走完所有步數,最終位置停在P位置,說明答案有效,返回一種答案;如果最終位置不在P位置,說明答案無效,返回0種答案
dfs表示如果當前來到cur位置,還剩下rest步要走,下一步的走法
- 如果 c u r = = 1 cur==1 cur==1,下一步只能走2位置,后續剩下rest-1步數
- 如果 c u r = = N cur==N cur==N,下一步只能走N-1位置,后續剩下rest-1步數
- 如果 c u r ∈ ( 1 , N ) cur\in(1,N) cur∈(1,N),下一步可以走cur-1位置或者cur+1位置,后續剩下rest-1步數
1.2 記憶化搜索
由于在暴力遞歸過程存在大量重復計算,復雜度是指數級別,因此設計緩存結構存儲計算過的結果。
public int dfs(int N, int P, int cur, int rest, int[][] cache) {// 查看緩存中是否具有答案if (cache[rest][cur] != -1) {return cache[rest][cur];}// 遞歸終止條件修改成緩存結構if (rest == 0) {// 如果剩余步數為0,并且來到P位置,答案有效cache[rest][cur] = cur == P ? 1 : 0;return cache[rest][cur];}// 單次遍歷邏輯if (cur == 1) { // 來到1位置只能向右走cache[rest][cur] = dfs(N, P, cur + 1, rest - 1);return cache[rest][cur];}if (cur == N) { // 來到N位置只能向左走cache[rest][cur] = dfs(N, P, cur - 1, rest - 1);return cache[rest][cur];}// 來到一般位置可以向左或者向右走cache[rest][cur] = dfs(N, P, cur - 1, rest -1) + dfs(N, P, cur + 1, rest -1);return cache[rest][cur]; }/*** 主函數調用dfs* @param N N個位置* @param M 當前在M位置* @param K 只能走K步* @param P 目標位置P* @return 返回從M位置出發,只走K步,最終到達P位置的方法數*/ public int ways(int N, int M, int K, int P) {// 非法條件if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}// 確定可變參數范圍 rest范圍[0,K],cur范圍[1,N]int[][] cache= new int[K + 1][N + 1];// 初始化緩存結構for (int i = 0; i <= K; ++i) {for (int j = 0; j <= N; j++) {cache[i][j] = -1;}}// 調用dfs函數return dfs(N, P, M, K, cache); }1.3 動態規劃
使用該方法優化成dp前提是所求問題具有無后效性,即一個遞歸狀態的返回值與怎么到達這個狀態的路徑無關
分析是否具有后效性
- dfs兩個固定參數N、P,任何時候都不變,說明N和P與具體遞歸狀態無關,關注其他兩個可變參數rest和cur
- dfs(5,5)出現了兩次,不管從dfs(4,6)來到dfs(5,5),還是從dfs(6,6)來到dfs(5,5),只要是當前來到5位置,還剩5步,返回值都是不變的,所以是一個無后效性問題
優化步驟
- 可變參數為cur和rest
- rest作為行,cur作為列,映射成二維表,返回值為dp[rest][cur]
- 對于N=7,P=5,M=4,K=9,最終答案在dp[9][4]
- 填寫base case位置
分析普遍位置如何依賴其他位置
if (cur == 1) {dfs(N, P, 2, rest - 1); } if (cur == N) {dfs(N, P, N - 1, rest - 1); } return dfs(N, P, cur - 1, rest - 1) + dfs(N, P, cur + 1, rest - 1);- 如果cur在1位置,最終返回值 d p [ r e s t ] [ c u r ] = d p [ r e s t ? 1 ] [ 2 ] dp[rest][cur]=dp[rest-1][2] dp[rest][cur]=dp[rest?1][2]A點依賴B
- 如果cur在N位置,最終返回值 d p [ r e s t ] [ c u r ] = d p [ r e s t ? 1 ] [ N ? 1 ] dp[rest][cur]=dp[rest-1][N-1] dp[rest][cur]=dp[rest?1][N?1] C點依賴D
- 如果cur在中間位置,最終返回值 d p [ r e s t ] [ c u r ] = d p [ r e s t ? 1 ] [ c u r ? 1 ] + d p [ r e s t ? 1 ] [ c u r + 1 ] dp[rest][cur]=dp[rest-1][cur-1]+dp[rest-1][cur+1] dp[rest][cur]=dp[rest?1][cur?1]+dp[rest?1][cur+1] E點依賴F、G
確定計算順序,求出最終答案
說明每一行的值依賴上一行的值
本題動態規劃解法就是把 N × K N×K N×K規模的表填好,填寫每個位置的復雜度是 O ( 1 ) O(1) O(1),整個時間復雜度是 O ( N × K ) O(N×K) O(N×K)
/*** 動態規劃版本* @param N N個位置* @param M 當前位置* @param K 只能走K步* @param P 目標位置* @return*/ public int ways(int N, int P, int M, int K) {// 非法條件if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}// 定義dp數組int[][] dp = new int[K + 1][N + 1];// 填寫簡單位置的答案dp[0][P] = 1;// 填寫普遍位置for (int rest = 1; rest <= K; rest++) {for (int cur = 1; cur <= N; cur++) {// 如果來到位置1,下一步只能走2位置if (cur == 1) {dp[rest][cur] = dp[rest - 1][2];} else if (cur == N) { // 如果來到位置N,下一步只能走N-1位置dp[rest][cur] = dp[rest - 1][N - 1];} else {dp[rest][cur] = dp[rest - 1][cur - 1] + dp[rest - 1][cur + 1];}}} // 返回當剩余步數K,初始位置M的答案return dp[K][M]; }2 換錢的最少貨幣數
[問題]
給定數組arr,arr 中所有的值都為正數且不重復。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數aim,代表要找的錢數,求組成aim的最少貨幣數。
[示例]
4張5元可以組成20元,其他的找錢方案都要使用更多張的貨幣,所以返回4。
arr=[5,2,3],aim=0不用任何貨幣就可以組成0元,返回0。
arr=[3,5],aim=2根本無法組成2元,錢不能找開的情況下默認返回-1。
2.1 暴力遞歸
遞歸函數參數有i,表示當前處在的位置和rest,表示剩余面值,返回組合數
如果走完所有步數i==arr.length,如果剩余面值為0,表示不需要貨幣了,返回0;如果剩余面值不為0,表示無法組成目標面值,返回-1
dfs表示如果當前來到i位置,還剩下rest面值,下一步的組合方式
- cur位置前表示已經做出的選擇,i位置之后表示后續將要做出的選擇
- 從cur位置出發選擇任意 [ 0 , K ] [0,K] [0,K]當前貨幣組成面值
- cur位置狀態具有選和不選兩種狀態
2.2 記憶化搜索
致命錯誤:數組無效狀態沒有初始化,這里-2表示未填寫狀態,-1表示無效狀態,非basecase后面有個判斷是否無效,該狀態沒有初始化,還有注意初始化順序
public int minCoins(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return -1;}int[][] cache = new int[arr.length + 1][aim + 1];for (int i = 0; i <= arr.length; i++) {for (int j = 0; j <= aim; j++) {cache[i][j] = -2;}}return dfs(arr, 0, aim, dp); }public int dfs(int[] arr, int i, int rest, int[][] cache) {for (int row = 0; row < dp.length; row++) {for (int col = 0; col < cache[0].length; col++) {cache[row][col] = -1;}}if (cur == arr.length) {cache[i][rest] = rest == 0 ? 0 : -1;return cache[i][rest];}// 嘗試當前貨幣0...k張情況,但不能超過restfor (int k = 0; k <= rest; k++) {// 使用k張arr[i],剩下的面值是rest-k*arr[i]// next表示決策i位置向后剩下的面值(arr[i+1...N-1])int next = dfs(arr, i+ 1, rest - k * arr[i]);if (next != -1) { // 當i后續決策合法時// 在使用k張當前貨幣并且后續位置合法的情況下返回較少的貨幣數// next+k表示整個決策過程使用的貨幣cache[i][rest] = cache[i][rest] == -1 ? next + k : Math.min(cache[i][rest], next + k);}}return cache[i][rest]; }2.3 動態規劃
優化步驟
- 可變參數為i和rest
- rest作為行,i作為列,映射成二維表,剩余面值數不超過aim,因此 r e s t ∈ [ 0 , a i m ] rest\in[0,aim] rest∈[0,aim]
- 確定最終答案dfs(arr,0,aim), d p [ 0 ] [ a i m ] dp[0][aim] dp[0][aim]
5. 填寫普遍位置依賴
dfs(arr,i,rest)返回值就是 d p [ i ] [ r e s t ] dp[i][rest] dp[i][rest]
表中右上角位置時d p [ i ] [ r e s t ] p[i] [rest] p[i][rest],根據dfs(arr,i,rest),
d p [ i ] [ r e s t ] = min ? { d p [ i + 1 ] [ r e s t ? 0 ? a r r [ i ] ] + 0 dp[i] [rest] = \min\{dp[i+1] [rest - 0*arr[i]] + 0 dp[i][rest]=min{dp[i+1][rest?0?arr[i]]+0,
? d p [ i + 1 ] [ r e s t ? 1 ? a r r [ i ] ] + 1 \quad \quad dp[i+1] [rest - 1*arr[i]] + 1 dp[i+1][rest?1?arr[i]]+1,
? . . . \quad\quad\quad\quad\quad\quad\quad... ...
? d p [ i + 1 ] [ r e s t ? k ? a r r [ i ] ] + k } dp[i+1] [rest - k*arr[i]] + k\} dp[i+1][rest?k?arr[i]]+k}
要想得到 a r r [ i ] [ r e s t ] arr[i] [rest] arr[i][rest],必須得到i+1行的值
求 d p [ i ] [ r e s t ] dp[i] [rest] dp[i][rest] 前, d p [ i ] [ r e s t ? a r r [ i ] ] dp[i] [rest-arr[i]] dp[i][rest?arr[i]]已經計算過
d p [ i ] [ r e s t ? a r r [ i ] ] = min ? { dp[i] [rest-arr[i]] = \min\{ dp[i][rest?arr[i]]=min{
? d p [ i + 1 ] [ r e s t ? 1 ? a r r [ i ] ] + 0 \quad \quad\quad dp[i+1] [rest - 1 * arr[i]] + 0 dp[i+1][rest?1?arr[i]]+0,
? d p [ i + 1 ] [ r e s t ? 2 ? a r r [ i ] ] + 1 , \quad \quad\quad dp[i+1] [rest - 2*arr[i]] + 1, dp[i+1][rest?2?arr[i]]+1,
? . . . \quad \quad\quad\quad \quad\quad\quad \quad\quad... ...
? d p [ i + 1 ] [ r e s t ? k ? a r r [ i ] ] + k ? 1 } dp[i+1] [rest - k*arr[i]] + k-1\} dp[i+1][rest?k?arr[i]]+k?1}
圖解
因此, d p [ i ] [ r e s t ] = m i n ( d p [ i ] [ r e s t ? a r r [ i ] + 1 , d p [ i + 1 ] [ r e s t ] ) dp[i] [rest] = min (dp[i] [rest-arr[i] + 1, dp[i+1] [rest]) dp[i][rest]=min(dp[i][rest?arr[i]+1,dp[i+1][rest]),就是說dp[i] [rest]依賴下面一個位置和左邊一個位置
最后一排值已經確定,剩下的位置只依賴下面和左邊的位置,只要求從左到右求倒數第二排,從左到右求倒數第三排…從做到到右求第一排即可
3 紙牌博弈問題
[問題]
給定一個整型數組arr,代表數值不同的紙牌排成一條線。玩家A和玩家B依次拿走每張紙牌,規定玩家A先拿,玩家B后拿,但是每個玩家每次只能拿走最左或最右的紙牌,玩家A和玩家B都絕頂聰明。請返回最后獲勝者的分數。
[示例]
開始時,玩家A只能拿走1或4。如果開始時玩家A拿走1,則排列變為[2,100,4],接下來玩家B可以拿走2或4,然后繼續輪到玩家A…
如果開始時玩家A拿走4,則排列變為[1,2,100],接下來玩家B可以拿走1或100,然后繼續輪到玩家A…
玩家A作為絕頂聰明的人不會先拿4,因為拿4之后,玩家B將拿走100。所以玩家A會先拿1,讓排列變為[2,100,4],接下來玩家B不管怎么選,100都會被玩家A拿走。玩家A會獲勝,分數為101。所以返回101。
開始時,玩家A不管拿1還是2,玩家B作為絕頂聰明的人,都會把100拿走。玩家B會獲勝,分數為100。所以返回100。
3.1 暴力遞歸
- 定義遞歸函數first(i,j),表示arr[i…j]這個排列上的紙牌被絕頂聰明的人先拿,最終返回的分數
- 定義遞歸函數second(i,j),表示arr[i…j]這個排列上的紙牌被絕頂聰明的人后拿,最終返回的分數
分析先手first(i,j)
- i==j,即只剩一張牌。當然會被先拿紙牌的人拿走,返回arr[i]
- i!=j,當前拿紙牌的人,要么拿arr[i],要么拿arr[j]。
- 如果拿arr[i],那么排列只剩下arr[i+1…j]。對當前玩家,面對arr[i+1…j],他將成為后手,后續獲得分數是second(i+1,j)
- 如果拿arr[j],那么排列只剩下arr[i…j-1]。對當前玩家,面對arr[i…j-1],他將成為后手,后續獲得分數是second(i,j-1)
- 作為絕頂聰明的人,兩種決策都是最優的,返回max{arr[i]+second(i+1,j), arr[j]+second(i,j-1)}
分析后手second(i,j)
- i==j,即只剩下一張牌。作為后手,什么都拿不到,得分0
- i!=j,該玩家對手先拿紙牌。對手要么拿走arr[i],要么拿走arr[j]
- 如果對手拿走arr[i],排列剩下arr[i+1…j],然后輪到該玩家先拿
- 如果對手拿走arr[j],排列剩下arr[i…j-1],然后輪到該玩家先拿
- 對手也是絕頂聰明的人,返回Min{first(i+1,j), first(i,j-1)}
先手函數
public int first(int[] arr, int i, int j) {if (i == j) {return arr[i];}return Math.max(arr[i] + second(arr, i + 1, j),arr[j] + second(arr, i, j - 1)); }后手函數
public int second(int[] arr, int i, int j) {if (i == j) {return 0;}return Math.min(first(arr, i + 1, j),first(arr, i, j - 1)); }主函數調用
public int win(int[] arr) {if (arr == null || arr.length == 0) {return 0;}return Math.max(first(arr, 0, arr.length - 1), second(arr, 0, arr.length - 1)); }3.2 動態規劃
經典的范圍嘗試模型
1.分析first(i,j)可變i,j參數范圍
2.標出計算的終止位置
3.標出basecase
-
i不會超過j,即下三角部分無效
-
first:對角線basecase
if (i == j) {return arr[i]; } -
second 對角線basecase
if (i == j) {return 0; }
3.標出非basecase的普遍位置依賴
-
first
return Math.max(arr[i] + second(arr, i + 1, j),arr[j] + second(arr, i, j - 1) ); -
second
return Math.min(first(arr, i + 1, j),first(arr, i, j - 1) );
5.確定計算次序
public int win(int[] arr) {if (arr == null || arr.length == 0) {return 0;}int[][] first = new int[arr.length][arr.length];int[][] second = new int[arr.length][arr.length];for (int col = 0; col < arr.length; col++) {first[col][col] = arr[col];for (int row = col - 1; row >= 0; row--) {first[row][col] = Math.max(arr[row] + second[row + 1][col], arr[col] + second[row][col - 1]);second[row][col] = Math.min(first[row + 1][col], first[row][col - 1]);}}return Math.max(first[0][arr.length - 1], second[0][arr.length - 1]); }4 高維動態規劃
4.1 中國象棋馬的跳法
[問題]
把棋盤放入第一象限,棋盤的最左下角是 ( 0 , 0 ) (0,0) (0,0)位置。那么整個棋盤就是橫坐標上9條線、縱坐標上10條線的一個區域。給你三個參數,x,y,k,返回如果“馬”從(0,0)位置出發,必須走k步,最后落在(x, y)上的方法數有多少種?
2.1.1 暴力遞歸
public int getWays(int x, int y, int step) {return process(x, y, step); }public int process(int x, int y, int step) {if (x < 0 || x > 8 || y < 0 || y > 9) {return 0;}if (step == 0) {return (x == 0 && y == 0) ? 1 : 0;}return process(x - 1, y - 2, step - 1) +process(x + 1, y - 2, step - 1) +process(x - 2, y - 1, step - 1) +process(x + 2, y - 1, step - 1) +process(x - 2, y + 1, step - 1) +process(x + 2, y + 1, step - 1) +process(x - 1, y + 2, step - 1) +process(x + 1, y + 2, step - 1); }2.1.2 動態規劃
1.分析可變x,y,step參數范圍
2.標出計算終止位置
3.標出basecase
if (step == 0) {return (x == 0 && y == 0) ? 1 : 0; } public int getWays(int x, int y, int step) {if (x < 0 || x > 8 || y < 0 || y > 9 || step < 0) {return 0;}int[][][] dp = new int[9][10][step + 1];// base casedp[0][0][0] = 1;// 從底層往上計算for (int height = 1; height <= step; height++) {for (int row = 0; row < 9; row++) {for (int col = 0; col < 10; col++) {dp[row][col][height] += getValue(dp, row - 1, col - 2, height - 1);dp[row][col][height] += getValue(dp, row + 1, col - 2, height - 1);dp[row][col][height] += getValue(dp, row - 2, col - 1, height - 1);dp[row][col][height] += getValue(dp, row + 2, col - 1, height - 1);dp[row][col][height] += getValue(dp, row - 2, col + 1, height - 1);dp[row][col][height] += getValue(dp, row + 2, col + 1, height - 1);dp[row][col][height] += getValue(dp, row - 1, col + 2, height - 1);dp[row][col][height] += getValue(dp, row + 1, col + 2, height - 1);}}}return dp[x][y][step]; }// 防止出現越界,同時返回數組值 public int getValue(int[][][] dp, int row, int col, int step) {if (row < 0 || row > 8 || col < 0 || col > 9) {return 0;}return dp[row][col][step]; }2.2 生存問題
[問題]
給定五個參數n, m,i, j, k。表示在一個 N × M N×M N×M的區域,Bob處在 ( i , j ) (i,j) (i,j)點,每次Bob等概率的向上、下、左、右四個方向移動一步,Bob必須走K步。如果走完之后,Bob還停留在這個區域上,就算Bob存活,否則就算Bob死亡。請求解Bob的生存概率,返回字符串表示分數的方式。
2.2.1 暴力遞歸
public String bob(int N, int M, int i, int j, int k) {// 總步數4^klong allStep = (long)Math.pow(4,k);long live = process(N, M, i, j, k);long gcd = gcd(allStep, live);return String.valueOf((live / gcd) + "/" + (allStep / gcd)); }/*** N*M區域內,Bob從(row,col)位置出發,走rest步,獲得生存點數* @param N 矩陣長度* @param M 矩陣寬度* @param row 出發位置的橫坐標* @param col 出發位置的縱坐標* @param rest 剩余步數* @return 生存點數*/ public long process(int N, int M, int row, int col, int rest) {//違規條件if(row < 0 || row == N || col < 0 || col == M) {return 0;}if (rest == 0) { //剩余步數0,說明走完return 1;}long live =//往上走process(N, M, row - 1, col, rest - 1) +//往下走process(N, M, row + 1, col, rest - 1) +//往左走process(N, M, row, col - 1, rest - 1) +//往右走process(N, M, row, col + 1, rest - 1);return live; }// 最大公約數 public long gcd(long m, long n) {return n == 0 ? m : gcd(n, m % n); }4.2.2 動態規劃
public String bob(int N, int M, int i, int j, int K) {int[][][] dp = new int[N + 2][M + 2][K + 1];// base casefor (int row = 1; row <= N; row++) {for (int col = 1; col <= M; col++) {dp[row][col][0] = 1;}}// 從底層往高層計算for (int rest = 1; rest <= K; rest++) {for (int row = 1; row <= N; row++) {for (int col = 1; col <= M; col++) {dp[row][col][rest] = dp[row - 1][col][rest - 1];dp[row][col][rest] += dp[row + 1][col][rest - 1];dp[row][col][rest] += dp[row][col - 1][rest - 1];dp[row][col][rest] += dp[row][col + 1][rest - 1];}}}long all = (long) Math.pow(4, K);long live = dp[i + 1][j + 1][K];long gcd = gcd(all, live);return String.valueOf((live / gcd) + "/" + (all / gcd)); }5 空間壓縮技巧
壓縮技巧1
將二維數組壓縮成一維數組
壓縮技巧2
壓縮技巧3
壓縮技巧4
壓縮技巧5
其他技巧
矩陣的最小路徑和
暴力遞歸
1.考慮走到邊界,怎么處理 2.寫對返回值
1. 分析basecase,終止位置—2.分析邊界條件和違規條件,返回值怎么處理—3.普遍位置時如何處理返回值
- 要得到a(i,j)到b路徑和,先求a右邊點到b的路徑和right,以及a下面點到點b的路徑和down,最后a到b路徑和為 min ? { r i g h t , d o w n } + a r r [ i ] [ j ] \min\{right,down\}+arr[i] [j] min{right,down}+arr[i][j]
- a(i,j)到達
- 只能向右移動,其路徑和是a點+右邊到b的路徑和
- a(i,j)到達最后一列,他只能向下移動,其路徑和是a點+下邊到b的路徑和
動態規劃
-
對于第一行所有的位置(0,j)來說,從(0,0)位置到(0,j)位置只能向右走,所以(0,0)到(0,j)位置的路徑和是m[0] [0…j]累加的結果
-
對于m的第一列的所有位置來說,即(i,0)從(0,0)位置走到(i,0)位置只能向下走,所以(0,0)位置到(i,0)位置的路徑和就是m[0…i] [0]累加的結果
-
除了第一行和第一列的位置外,都有左邊位置(i-1,j)和上邊位置(i,j-1)
-
從(0,0)到(i,j)位置的路徑必然經過位置(i-1,j)或位置(i,j-1)
-
所以 d p [ i ] [ j ] = min ? { d p [ i ? 1 ] [ j ] , d p [ i ] [ j ? 1 ] } + m [ i ] [ j ] dp[i] [j] = \min\{dp[i-1] [j],dp[i] [j-1]\}+m[i] [j] dp[i][j]=min{dp[i?1][j],dp[i][j?1]}+m[i][j]
-
含義是比較從(0,0)位置開始,經過(i-1,j)位置最終到到達(i,j)的最小路徑和經過(i,j-1)位置最終到達(i,j)的最小路徑,誰最小
-
除第一行和第一列位置外,每一個位置考慮從左邊到達自己的路徑和更小還是從上邊到達自己的路徑和更小,最右下角位置就是整個問題的答案
動態規劃+空間壓縮技巧
1 3 5 9 8 1 3 4 5 0 6 1 8 8 4 0 \begin{matrix} 1 & 3 & 5 & 9\\ 8 & 1 & 3 & 4\\ 5 & 0 & 6 & 1\\ 8 & 8 & 4 & 0\\ \end{matrix} 1858?3108?5364?9410?
- 生成大小為 min ? { M , N } \min\{M,N\} min{M,N}的一維數組,本測試用例長度為4,初始 a r r = [ 0 , 0 , 0 , 0 ] arr=[0,0,0,0] arr=[0,0,0,0],從 ( 0 , 0 ) (0,0) (0,0)位置出發到達m第一行的每個位置,最小路徑和時從 ( 0 , 0 ) (0,0) (0,0)位置開始依次累加的結果, a r r = [ 1 , 4 , 9 , 18 ] arr=[1,4,9,18] arr=[1,4,9,18],此時arr[j]代表從 ( 0 , 0 ) (0,0) (0,0)位置到 ( 0 , j ) (0,j) (0,j)位置的最小路徑和
-
準備把arr[j]的值更新成 ( i , j ) (i,j) (i,j)位置上的和
-
更新arr[0], a r r [ 0 ] = a r r [ 0 ] + m [ 1 ] [ 0 ] arr[0]=arr[0]+m[1] [0] arr[0]=arr[0]+m[1][0]
- 更新arr[1]
- 從 [ 1 ] [ 0 ] [1] [0] [1][0]位置到達 [ 1 ] [ 1 ] [1] [1] [1][1]位置 d p [ 1 ] [ 0 ] + m [ 1 ] [ 1 ] dp[1] [0]+m[1] [1] dp[1][0]+m[1][1]
- 從 [ 0 ] [ 1 ] [0] [1] [0][1]位置到達 [ 1 ] [ 1 ] [1] [1] [1][1]位置 d p [ 0 ] [ 1 ] + m [ 1 ] [ 1 ] dp[0] [1]+m[1] [1] dp[0][1]+m[1][1]
最終arr更新成 [ 9 , 5 , 8 , 2 ] [9, 5, 8, 2] [9,5,8,2]
- 整個過程不斷滾動更新arr[],讓arr[]依次變成個dp矩陣的每一行,最終變成dp矩陣最后一行的值
NOTICE
- 給定矩陣列數小于行數(N<M),可以進行空間壓縮
- 給定矩陣列數大于行數(M<N),就生成長度為M的arr,令arr更新成dp的每一列的值,從左向右滾動
總結
以上是生活随笔為你收集整理的[算法入门笔记] 18. 动态规划的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2018(农历年)封山之作,和我一起嚼烂
- 下一篇: 实验一:彩色空间转换(YUV2RGB)