算法学习系列(9)—— 递归与动态规划

1.介绍递归和动态规划

暴力递归
1,把问题转化为规模缩小了的同类问题的子问题
2,有明确的不需要继续进行递归的条件(base case)
3,有当得到了子问题的结果之后的决策过程
4,不记录每一个子问题的解
动态规划
1,从暴力递归中来
2,将每一个子问题的解记录下来,避免重复计算
3,把暴力递归的过程,抽象成了状态表达
4,并且存在化简状态表达,使其更加简洁的可能

1.1 引入题目:n!

public class Code_01_Factorial {

    public static int getFactorial1(int n){
        if (n <= 0){
            return 0;
        }
        int res = 1;
        for(int i = 1;i <= n;i++){
            res *= i;
        }
        return res;
    }

    public static int getFactorial2(int n){
        if(n == 1){
            return 1;
        }
        return n * getFactorial2(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(getFactorial1(10));
        System.out.println(getFactorial2(10));
    }

}

1.2 经典的汉诺塔问题

打印n层汉诺塔从最左边移动到最右边的全部过程
【解题】
1)1到n-1个从from租组移动到help组
2)单独地把from上的n移动到to上去
3)把1到n-1从help上移动到to上去
三层的图解:
在这里插入图片描述
代码:

public class Code_02_Hannoi {

    public static void process(int N,String from,String help,String to){
        if(N == 1){
            System.out.println("Move " + N + " from " + from + " to "+ to);
        }else {
            process(N-1,from,to,help);//第一步:把1到n-1从from移动到help组上去
            System.out.println("Move " + N + " from " + from + " to "+ to);
            process(N-1,help,from,to);//第三步:把1到n-1从help上移动到to上去
        }
    }

    public static void main(String[] args) {
        int N = 3;
        process(N,"左边","中间","右边");
    }
}

结果:
在这里插入图片描述
需要的时间复杂度分析:
T(n) = T(n-1)+1+T(n-1)=2T(n-1)+1 ==>
T(n) + 1= 2*(T(n-1)+1) ==>
(T(n) + 1)/(T(n-1)+1) = 2 ==>T(n) + 1是一个以2为底的等比数列,T(n) = 2^n-1
也就是说利用上边的递归解决汉诺塔问题需要2^n-1步 ==>该算法的时间复杂度为O(2 ^ n)

1.3 打印一个字符串的全部子序列,包括空字符串

public class Code_03_PrintSubString {

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

    public static void main(String[] args) {
        String s = "abc";
        printSubStr(s.toCharArray(),0,"");
    }

}

1.4 打印一个字符串的全部排列

进阶:打印一个字符串的全部排列,要求不要出现重复的排列
两个的代码:

public class Code_04_Print_All_Permutations {

	public static void printAllPermutations1(String str) {
		char[] chs = str.toCharArray();
		process1(chs, 0);
	}

	public static void process1(char[] chs, int i) {//交换的是当前位置之后的元素
		if (i == chs.length) {
			System.out.println(String.valueOf(chs));
		}
		for (int j = i; j < chs.length; j++) {//这个的理解是难点
			swap(chs, i, j);
			process1(chs, i + 1);
			//swap(chs, i, j);
		}
	}

	public static void printAllPermutations2(String str) {
		char[] chs = str.toCharArray();
		process2(chs, 0);
	}

	public static void process2(char[] chs, int i) {
		if (i == chs.length) {
			System.out.println(String.valueOf(chs));
		}
		HashSet<Character> set = new HashSet<>();
		for (int j = i; j < chs.length; j++) {
			if (!set.contains(chs[j])) {
				set.add(chs[j]);
				swap(chs, i, j);
				process2(chs, i + 1);
				//swap(chs, i, j);
			}
		}
	}

	public static void swap(char[] chs, int i, int j) {
		char tmp = chs[i];
		chs[i] = chs[j];
		chs[j] = tmp;
	}

	public static void main(String[] args) {
		String test1 = "abc";
		printAllPermutations1(test1);
		System.out.println("======");
		printAllPermutations2(test1);
		System.out.println("======");

		String test2 = "acc";
		printAllPermutations1(test2);
		System.out.println("======");
		printAllPermutations2(test2);
		System.out.println("======");
	}

}

1.5 母牛生母牛问题

母牛每年生一只母牛,新出生的母牛成长三年后也能每年生一只 母牛,假设不会死。求N年后,母牛的数量。
在这里插入图片描述
增长的规律是:1, 2 , 3 , 4 , 6 , 9

public class Code_05_Cow {
	//递归版本
	public static int cowNumber1(int n) {
		if (n < 1) {
			return 0;
		}
		if (n == 1 || n == 2 || n == 3) {
			return n;
		}
		return cowNumber1(n - 1) + cowNumber1(n - 3);
	}
	//非递归版本
	public static int cowNumber2(int n) {
		if (n < 1) {
			return 0;
		}
		if (n == 1 || n == 2 || n == 3) {
			return n;
		}
		int res = 3;
		int pre = 2;
		int prepre = 1;
		int tmp1 = 0;
		int tmp2 = 0;
		for (int i = 4; i <= n; i++) {
			tmp1 = res;
			tmp2 = pre;
			res = res + prepre;
			pre = tmp1;
			prepre = tmp2;
		}
		return res;
	}

	public static void main(String[] args) {
		int n = 20;
		System.out.println(cowNumber1(n));
		System.out.println(cowNumber2(n));
	}

}

进阶:如果每只母牛只能活10年,求N年后,母牛的数量。
在这里插入图片描述

1.6 给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。如何实现?

实现得到栈中最后一个元素并返回的递归调用过程:
在这里插入图片描述
实现逆序当前栈的功能:
在这里插入图片描述

public class Code_06_ReverseStackUsingRecursive {

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

	public static int getAndRemoveLastElement(Stack<Integer> stack) {
		int result = stack.pop();
		if (stack.isEmpty()) {
			return result;
		} else {
			int last = getAndRemoveLastElement(stack);
			stack.push(result);
			return last;
		}
	}

	public static void main(String[] args) {
		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.首先写出尝试版本,也就是暴力递归实现的版本
2.列出可变参数范围,在最小路径和中可变参数就是i,j i,ji,j,所以空间表为二维,这里变换参数为i,sum i,sumi,sum,也是两个值,所以空间记录表也是两维的。范围,i就是arr的长度,sum最大范围就是所有数字加起来的和
3.标记终止位置,也就是最后要得到的哪个位置的值
4.根据base case整理出空间表的哪些位置可以提前填好
5.最后普遍位置的值依赖哪些位置,将普通位置的结果填到空间表中

2.1返回二维数组的最小的路径和

给你一个二维数组,二维数组中的每个数都是正数,要求从左上 角走到右下角,每一步只能向右或者向下。沿途经过的数字要累 加起来。返回最小的路径和。
先使用暴力递归解:

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 getMinPath2(int[][] matrix){
        if(matrix == null || matrix.length < 1 || matrix[0] == null || matrix[0].length < 1){
            return 0;
        }
        int row = matrix.length;
        int col = matrix[0].length;
        int[][] dp = new int[row][col];
        dp[0][0] = matrix[0][0];
        for(int i = 1;i < row;i++){//填满第一列
            dp[i][0] = dp[i - 1][0] + matrix[i][0];
        }
        for(int j = 1;j < col;j++){//填满第一行
            dp[0][j] = dp[0][j - 1] + matrix[0][j];
        }
        for(int i = 1;i < row;i++){
            for(int j = 1;j < col;j++){
                dp[i][j] = Math.min(dp[i][j - 1],dp[i - 1][j]) + matrix[i][j];
            }
        }
        return dp[row - 1][col - 1];
    }

	//test
	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));//12
        System.out.println(getMinPath2(m));//12
    }

2.2.数组与目标数

给你一个数组arr,和一个整数aim。如果可以任意选择arr中的 数字,能不能累加得到aim,返回true或者false。(aim与arr中都是正数)
在这里插入图片描述
上述图的Demo:

public class Code_08_GetTargetNum {

    public static boolean isSum(int[] arr,int target){
        return getTarget(arr,0,0,target);
    }

    public static boolean getTarget(int[] arr,int i,int sum,int target){
        if(sum == target){
            return true;
        }
        //sum!=target
        if(i == arr.length){
            return false;
        }
        return getTarget(arr,i + 1,sum,target)
                || getTarget(arr,i + 1,sum + arr[i],target);
    }

    public static void main(String[] args) {
        int[] arr = {1,2,3};
        int target = 6;
        System.out.println(isSum(arr,target));
    }
}

下边使用动态规划来解决这个问题:

1.写出尝试(递归)版本

上边的代码已经实现。

2.分析可变参数,哪几个可变参数的值能代表返回状态,几个可变参数,就构造几维表。

数组和aim不可变,i,sum可变,本题为后效性问题

3.看base case,列出不依赖的位置。

二维表行代表sum,列代表i。
在这里插入图片描述

4.分析一个普遍位置的依赖。

return isSum1(arr, i + 1, sum, aim) || isSum1(arr, i + 1, sum + arr[i], aim);

要知道一个普遍位置的值,就得知道它的下一行的值和下一行,向右加arr[i]列的值。

最后一列和最后一行知道了,反过来就能填完了整张表。
在这里插入图片描述
使用动态规划的代码:

public static boolean isSum2(int[] arr,int target){
        boolean[][] dp = new boolean[arr.length + 1][target + 1];
        for(int i = arr.length;i >= 0;i--){
            dp[i][target] = true;
        }
        for(int i = arr.length - 1;i >= 0;i--){
            for (int j = target - 1;j >= 0;j--){
                dp[i][j] = dp[i + 1][j];
                if(j + arr[i] <= target){
                   dp[i][j] = dp[i][j] || dp[i + 1][j + arr[i]];
                }
            }
        }
        return dp[0][0];
    }

2.3经典背包问题

给定两个数组w和v,两个数组长度相等,w[i]表示第i件商品的 重量,v[i]表示第i件商品的价值。 再给定一个整数bag,要求你挑选商品的重量加起来一定不能超 过bag,返回满足这个条件下,你能获得的最大价值。
【思路】和上边的题目分析思路基本是一样的。

public class Code_09_Knapsack {

	public static int maxValue1(int[] c, int[] p, int bag) {
		return process1(c, p, 0, 0, bag);
	}

	public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {
		if (alreadyweight > bag) {
			return 0;
		}
		if (i == weights.length) {
			return 0;
		}
		return Math.max(//就分两种情况,就分两种情况,一种是需要当前商品的,一种是不需要的
				
				process1(weights, values, i + 1, alreadyweight, bag),
				
				values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag));
	}

	public static int maxValue2(int[] c, int[] p, int bag) {
		int[][] dp = new int[c.length + 1][bag + 1];
		for (int i = c.length - 1; i >= 0; i--) {
			for (int j = bag; j >= 0; j--) {
				dp[i][j] = dp[i + 1][j];
				if (j + c[i] <= bag) {
					dp[i][j] = Math.max(dp[i][j], p[i] + dp[i + 1][j + c[i]]);
				}
			}
		}
		return dp[0][0];
	}

	public static void main(String[] args) {
		int[] c = { 3, 2, 4, 7 };
		int[] p = { 5, 6, 3, 19 };
		int bag = 11;
		System.out.println(maxValue1(c, p, bag));
		System.out.println(maxValue2(c, p, bag));
	}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值