深入递归 【上】双管齐下解决递归问题
深搜、回溯、剪枝
递归有更强的表达力
1.1 “逐步生成结果”类问题之数值型

上楼梯
题源 👉 CC150走楼梯
推理过程:
-  若只有1个阶梯,共1种走法: 直接一步到位,达到剩0个阶梯的状态,剩0个阶梯时走法有1种 f (1) = f (0) = 1,设 f (0) 时走法为1 
-  若只有2个阶梯,共2种走法: 1、第一步直接一次上2阶,达到剩0个阶梯的状态,剩0个阶梯时走法有1种 2、第一步上1阶,达到剩1个阶梯的状态,剩1个阶梯时走法有1种 f (2) = f (0) + f (1) = 2 
-  若只有3个阶梯,共4种走法: 1、第一步直接一次上3阶,达到剩0个阶梯的状态,剩0个阶梯时走法有1种 2、第一步上2阶,达到剩1个阶梯的状态,剩1个阶梯时走法有1种 3、第一步上1阶,达到剩2个阶梯的状态,剩2个阶梯时走法有2种 故总共有 1 + 1 + 2 种走法 f (3) = 1 + f (1) + f (2) = 4 
-  若只有4个阶梯,共7种走法: 1、第一步上3阶,达到剩1个阶梯的状态,剩1个阶梯时走法有1种 2、第一步上2阶,达到剩2个阶梯的状态,剩2个阶梯时走法有2种 3、第一步上1阶,达到剩3个阶梯的状态,剩3个阶梯时走法有4种 故总共有 1 + 2 + 4 种走法 f (4) = f (1) + f (2) + f (3) = 7 
-  若只有5个阶梯,同理 f (5) = f (2) + f (3) + f (4) = 13 
-  … 
-  可知有n个阶梯时, f (n) = f (n - 3) + f (n - 2) + f (n - 1),n ≠ 0,1,2 f (0) = f (1) = 1,f (2) = 2 
public class case01_走楼梯 {
	static final int mod = 1000000007;
	public static void main(String[] args) {
		System.out.println(f1(7));
		System.out.println(f2(7));
	}
	// 递归调用方法
	public static long f1(int n) {
		if (n < 0)
			return 0;
		if (n == 0 || n == 1)
			return 1;
		if (n == 2)
			return 2;
		return f1(n - 3) % mod + f1(n - 2) % mod + f1(n - 1) % mod;
	}
	// 不使用递归调用方法
	// 1 1 2 4 7 13 24 44
	public static long f2(int n) {
		if (n < 0)
			return 0;
		if (n == 0 || n == 1)
			return 1;
		if (n == 2)
			return 2;
		int f0 = 1;
		int f1 = 1;
		int f2 = 2;
		for (int i = 3; i <= n; i++) {
			int temp = f2;
			f2 = ((f0 + f1) % mod + f2) % mod;
			f0 = f1 % mod;
			f1 = temp % mod;
		}
		return f2;
	}
}
机器人走方格
推理过程:(X,Y)表示 X 行 Y 列格子
-  (1,1)时,1种走法,f (1, 1) = 1; (1,2)时,1种走法,f (1, 2) = 1; (2,1)时,1种走法,f (2, 1) = 1; (3,1)时,1种走法,f (3, 1) = 1; … 当 X 或 Y = 1 时,都只有 1 种走法! 
-  (2,2)时,2种走法: 可以右走1格达到(2,1)的状态 可以下走1格达到(1,2)的状态 f (2, 2) = f (1, 2) + f (2, 1) = 2 
-  (3,2)时,3种走法: 可以右走1格达到(3,1)的状态 可以下走1格达到(2,2)的状态 f (3, 2) = f (3, 1) + f (2, 2) = 1 + 2 = 3; 
-  … 
-  可知有 (x, y) 格子时: f (x , y) = f (x, y - 1) + f (x - 1, y) 
注意:
- 递归形式时,以 x == 1 || y == 1 为边界条件
- 迭代形式时,以一个 x * y 的二维数组进行记录
public class case02_机器人走格子 {
	public static void main(String[] args) {
		System.out.println(solve1(6, 6));
		System.out.println(solve2(6, 6));
	}
	// 递归形式
	private static int solve1(int x, int y) {
		if (x == 1 || y == 1)
			return 1;
		return solve1(x - 1, y) + solve1(x, y - 1);
	}
	// 迭代形式
	private static int solve2(int x, int y) {
		int[][] state = new int[x + 1][y + 1]; // +1是因为等会循环从1开始
		for (int i = 1; i <= x; i++) { // 初始话第一列
			state[i][1] = 1;
		}
		for (int i = 1; i <= y; i++) { // 初始话第一行
			state[1][i] = 1;
		}
		for (int i = 2; i <= x; i++) {
			for (int j = 2; j <= y; j++) {
				state[i][j] = state[i - 1][j] + state[i][j - 1];
			}
		}
		return state[x][y];
	}
}
硬币表示
题源 👉 编程网站ProjectEuler
题源 👉 华为面试题
题源 👉 创新工厂笔试题
题源 👉 CC150硬币表示
对于 CC150硬币表示题推理过程:
对于数值n,用 {1,5,10,25}进行组合,
递归方法:
-  若只能用 硬币值为1 进行组合,则对于每个 n ,都只有1种组合方式; 
-  若只能用 硬币值为1、5 进行组合,对于面值较大的硬币,即对5有 n / 5 + 1 种可能 (如 n = 10 时,可以选 10 / 5 + 1 = 3 种 ,即 0、1、2张5) 使用 i 张 5 时,剩下 n - i*5 的 价值由 硬币 {1} 进行组合 
-  若只能用 硬币值为1、5、10 进行组合,对于面值较大的硬币,即对10有 n / 10 + 1 种可能 (如 n = 40 时,可以选 40 / 10 + 1 = 3 种 ,即 0、1、2、3、4张10) 使用 i 张 10 时,剩下 n - i*10 的 价值由 硬币 {1、5} 进行组合 
-  若只能用 硬币值为1、5、10、25 进行组合,对于面值较大的硬币,即对25有 n / 25 + 1 种可能 (如 n = 40 时,可以选 40 / 25 + 1 = 2 种 ,即 0、1张25) 使用 i 张 25 时,剩下 n - i*25 的 价值由 硬币 {1、5、10} 进行组合 
// 递归
/**
	 * 
	 * @param n     要组合的面值
	 * @param coins 硬币数组
	 * @param cur   最大的硬币的数组下标
	 * @return
	 */
private static int countWays1(int n, int[] coins, int cur) {
    if (cur == 0) // 当只能使用硬币1进行组合时,任何面值都有1种方法
        return 1;
    int res = 0;
    // 对于最大的那个硬币,可以有i种选择
    for (int i = 0; i * coins[cur] <= n; i++) {
        int rest = n - i * coins[cur]; // 剩余面值
        res += countWays1(rest, coins, cur - 1);
    }
    return res;
}
迭代方法:

主要看黄色栏,数组的第 i 行表示可以使用第 i 行及其之上的硬币
要凑面值n,使用数组为arr[4][n + 1]进行标记:
-  若只能用 {1},只有1种; 
-  若只能用 {1,5},对于面值 k ,硬币5的取法有 k / 5 + 1 种: 取0个5时,f0 = arr[0][k - 0 * 5] = arr[0][k] = 1; 取1个5时,f1 = arr[0][k - 1 * 5] = arr[0][k - 5]; … 取k / 5个5时,f k / 5 = arr [0][k - (k/5)*5] = arr[0][0] = 1; 故 arr[1][k] = f0 + f1 + … + f k / 5 = arr[0][k] + arr[0][k-5] + arr[0][k-10] + … + arr[0][k-(k/5)*5] 注意:除号"/"均为向下取整方式 ,如 10 / 4 = 2 
-  若只能用 {1,5,10},对于面值 k ,硬币10的取法有 k / 10 + 1 种: 同理可得: arr[2][k] = f0 + f1+ … + f k / 10 = arr[1][k] + arr[1][k-10] + arr[1][k-20] + … + arr[1][k-(k/10)*10] 
…
// 迭代
private static int countWays2(int n, int[] coins) {
    int[][] dp = new int[coins.length][n + 1]; // 前i种面值,组合出面值j
    // 面值为0,每行都初始化为1
    for (int i = 0; i < coins.length; i++)
        dp[i][0] = 1;
    // 对于硬币1,可凑出每个面值,即将第一行全部初始化为1
    for (int j = 1; j < n + 1; j++)
        dp[0][j] = 1;
    for (int i = 1; i < coins.length; i++) { // 可以使用i及前i种面值
        for (int j = 1; j < n + 1; j++) { // 对于面值j
            // 使用i的硬币有 k = n/coins[i]+1种可能
            for (int k = 0; k * coins[i] <= j; k++) {
                dp[i][j] += dp[i - 1][j - k * coins[i]];
            }
        }
    }
    return dp[coins.length - 1][n];
}
1.2 "逐步生成结果"类问题之非数值型
需要用 容器 去装
合法括号
题源 👉 CC150 9.6
思考过程:
S(1)层:n = 1, ()
S(2)层:n = 2,()()、(())、()()
S(3)层:n = 3,对于()():()()()左、()()()右、(()()())外、(())()内、()(())内
…
S(n)层:对S(n-1)层中每一个元素左边、右边、外层、元素内部每个左括号后,生成一对括号
- 每层使用Set类进行去重
import java.util.HashSet;
import java.util.Set;
public class case04_合法括号 {
	public static void main(String[] args) {
		Set<String> parenthesis = parenthesis1(3);
		System.out.println(parenthesis);
		parenthesis = parenthesis2(3);
		System.out.println(parenthesis);
	}
	// 递归形式
	public static Set<String> parenthesis1(int n) {
		Set<String> s_n = new HashSet<String>(); // n层元素集S(n)
		if (n == 1) {
			s_n.add("()");
			return s_n;
		}
		Set<String> s_n_1 = parenthesis1(n - 1); // 上一层元素集S(n-1)
		// 对S(n-1)的每一个元素进行添左、添右、最外层、内层每个字符添加
		for (String s : s_n_1) {
			s_n.add("()" + s); // 添左
			s_n.add(s + "()"); // 添右
			s_n.add("(" + s + ")"); // 添在外面
			// 元素内部每个左括号后
			for (int i = 0; i < s.length(); i++) {
				char c = s.charAt(i);
				if (c == '(')
					s_n.add(s.substring(0, i + 1) + "()" + s.substring(i + 1));
			}
		}
		return s_n;
	}
	// 迭代
	public static Set<String> parenthesis2(int n) {
		Set<String> res = new HashSet<String>(); // 保存上次迭代状态
		res.add("()"); // 加入第一对括号
		if (n == 1)
			return res;
		for (int i = 2; i <= n; i++) {
			Set<String> res_new = new HashSet<>();
			for (String e : res) {
				res_new.add(e + "()");
				res_new.add("()" + e);
				res_new.add("(" + e + ")");
				for (int j = 0; j < e.length(); j++) {
					char c = e.charAt(j);
					if (c == '(')
						res_new.add(e.substring(0, j + 1) + "()" + e.substring(j + 1));
				}
			}
			res = res_new;
		}
		return res;
	}
}
非空子集
题源 👉 CC150 9.4
思考:
-  方法一:  对每个元素依次进行选取,每个元素有取和不取两种选择 import java.util.HashSet; import java.util.Set; public class case05_非空子集 { public static void main(String[] args) { int[] A = { 1, 2, 3 }; Set<Set<Integer>> subsets = getSubSets1(A, A.length); System.out.println(subsets); subsets = getSubSets2(A, A.length); System.out.println(subsets); } /** * 递归 * * @param A 数组 * @param n 数组长度 * @return */ public static Set<Set<Integer>> getSubSets1(int[] A, int n) { return getSubsets1Core(A, n, n - 1); } private static Set<Set<Integer>> getSubsets1Core(int[] a, int n, int cur) { Set<Set<Integer>> newSet = new HashSet<>(); if (cur == 0) { // 处理第一个元素 Set<Integer> empty = new HashSet<>(); // 空集 Set<Integer> first = new HashSet<>(); // 选择第一个元素 first.add(a[0]); newSet.add(empty); newSet.add(first); return newSet; } Set<Set<Integer>> oldSet = getSubsets1Core(a, n, cur - 1); // 对于上一层选取后的每一个元素集合,可选择加入或者不加入a[cur] for (Set<Integer> set : oldSet) { newSet.add(set); // 不选择a[cur] Set<Integer> clone = (Set<Integer>) ((HashSet) set).clone(); clone.add(a[cur]); // 选择a[cur] newSet.add(clone); } return newSet; } /** * 迭代 * * @param A * @param n * @return */ public static Set<Set<Integer>> getSubSets2(int[] A, int n) { Set<Set<Integer>> res = new HashSet<>(); res.add(new HashSet<>()); // 初始化为空集 for (int i = 0; i < n; i++) { Set<Set<Integer>> res_new = new HashSet<>(); res_new.addAll(res);// 把原来集合中的每个子集都加入到新集合中 for (Set e : res) { Set clone = (Set) ((HashSet) e).clone(); clone.add(A[i]); res_new.add(clone); } res = res_new; } return res; } }
-  方法二:二进制法,迭代法 /** * 二进制法 * * @param A * @param n * @return */ public static ArrayList<ArrayList<Integer>> getSubSets3(int[] A, int n) { Arrays.sort(A); // 正序排序 ArrayList<ArrayList<Integer>> res = new ArrayList<>();// 大集合 for (int i = ex(2, n) - 1; i > 0; i--) {// 大数字-1 ArrayList<Integer> s = new ArrayList<>();// 对每个i建立一个集合 for (int j = n - 1; j >= 0; j--) {// 检查哪个位上的二进制为1,从高位开始检查,高位对应着数组靠后的元素 if (((i >> j) & 1) == 1) { s.add(A[j]); } } res.add(s); } return res; } public static int ex(int a, int n) {...} // 与05数学问题中求快速幂的代码相同
字符串(集合)全排列
题源 👉 CC150 9.5
如 {A,B,C}进行全排列,有 3 * 2 * 1 = 6种可能排列,即 2 3 - 1 种
逐步生成大法-迭代法
和 “ 合法括号” 思路相似,先选定第一个元素,接着对第二个字符分别加在第一个的左边、右边…
1、{A}
2、{AB,BA}
3、{【CAB,ACB,ABC】,【CBA,BCA,BAC】}
import java.util.ArrayList;
public class case06_全排列I {
	public static void main(String[] args) {
		ArrayList<String> res = getPermutation("ABC", 3);
		System.out.println(res);
	}
	public static ArrayList<String> getPermutation(String A, int n) {
		ArrayList<String> res = new ArrayList<>();
		res.add(A.charAt(0) + ""); // 初始化,加入第一个字符
		for (int i = 1; i < n; i++) { // 循环进行排列的元素
			ArrayList<String> res_new = new ArrayList<>();
			char c = A.charAt(i); // 即将插入的元素
			for (String str : res) { // 循环上一层完成的排列
				// 对上层完成的每种排列插入当前元素
				res_new.add(c + str);
				res_new.add(str + c);
				// 往中间缝隙插
				for (int j = 1; j < str.length(); j++) {
					String newString = str.substring(0, j) + c + str.substring(j);
					res_new.add(newString);
				}
			}
			res = res_new; // 每层结束后进行更新
		}
		return res;
	}
}
升级🆙:
经典写法:【回溯】

import java.util.ArrayList;
import java.util.Arrays;
public class case06_全排列II {
	static ArrayList<String> res = new ArrayList<>();
	public static void main(String[] args) {
		getPermutation("ABC");
		System.out.println(res);
	}
	public static ArrayList<String> getPermutation(String A) {
		char[] arr = A.toCharArray(); // 转为数组
		Arrays.sort(arr); // 要求按顺序
		getPermutationCore(arr, 0);
		return res;
	}
	private static void getPermutationCore(char[] arr, int k) {
		if (k == arr.length) // 排好了一种情况,递归的支路走到底了
			res.add(new String(arr));
		// 从k位开始的每个字符,都尝试放在新排列的k这个位置
		for (int i = k; i < arr.length; i++) {
			swap(arr, k, i); // 把后面每个字符换到k位
			getPermutationCore(arr, k + 1);
			swap(arr, k, i); // 回溯
		}
	}
	private static void swap(char[] arr, int k, int i) {
		char temp = arr[k];
		arr[k] = arr[i];
		arr[i] = temp;
	}
}
升级🆙:全排列III第k个排列
上一种方法完成全排列后再进行排序也可以
下面是前缀法:
public class case06_全排列III第k个排列 {
	static int cnt = 0;
	static int k = 6;
	public static void main(String[] args) {
		String s = "123";
		permutation("", s.toCharArray());
	}
	private static void permutation(String prefix, char[] arr) {
		if (prefix.length() == arr.length) { // 前缀的长度==字符集的长度,完成一个排列
			cnt++;
			if (cnt == k) {
				System.out.println(prefix);
				System.exit(0);
			}
		}
		// 每次都从头扫描,只要该字符可用,我们就附加到前缀后面,前缀变长了
		for (int i = 0; i < arr.length; i++) {
			char c = arr[i];
			// 这个字符可用:在pre中出现次数<在字符集中的出现次数
			if (count(prefix, c) < count(arr, c)) {
				permutation(prefix + c, arr);
			}
		}
	}
	private static int count(char[] arr, char c) {
		int count = 0;
		for (char ch : arr) {
			if (c == ch)
				count++;
		}
		return count;
	}
	private static int count(String s, char c) {
		int count = 0;
		for (int i = 0; i < s.length(); i++) {
			if (s.charAt(i) == c)
				count++;
		}
		return count;
	}
}










