算法笔记——左神初级(10)暴力递归与动态规划、汉诺塔、数组累加和

从暴力递归到动态规划

动态规划就是优化的暴力递归。
在这里插入图片描述


题目1:求n!

递归思路: n*(n-1)!

public static long getFactorial1(int n) {
		if (n == 1) {
			return 1L;
		}
		return (long) n * getFactorial1(n - 1);
	}

非递归思路: 是递归思路的逆顺序

public static long getFactorial2(int n) {
		long result = 1L;
		for (int i = 1; i <= n; i++) {
			result *= i;
		}
		return result;
	}

题目2:汉诺塔问题:打印n层汉诺塔从最左边移动到最右边的全部过程

如下图所示三个竹竿,最开始最左边有n层汉诺塔,要将其全部移动到最右边的竹竿上,在移动的过程中只能小压大,不能大压小。
在这里插入图片描述

核心思路:把问题转化为规模缩小的同类子问题

现在分为三个杆,from,to,help。
具体步骤如下:

  1. 将上面n-1全部挪到help上
  2. 将n挪到to上去
  3. 将n-2挪到from上,将n-1挪到to上去
  4. 循环,直到N==1

代码如下:

//N:1-N
//
public static void process(int N ,String from,String to,String help){
	if (N == 1){
		System.out.println("Move 1 from"+from+"to"+to);
	}else{
		process(N-1,from,help,to);
		System.out.println("Move "+N+" from"+from+"to"+to);
		process(N-1,help,to,from);
	}
}

题目3:打印一个字符串的全部子序列,包括空字符串。

尝试过程:字符串共有多少长度,对于每一位,都有两个决策,要该位字符或者不要,所以总的可能性2^N。

PS分清楚什么是子串,什么是子序列

看文章前首先要搞清楚什么是子序列,什么是子串;子序列是指一个字串中非连续的字串,例如:字串A:123456789 它有一个子序列a:13579(非连续) 它有一个子串b:12345(连续)。

代码如下:每一次递归两条路都走

public static void printAllSub(char[] Str,int i,String res){
	if(i == str.length){
		System.out.println(res);
		return;
	}
	//不要当前字符
	printAllSub(str,i+1,res);
	//要当前字符
	printAllSub(str,i+1,res+String.valueof(str[i]));
}

题目4:牛群繁衍数量问题

母牛每年生一只母牛,新出生的母牛成长三年后也可以每年生一只母牛,假设母牛不会死,求N年后,母牛的数量。假设最开始母牛是A(就一个牛)。
在这里插入图片描述
【思路】列出前几项,可以找到规律,递归问题高度规律性
F(n) = F(n-1)+F(n-3)。今年的牛=去年的牛+三年前的牛数量(三年前是今年能生育牛)。

题目5:数组最小路径(暴力递归&动态规划)

给定一个二维数组,二维数组中的每个数都是正数,要求从左上角到右下,每一步只能向右或向下,沿途经过的数字要累加起来,返回最小路径和。

【思路】暴力递归:先考虑右下方最后一格,再考虑到达下边界或右边界的情况,最后考虑一般情况,进行递归。
【代码如下】

public static int walk(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] + walk(matrix,i,j+1);
	}
	if(j == matrix[0].length-1){
		return matrix[i]ij]+ walk(matrix,i+1,j);
	}
	int right = walk(matrix,i,j+1);//右边位置到右下角的最短路径和
	int down = walk(matrix,i+1,j);//下边位置到右下角最短路径和
	return matrix[i][j] + Math.min(right,down);
} 

上述暴力递归中存在很多的重复计算(一个先向下再像右,一个先向右再向下,此时都会到(1,1)位置,上述题目会存在重复计算)
当发现递归中存在重复状态,并且重复状态和与到达他的路径没有关系的时候,可以将递归改成动态规划

动态规划要求点无后效性,也就是说当给出了参数之后,结果唯一。如果给定参数之后,返回值不是唯一确定的时候,此时成为有后效性。(比如要求记录路径的时候,路径方式不一定唯一)

改为 动态规划:
------对于这道题目,可以按照上述代码,可以首先将最后一行中到右下角的距离挨个计算出,此时为最后一行的点到右下角的距离。也要先将最后一列中到右下角的距离挨个算出,为最后一列的点到右下角的距离。此时可以从右到左,再从下到上,就可以 计算出整个距离。 用一个二维表把整个返回值装下来

暴力递归改成动态规划的步骤: 空间换时间

  1. 先写出一个暴力递归(尝试版本)
  2. 确定无后效性,列出可变参数(哪几个可变参数可以代表返回值的状态)可变参数几维的那就是一张几维的表。
  3. 看最后需要终止的位置是哪一个,在表中确定出来。回到baseCase中,将完全不依赖的值设置好(本题中就是设置最后一行和最后一列)
  4. 最后普遍位置看看需要哪些位置,逆序返回,就是填表的顺序。
  5. 最后就可以将暴力递归改成动态规划。

题目6:数组与累加和

给定一个数组arr和一个整数aim,如果可以任意选择arr中的数字,能不能累加得到aim,返回true或者false。

动态规划的套路:

使用动态规划套路的一个原则是能想出递归的解法。由递归来改成动态规划。

【步骤】
第一步: 写出递归的“试”法,看看如何尝试可以解决问题。
思路:设置sum,对于每一个数组中的数字,都可以进行判断,是否要当前的数字,分为两种情况,然后最后和aim相比较。如果发现最后的结果中存在aim,则返回true。

//{3,1,4,2,7}
public static boolean isSum(int[] nums, int i, int sum, int target){
	if(i == nums.length-1)
		return sum == target;
		//这个方法会遍历所有的子集合
	return isSum(nums, i+1, sum, target)||  //注意这个||
	isSum(nums, i+1, sum+nums[i], target);
	}

第二步: 判断是否是无后效性问题,分析是否有后效性:之前形成的累加和确定跟之后的数组无关,分析可变参数:i,sum是可变参数,arr和aim确定,所以建立二维表(i,sum)。

第三步:结合暴力版本中i==arr.length,所以i为0~N,起始位置为(0,0)。最后一行中只有sum等于aim上的值时返回true。

第四步:子过程i+1,所以可以从最后一行推出倒数第二行,上一行的位置有sum的位置和sum+arr[i]确定,从而每个位置都可以推出来。

DP的代码不过就是把刷表的过程模拟了一遍而已。

	public static boolean isSumByDP(int target,int[] arrs){
		
		int sum = 0;
		for(int i = 0; i < arrs.length; i++)
		{
			sum += arrs[i];
		}
		if(target > sum)return false;//所有值加起来都没有目标值大,直接返回false
		boolean[][] dp = new boolean[arrs.length+1][sum+1];
		 for (int j = 0; j <= sum; j++) {//先将最后一行已经知道结果的填充进去
	            dp[arrs.length][j] = j==target;
	        }
		for(int i = arrs.length - 1; i >= 0; i--)
			for(int j = 0; j <= sum; j++)
			{

				if(j + arrs[i] <= sum){//不超出部分,j+arrs[i]表示当前叠加值加上该位置值
					dp[i][j] = dp[i+1][j] || dp[i+1][j+arrs[i]];//选中和不选中方案
				}
			}

		return dp[0][0];
	}
发布了27 篇原创文章 · 获赞 4 · 访问量 817

猜你喜欢

转载自blog.csdn.net/qq_25414107/article/details/104584090