【算法很美】深入递归 (上)双管齐下解决递归问题


深搜、回溯、剪枝

递归有更强的表达力

1.1 “逐步生成结果”类问题之数值型

在这里插入图片描述

上楼梯

题源 👉 CC150走楼梯

有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶、3阶。
请实现一个方法,计算小孩有多少种上楼的方式。
为了防止溢出,请将结果Mod 1000000007

给定一个正整数int n,请返回一个数,代表上楼的方式数。
保证n小于等于100000。

推理过程:

  • 若只有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的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。
请设计一个算法,计算机器人有多少种走法。
给定两个正整数int x,int y,请返回机器人的走法数目。保证x+y小于等于12。

推理过程:(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

假设我们有8种不同面值的硬币{1,2,5,10,20,50,100,200},用这些硬币组合构成一个给定的数值n。

例如n=200,那么一种可能的组合方式为 200 = 3 * 1 + 1*2 + 1*5 + 2*20 + 1 * 50 + 1 * 100.

问总共有多少种可能的组合方式?

题源 👉 华为面试题

1分2分5分的硬币三种,组合成1角,共有多少种组合

直接暴力即可 1x + 2y + 5*z=10

题源 👉 创新工厂笔试题

有1分,2分,5分,10分四种硬币,每种硬币数量无限,给定n分钱,有多少组合可以组成n分钱

题源 👉 CC150硬币表示

1 5 10 25 分 n,多少种组合方法

对于 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

输入括号对数,判断一个字符串是否合法(即左右括号是否正确配对),输出所有合法的括号组合

示例:

输入:3

输出:()()(),((())),(()()),()(()),(())(),

思考过程:

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

请编写一个方法,返回某集合的所有非空子集。

给定一个int数组A和数组的大小int n,请返回A的所有非空子集。
保证A的元素个数小于等于20,且元素互异。

各子集内部从大到小排序,子集之间字典逆序排序

思考:

  • 方法一:

    在这里插入图片描述

    对每个元素依次进行选取,每个元素有取和不取两种选择

    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

编写一个方法,确定某字符串的所有排列组合。

给定一个string A和一个int n,代表字符串和其长度,请返回所有该字符串字符的排列,保证字符串长度小于等于11且字符串中字符均为大写英文字符。

如 {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个排列

LeetCode60 n个数的排列组合找出字典序的第k个排列

The set[1,2,3,…,n]contains a total of n! unique permutations.
By listing and labeling all of the permutations in order,
We get the following sequence (ie, for n = 3):
“123”
“132”
“213”
“231”
“312”
“321”

Given n and k, return the k th permutation sequence.
Note: Given n will be between 1 and 9 inclusive.

时间限制:1秒

上一种方法完成全排列后再进行排序也可以

下面是前缀法:

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;
	}
}

 

1.3 封闭形式的直接解(这部分以后再补…)

汉诺塔移动次数

斐波那契数列第n项

上楼梯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值