竞赛算法–深入递归(中)(DFS、回溯、剪枝等)
竞赛算法–深入递归(下)(DFS、回溯、剪枝等)
1、双管齐下解递归
1、自上而下的递归(分治(分解、递归、合并))直观
- //快速排序。
2、自下而上的递归(递推,数学归纳,动态规划)
- 解决简单情况下的问题
- 推广到稍复杂情况下的问题
- …………
- 如果递推次数很明确,用迭代
- 如果有封闭形式,可以直接求解
1.1 “逐步生成结果”类问题之数值型
例题1.1.1:上楼梯
题目描述:
- 有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶,2阶,3阶。
- 请实现一个方法,计算小孩有多少种上楼方式。
- 为了防止溢出,请将结果 mod 1000000007
- 给定一个正整数 int n ,请返回一个数,代表上楼的方式数
题目分析+题解代码:
- 先把n从小往大想,逐步分析
public class Main{
static final int MOD = 1000000007;
public static void main(String [] args){
System.out.println(recusion1(7));
System.out.println(recusion2(7));
//System.out.println(recusion3(7));
}
// n较小时就会超时,效率低 O(3^n)
// 递归方法 顺着想,倒着写(逆向思维)
public static long recursion1(int n){
if(n < 0) return 0;
if(n==0 || n==1) return 1;
if(n==2) return 2;
return recursion1(n-1)%MOD + recursion1(n-2)%MOD + recursion1(n-3)%MOD;
}
/**
* 迭代方法, O(n)
* 顺着想,顺着写 */
public static int recursion2(int n){
if(n < 0) return 0;
if(n==0 || n==1) return 1;
if(n==2) return 2;
if(n==3) return 4;
int x1 = 1 , x2=2 , x3=4 ;
for(int i =4;i<=n;i++){
int temp = x1;
x1 = x2 % MOD;
x2 = x3 % MOD;
X3 = ((x1+x2) % MOD + temp ) % MOD ;
}
return x3;
}
}
例题1.1.2:机器人走方格
题目描述:
- 有一个 X*Y的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。
- 请设计一个算法,计算机器人有多少种走法。
- 给定两个正整数 int X,int Y,请返回机器人走法的数目。保证X+Y<= 12.
题目分析+题解代码:
public class Main{
public static void main(String [] args){
System.out.printlm(solve1(2,3));
System.out.printlm(solve2(2,3));
}
// 递归解法
public static int solve1(int x,int y){
if(x ==1 || y==1) return 1;
return solve(x-1,y)+solve(x,y-1);
}
// ※ 迭代解法(动态规划)
public static int solve2(int m,int n){
int [][] state = new int[m+1][n+1];
for(int i=1;i<n;i++)
state[1][i] = 1;
for(int i=1;i<m;i++)
state[i][1] = 1;
for(int i=2;i<=m;i++){
for(int j =2;j<=n;j++){
state[i][j] = state[i][j-1]+state[i-1][j];
}
}
return state[m][n];
}
}
例题1.1.3:硬币表示
1.1 “逐步生成结果”类问题之非数值型
此时就要用容器去装了
- 生成一点,装一点,所谓迭代就是慢慢改变
例题:
- 1.2.1 合法括号
- 1.2.2 非空子集
- 1.2.3 字符串(集合)全排列
例题1.2.1:合法括号
题目描述:
- 实现一种算法,打印n 对括号的全部有效组合(即,左右括号正确配对)。
- 示例 :
- 输入 : 3
- 输出 : ( ( ( ) ) ) , ( ( ) ( ) ) , ( ) ( ( ) ) , ( ) ( ) ( ) ,
题目分析+题解代码:
import java.util.*;
public class Mian{
public static void main(String [] args){
//Main obj = new Main();
Set<String> bracketSort = parenthesis(4);
System.out.println(bracketSort);
}
public boolean bracketsSort(int n){
}
// 逐步生成 之 递归解法
public static Set<String> parenthesis(int n){
Set<String> s_n = new HashSet<>();
if(n==1){
s_n.add("()");
return s_n;
}
Set<String> s_n_1 = parenthesis(n-1);
for(String eOfN_1 : s_n_1){
s_n.add("()"+eOfN_1);
s_n.add(eOfN_1 + "()");
s_n.add("("+eOfN_1+")");
}
return s_n ;
}
// ※ 迭代方法
public static Set<String> parenthesis_1(int n){
Set<String> res = new HashSet<>();//保存上次迭代的状态
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+")");
}
res = res_new ;
}
return res;
}
}
例题1.2.2:非空子集
题目描述:
- 编写一个方法,返回某个集合的所有的子集。
题目分析+题解代码:
import java.util.*;
public class Main{
public static void main(String[]args){
Main obj = new Main();
int [] A = {123,456,789};
System.out.println(obj.getSubsets3Core(A,A.length,A.length-1));
System.out.println(obj.getSubsets2(A,A.length));
}
//递归增量构造法
private Set<Set<Integer>> getSubsets3Core(int[] A,int n , int cur){
Set<Set<Integer>> newSet = new HashSet<>();
if(cur == 0){ // 处理第一个元素
Set<Integer> nil = new HashSet<>(); // 空集
Set<Integer> first = new HashSet<>(); // 包含第一个元素
first.add(A[0]);
newSet.add(nil);
newSet.add(first);
return newSet;
}
Set<Set<Integer>> oldSet = getSubsets3Core(A,n,cur-1);
for(Set<Integer> set : oldSet){
// 对于每一个子集,cur这个元素可以加进去,也可以不加进去
newSet.add(set); // 保留原样
Set<Integer> clone = (Set<Integer>)((HashSet)set).clone();
clone.add(A[cur]); //添加当前元素到“元素集合”中
newSet.add(clone); // 添加新元素
}
return newSet;
}
// 逐步生成迭代大法
public 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 a = (Set)((HashSet) e ).clone();
a.add(A[i]); // 把当前元素加进去
res_new.add(a); //把克隆的子集加到大集合中
}
res = res_new;
}
return res;
}
}
二进制解法
public ArrayList<ArrayList<Integer>> getSubsets(int []A,int n){
Arrays.sort(A); // 按字典序正序排列
ArrayList<ArrayList<Integer>> res = new ArrayList<>(); // 大集合
for(int i=(int)Math.pow(2,n)-1;i>0;i--){
// 对每一个i 建立一个集合
ArrayList<Integer> ints = new ArrayList<>();
for(int j=n-1;j>=0;j--){//检查哪个位上的二进制为1
if(((i >> j) & 1) == 1 )
ints.add(A[j]);
}
res.add(ints);
}
return res;
}
例题1.2.3:全排列(上)
题目描述:
- 将一个字符串的每个字符全排列。
- 例 输入:ABC
- 输出 :ABC 、 ACB 、BAC 、 BCA 、 CAB 、CBA.
import java.util.*;
public class Main{
public static void main(String args[]){
Main obj = new Main();
ArrayList<String> res = obj.getPermutation0("ABCD");
System.out.println(res.size());
System.out.println(res);
}
// 逐步生成法--迭代法
public ArrayList<String> getPermutation0(String A){
int n = A.length();
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){ // 访问上一趟集合中的每个字符串
//插入到每个位置,形成一个新串
String newStr = c + str; // 加在前面
res_new.add(newStr);
newStr = str + c ; // 加在后面
res_new.add(newStr);
// 加在中间
for(int j = 1;j<str.length();j++){
newStr = str.substring(0,j)+c+str.substring(j);
res_new.add(newStr);
}
}
res = res_new; // 更新
}
return res;
}
}
例题1.2.3:全排列(中)
交换法—简洁、方便应对重复如AABC
题目描述:
- 将一个字符串的每个字符全排列
- 例:输入 : ABC
- 输出 :ABC 、 ACB 、BAC 、 BCA 、 CAB 、CBA.
import java.util.*;
public class Main{
ArrayList<String> res = new ArrayList<>();
public static void main(String [] args){
Main obj = new Main();
ArrayList<String> res = obj.getPermutation("ABCD");
System.out.println(res.size());
System.out.println(res);
}
public ArrayList<String> getPermutation(String A){
char[] arr = A.toCharArray();
Arrays.sort(arr); // abcd
getPermutationCore(arr,0);
return res;
}
public 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); // 回溯
}
}
static void swap(char [] arr,int i,int j){
char temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
例题1.2.4:全排列(下)
前缀法–复杂-能维持良好的字典序,但在处理重复字符时较为繁琐
题目描述:
- 将一个字符串的每个字符全排列,按字典序输出
- 例:输入 : ABC
- 输出 :ABC 、 ACB 、BAC 、 BCA 、 CAB 、CBA.
public class Main{
public static void main(String [] args){
String s = "123";
permutation("",s.toCharArray());
}
final static int k = 3;
static int count = 0;
private static void permutation(String prefix , char [] arr){
if(prefix.length() == arr.length){ // 前缀的长度 == 字符集的长度,一个排列就完成了
count++;
if(count == k){
System.out.println(prefix);
System.out.exit(0);
}
}
// 每次都从头扫描,只要该字符可用,我们就附加到前缀后面,前缀变长了
for(int i=0;i<arr.length;i++){
char ch = arr[i];
// 这个字符可用:在pre中出现的次数 < 在字符集中出现的次数
if( count(prefix,ch)<count(arr,ch)){
permutation(prefix+ch,arr);
}
}
}
public static int count(char[] arr ,char ch){
int cnt = 0;
for(char c : arr){
if(c == ch) cnt++;
}
return cnt;
}
}