日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

[算法入门笔记] 18. 动态规划

發(fā)布時間:2024/3/13 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [算法入门笔记] 18. 动态规划 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

動態(tài)規(guī)劃往往是有套路的,但套路是建立在熟練的基礎(chǔ)上的~

文章目錄

  • 0 建議
  • 1 機(jī)器人達(dá)到指定位置的方法數(shù)
    • 1.1 暴力遞歸
    • 1.2 記憶化搜索
    • 1.3 動態(tài)規(guī)劃
  • 2 換錢的最少貨幣數(shù)
    • 2.1 暴力遞歸
    • 2.2 記憶化搜索
    • 2.3 動態(tài)規(guī)劃
  • 3 紙牌博弈問題
    • 3.1 暴力遞歸
    • 3.2 動態(tài)規(guī)劃
  • 4 高維動態(tài)規(guī)劃
    • 4.1 中國象棋馬的跳法
      • 2.1.1 暴力遞歸
      • 2.1.2 動態(tài)規(guī)劃
    • 2.2 生存問題
      • 2.2.1 暴力遞歸
      • 4.2.2 動態(tài)規(guī)劃
  • 5 空間壓縮技巧
    • 壓縮技巧1
    • 壓縮技巧2
    • 壓縮技巧3
    • 壓縮技巧4
    • 壓縮技巧5
    • 矩陣的最小路徑和
      • 暴力遞歸
      • 動態(tài)規(guī)劃
      • 動態(tài)規(guī)劃+空間壓縮技巧

0 建議

動態(tài)規(guī)劃流程

  • 嘗試使用暴力遞歸求出答案
  • 將暴力遞歸求出的答案緩存下來,改寫記憶化搜索
  • 在記憶化搜索的基礎(chǔ)上打表,形成表結(jié)構(gòu),即dp
  • 緩存結(jié)構(gòu)如何優(yōu)化成表結(jié)構(gòu)

  • 確定遞歸函數(shù)參數(shù)和返回值,清楚可變參數(shù)代表的遞歸狀態(tài)
  • 將可變參數(shù)映射成表格結(jié)構(gòu)(單參數(shù)映射成一維表,雙參數(shù)映射成二維表…)
  • 標(biāo)出最終答案在表中的位置
  • 標(biāo)出遞歸的base case和最簡單、不需要依賴其他位置的答案
  • 分析普遍位置如何依賴其他位置
  • 確定計算順序,求出最終答案
  • 1 機(jī)器人達(dá)到指定位置的方法數(shù)

    [問題]
    假設(shè)有排成一行的N個位置,記為 [ 1 , N ] ( N ≥ 2 ) [1,N](N\ge2) [1,N](N2)。開始時機(jī)器人在其中的M位置上(M一定是 [ 1 , N ] [1,N] [1,N]中的一個),機(jī)器人可以往左走或者往右走
    如果機(jī)器人來到1位置,那么下一步只能往右來到2位置;
    如果機(jī)器人來到N位置,那么下一步只能往左來到N-1位置。
    規(guī)定機(jī)器人必須走K步,最終能來到Р位置(P也一定是 [ 1 , N ] [1,N] [1,N]中的一個)的方法有多少種。給定四個參數(shù)N、M、K、P,返回方法數(shù)。
    [示例]

    N=5,M=2,K=3,P=3

    上面的參數(shù)代表所有位置為1 2 3 4 5。機(jī)器人最開始在2位置上,必須經(jīng)過3步,最后到達(dá)3位置。走的方法只有如下3種:

    • 從2到1,從1到2,從2到3
    • 從2到3,從3到2,從2到3
    • 從2到3,從3到4,從4到3

    所以返回方法數(shù)3。

    N=3。M=1,K=3,P=3

    上面的參數(shù)代表所有位置為1 2 3。機(jī)器人最開始在1位置上,必須經(jīng)過3步,最后到達(dá)3位置。怎么走也不可能,所以返回方法數(shù)0。

    題目特點分析

    本題是經(jīng)典的從左向右嘗試模型,考慮base case,然后有兩種狀態(tài),向左走和向右走

    1.1 暴力遞歸

  • 確定遞歸函數(shù)參數(shù)和返回值
    遞歸函數(shù)參數(shù)有cur,表示當(dāng)前處在的位置和rest,表示剩余步數(shù),返回多少走法
  • 確定遞歸終止條件
    如果走完所有步數(shù),最終位置停在P位置,說明答案有效,返回一種答案;如果最終位置不在P位置,說明答案無效,返回0種答案
  • 確定單次遍歷邏輯
    dfs表示如果當(dāng)前來到cur位置,還剩下rest步要走,下一步的走法
    • 如果 c u r = = 1 cur==1 cur==1,下一步只能走2位置,后續(xù)剩下rest-1步數(shù)
    • 如果 c u r = = N cur==N cur==N,下一步只能走N-1位置,后續(xù)剩下rest-1步數(shù)
    • 如果 c u r ∈ ( 1 , N ) cur\in(1,N) cur(1,N),下一步可以走cur-1位置或者cur+1位置,后續(xù)剩下rest-1步數(shù)
  • /*** 機(jī)器人在1-N位置上移動,當(dāng)前在cur位置,走完rest步后停在p位置的方法數(shù)* @param N 位置為1~N,固定參數(shù)* @param cur 當(dāng)前所在位置,可變參數(shù)* @param rest 剩余步數(shù),可變參數(shù)* @param p 最終目標(biāo)位置,固定參數(shù)* @return 停在p位置的方法數(shù)*/ public int dfs(int N, int P, int cur, int rest) {// 遞歸終止條件if (rest == 0) {// 如果剩余步數(shù)為0,并且來到P位置,答案有效return cur == P ? 1 : 0;}// 單次遍歷邏輯if (cur == 1) { // 來到1位置只能向右走return dfs(N, P, cur + 1, rest - 1);}if (cur == N) { // 來到N位置只能向左走return dfs(N, P, cur - 1, rest - 1);}// 來到一般位置可以向左或者向右走return dfs(N, P, cur - 1, rest -1) + dfs(N, P, cur + 1, rest -1); }/*** 主函數(shù)調(diào)用dfs* @param N N個位置* @param M 當(dāng)前在M位置* @param K 只能走K步* @param P 目標(biāo)位置P* @return 返回從M位置出發(fā),只走K步,最終到達(dá)P位置的方法數(shù)*/ 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;}return dfs(N, P, M, K); }

    1.2 記憶化搜索

    由于在暴力遞歸過程存在大量重復(fù)計算,復(fù)雜度是指數(shù)級別,因此設(shè)計緩存結(jié)構(gòu)存儲計算過的結(jié)果。

    public int dfs(int N, int P, int cur, int rest, int[][] cache) {// 查看緩存中是否具有答案if (cache[rest][cur] != -1) {return cache[rest][cur];}// 遞歸終止條件修改成緩存結(jié)構(gòu)if (rest == 0) {// 如果剩余步數(shù)為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]; }/*** 主函數(shù)調(diào)用dfs* @param N N個位置* @param M 當(dāng)前在M位置* @param K 只能走K步* @param P 目標(biāo)位置P* @return 返回從M位置出發(fā),只走K步,最終到達(dá)P位置的方法數(shù)*/ 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;}// 確定可變參數(shù)范圍 rest范圍[0,K],cur范圍[1,N]int[][] cache= new int[K + 1][N + 1];// 初始化緩存結(jié)構(gòu)for (int i = 0; i <= K; ++i) {for (int j = 0; j <= N; j++) {cache[i][j] = -1;}}// 調(diào)用dfs函數(shù)return dfs(N, P, M, K, cache); }

    1.3 動態(tài)規(guī)劃

    使用該方法優(yōu)化成dp前提是所求問題具有無后效性,即一個遞歸狀態(tài)的返回值與怎么到達(dá)這個狀態(tài)的路徑無關(guān)

    分析是否具有后效性

    • dfs兩個固定參數(shù)N、P,任何時候都不變,說明N和P與具體遞歸狀態(tài)無關(guān),關(guān)注其他兩個可變參數(shù)rest和cur
    • dfs(5,5)出現(xiàn)了兩次,不管從dfs(4,6)來到dfs(5,5),還是從dfs(6,6)來到dfs(5,5),只要是當(dāng)前來到5位置,還剩5步,返回值都是不變的,所以是一個無后效性問題

    優(yōu)化步驟

  • 確定遞歸函數(shù)參數(shù)和返回值,清楚可變參數(shù)代表的遞歸狀態(tài)
    • 可變參數(shù)為cur和rest
  • 將可變參數(shù)映射成表格結(jié)構(gòu)
    • rest作為行,cur作為列,映射成二維表,返回值為dp[rest][cur]
  • 標(biāo)出最終答案在表中的位置
    • 對于N=7,P=5,M=4,K=9,最終答案在dp[9][4]
  • 標(biāo)出遞歸的base case和最簡單、不需要依賴其他位置的答案
    • 填寫base case位置
    if (rest == 0) {return cur == p ? 1 : 0; }
  • 分析普遍位置如何依賴其他位置

    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
  • 確定計算順序,求出最終答案
    說明每一行的值依賴上一行的值

  • 本題動態(tài)規(guī)劃解法就是把 N × K N×K N×K規(guī)模的表填好,填寫每個位置的復(fù)雜度是 O ( 1 ) O(1) O(1),整個時間復(fù)雜度是 O ( N × K ) O(N×K) O(N×K)

    /*** 動態(tài)規(guī)劃版本* @param N N個位置* @param M 當(dāng)前位置* @param K 只能走K步* @param P 目標(biāo)位置* @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數(shù)組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];}}} // 返回當(dāng)剩余步數(shù)K,初始位置M的答案return dp[K][M]; }

    2 換錢的最少貨幣數(shù)

    [問題]
    給定數(shù)組arr,arr 中所有的值都為正數(shù)且不重復(fù)。每個值代表一種面值的貨幣,每種面值的貨幣可以使用任意張,再給定一個整數(shù)aim,代表要找的錢數(shù),求組成aim的最少貨幣數(shù)。
    [示例]

    arr=[5,2,3],aim=20。

    4張5元可以組成20元,其他的找錢方案都要使用更多張的貨幣,所以返回4。

    arr=[5,2,3],aim=0

    不用任何貨幣就可以組成0元,返回0。

    arr=[3,5],aim=2

    根本無法組成2元,錢不能找開的情況下默認(rèn)返回-1。

    2.1 暴力遞歸

  • 確定遞歸函數(shù)參數(shù)和返回值
    遞歸函數(shù)參數(shù)有i,表示當(dāng)前處在的位置和rest,表示剩余面值,返回組合數(shù)
  • 確定遞歸終止條件
    如果走完所有步數(shù)i==arr.length,如果剩余面值為0,表示不需要貨幣了,返回0;如果剩余面值不為0,表示無法組成目標(biāo)面值,返回-1
  • 確定單次遍歷邏輯
    dfs表示如果當(dāng)前來到i位置,還剩下rest面值,下一步的組合方式
    • cur位置前表示已經(jīng)做出的選擇,i位置之后表示后續(xù)將要做出的選擇
    • 從cur位置出發(fā)選擇任意 [ 0 , K ] [0,K] [0,K]當(dāng)前貨幣組成面值
    • cur位置狀態(tài)具有選和不選兩種狀態(tài)
  • /*** 從左向右嘗試* @param arr 面值數(shù)組* @param i 當(dāng)前來到i位置嘗試* @param rest 還剩多少錢才能組合成aim* @return 返回-1,說明i位置后續(xù)情況下,怎么都組合不出aim;返回不是-1,代表i位置后續(xù),組合出rest最少的貨幣數(shù)*/public int dfs(int[] arr, int cur, int rest) {// 遞歸終止條件if (i == arr.length) {return rest == 0 ? 0 : -1;}// 保存答案int ans = -1;// 嘗試當(dāng)前貨幣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) { // 當(dāng)i后續(xù)選擇合理時// 在使用k張當(dāng)前貨幣并且后續(xù)位置合法的情況下返回較少的貨幣數(shù)// next+k表示整個選擇過程使用的貨幣ans = ans == -1 ? next + k : Math.min(ans, next + k);}}return ans;}public int minCoins(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return -1;}// 從0位置開始嘗試return dfs(arr, 0, aim); }

    2.2 記憶化搜索

    致命錯誤:數(shù)組無效狀態(tài)沒有初始化,這里-2表示未填寫狀態(tài),-1表示無效狀態(tài),非basecase后面有個判斷是否無效,該狀態(tài)沒有初始化,還有注意初始化順序

    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];}// 嘗試當(dāng)前貨幣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) { // 當(dāng)i后續(xù)決策合法時// 在使用k張當(dāng)前貨幣并且后續(xù)位置合法的情況下返回較少的貨幣數(shù)// next+k表示整個決策過程使用的貨幣cache[i][rest] = cache[i][rest] == -1 ? next + k : Math.min(cache[i][rest], next + k);}}return cache[i][rest]; }

    2.3 動態(tài)規(guī)劃

    優(yōu)化步驟

  • 確定遞歸函數(shù)參數(shù)和返回值,清楚可變參數(shù)代表的遞歸狀態(tài)
    • 可變參數(shù)為i和rest
  • 將可變參數(shù)映射成表格結(jié)構(gòu),i表示當(dāng)前位置,允許cur來到終止位置
    • rest作為行,i作為列,映射成二維表,剩余面值數(shù)不超過aim,因此 r e s t ∈ [ 0 , a i m ] rest\in[0,aim] rest[0,aim]
  • 標(biāo)出最終答案在表中的位置
    • 確定最終答案dfs(arr,0,aim), d p [ 0 ] [ a i m ] dp[0][aim] dp[0][aim]
  • 填寫base case
  • if (cur == arr.length) {return rest == 0 ? 0 : -1; }


    5. 填寫普遍位置依賴

    // 保存最少貨幣數(shù) int ans = -1; // 嘗試當(dāng)前貨幣0...k張情況,但不能超過rest for (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) { //當(dāng)i后續(xù)決策合法時// 在使用k張當(dāng)前貨幣并且后續(xù)位置合法的情況下返回較少的貨幣數(shù)// next+k表示整個決策過程使用的貨幣ans = ans == -1 ? next + k : Math.min(ans, next + k);} } return ans;

    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],根據(jù)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]]已經(jīng)計算過

    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]依賴下面一個位置和左邊一個位置

    最后一排值已經(jīng)確定,剩下的位置只依賴下面和左邊的位置,只要求從左到右求倒數(shù)第二排,從左到右求倒數(shù)第三排…從做到到右求第一排即可

    public int minCoins(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return -1;}int N = arr.length;int[][] dp = new int[N + 1][aim + 1];// base case// 設(shè)置最后一排的值,除了dp[N][0]為0之外,其他都是-1for (int col = 1; col <= aim; col++) {dp[N][col] = -1;}// 計算順序從下到上for (int i = N - 1; i >= 0; i--) {// 從左到右計算for (int rest = 0; rest <= aim; rest++) {// 初始時先設(shè)置dp[i][rest]的值無效dp[i][rest] = -1;// 下面的值如果有效if (dp[i + 1][rest] != -1) {// 先保存起來dp[i][rest] = dp[i + 1][rest];}// 如果左邊位置不越界并且有效if (rest - arr[i] >= 0 && dp[i][rest - arr[i]] != -1) {if (dp[i][rest] == -1) { // 如果下面位置無效dp[i][rest] = dp[i][rest - arr[i]] + 1;} else {dp[i][rest] = Math.min(dp[i][rest - arr[i]] + 1, dp[i][rest]);}}}}return dp[0][aim]; }

    3 紙牌博弈問題

    [問題]
    給定一個整型數(shù)組arr,代表數(shù)值不同的紙牌排成一條線。玩家A和玩家B依次拿走每張紙牌,規(guī)定玩家A先拿,玩家B后拿,但是每個玩家每次只能拿走最左或最右的紙牌,玩家A和玩家B都絕頂聰明。請返回最后獲勝者的分?jǐn)?shù)。
    [示例]

    arr = [1, 2, 100, 4]

    開始時,玩家A只能拿走1或4。如果開始時玩家A拿走1,則排列變?yōu)閇2,100,4],接下來玩家B可以拿走2或4,然后繼續(xù)輪到玩家A…
    如果開始時玩家A拿走4,則排列變?yōu)閇1,2,100],接下來玩家B可以拿走1或100,然后繼續(xù)輪到玩家A…
    玩家A作為絕頂聰明的人不會先拿4,因為拿4之后,玩家B將拿走100。所以玩家A會先拿1,讓排列變?yōu)閇2,100,4],接下來玩家B不管怎么選,100都會被玩家A拿走。玩家A會獲勝,分?jǐn)?shù)為101。所以返回101。

    arr=[1,100,2]

    開始時,玩家A不管拿1還是2,玩家B作為絕頂聰明的人,都會把100拿走。玩家B會獲勝,分?jǐn)?shù)為100。所以返回100。

    3.1 暴力遞歸

    • 定義遞歸函數(shù)first(i,j),表示arr[i…j]這個排列上的紙牌被絕頂聰明的人先拿,最終返回的分?jǐn)?shù)
    • 定義遞歸函數(shù)second(i,j),表示arr[i…j]這個排列上的紙牌被絕頂聰明的人后拿,最終返回的分?jǐn)?shù)

    分析先手first(i,j)

    • i==j,即只剩一張牌。當(dāng)然會被先拿紙牌的人拿走,返回arr[i]
    • i!=j,當(dāng)前拿紙牌的人,要么拿arr[i],要么拿arr[j]。
    • 如果拿arr[i],那么排列只剩下arr[i+1…j]。對當(dāng)前玩家,面對arr[i+1…j],他將成為后手,后續(xù)獲得分?jǐn)?shù)是second(i+1,j)
    • 如果拿arr[j],那么排列只剩下arr[i…j-1]。對當(dāng)前玩家,面對arr[i…j-1],他將成為后手,后續(xù)獲得分?jǐn)?shù)是second(i,j-1)
    • 作為絕頂聰明的人,兩種決策都是最優(yōu)的,返回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)}

    先手函數(shù)

    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)); }

    后手函數(shù)

    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)); }

    主函數(shù)調(diào)用

    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 動態(tài)規(guī)劃

    經(jīng)典的范圍嘗試模型

    1.分析first(i,j)可變i,j參數(shù)范圍

    2.標(biāo)出計算的終止位置

    3.標(biāo)出basecase

    • i不會超過j,即下三角部分無效

    • first:對角線basecase

      if (i == j) {return arr[i]; }
    • second 對角線basecase

      if (i == j) {return 0; }

    3.標(biāo)出非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 高維動態(tài)規(guī)劃

    4.1 中國象棋馬的跳法

    [問題]
    把棋盤放入第一象限,棋盤的最左下角是 ( 0 , 0 ) (0,0) (0,0)位置。那么整個棋盤就是橫坐標(biāo)上9條線、縱坐標(biāo)上10條線的一個區(qū)域。給你三個參數(shù),x,y,k,返回如果“馬”從(0,0)位置出發(fā),必須走k步,最后落在(x, y)上的方法數(shù)有多少種?


    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 動態(tài)規(guī)劃

    1.分析可變x,y,step參數(shù)范圍

    2.標(biāo)出計算終止位置

    3.標(biāo)出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]; }// 防止出現(xiàn)越界,同時返回數(shù)組值 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 生存問題

    [問題]
    給定五個參數(shù)n, m,i, j, k。表示在一個 N × M N×M N×M的區(qū)域,Bob處在 ( i , j ) (i,j) (i,j)點,每次Bob等概率的向上、下、左、右四個方向移動一步,Bob必須走K步。如果走完之后,Bob還停留在這個區(qū)域上,就算Bob存活,否則就算Bob死亡。請求解Bob的生存概率,返回字符串表示分?jǐn)?shù)的方式。

    2.2.1 暴力遞歸

    public String bob(int N, int M, int i, int j, int k) {// 總步數(shù)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區(qū)域內(nèi),Bob從(row,col)位置出發(fā),走rest步,獲得生存點數(shù)* @param N 矩陣長度* @param M 矩陣寬度* @param row 出發(fā)位置的橫坐標(biāo)* @param col 出發(fā)位置的縱坐標(biāo)* @param rest 剩余步數(shù)* @return 生存點數(shù)*/ public long process(int N, int M, int row, int col, int rest) {//違規(guī)條件if(row < 0 || row == N || col < 0 || col == M) {return 0;}if (rest == 0) { //剩余步數(shù)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; }// 最大公約數(shù) public long gcd(long m, long n) {return n == 0 ? m : gcd(n, m % n); }

    4.2.2 動態(tài)規(guī)劃

    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

    將二維數(shù)組壓縮成一維數(shù)組

    壓縮技巧2

    壓縮技巧3

    壓縮技巧4

    壓縮技巧5

    其他技巧

    矩陣的最小路徑和

    暴力遞歸

    1.考慮走到邊界,怎么處理 2.寫對返回值

    1. 分析basecase,終止位置—2.分析邊界條件和違規(guī)條件,返回值怎么處理—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)到達(dá)
    • 只能向右移動,其路徑和是a點+右邊到b的路徑和
    • a(i,j)到達(dá)最后一列,他只能向下移動,其路徑和是a點+下邊到b的路徑和
    public int minpathSum(int[][] m) {if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {return 0;}return process(m, 0, 0, m.length, m[0].length); }public int process(int[][] m, int i, int j, int row, int col) {// base case a來到右下角if (i == row - 1 && j == col - 1) { //遞歸結(jié)束條件return m[i][j];}// a來到最后一行if (i == row - 1) { // 只能向右走return m[i][j] + process(m, i, j + 1, row, col);}// a來到最后一列if (j == col - 1) { // 只能向下走return m[i][j] + process(m, i + 1, j, row, col);}// 選擇1 向右走的路徑和int rightPath = process(m, i + 1, j, row, col);// 選擇2 向下走的路徑和int downPath = process(m, i, j + 1, row, col);return m[i][j] + Math.min(rightPath, downPath); }

    動態(tài)規(guī)劃

    • 對于第一行所有的位置(0,j)來說,從(0,0)位置到(0,j)位置只能向右走,所以(0,0)到(0,j)位置的路徑和是m[0] [0…j]累加的結(jié)果

    • 對于m的第一列的所有位置來說,即(i,0)從(0,0)位置走到(i,0)位置只能向下走,所以(0,0)位置到(i,0)位置的路徑和就是m[0…i] [0]累加的結(jié)果

    • 除了第一行和第一列的位置外,都有左邊位置(i-1,j)和上邊位置(i,j-1)

    • 從(0,0)到(i,j)位置的路徑必然經(jīng)過位置(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)位置開始,經(jīng)過(i-1,j)位置最終到到達(dá)(i,j)的最小路徑和經(jīng)過(i,j-1)位置最終到達(dá)(i,j)的最小路徑,誰最小

    • 除第一行和第一列位置外,每一個位置考慮從左邊到達(dá)自己的路徑和更小還是從上邊到達(dá)自己的路徑和更小,最右下角位置就是整個問題的答案

    public int minPathSum(int[][] m) {if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {return 0;}int row = m.length;int col = m[0].length;int[][] dp = new int[row][col];// 起始位置dp[0][0] = m[0][0];// 初始化第一行for (int i = 1; i < row; i++) {dp[i][0] = dp[i - 1][0] + m[i][0];}// 初始化第一列for (int j = 1; j < col; j++) {dp[0][j] = dp[0][j - 1] + m[0][j];}for (int i = 1; i < row; i++) {for (int j = 1; j < col; j++) {dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];}}return dp[row - 1][col - 1]; }

    動態(tài)規(guī)劃+空間壓縮技巧

    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}的一維數(shù)組,本測試用例長度為4,初始 a r r = [ 0 , 0 , 0 , 0 ] arr=[0,0,0,0] arr=[0,0,0,0],從 ( 0 , 0 ) (0,0) (0,0)位置出發(fā)到達(dá)m第一行的每個位置,最小路徑和時從 ( 0 , 0 ) (0,0) (0,0)位置開始依次累加的結(jié)果, 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)位置的最小路徑和
    • 準(zhǔn)備把a(bǔ)rr[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]位置到達(dá) [ 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]位置到達(dá) [ 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

    • 給定矩陣列數(shù)小于行數(shù)(N<M),可以進(jìn)行空間壓縮
    • 給定矩陣列數(shù)大于行數(shù)(M<N),就生成長度為M的arr,令arr更新成dp的每一列的值,從左向右滾動
    /*** 動態(tài)規(guī)劃+空間壓縮* @param m* @return*/ public int minPathSum(int[][] m) {if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {return 0;}// 行數(shù)與列數(shù)較大的為moreint more = Math.max(m.length, m[0].length);// 行數(shù)與列數(shù)較小的為lessint less = Math.min(m.length, m[0].length);// 行數(shù)是否大于等于列數(shù)boolean rowmore = more == m.length;// 輔助數(shù)組長度是行數(shù)或列數(shù)的較小值int[] arr = new int[less];// 出發(fā)位置arr[0] = m[0][0];for (int i = 1; i < less; i++) {// rowmore為true代表行數(shù)較大,更新列位置,否則更新行位置arr[i] = arr[i - 1] + (rowmore ? m[0][i] : m[i][0]);}for (int i = 1; i < more; i++) {arr[0] = arr[0] + (rowmore ? m[i][0] : m[0][i]);for (int j = 1; j < less; j++) {arr[j] = Math.min(arr[j - 1], arr[j])+ (rowmore ? m[i][j] : m[j][i]);}}return arr[less - 1]; }

    總結(jié)

    以上是生活随笔為你收集整理的[算法入门笔记] 18. 动态规划的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。