牛客网算法基础提升班---第7、8课---暴力递归到动态规划

暴力递归到动态规划

动态规划就是暴力尝试减少重复计算的技巧整,而已这种技巧就是一个大型套路先写出用尝试的思路解决问题的递归函数,而不用操心时间复杂度这个过程是无可替代的,没有套路的,只能依靠个人智慧,或者足够多的经验。

但是怎么把尝试的版本,优化成动态规划,是有固定套路的,大体步骤如下:

  • 1)找到什么可变参数可以代表一个递归状态,也就是哪些参数一旦确定,返回值就确定了
  • 2)把可变参数的所有组合映射成一张表,有 1 个可变参数就是一维表,2 个可变参数就是二维表,…
  • 3)最终答案要的是表中的哪个位置,在表中标出
  • 4)根据递归过程的 base case,把这张表的最简单、不需要依赖其他位置的那些位置填好值
  • 5)根据递归过程非base case的部分,也就是分析表中的普遍位置需要怎么计算得到,那么这张表的填写顺序也就确定了
  • 6)填好表,返回最终答案在表中位置的值

机器人达到指定位置方法数

【题目】
假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于 2。开始时机器人在其中的 M 位置上(M 一定是 1-N 中的一个),机器人可以往左走或者往右走,如果机器人来到 1 位置, 那么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到 N-1 位置。
规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是 1~N 中的一个)的方法有多少种。给定四个参数 N、M、K、P,返回方法数。

【举例】
N=5,M=2,K=3,P=3
上面的参数代表所有位置为 1 2 3 4 5。机器人最开始在 2 位置上,必须经过 3 步,最后到达 3 位置。走的方法只有如下 3 种: 1)从2到1,从1到2,从2到3 2)从2到3,从3到2,从2到33)从2到3,从3到4,从4到3
所以返回方法数 3。 N=3,M=1,K=3,P=3上面的参数代表所有位置为 1 2 3。机器人最开始在 1 位置上,必须经过 3 步,最后到达 3位置。怎么走也不可能,所以返回方法数 0。

【暴力尝试】


import java.util.*;

public class Main {

	public static int robot(int n, int m, int k, int p){
		if(n < 2 || m < 1 || m > n || k < 1 || p <1 || p > n){
			return 0;
		}
		return process(n, m, k, p);
	}

	//在m位置,走k次,走到p的方法数?
	public static int process(int n, int m, int k, int p){
		
		if(k == 0){
			return m == p ? 1 : 0;
		}
		if(m == 1){
			return process(n, m+1, k-1, p);
		}
		if(m == n){
			return process(n, --m, --k, p);
		}
		return process(n, m+1, k-1, p) + process(n, m-1, k-1, p);
	}

	public static void main(String[] args) {
		System.out.println(process(5,2,3,3));
	}
}

【转成dp】
在这里插入图片描述`
import java.util.*;

public class Main {

public static int robot(int n, int m, int k, int p){
	if(n < 2 || m < 1 || m > n || k < 1 || p <1 || p > n){
		return 0;
	}
	return process(n, m, k, p);
}

//在m位置,走k次,走到p的方法数?
public static int process(int n, int m, int k, int p){
	
	if(k == 0){
		return m == p ? 1 : 0;
	}
	if(m == 1){
		return process(n, m+1, k-1, p);
	}
	if(m == n){
		return process(n, m-1, k-1, p);
	}
	return process(n, m+1, k-1, p) + process(n, m-1, k-1, p);
}

public static int dp(int n, int m, int k, int p){
	if(n < 2 || m < 1 || m > n || k < 1 || p <1 || p > n){
		return 0;
	}
	int[][] d = new int[k+1][n+1];
	for(int i = 1; i <= n; i++){
		d[0][i] = (i == p ? 1 : 0);
	}
	for(int i = 1; i <= k; i++){
		for(int j = 1; j <=n; j++){
			if(j == 1){
				d[i][j] = d[i-1][j+1];
			}else if(j == n){
				d[i][j] = d[i-1][j-1];
			}else{
				d[i][j] = d[i-1][j+1] + d[i-1][j-1];
			}
		}
	} 
	return d[k][m];
}

public static void main(String[] args) {
	System.out.println(robot(5,2,3,3));
	System.out.println(dp(5,2,3,3));
}

}`

换钱的最少货币数

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

package class07;

public class Code02_CoinsMin {

	public static int minCoins1(int[] arr, int aim) {
		if (arr == null || arr.length == 0 || aim < 0) {
			return -1;
		}
		return process(arr, 0, aim);
	}

	// 当前考虑的面值是arr[i],还剩rest的钱需要找零
	// 如果返回-1说明自由使用arr[i..N-1]面值的情况下,无论如何也无法找零rest
	// 如果返回不是-1,代表自由使用arr[i..N-1]面值的情况下,找零rest需要的最少张数
	public static int process(int[] arr, int i, int rest) {
		// base case:
		// 已经没有面值能够考虑了
		// 如果此时剩余的钱为0,返回0张
		// 如果此时剩余的钱不是0,返回-1
		if (i == arr.length) {
			return rest == 0 ? 0 : -1;
		}
		// 最少张数,初始时为-1,因为还没找到有效解
		int res = -1;
		// 依次尝试使用当前面值(arr[i])0张、1张、k张,但不能超过rest
		for (int k = 0; k * arr[i] <= rest; k++) {
			// 使用了k张arr[i],剩下的钱为rest - k * arr[i]
			// 交给剩下的面值去搞定(arr[i+1..N-1])
			int next = process(arr, i + 1, rest - k * arr[i]);
			if (next != -1) { // 说明这个后续过程有效
				res = res == -1 ? next + k : Math.min(res, next + k);
			}
		}
		return res;
	}

	public static int minCoins2(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];
		// 设置最后一排的值,除了dp[N][0]为0之外,其他都是-1
		for (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] = -1; // 初始时先设置dp[i][rest]的值无效
				if (dp[i + 1][rest] != -1) { // 下面的值如果有效
					dp[i][rest] = dp[i + 1][rest]; // dp[i][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],
								dp[i][rest - arr[i]] + 1);
					}
				}
			}
		}
		return dp[0][aim];
	}

	// for test
	public static int[] generateRandomArray(int len, int max) {
		int[] arr = new int[(int) (Math.random() * len) + 1];
		for (int i = 0; i < arr.length; i++) {
			arr[i] = (int) (Math.random() * max) + 1;
		}
		return arr;
	}

	public static void main(String[] args) {
		int len = 10;
		int max = 10;
		int testTime = 10000;
		for (int i = 0; i < testTime; i++) {
			int[] arr = generateRandomArray(len, max);
			int aim = (int) (Math.random() * 3 * max) + max;
			if (minCoins1(arr, aim) != minCoins2(arr, aim)) {
				System.out.println("ooops!");
				break;
			}
		}
	}
}

排成一条线的纸牌博弈问题

【题目】
给定一个整型数组 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。


import java.util.*;

public class Main {

	public static int win1(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
	}

	public static int f(int[] arr, int i, int j) {
		if (i == j) {
			return arr[i];
		}
		return Math.max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));
	}

	public static int s(int[] arr, int i, int j) {
		if (i == j) {
			return 0;
		}
		return Math.min(f(arr, i + 1, j), f(arr, i, j - 1));
	}

	public static int win2(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		int[][] f = new int[arr.length][arr.length];
		int[][] s = new int[arr.length][arr.length];
		for (int j = 0; j < arr.length; j++) {
			f[j][j] = arr[j];
			for (int i = j - 1; i >= 0; i--) {
				f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
				s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
			}
		}
		return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
	}

	public static void main(String[] args) {
		int[] arr = { 1, 9, 1 };
		System.out.println(win1(arr));
		System.out.println(win2(arr));

	}
}

象棋中马的跳法

【题目】
请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下角是(0,0)位置。那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。给你三个参数,x,y,k,返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数有多少种?

package class07;

public class Code04_HorseJump {

	public static int getWays(int x, int y, int step) {
		return process(x, y, step);
	}

	public static 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 + 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);
	}

	public static int dpWays(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];
		dp[0][0][0] = 1;
		for (int h = 1; h <= step; h++) {
			for (int r = 0; r < 9; r++) {
				for (int c = 0; c < 10; c++) {
					dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
					dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
				}
			}
		}
		return dp[x][y][step];
	}

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

	public static void main(String[] args) {
		int x = 7;
		int y = 7;
		int step = 10;
		System.out.println(getWays(x, y, step));
		System.out.println(dpWays(x, y, step));
	}
}

Bob的生存概率

【题目】
给定五个参数n,m,i,j,k。表示在一个N*M的区域,Bob处在(i,j)点,每次Bob等概率的向上、下、左、右四个方向移动一步,Bob必须走K步。如果走完之后,Bob还停留在这个区域上,就算Bob存活,否则就算Bob死亡。请求解Bob的生存概率,返回字符串表示分数的方式。

package class07;

public class Code05_BobDie {

	public static String bob1(int N, int M, int i, int j, int K) {
		long all = (long) Math.pow(4, K);
		long live = process(N, M, i, j, K);
		long gcd = gcd(all, live);
		return String.valueOf((live / gcd) + "/" + (all / gcd));
	}

	public static 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) {
			return 1;
		}
		long live = process(N, M, row - 1, col, rest - 1);
		live += process(N, M, row + 1, col, rest - 1);
		live += process(N, M, row, col - 1, rest - 1);
		live += process(N, M, row, col + 1, rest - 1);
		return live;
	}

	public static long gcd(long m, long n) {
		return n == 0 ? m : gcd(n, m % n);
	}

	public static String bob2(int N, int M, int i, int j, int K) {
		int[][][] dp = new int[N + 2][M + 2][K + 1];
		for (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));
	}

	public static void main(String[] args) {
		int N = 10;
		int M = 10;
		int i = 3;
		int j = 2;
		int K = 5;
		System.out.println(bob1(N, M, i, j, K));
		System.out.println(bob2(N, M, i, j, K));
	}

}

矩阵的最小路径和—讲解状态压缩

下面这个题目会给出状态压缩,对空间进行优化。空间优化其实就是覆盖掉了一些值。覆盖掉了就找不到原来的路径信息了哦。

【题目】
给定一个矩阵 m,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。
【举例】
如果给定的 m 如下: 1359 8134 5061 8840
路径 1,3,1,0,6,1,0 是所有路径中路径和最小的,所以返回12

【求解】
1.暴力递归
2.改成dp
在这里插入图片描述
3.状态压缩


import java.util.*;

public class Main {
	
	
	public static int minPathSum0(int[][] a){
		if(a == null || a.length == 0 || a[0] == null || a[0].length == 0){
			return 0;
		}
		int row = a.length;
		int col = a[0].length;
		return process(a, 0, 0, row, col);
		
	}
	//1.首先暴力递归尝试,主要是找准递归结束条件和递归方程
			//从a[i][j]出发(包含a[i][j])到右下角节点的路径最小值
	public static int process(int[][] a, int i, int j, int row, int col){
		if(i == row - 1 && j == col - 1){
			return a[i][j];
		}else if(i == row - 1){
			return a[i][j] + process(a, i, j+1, row, col);
		}else if(j == col - 1){
			return a[i][j] + process(a, i+1, j, row, col);
		}else{
			return Math.min(a[i][j] + process(a, i+1, j, row, col),
					a[i][j] + process(a, i, j+1, row, col));
		}
	}
	//2.然后把暴力递归改写成动态规划(分析变量和不变量,搞出表,初始化,依赖分析,推导)
	public static int minPathSumDP0(int[][] a){
		if(a == null || a.length == 0 || a[0] == null || a[0].length == 0){
			return 0;
		}
		int row = a.length;
		int col = a[0].length;
		int[][] d = new int[row][col];
		d[row-1][col-1] = a[row-1][col-1];
		for(int i = row - 1; i >= 0; i--){
			for(int j = col - 1; j >= 0; j--){
				if(i == row - 1 && j + 1 < col){
					d[i][j] = d[i][j+1] + a[i][j];
				}
				if(j == col - 1 && i + 1 < row){
					d[i][j] = d[i+1][j] + a[i][j];
				}
				if(i != row - 1 && j != col - 1){
					d[i][j] = Math.min(a[i][j] + d[i+1][j], a[i][j] + d[i][j+1]);
				}
			}
		}
		return d[0][0];
		
	}
	//3.对动态规划状态进行压缩(根据原始动态规划,观察依赖关系,看看是否需要借助临时变量,本题不需要)
	public static int minPathSumDP1(int[][] a){
		if(a == null || a.length == 0 || a[0] == null || a[0].length == 0){
			return 0;
		}
		int row = a.length;
		int col = a[0].length;
		int[] d = new int[col];
		d[col-1] = a[row-1][col-1];

		for(int i = row - 1; i >= 0; i--){
			for(int j = col - 1; j >= 0; j--){
				
				if(i == row - 1 && j + 1 < col){
					d[j] = a[i][j] + d[j+1];
				}
				if(j == col - 1 && i + 1 < row){
					d[j] = d[j] + a[i][j];
				}
				if(i != row - 1 && j != col - 1){
					d[j] = Math.min(d[j+1], d[j]) + a[i][j];
				}
			}
		}
		return d[0];
		
	}
	
	
	public static int[][] generateRandomMatrix(int rowSize, int colSize) {
		if (rowSize < 0 || colSize < 0) {
			return null;
		}
		int[][] result = new int[rowSize][colSize];
		for (int i = 0; i != result.length; i++) {
			for (int j = 0; j != result[0].length; j++) {
				result[i][j] = (int) (Math.random() * 10);
			}
		}
		return result;
	}

	// for test
	public static void printMatrix(int[][] matrix) {
		for (int i = 0; i != matrix.length; i++) {
			for (int j = 0; j != matrix[0].length; j++) {
				System.out.print(matrix[i][j] + " ");
			}
			System.out.println();
		}
	}

	public static void main(String[] args) {
		int N = 500000;
		boolean flag = true;
		for(int i = 0; i < N; i++){
			int[][] m = generateRandomMatrix(3, 4);
			if(minPathSumDP0(m) != minPathSumDP1(m)){
				printMatrix(m);
				System.out.println(minPathSumDP0(m));
				System.out.println(minPathSumDP1(m));
				flag = false;
				break;
			}
			if(minPathSumDP0(m) != minPathSum0(m)){
				printMatrix(m);
				System.out.println(minPathSumDP0(m));
				System.out.println(minPathSum0(m));
				flag = false;
				break;
			}
			if(minPathSumDP1(m) != minPathSum0(m)){
				printMatrix(m);
				System.out.println(minPathSumDP1(m));
				System.out.println(minPathSum0(m));
				flag = false;
				break;
			}
		}
		System.out.println(flag);
	}
}

参考资料

牛客网左神(左程云)算法基础提升班课程资料

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值