前言
动态规划下面继续开始算法的学习,算法是一种需要长期练习,增强灵感~~~
一、最小路径累加和
题目:
* 给定一个二维数组matrix,一个人必须从左上角出发,最后到达右下角
* 沿途只可以向下或者向右走,沿途的数字都累加就是距离累加和
* 返回最小距离累加和
分析:
还是从尝试入手,从(0,0)出发到达右下角的目标
递归函数的含义定义为:从哪个位置出发,到达右下角沿途最小累加和
递归函数中,分三种情况讨论
1、最后一行的时候,只能向右
2、最后一列的时候,只能向下
3、其他普通情况,分别求出向右和向下的累加和,取最小
再通过递归函数改动态规划。
递归代码:
public static int minPathSum(int[][] m) {
if (m == null || m.length == 0) {
return 0;
}
return process(m, 0, 0);
}
/**
* 返回从(row,col)位置出发到右下角沿途累加和最小值
*
* @param m
* @param row
* @param col
* @return
*/
public static int process(int[][] m, int row, int col) {
int rowLength = m.length;
int colLength = m[0].length;
int lastRow = rowLength - 1;
int lastCol = colLength - 1;
//base case
if (row == lastRow && col == lastCol) {
return m[lastRow][lastCol];
}
//最后一行,只能向右
if (row == lastRow) {
return m[row][col] + process(m, row, col + 1);
}
//最后一列,只能向下
if (col == lastCol) {
return m[row][col] + process(m, row + 1, col);
}
//普通情况
int down = m[row][col] + process(m, row + 1, col);
int right = m[row][col] + process(m, row, col + 1);
return Math.min(down, right);
}
动态规划代码;
public static int minPathSum(int[][] m) {
if (m == null || m.length == 0) {
return 0;
}
int rowSize = m.length;
int colSize = m[0].length;
int lastRow = rowSize - 1;
int lastCol = colSize - 1;
int[][] dp = new int[rowSize][colSize];
dp[lastRow][lastRow] = m[lastRow][lastCol];
//最后一行,只能向右
for (int i = lastCol - 1; i >= 0; i--) {
dp[lastRow][i] = m[lastRow][i] + dp[lastRow][i + 1];
}
//最后一列,只能向下
for (int i = lastRow - 1; i >= 0; i--) {
dp[i][lastCol] = m[i][lastCol] + dp[i + 1][lastCol];
}
for (int row = lastRow - 1; row >= 0; row--) {
for (int col = lastCol - 1; col >= 0; col--) {
//普通情况
int down = m[row][col] + dp[row + 1][col];
int right = m[row][col] + dp[row][col + 1];
dp[row][col] = Math.min(down, right);
}
}
return dp[0][0];
}
思考:
这里有办法再优化吗?
我们可以看到,动态规划版本,我们需要再新增和m数组等规模的二维数组,但是我们可以改成一维数组,因为从dp动态规划版本中,我们知道,中间任一格子只依赖自己下一个格子和右边的格子
空间压缩的代码:
public static int minPathSum(int[][] m) {
if (m == null || m.length == 0) {
return 0;
}
int rowSize = m.length;
int colSize = m[0].length;
int lastRow = rowSize - 1;
int lastCol = colSize - 1;
int[] dp = new int[colSize];
dp[lastCol] = m[lastRow][lastCol];
//最后一行,只能向右
for (int i = lastCol - 1; i >= 0; i--) {
dp[i] = m[lastRow][i] + dp[i + 1];
}
for (int row = lastRow - 1; row >= 0; row--) {
dp[lastCol] = m[row][lastCol] + dp[lastCol];
for (int col = lastCol - 1; col >= 0; col--) {
int p1 = dp[col + 1] + m[row][col];
int p2 = dp[col] + m[row][col];
dp[col] = Math.min(p1, p2);
}
}
return dp[0];
}
二、Bob求生概率
题目:
* 给定5个参数,N,M,row,col,k
* 表示在N*M的区域上,醉汉Bob初始在(row,col)位置
* Bob一共要迈出k步,且每步都会等概率向上下左右四个方向走一个单位
* 任何时候Bob只要离开N*M的区域,就直接死亡
* 返回k步之后,Bob还在N*M的区域的概率
分析:
题型和象棋马走日的题目相似,通过递归求出Bob走k步还在区域内的所有的可能数量,然后在除以4的k次方。
因为Bob每次有4种选择,并且可以走k次,所以Bob所有可以走的次数就是4的k次方。
在通过递归函数改写动态规划
递归代码:
public static double livePosibility(int row, int col, int k, int N, int M) {
return process(row, col, k, N, M) / Math.pow(4, k);
}
/**
* 返回走rest步数后,还在N*M区域内的路径数量
*
* @param row
* @param col
* @param rest
* @param N
* @param M
* @return
*/
public static long process(int row, int col, int rest, int N, int M) {
//越界判断
if (row < 0 || col < 0 || row >= N || col >= M) {
return 0;
}
//base case
if (rest == 0) {
return 1;
}
long up = process(row + 1, col, rest - 1, N, M);
long down = process(row - 1, col, rest - 1, N, M);
long left = process(row, col - 1, rest - 1, N, M);
long right = process(row, col + 1, rest - 1, N, M);
return up + down + left + right;
}
动态规划代码:
public static double livePosibility2(int row, int col, int k, int N, int M) {
long[][][] dp = new long[N + 1][M + 1][k + 1];
for (int r = 0; r <= N; r++) {
for (int c = 0; c <= M; c++) {
dp[r][c][0] = 1;
}
}
for (int rest = 1; rest <= k; rest++) {
for (int r = 0; r <= N; r++) {
for (int c = 0; c <= M; c++) {
long up = pick(dp, r + 1, c, rest - 1, N, M);
long down = pick(dp, r - 1, c, rest - 1, N, M);
long left = pick(dp, r, c - 1, rest - 1, N, M);
long right = pick(dp, r, c + 1, rest - 1, N, M);
dp[r][c][rest] = up + down + left + right;
}
}
}
return dp[row][col][k] / Math.pow(4, k);
}
public static long pick(long[][][] dp, int row, int col, int rest, int N, int M) {
//越界判断
if (row < 0 || col < 0 || row >= N || col >= M) {
return 0;
}
return dp[row][col][rest];
}
三、不同货币组成目标面值
题目:
* arr是货币数组,其中的值都是正数。再给定一个正数aim。
* 每个值都认为是一张货币,
* 即便是值相同的货币也认为每一张都是不同的,
* 返回组成aim的方法数
* 例如:arr = {1,1,1},aim = 2
* 第0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2
* 一共就3种方法,所以返回3
分析:
递归函数中,我们要讨论当前值要和不要两种,然后将两种情况求和
递归代码:
public static int coinWays(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process(arr, 0, aim);
}
public static int process(int[] arr, int index, int rest) {
//base case
if (index == arr.length) {
return rest == 0 ? 1 : 0;
}
//当前位置不选择
int p1 = process(arr, index + 1, rest);
//选择当前位置
int p2 = process(arr, index + 1, rest - arr[index]);
return p1 + p2;
}
动态规划代码:
public static int dp(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int p1 = dp[index + 1][rest];
int p2 = 0;
if (rest - arr[index] >= 0) {
p2 = dp[index + 1][rest - arr[index]];
}
dp[index][rest] = p1 + p2;
}
}
return dp[0][aim];
}
四、货币不限张数组成目标面值
题目:
* arr是面值数组,其中的值都是正数且没有重复。再给定一个正数aim。
* 每个值都认为是一种面值,且认为张数是无限的。
* 返回组成aim的方法数
* 例如:arr = {1,2},aim = 4
* 方法如下:1+1+1+1、1+1+2、2+2
* 一共就3种方法,所以返回3
分析:
递归函数中,需要考虑张数,讨论张数从0到小于等于目标面值的情况去计算
递归代码:
public static int coinsWay(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process(arr, 0, aim);
}
public static int process(int[] arr, int index, int rest) {
//base case
if (index == arr.length) {
return rest == 0 ? 1 : 0;
}
int ways = 0;
for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
ways += process(arr, index + 1, rest - zhang * arr[index]);
}
return ways;
}
动态规划版本:
public static int dp(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
int ways = 0;
for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
ways += dp[index + 1][rest - zhang * arr[index]];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}
思考:
通过动态规划的代码,居然出现三个for循环,时间复杂度升高了,要想办法优化,这里通过学习,我们可以通过观察动态规划的位置依赖的方法,来看下优化的可能性。通过观察我们知道,中间的格子依赖自己下一行以及往左的格子,进一步观察,可以知道距离自己左边-arr[index]位置的格子也是依赖下一行的格子,并且和我们重复了。所以存在优化的空间
动态规划优化代码:
public static int dp(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
int N = arr.length;
int[][] dp = new int[N + 1][aim + 1];
dp[N][0] = 1;
for (int index = N - 1; index >= 0; index--) {
for (int rest = 0; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
//当自己左边的格子存在
if (rest - arr[index] >= 0) {
//这一步相当于将上个代码的for循环给简化
dp[index][rest] += dp[index][rest - arr[index]];
}
}
}
return dp[0][aim];
}
五、货币限定张数组成目标面值
题目:
* arr是货币数组,其中的值都是正数。再给定一个正数aim。
* 每个值都认为是一张货币,
* 认为值相同的货币没有任何不同,
* 返回组成aim的方法数
* 例如:arr = {1,2,1,1,2,1,2},aim = 4
* 方法:1+1+1+1、1+1+2、2+2
* 一共就3种方法,所以返回3
分析:
和第4题差不多,只是在递归尝试的时候,张数需要加限定条件
递归代码:
public static class Info {
private int[] coins;
private int[] zhangs;
public Info(int[] coins, int[] zhangs) {
this.coins = coins;
this.zhangs = zhangs;
}
}
public static Info getInfo(int[] arr) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arr.length; i++) {
if (map.containsKey(arr[i])) {
map.put(arr[i], 1 + map.get(arr[i]));
} else {
map.put(arr[i], 1);
}
}
Set<Integer> keys = map.keySet();
int[] coins = new int[map.size()];
int[] zhangs = new int[map.size()];
int index = 0;
for (Integer key : keys) {
coins[index] = key;
zhangs[index++] = map.get(key);
}
Info info = new Info(coins, zhangs);
return info;
}
public static int coinsWay(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
Info info = getInfo(arr);
return process(info.coins, info.zhangs, 0, aim);
}
/**
* 返回在限定张数和面值的情况下组成目标面额的所有方法
*
* @param coins 每个货币的面值
* @param zhangs 每种货币张数
* @param index 计算到哪种货币的下标
* @param rest 剩余目标数
* @return
*/
public static int process(int[] coins, int[] zhangs, int index, int rest) {
//base case
if (rest == 0) {
return 1;
}
//越界情况
if (index == coins.length) {
return 0;
}
int ways = 0;
for (int zhang = 0; zhang <= zhangs[index] && rest - zhang * coins[index] >= 0; zhang++) {
ways += process(coins, zhangs, index + 1, rest - zhang * coins[index]);
}
return ways;
}
递归代码:
public static int dp(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
Info info = getInfo(arr);
int[] coins = info.coins;
int[] zhangs = info.zhangs;
int N = coins.length;
int[][] dp = new int[N + 1][aim + 1];
//第一列都是1
for (int index = 0; index <= N; index++) {
dp[index][0] = 1;
}
//最后一行,除第一列,其余都是0
//普遍情况
for (int index = N - 1; index >= 0; index--) {
for (int rest = 1; rest <= aim; rest++) {
int ways = 0;
for (int zhang = 0; zhang <= zhangs[index] && rest - (zhang * coins[index]) >= 0; zhang++) {
ways += dp[index + 1][rest - zhang * coins[index]];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}
同理这里需要进行优化
优化后的动态规划代码:
public static int dp(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
Info info = getInfo(arr);
int[] coins = info.coins;
int[] zhangs = info.zhangs;
int N = coins.length;
int[][] dp = new int[N + 1][aim + 1];
//第一列都是1
for (int index = 0; index <= N; index++) {
dp[index][0] = 1;
}
//最后一行,除第一列,其余都是0
//普遍情况
for (int index = N - 1; index >= 0; index--) {
for (int rest = 1; rest <= aim; rest++) {
dp[index][rest] = dp[index + 1][rest];
if (rest - coins[index] >= 0) {
dp[index][rest] += dp[index][rest - coins[index]];
}
if (rest - coins[index] * (zhangs[index] + 1) >= 0) {
dp[index][rest] -= dp[index + 1][rest - coins[index] * (zhangs[index] + 1)];
}
}
}
return dp[0][aim];
}
总结
1、向第一道题目,后面刷题的过程中,如果遇到数组情况,并且存在可以压缩的情况,尽量进行优化,做人要对自己有要求才能不断提高。
2、像第4和第5题,动态规划中居然出现三个for循环,这个时候,我们需要去观察动态规划位置依赖,如果存在重复计算的格子,那么我们就可以进行优化
算法之路继续前行,感谢网上大佬提供的学习资料