算法<初级> - 第五章 递归与动规相关问题(完结)
算法<初级> - 第五章 递归与动规相关问题(完结)
<一>递归和动态规划
暴力递归
转化为规模缩小了的同问题的子问题 - 时间复杂度O(2n-1)
有明确的边界条件(base case) - 先写base case,再写问题递归的过程
有得到子问题结果后决策过程
不记录每个子问题的解 - 每次求解子问题都交给递归去解决,不会在全局保存子问题的解(与动规形成对比)
动态规划DP
从暴力递归中延申 - 过程中还经历过<记忆化搜索>,相当于暴力递归+cache缓存(用hashmap实现),他并没有状态方程依赖,只是单纯的记忆并给出。
记录每个问题的子问题,避免重复计算(与暴力递归之间最大的区别)
- eg. 求n!问题:暴力递归f(n)=n * f(n-1);动规使用数组存储,f[n] = n * f[n-1],这样记录每个子问题避免了重复计算
把暴力递归的过程,抽象成状态转移 - 子函数的嵌套调用转换成数组序号的关系(依赖)
并且存在化简状态转移使其更简洁的过程(状态方程的化简)
- eg. 格子值等于上一行该格子位置上前数Σ;状态方程(A+B+C) = A+B+C;化为动规就是将上一行该格子位置上前数组索引Σ - 再化简就是该格子值等于上格子值+左格子值;状态方程(A+B+C) = ((A+B)+C),(A+B)是左格子,C是上格子
题目七:二维数组最小路径和
题目表述:给予一个二维数组,从左上走到右下,每步只能向下或者向右,返回最短路径和。
思想:某点的最短路径和,是min(上面格子最短+距离,左边格子最短+距离)
注意最右边和最下面时只能朝一个方向走
暴力递归不会记录子问题解,O(2n2);记忆化搜索O(n2);动态规划用数组记录子问题解,优化过程与状态转移方程有关,与原始题目相关性不大
暴力递归的可变参数 = 动规数组几维表 - 解空间大小
确定目标点;确定basecase(不依赖项);
本题的动规:先由最后点推出最下面行以及最右边行,然后依次往上确定次右与次下行。
算法实现(Java)
public static int minPath1(int[][] matrix) { // 暴力递归 return process1(matrix, matrix.length - 1, matrix[0].length - 1); // 0,0位置到x,y位 } public static int process1(int[][] matrix, int i, int j) { //相当于从右下走到左上 int res = matrix[i][j]; if (i == 0 && j == 0) { // 在左上点 return res; } if (i == 0 && j != 0) { // 在最左边,只能向上 return res + process1(matrix, i, j - 1); } if (i != 0 && j == 0) { // 在最上边,只能向左 return res + process1(matrix, i - 1, j); } return res + Math.min(process1(matrix, i, j - 1), process1(matrix, i - 1, j)); } public static int minPath2(int[][] m) { // 动态规划 if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) { // 相当于每一步记录在了数组中,不会重复计算,之前计算过的直接索引即可 return 0; } int row = m.length; int col = m[0].length; int[][] dp = new int[row][col]; dp[0][0] = m[0][0]; for (int i = 1; i < row; i++) { dp[i][0] = dp[i - 1][0] + m[i][0]; } for (int j = 1; j < col; j++) { dp[0][j] = dp[0][j - 1] + m[0][j]; } for (int i = 1; i < row; i++) { for (int j = 1; j < col; j++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j]; } } return dp[row - 1][col - 1]; } // for test 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; } 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(minPath1(m)); System.out.println(minPath2(m)); m = generateRandomMatrix(6, 7); System.out.println(minPath1(m)); System.out.println(minPath2(m)); }
题目八:数组中Σ是否相等所给数(背包问题)
题目表述:给予一个数组和一个数,判断是否能从数组中选取任意数之和与所给数相等。
思想:
对于每一个数组元素,都有判断拿或者不拿,相当于一个状态,状态值就是拿了的数Σ,其中变化参数有两个,索引与Σ值,所以动规解空间大小就是二维表[n][aim]
eg. [3,2,7],aim=10。一开始状态a,对应值sum=0,索引i=0(相当于就是状态a),状态转移b,看索引i=1,对应值sum=3/0(拿或者不拿),循环重复。
边界条件就是任意i,只要sum等于aim即True;最大i,sum!=aim即False - 所以仍是从数组[任意i,aim]往前面推是true还是false。
算法实现(Java)
public static boolean money1(int[] arr, int aim) { return process1(arr, 0, 0, aim); } public static boolean process1(int[] arr, int i, int sum, int aim) { // (i,sum) i表示现在考虑数组下标i位置,sum表示目前选取数字Σ是sum,aim表示总目标 if (sum == aim) { // 边界条件 return true; } if (i == arr.length) { // i=length,在最后元素后下标 return false; } return process1(arr, i + 1, sum, aim) || process1(arr, i + 1, sum + arr[i], aim); // 前不要a[i];后要a[i+1] } public static boolean money2(int[] arr, int aim) { // 可变参数两个,所以创建二维表 - 一个存储i变化一个存储sum变化 boolean[][] dp = new boolean[arr.length + 1][aim + 1]; for (int i = 0; i < dp.length; i++) { // 边界条件就是所有aim位置都是true dp[i][aim] = true; } for (int i = arr.length - 1; i >= 0; i--) { for (int j = aim - 1; j >= 0; j--) { dp[i][j] = dp[i + 1][j]; if (j + arr[i] <= aim) { dp[i][j] = dp[i][j] || dp[i + 1][j + arr[i]]; //前者不要i后者要i } } } return dp[0][0]; // [][]表示是否可达 0,0初始 } public static void main(String[] args) { int[] arr = { 1, 4, 8 }; int aim = 12; System.out.println(money1(arr, aim)); System.out.println(money2(arr, aim)); }
题目九:整数背包问题
题目表述:给定一个w[]和v[],背包一定空间bag,装一组商品放背包,价值p[],重量c[],求能拿的最大价值。 - 商品整体不能细分。
思想:
暴力递归跟上述一样,return的不是true/false而是价值,可变参数仍然是遍历i与重量cost
动态规划跟上题类似,转为二维表,[][]表示可以添加的最大价值,从最后开始往前状态转移 - [bag]后都是int系统最小值(相当于负值),而[i]值处都是0,相当于已经把物体都处理完成了,没有其他价值可以加进来了。 - 往前每遍历一个都在[][]赋值其价值,分为取跟不取。
无后效性:能改成动态规划的重要性质,存在空间换时间的解,怎么到子状态的路径不影响子状态的返回值。eg. dp[3][3]可能有多种到此的情况,但是前面的不同路径并不影响dp[3][3]的返回值结果。
分析可变参数(解空间大小) - 需要得到的结果/值直接作为返回值而不是作为可变参
确定最终状态 - 要返回的状态
根据basecase确定初始状态 - 边界条件
确定普遍位置依赖的状态转移函数
算法实现(Java)
public static int maxValue1(int[] c, int[] p, int bag) { //递归 return process1(c, p, 0, 0, bag); } public static int process1(int[] c, int[] p, int i, int cost, int bag) { // i索引cost所带重量和,可变参数两个,解空间二维表 if (cost > bag) { // 剪枝 - 如果当前重量大于背包重量 return Integer.MIN_VALUE; } if (i == c.length) { // 索引越界 return 0; } return Math.max(process1(c, p, i + 1, cost, bag), p[i] + process1(c, p, i + 1, cost + c[i], bag)); // return的就是价值,所以不用算它为变参 } public static int maxValue2(int[] c, int[] p, int bag) { //动规 int[][] dp = new int[c.length + 1][bag + 1]; // bag最大空间,c.length最多放的东西个数 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) { // 边界条件就是越界或者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)); }
题目二:汉诺塔问题(从此到后都是练习递归)
题目表述:三个柱子,最左边有n个环,最上到最下从小到大环,称为n层汉诺塔;打印n层汉诺塔从最左移到最右的的全部过程。 - 小环只能移到大环上(环下面的其他环必须比自己小)
思路:
n层分解:n-1层从from->mid,第n层from->to。然后是n-1层从mid->to;
最后一部n-1层从mid->to,相当于mid和from互换,形成了一个(n-1)汉诺塔的子问题
算法实现(Java)
public static void hanoi(int n) { if (n > 0) { func(n, "left", "mid", "right"); } } public static void func(int n, String from, String mid, String to) { //n表示环数 if (n == 1) { System.out.println("move from " + from + " to " + to); } else { func(n - 1, from, to, mid); // 前n-1,from—>mid func(1, from, mid, to); // 第n,from->to //剩一个环 func(n - 1, mid, from, to); // 前n-1,mid->to //相当于mid和from互换了 } } public static void main(String[] args) { int n = 3; hanoi(n); }
题目三:打印一个字符串的全部子序列
题目表述:打印一个字符串的全部子序列,包括空字符串。
思路:
跟之前的递归动规的题目一样,都是对于每个子元素要或不要的子问题。
由于每一步都需要打印,故不用对递归转变成动规。
算法实现(java)
public static void printAllSubsquence(String str) { // 如果只是单纯打印,那每个形成的序列都要打印,所以不用改成动规 char[] chs = str.toCharArray(); process(chs, 0); } public static void process(char[] chs, int i) { // char[]就是所有包含字符,i索引 if (i == chs.length) { System.out.println(String.valueOf(chs)); // 过了最后位置,打印 return; } process(chs, i + 1); // 递归,不要 char tmp = chs[i]; chs[i] = 0; process(chs, i + 1); // 递归,要 chs[i] = tmp; } public static void main(String[] args) { String test = "abc"; printAllSubsquence(test); }
题目四:打印一个字符串的全排列(要求出现/不出现重复字符串)
题目表述:打印一个字符串的全排列。 - (进阶:打印一个字符串的全排列,要求不要出现重复字符串)
思路:
① 跟之前的拿或不拿的递归不太一样,这次是(每个字符开头,和剩下字符全排列组合)的递归 - 也符合最优子结构问题
如何实现:固定i位置为头元素,从头元素开始遍历[j],与后续的每个
j
位置都进行交换一次,递归调用除头元素外的序列(i++),再把i
与j
交换回来,这样递归到i=length
打印,便实现了全排列的递归。② 不出现重复字符串只需要建立一个hashset即可。
算法实现(java)
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("======"); }
题目五:母牛数量问题
题目表述:母牛每年生一只母牛,新出生的母牛成长三年后也能每年生一只母牛,假设不会死。求N年后,母牛的数量。(进阶:如果每头牛只能活10年)
思路:
- 最优解使用矩阵乘法可以达到O(logN),这里只说递归做法(一切符合该通项的解都可以用logN时间复杂度,斐波拉契数列矩阵乘法)
- 递归:数列递归表达式:F(n) = F(n-1) + F(n-3),去年牛+可以生小牛的牛
- 不用递归:用三个变量来存储去年前年大前年的数量,遍历
n-4
得到n年后母牛的数量。 - 顺序计算O(n) - 进阶:F(n) = F(n-1) + F(n-3) - F(n-10)即可
- 算法实现(java):
// 假设不会死 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)); }
题目六:只使用递归实现逆序栈
题目表述:给予一个栈,返回逆序栈,要求不使用其他额外数据结构,只使用递归函数实现。
思路:
① getAndRemoveLastElement( ):得到并弹出栈底元素,并且使栈内其他元素保持原有顺序留在栈内
- 先result记录pop()栈顶元素,如果为空则返回;非空则递归调用getAndRemoveLastElement得到返回值last,把之前的栈顶元素压栈,返回last。
- 所以返回的一定是栈底元素,并且是通过递归一步一步传上来的,每一步递归都把栈顶元素拿出来再压回去。
② reverse( ):使栈逆序的函数
- 先判空,空则返回;调用getAndRemoveLastElement得到栈底元素记录为
i
并且使栈内其他元素保持原有顺序留在栈内 - 然后再调用自己reverse( ),调用最底层栈空,并且从外到内层记录的元素为从底到顶;之后把
i
压栈,则根据递归就是把最开始的栈顶元素压栈直到栈底元素压栈,这样就实现了栈的逆序。
- 先判空,空则返回;调用getAndRemoveLastElement得到栈底元素记录为
算法实现(java)
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()); } }