动态规划往往是有套路的,但套路是建立在熟练的基础上的~

文章目录

  • 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. 尝试使用暴力递归求出答案
  2. 将暴力递归求出的答案缓存下来,改写记忆化搜索
  3. 在记忆化搜索的基础上打表,形成表结构,即dp

缓存结构如何优化成表结构

  1. 确定递归函数参数和返回值,清楚可变参数代表的递归状态
  2. 将可变参数映射成表格结构(单参数映射成一维表,双参数映射成二维表…)
  3. 标出最终答案在表中的位置
  4. 标出递归的base case和最简单、不需要依赖其他位置的答案
  5. 分析普遍位置如何依赖其他位置
  6. 确定计算顺序,求出最终答案

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]中的一个)的方法有多少种。给定四个参数NMKP,返回方法数。
[示例]

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

上面的参数代表所有位置为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 暴力递归

  1. 确定递归函数参数和返回值
    递归函数参数有cur,表示当前处在的位置和rest,表示剩余步数,返回多少走法
  2. 确定递归终止条件
    如果走完所有步数,最终位置停在P位置,说明答案有效,返回一种答案;如果最终位置不在P位置,说明答案无效,返回0种答案
  3. 确定单次遍历逻辑
    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-N位置上移动,当前在cur位置,走完rest步后停在p位置的方法数* @param N 位置为1~N,固定参数* @param cur 当前所在位置,可变参数* @param rest 剩余步数,可变参数* @param p 最终目标位置,固定参数* @return 停在p位置的方法数*/
public int dfs(int N, int P, int cur, int rest) {// 递归终止条件if (rest == 0) {// 如果剩余步数为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);
}/*** 主函数调用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;}return dfs(N, P, M, K);
}

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两个固定参数NP,任何时候都不变,说明NP与具体递归状态无关,关注其他两个可变参数restcur
  • dfs(5,5)出现了两次,不管从dfs(4,6)来到dfs(5,5),还是从dfs(6,6)来到dfs(5,5),只要是当前来到5位置,还剩5步,返回值都是不变的,所以是一个无后效性问题

优化步骤

  1. 确定递归函数参数和返回值,清楚可变参数代表的递归状态

    • 可变参数为currest
  2. 将可变参数映射成表格结构
    • rest作为行,cur作为列,映射成二维表,返回值为dp[rest][cur]
  3. 标出最终答案在表中的位置
    • 对于N=7,P=5,M=4,K=9,最终答案在dp[9][4]
  4. 标出递归的base case和最简单、不需要依赖其他位置的答案
    • 填写base case位置
    if (rest == 0) {return cur == p ? 1 : 0;
    }
    

  1. 分析普遍位置如何依赖其他位置

    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
  2. 确定计算顺序,求出最终答案
    说明每一行的值依赖上一行的值

本题动态规划解法就是把 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 换钱的最少货币数

[问题]
给定数组arrarr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim,代表要找的钱数,求组成aim最少货币数。
[示例]

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

4张5元可以组成20元,其他的找钱方案都要使用更多张的货币,所以返回4。

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

不用任何货币就可以组成0元,返回0。

arr=[3,5],aim=2

根本无法组成2元,钱不能找开的情况下默认返回-1。

2.1 暴力递归

  1. 确定递归函数参数和返回值
    递归函数参数有i,表示当前处在的位置和rest,表示剩余面值,返回组合数
  2. 确定递归终止条件
    如果走完所有步数i==arr.length,如果剩余面值为0,表示不需要货币了,返回0;如果剩余面值不为0,表示无法组成目标面值,返回-1
  3. 确定单次遍历逻辑
    dfs表示如果当前来到i位置,还剩下rest面值,下一步的组合方式

    • cur位置前表示已经做出的选择,i位置之后表示后续将要做出的选择
    • cur位置出发选择任意 [ 0 , K ] [0,K] [0,K]当前货币组成面值
    • cur位置状态具有选和不选两种状态
/*** 从左向右尝试* @param arr 面值数组* @param i 当前来到i位置尝试* @param rest 还剩多少钱才能组合成aim* @return 返回-1,说明i位置后续情况下,怎么都组合不出aim;返回不是-1,代表i位置后续,组合出rest最少的货币数*/public int dfs(int[] arr, int cur, int rest) {// 递归终止条件if (i == arr.length) {return rest == 0 ? 0 : -1;}// 保存答案int ans = -1;// 尝试当前货币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表示整个选择过程使用的货币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 记忆化搜索

致命错误:数组无效状态没有初始化,这里-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 动态规划

优化步骤

  1. 确定递归函数参数和返回值,清楚可变参数代表的递归状态

    • 可变参数为irest
  2. 将可变参数映射成表格结构,i表示当前位置,允许cur来到终止位置
    • rest作为行,i作为列,映射成二维表,剩余面值数不超过aim,因此 r e s t ∈ [ 0 , a i m ] rest\in[0,aim] rest∈[0,aim]
  3. 标出最终答案在表中的位置
    • 确定最终答案dfs(arr,0,aim), d p [ 0 ] [ a i m ] dp[0][aim] dp[0][aim]
  4. 填写base case
if (cur == arr.length) {return rest == 0 ? 0 : -1;
}


5. 填写普遍位置依赖

// 保存最少货币数
int ans = -1;
// 尝试当前货币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) { //当i后续决策合法时// 在使用k张当前货币并且后续位置合法的情况下返回较少的货币数// 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],根据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]依赖下面一个位置和左边一个位置

最后一排值已经确定,剩下的位置只依赖下面和左边的位置,只要求从左到右求倒数第二排,从左到右求倒数第三排…从做到到右求第一排即可

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// 设置最后一排的值,除了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++) {// 初始时先设置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 纸牌博弈问题

[问题]
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
[示例]

arr = [1, 2, 100, 4]

开始时,玩家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。

arr=[1,100,2]

开始时,玩家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的路径和
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) { //递归结束条件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);
}

动态规划

  • 对于第一行所有的位置(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)的最小路径,谁最小

  • 除第一行和第一列位置外,每一个位置考虑从左边到达自己的路径和更小还是从上边到达自己的路径和更小,最右下角位置就是整个问题的答案

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

动态规划+空间压缩技巧

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​

    1. 生成大小为 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的每一列的值,从左向右滚动
/*** 动态规划+空间压缩* @param m* @return*/
public int minPathSum(int[][] m) {if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {return 0;}// 行数与列数较大的为moreint more = Math.max(m.length, m[0].length);// 行数与列数较小的为lessint less = Math.min(m.length, m[0].length);// 行数是否大于等于列数boolean rowmore = more == m.length;// 辅助数组长度是行数或列数的较小值int[] arr = new int[less];// 出发位置arr[0] = m[0][0];for (int i = 1; i < less; i++) {// rowmore为true代表行数较大,更新列位置,否则更新行位置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];
}

[算法入门笔记] 18. 动态规划相关推荐

  1. 刘汝佳算法入门笔记(1)

    刘汝佳算法入门笔记 习题4-2 习题4-2 有n行n列(2≤n≤9)的小黑点,还有m条线段连接其中的一些黑点.统计这些线段连成 了多少个正方形(每种边长分别统计). 行从上到下编号为1-n,列从左到右 ...

  2. zcy算法入门笔记004

    路径总和 判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum .如果存在,返回 true :否则,返回 false . public static c ...

  3. zcy算法入门笔记001

    笔记仅用于学习,如有错误欢迎指正,谢谢 位运算 public class Code01_PrintBinary {public static void print(int num){//十进制=> ...

  4. [算法入门笔记] 9. 哈希表与哈希函数

    文章目录 1. 哈希表与哈希函数的实现 2. 设计RandomPool结构 3. bitmap 3.1 概述 3.2 常用操作 3.2.1 存储数据 3.2.2 添加操作 3.2.3 删除操作 3.2 ...

  5. 算法学习笔记----用动态规划解决钢管切割问题

    (说明:由于CSDN的博客中不能添加下标等特殊符号,所以部分内容使用截图的形式) 通过对问题进行高度抽象,现在我们的问题,就是要递归地求解r n 的最大值,下面采用的是一种自顶向下的递归方法: int ...

  6. zcy算法入门笔记002-位图

    一.前置知识点:左移.右移.无符号右移 <<:左移 左边最高位丢弃,右边补0(高位向左移动,低位补零) 若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2. >>:右移 ...

  7. zcy算法入门笔记003-树

    相同的树 public static class TreeNode {public int val;public TreeNode left;public TreeNode right;}public ...

  8. S7-200 Smart入门笔记1——流水灯

    采用西门子PLC s7-200 smart编写流水灯,目的是熟悉位移指令.传送指令.比较指令,采用两种方法实现. 方法一采用按键,按下一次按钮,灯就变动一次: 方法二采用定时器,每个1秒,灯变动一次: ...

  9. 数据结构与算法入门(follow 左神)

    文章目录 一. 认识时间复杂度和简单排序算法 1.以选择排序为例 2.异或运算 3.插入排序 4.二分查找 5.对数器 二. 认识O(NlogN)的排序 1.递归==栈的后序遍历 2.归并排序 3.快 ...

最新文章

  1. dell电脑重装系统no bootable devices found
  2. virtual server2005下创建citrix集群的一点记录
  3. AnnotationConfigApplicationContext 通过调用注解Bean 定义读取器
  4. mavros 仿真与飞行器控制
  5. Windows 8 系列(九):关于VariableSizedWrapGrid与WrapGrid 对Visibility变化而引起的布局变化的支持...
  6. iOS开发之企业级账号的使用注意的坑
  7. 【linux系统学习笔记】运行startx后Ubuntu密码正确进不去的问题
  8. CentOs6.5安装使用数据恢复软件extundelete
  9. 小白都懂的Python爬虫之网易云音乐下载
  10. 现代控制理论——非线性系统的lyapunov
  11. Vue删除表格中的某一行数据
  12. Uva11500-Gambler's ruin
  13. iOS - 选取相册中iCloud云上图片和视频的处理
  14. CSS 文字两行显示,超出隐藏
  15. 熊大微评微内核|华为“鸿蒙”所涉及的微内核究竟是什么?
  16. OpenStack云平台搭建(5) | 部署Nova
  17. mysql isnum()_mysql 一些基础的语法和命令
  18. jenkins执行远程服务器命令返回code127解决办法
  19. html canvas php,HTML5 canvas实现画图程序(附代码)
  20. 在oracle中使用 PageHelper.startPage()分页遇到的问题

热门文章

  1. android 拍照申请权限,Android拍照6.0以上动态获取权限
  2. php的微信登录示例代码,关于微信用户注册登录实例代码汇总
  3. iOS面试题目及答案总结
  4. 价值7000万的商业模式,羊毛出在狗身上,猪来买单
  5. MAtlab wavefront,MATLAB:像Wavefront算法一样制作矩阵
  6. java pdf转图片原理_pdf转图片程序(java实现)
  7. 第十周 项目一(4)哈夫曼编码的算法验证
  8. 一篇读懂springboot用echarts实现实时柱状图和饼状图查询
  9. 机器学习PAI为你自动写歌词,妈妈再也不用担心我的freestyle了(提供数据、代码)...
  10. 读数据优化 绝对值函数优化 加速 示范程序 洛谷P2345奶牛集会