从暴力递归到动态规划
动态规划就是优化的暴力递归。
题目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。
具体步骤如下:
- 将上面n-1全部挪到help上
- 将n挪到to上去
- 将n-2挪到from上,将n-1挪到to上去
- 循环,直到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)位置,上述题目会存在重复计算)
当发现递归中存在重复状态,并且重复状态和与到达他的路径没有关系的时候,可以将递归改成动态规划
动态规划要求点无后效性,也就是说当给出了参数之后,结果唯一。如果给定参数之后,返回值不是唯一确定的时候,此时成为有后效性。(比如要求记录路径的时候,路径方式不一定唯一)
改为 动态规划:
------对于这道题目,可以按照上述代码,可以首先将最后一行中到右下角的距离挨个计算出,此时为最后一行的点到右下角的距离。也要先将最后一列中到右下角的距离挨个算出,为最后一列的点到右下角的距离。此时可以从右到左,再从下到上,就可以 计算出整个距离。 用一个二维表把整个返回值装下来
暴力递归改成动态规划的步骤: 空间换时间
- 先写出一个暴力递归(尝试版本)
- 确定无后效性,列出可变参数(哪几个可变参数可以代表返回值的状态)可变参数几维的那就是一张几维的表。
- 看最后需要终止的位置是哪一个,在表中确定出来。回到baseCase中,将完全不依赖的值设置好(本题中就是设置最后一行和最后一列)
- 最后普遍位置看看需要哪些位置,逆序返回,就是填表的顺序。
- 最后就可以将暴力递归改成动态规划。
题目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];
}