左神算法:从暴力递归到动态规划

1.暴力递归

递归行为是大问题和子问题同样的流程和标准,有自己的base case,递归代表尝试。我不知怎么算,知道怎么试

  1. n!问题
    怎么尝试,n!问题的子问题就是(n - 1)! * n,base case是什么,就是样本量划分到什么程度的时候就不用划分子问题了。n规模的问题,解决n - 1规模的问题,再去解决n - 2规模的问题,直到解决1规模的问题,如果尝试顺序定了的话,他就是这么个依赖关系
/*
 1. 求n!的结果
 */
public class Code_01_Factorial {
	public static long getFactorial(long N) {
		if(N == 1) {
			return N;
		}else {
			return (long)N * getFactorial(N - 1);
		}
	}
	
	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		long N = scanner.nextInt();
		long res = 1L;
		for (int i = 1; i <= N; i++) {
			res *= i;
		}
		System.out.println(res);
		res = getFactorial(N);
		System.out.println(res);
	}
	
}
  1. 汉诺塔问题:
    有左、中、右,左边有1,2,3三个圆盘,其中1最小放在上面,2比1大比3小放在中间,3最大放在下面,我们要把左边的三个圆盘移到右边,怎样走代价最低。不能大压小,只能小压大,

一上来只能动最上面的,因为2上面压着1.第一步是1从左边出来进右边,第二步是2从左边出来进中间,下一步1从右边出来套在2上,再下一步3从左边出来套在右边,下一步1从中间出来套在左边,然后2从中间出来套在3上,然后1从左边出来套在2上。只有三个杆,如果给你n个呢,n是这个汉诺塔在最左边有n层,怎样从左移动到右,怎样走代价最低。
把左、中、右看成from、to、help,from杆代表刚开始n层的汉诺塔在哪,to杆代表要移动到的杆,help杆是中间的辅助杆。那么把n层的汉诺塔从from移动到to,可以看成把n-1层的汉诺塔先从from–>help,再把第n个盘从from–>to,最后是把n-1层的汉诺塔从help–>to。
如果不能写成上面的,可以写出来六个过程,让他们之间彼此嵌套。
n层汉诺塔问题,走2^n - 1步,因为f(n)= f(n - 1) +1+f(n - 1),也就是f(n) = 2f(n - 1) + 1,这是一个等比数列的变形

/*
 * 汉诺塔问题:打印n层汉诺塔从最左边移动到最右边的全部过程
 */
public class Code_02_Hanoi {
	
	//构建这样的参数传进去
	public static void process(int N,String from,String to,String help) {
		if(N == 1) {
			System.out.println(N +" from "+from+" to "+to);
			return ;
		}
		process(N - 1, from,help, to);
		System.out.println(N +" from "+from+" to "+to);
		process(N - 1, help,to,from);
	}

	//只传一个参数
	public static void MoveLeftToRight(int N) {
		if(N == 1) {
			System.out.println("1 from Left to Right");
			return ;
		}
		MoveLeftToMiddle(N - 1);
		System.out.println(N +" from Left to Right");
		MoveMiddleToRight(N - 1);
	}
	
	private static void MoveMiddleToLeft(int N) {
		// TODO Auto-generated method stub
		if(N == 1) {
			System.out.println("1 Middle to Left");
			return ;
		}
		MoveMiddleToRight(N - 1);
		System.out.println(N + " from Middle to Left");
		MoveRightToLeft(N - 1);
	}

	private static void MoveRightToMiddle(int N) {
		// TODO Auto-generated method stub
		if(N == 1) {
			System.out.println("1 from Right to Middle");
			return ;
		}
		MoveRightToLeft(N - 1);
		System.out.println(N + " from Right to Middle");
		MoveLeftToMiddle(N - 1);
	}

	private static void MoveMiddleToRight(int N) {
		// TODO Auto-generated method stub
		if(N == 1) {
			System.out.println("1 from Middle to Right");
			return ;
		}
		MoveMiddleToLeft(N - 1);
		System.out.println(N + " from Middle to Right");
		MoveLeftToRight(N - 1);
		
	}

	private static void MoveLeftToMiddle(int N) {
		// TODO Auto-generated method stub
		if(N == 1) {
			System.out.println("1 from Left to Middle");
			return ;
		}
		MoveLeftToRight(N - 1);
		System.out.println(N+" from Left to Middle");
		MoveRightToMiddle(N - 1);
	}

	private static void MoveRightToLeft(int N) {
		// TODO Auto-generated method stub
		if(N == 1) {
			System.out.println("1 from Right To Left");
			return ;
		}
		MoveRightToMiddle(N - 1);
		System.out.println(N + " from Right to Middle");
		MoveMiddleToLeft(N - 1);
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		process(3, "left", "right", "middle");
		System.out.println("========");
		MoveLeftToRight(3);
	}

}
  1. 打印一个字符串的全部子序列,包括空字符串。

把脑海中的尝试写成code的能力就是尝试。对于这个假如每个位置上都可以选择要或不要,这样子往下走,直到base case,即最后来到了这个字符串的最后一个字符。一个子序列是你的相对顺序和原始顺序一样,但是可以不连续,也包括空字符串。

/*
 * 打印一个字符串的全部子序列,包括空字符串
 * 
 * 这个题的base case既可以写 i == str.length 也可以写i == str.length - 1
 * 只不过是不同的base case有不用的输出语句,只用清楚到那个地方问题不能再划分了就可以了
 */
public class Code_03_Print_All_Subsquences {
	
	public static void PrintAllSubsquences(char[] str,int i,String res) {
		if(i == str.length - 1) {
			System.out.println(res + " ");
			System.out.println(res + String.valueOf(str[i]));
			return;
		}
//		base case可以这样写,这样就是多划分了一步
//		if(i == str.length) {
//			System.out.println(res);
//			return ;
//		}
		PrintAllSubsquences(str, i + 1, res + String.valueOf(str[i]));
		PrintAllSubsquences(str, i + 1, res + " ");
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		PrintAllSubsquences("abc".toCharArray(),0,"");
	}

}

  1. 母牛问题
    有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。假设所有的牛都不会死。

f(n) = f(n - 1)+f(n - 3),这种的递归题目,不知道怎么试的时候,先列出前几项,递归是有个高度的结构化的解的。今年的牛的数量是什么呢:因为所有的牛都不会死,所以去年的牛会留下来,并且三年前的牛成熟了可以生小牛了,再加上三年前的牛数量。写出来几个初始项,试完后看他有没有道理。

public class Code_05_Cow {

	public static int cow(int N) {
		if(N < 0) {
			return 0;
		}
		if(N == 1||N == 2||N == 3) {
			return N;
		}
		return cow(N - 1)+cow(N - 3);
	}
	
	//进阶:每只母牛只能活10年,求N年后,母牛的数量
	public static int cow2(int n,int i) {
		if (n < 1) {
			return 0;
		}
		if (n == 1 || n == 2 || n == 3) {
			return n;
		}
		if(i >= 10) {
			return cow2(n - 1, i - 1) + cow2(n - 3, i - 1) - cow2(n - 10,i - 1);
		}else {
			return cow2(n - 1,i - 1) + cow2(n - 3,i - 1);
		}
	}
	
	public static void main(String[] args) {
		System.out.println(cow(13));
		System.out.println(cow2(13,13));
	}
}
  1. 逆序一个栈:
    给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?递归函数就是函数栈,所以自己不能申请栈,利用系统自动分配的栈。

这个题真的是对于递归的想法很好的一个题,先想一个问题,每次怎么删除的栈底元素,不借助额外的数据结构,只使用递归。然后在解决了这个问题的基础上再去逆序整个栈。

public class Code_06_ReverseStackUsingRecursive {
	
	public static void reverse(Stack<Integer> stack) {
		int r = getAndRemoveLastElement(stack);
		if(stack.isEmpty()) {
			stack.push(r);
			return ;
		}else {
			reverse(stack);
			stack.push(r);
		}
	}

	public static int getAndRemoveLastElement(Stack<Integer> stack) {
		int r = stack.pop();
		if(stack.isEmpty()) {
			return r;
		}
		int i = getAndRemoveLastElement(stack);
		stack.push(r);
		return i;
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Stack<Integer> test = new Stack<Integer>();
		test.push(1);
		test.push(2);
		test.push(3);
		test.push(4);
		test.push(5);
		reverse(test);
		while (!test.isEmpty()) {
			System.out.println(test.pop());
		}

	}

}

2.从暴力递归到动态规划

我是真的再听左神的课之前,一直都没有理解动态规划的表达,在网上也看了一些的帖子,但是都没有理解,左神讲的是真的好。
面试中遇到动态规划,如果之前遇到过就直接写动态规划的经典方法,没见过的,先写出递归的版本,尝试的版本后,得出动态规划的是高度套路。所有的动态规划都是从暴力递归改出来的。但是有些问题是改不成动态规划的,比如说汉诺塔问题,他的解空间就是所有的步骤,你要打印所有的步骤,改不成动态规划。因为没有重复计算。

  1. 从某个位置到矩阵最右下角的最短路径和:

如果来到了最右下角的位置,那么最短路径和就是他本身;如果来到了最后一行,那么当前的数只能往右走,那么当前位置到右下角的最短路径就变成了当前位置的值加上右边的位置到右下角的最短路径;那么到了最后一列就是只能往下走;那么普遍的情况就是既可以往下走,也可以往右走。往右走就是当前位置加上右边的位置到右下角的最短路径,往下走就是当前位置加上下面的位置到右下角的位置的最短路径,这两个中取最小值。

暴力递归不行的原因,在整个的过程中,出现的重复状态是非常多的。所以 我们有个最初始的思路,就是用一种机制,把(1,1)状态的返回值记录下来,作为一个缓存,当计算f(0,1)的时候,走过f(1,1)状态,如果下次再遇到f(1,1)状态的时候,直接从缓存里面拿出来,不就可以省时间了。所以就可以在递归里面改,做一个缓存,把(1,1)作为一个key,把他的返回值作为一个value,存起来。当下回进行递归之前,查一下这个key是不是已经算过了,如果算过了直接从map里面把值取出来,这叫记忆化搜索的方法。

什么版本的尝试递归可以改成动态规划,当递归展开的过程中有重复状态,而且这个重复状态与到达他的路径是没有关系的,那么他一定可以改成动态规划。就是不管怎么到达(1,1)这个位置,但是(1,1)到达右下角的位置的最短路径的返回值是一样的,这种问题我们说叫,无后效性问题,就是与到达这个状态的路径没有关系,之前怎么选择、做了什么决定没有关系,只要这个状态的参数定了,返回值一定确定。 这个真的超级关键就是能不能从暴力递归到动态规划的初始条件。

如果这个尝试函数,可变参数固定了,返回值一定是确定的,无后效性问题。一定可以改成动态规划。如果参数固定,返回值固定。那么参数的变化范围,这个题目的i,j的变化范围就是一张二维表。i就是行号的变化范围,j就是列号的变化范围。既然我们的i,j确定,返回值一定确定,所以我们把i的所有情况都列出来,作为行的对应,把j的所有情况都列出来,作为列的对应,那么所有的返回值一定可以装在一个二维表(我们叫dp)里面。然后看题目想要的状态是什么,这个题要的就是从(0,0)位置到右下角的最短路径,那么最终要的就是(0,0)位置的值,(0,0)位置要想得到他需要递归展开的过程。那么(0,0)状态就是最终要的状态,那么在这张表(有所有的返回值确定的表)中把这个位置标上,然后回到递归函数中,看看哪些位置上的值,是不依赖其他位置的,看什么,看base case,base case代表了什么,一个问题划到什么程度就不用往下划分了,直接答案就确定了。最右下角的位置是可以直接确定的路径和,就是matrix[i][j]的位置。那么就可以确定dp表中的最右下角的位置了,那么dp表中的最后一行和最后一列可以直接填出,根据上面递归的base case可以得到。

在一张表中,如果你的可变参数是二维的,那么dp表就是二维的;可变参数是三维的,那么dp表就是三维的;如果可变参数是一维的,那么dp表就是一维的,是数组。整个套路就是:①先点出需要的位置;②回到base case中把不被依赖的位置设置好;③分析一个普遍位置是怎么依赖的,回到递归中看,这个道题中一个普遍的位置,是依赖他的右边位置的状态和下边位置的状态。 推出普遍位置的依赖之后,反过去就是你整体的计算顺序,最后一行和最后一列弄好,中间的每个位置,从右到左再从下到上,依次推到顶部。就是答案。

先写出暴力版本,再分析可变参数,哪几个可变参数,可以代表返回值的状态,可变参数是几维的,他就是一张几维表(dp),看需要的终止状态是哪个在dp中点出来,回到base case中把完全不依赖的位置上的值设置好,然后一个普遍位置看看他需要哪些位置,那么逆着回去就是填表的顺序。

public class Code_07_MinPath {

	//递归版本
	public static int getMinPath(int[][] matrix,int i,int j) {
		if(i == matrix.length - 1 && j == matrix[0].length - 1) {
			return matrix[i][j];
		}
		if(i == matrix.length - 1) {
			return matrix[i][j] + getMinPath(matrix, i, j + 1);
		}
		if(j == matrix[0].length - 1) {
			return matrix[i][j] + getMinPath(matrix, i + 1, j);
		}
		int right = getMinPath(matrix, i, j + 1);
		int down = getMinPath(matrix, i + 1, j);
		return matrix[i][j] + Math.min(right, down);
	}
	
	//动态规划
	public static int getMinPath1(int[][] matrix) {
		if(matrix.length == 0 || matrix == null || matrix[0] == null || matrix[0].length == 0) {
			return 0;
		}
		int row = matrix.length;
		int col = matrix[0].length;
		int[][] dp = new int[row][col];
		//最后一列每一行的值根据base case确定了
		dp[row - 1][col - 1] = matrix[row - 1][col - 1];
		for (int i = row - 2; i >= 0; i--) {
			dp[i][col - 1] = matrix[i][col - 1] + dp[i + 1][col - 1];
		}
		//最后一行每一列的值确定
		for (int i = col - 2; i >= 0; i--) {
			dp[row - 1][i] = matrix[row - 1][i] + dp[row - 1][i + 1];
		}
		//根据普遍的规律,填满这个dp表
		for (int i = row - 2; i >= 0; i--) {
			for (int j = col - 2; j >= 0; j--) {
				dp[i][j] = Math.min(dp[i + 1][j], dp[i][j + 1]) + matrix[i][j];
			}
		}
		return dp[0][0];
	}
	
	public static void main(String[] args) {
		int[][] m = { { 1, 3, 5, 9 }, { 8, 1, 3, 4 }, { 5, 0, 6, 1 }, { 8, 8, 4, 0 } };
		System.out.println(getMinPath(m, 0, 0));
		System.out.println(getMinPath1(m));
	}
}
  1. 给你一个数组arr,和一个整数aim。如果可以任意选择arr中的数字,能不能累加得到aim,返回true或者false。(arr中全是正数)

思路和上面的一样 直接上code

public class Code_08_Money_Problem {

	//递归版本
	public static boolean process1(int[] arr,int aim,int res,int i) {
		if(i == arr.length) {
			return res == aim;
		}
		return process1(arr, aim, res, i + 1) || process1(arr, aim, res + arr[i], i + 1);
	}
	
	//动态规划
	public static boolean process2(int[] arr,int aim) {
		int sum = 0;
		for (int i = 0; i < arr.length; i++) {
			sum += arr[i];
		}
		if(aim > sum) {
			return false;
		}
		int row = arr.length;
		boolean[][] dp = new boolean[row + 1][sum + 1];
		dp[row][aim] = true;
		for (int i = row - 1; i > 0; i--) {
			for (int j = sum - 1; j > 0; j--) {
				if(j + arr[i] <= sum) {
					dp[i][j] = dp[i + 1][j] || dp[i+1][j + arr[i]];
				}
			} 
		}
		return dp[0][0];
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] arr = {3,6,8,8};
		int aim = 12;
		System.out.println(process1(arr, aim, 0, 0));
		System.out.println(process2(arr, aim));
	}

}
  1. 给定两个数组w和v,两个数组长度相等,w[i]表示第i件商品的重量,v[i]表示第i件商品的价值。再给定一个数组bag,要求你挑选商品的重量一定不能超过bag,返回满足这个条件下,你能获得的最大价值。

经典的背包问题

public class Code_09_Knapsack {

	// 递归
	public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {
		if (alreadyweight > bag) {
			return Integer.MIN_VALUE;// 这个地方不能写 return
										// 0;因为这个这个地方是当超过背包重量的时候上一个就不算的,但是上一个的value的值已经加过了,为了让他无效,需要使用系统最小值。
		}
		if (i == weights.length) {
			return 0;
		}
		return Math.max(values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag),
				process1(weights, values, i + 1, alreadyweight, bag));
	}
	
	//动态规划
	public static int process2(int[] weights,int[] values,int bag) {
		int row = weights.length;
		int[][] dp = new int[row + 1][bag];
		for (int i = 0; i < dp[0].length; i++) {
			dp[row][i] = 0;
		}
		for (int i = row - 1; i >= 0; i--) {
			for (int j = 0; j < dp[0].length; j++) {
				if((j + weights[i]) < bag) {//*****  这个地方的条件要注意,在将递归改成动态规划的时候,一定要想清楚dp表里面的数代表是啥。
					dp[i][j] = Math.max(dp[i+1][j], dp[i+1][j+weights[i]] + values[i]);
				}
			}
		}
		return dp[0][0];
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] c = { 3, 2 };
		int[] p = { 5, 6 };
		int bag = 4;
		int value = process1(c, p, 0, 0, bag);
		System.out.println(value);
		int value2 = process2(c, p, bag);
		System.out.println(value2);
	}

}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值