7、深入递归,DFS(深度搜索),回溯,剪枝

"逐步生成结果"类问题之数值型

自上而下 --递归

自下而上 --递推,数学归纳,动态规划

1、先解决简单下的问题
2、然后推广到复杂项的问题
3、如果递推次数很明确,最好用迭代(即从开始,一步一步往后推)
4、如果有封闭形式,可以直接求解

题1:爬楼梯问题

三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
思考一下,小孩最后迈出了多少步?
小孩上楼梯的最后一步,就是抵达第n 阶的那一步,迈过的台阶数可以是3、2 或者1。
那么小孩有多少种方法走到第n 阶台阶呢?目前还不知道,但我们可以把它与一些子问题联系起来。
到第n 阶台阶的所有路径,可以建立在前面3 步路径的基础之上。我们可以通过以下任意方式走到第n 阶台阶。
 在第n-1 处往上迈1 步。
 在第n-2 处往上迈2 步。
 在第n-3 处往上迈3 步。
因此,我们只需把这3 种方式的路径数相加即可。
这里要非常小心,有很多人会把它们相乘。相乘应该是走完一个再走另一个,显然和以上
情况不符。

采取两种方法,一种是迭代,一种是递推
递推

public static int kind(int n){
    if(n==1){
        return 1;
    }else if(n==2){
        return 2;
    }else if(n==3){
        return 4;
    }else{
        int x1=1;
        int x2=2;
        int x3=4;
        for(int i=0;i<n-3;i++ ){
            int t=x1;
            x1=x2;
            x2=x3;
            x3=x1+x2+t;
        }
        return x3%1000000007;
    }

迭代

public static int kind(int n){
    if(n==1){
        return 1;
    }else if(n==2){
        return 2;
    }else if(n==3){
        return 4;
    }else{
        return kind(n-1)+kind(n-2)+kind(n-3);
    }
题2:机器人走方格

有一个XxY的网有一个XxY的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。请设计一个算法,计算机器人有多少种走法。
给定两个正整数int x,int y,请返回机器人的走法数目。保证x+y小于等于12格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。请设计一个算法,计算机器人有多少种走法。
给定两个正整数int x,int y,请返回机器人的走法数目。保证x+y小于等于12。
先不断在小范围内发现规律,在求解新问题时尽可能用到原来求的,得到新问题的答案

public static int fun(int x,int y){
    int [][]m=new int[x+1][y+1];
    for(int i=1;i<=x;i++){
        m[i][1]=1;
    }
    for(int i=1;i<=y;i++){
        m[1][i]=1;
    }
    for(int i=2;i<=x;i++){
        for(int j=2;j<=y;j++){
            m[i][j]=m[i][j-1]+m[i-1][j];
        }
    }
    return m[x][y];
}
题3:硬币

"逐步生成结果"类问题之非数值型

对于非数值型,此时就要用容器来装了
生成一点,装一点,所谓迭代就是慢慢改变
此时就需要用到容器的知识了
Set为集合,可自动消除重复的元素,并且没有下标,只能用for-each进行遍历
List为表,有下标

题1:括号

括号。设计一种算法,打印n对括号的所有合法的(例如,开闭一一对应)组合。
说明:解集不能包含重复的子集。
例如,给出 n = 3,生成结果为:
[ “((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”]

递归

public static Set<String> fun(int n){
    Set<String> s_n=new HashSet<>();
    if(n==1){
        s_n.add("()");
    }
    else {
        Set<String> s_n_1 = fun(n - 1);
        for (String s : s_n_1) {
            s_n.add("()" + s);
            s_n.add("(" + s + ")");
            s_n.add(s + "()");
        }
    }
    return s_n;
}

迭代

public static Set<String> fun(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 s:res) {
                res_new.add("()" + s);
                res_new.add("(" + s + ")");
                res_new.add(s + "()");
                res = res_new;
            }
        }
        return res;

    }
题2:幂级

幂集。编写一种方法,返回某集合的所有子集。集合中不包含重复的元素。
说明:解集不能包含重复的子集。
在这里插入图片描述

初步生成 迭代大法
public static Set<Set<Integer>> fun(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;
}
增量构造法(递归)
public static Set<Set<Integer>> fun(int []A,int n,int cur){
    Set<Set<Integer>> res=new HashSet<>();
    //处理第一个元素
    if (cur==0){
        res.add(new HashSet<>());
        Set<Integer> first=new HashSet<>();
        first.add(A[0]);
        res.add(first);
        return res;
    }
    Set<Set<Integer>> oldres=fun(A,n,cur-1);
    for(Set s:oldres){
        res.add(s);
        Set clone =(Set)((HashSet) s).clone();
        clone.add(A[cur]);
        res.add(clone);
}
    return res;

}
二进制的方法(记住)
public static List<List<Integer>> fun(int []nums){
    Arrays.sort(nums);
    List<List<Integer>> res=new LinkedList<>();
    for(int i = (int) (Math.pow(2,nums.length)-1); i>=0; i--){
        List<Integer> mm=new LinkedList<>();
        for(int j=0;j<nums.length;j++){
            if(((i>>j)&1)==1) {
                mm.add(nums[j]);
            }
        }
        res.add(mm);
    }
    return res;
}
题3:全排列

给定一个没有重复数字的序列,返回其所有可能的全排列。

方法2:交换使不同元素当第k位

交换,递归,回溯 缺点:无法维持字典序,只是输出用此方法
递归先走到底才回溯,到离自己最近的兄弟,继续到底再回溯
在这里插入图片描述

static List<List<Integer>> res;
public static List<List<Integer>> fun1(int []list){
    res=new LinkedList<>();
    Arrays.sort(list);
    fun2(list,0);
    return res;

}
public static void swap(int []l,int i,int j){
    int t=l[i];
    l[i]=l[j];
    l[j]=t;
}
public static void fun2(int []list,int k){

    if(k==list.length){//说明已经走到顶了
        Integer [] integers=new Integer[list.length];
        for(int i=0;i<list.length;i++){
            integers[i]=list[i];
        }
        res.add(Arrays.asList(integers));
    }
    for(int i=k;i<list.length;i++){//k为基准,k及其以后的依次换到k这里
        swap(list,k,i);
        fun2(list,k+1);
        swap(list,k,i);//回溯,由于是对同一个数组进行操作,恢复原样
    }
方法3:前缀法

初始是一个筐,每次选没有的入围,判断标准:筐里这个字符数量小于字符集这个元素的数量
优点:可以维持字典序,需要求第K个,要用这个方法,如果没有要求第K个,可以优先选用第二个
在这里插入图片描述


import jdk.internal.dynalink.support.CallSiteDescriptorFactory;

import java.util.*;

public class Main{
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        String arr=sc.next();
        permutation("",arr);
    }
    static int count=0;
    static int k=6;
    public static void permutation(String prefix,String arr){
        if(prefix.length()==arr.length()) {//前缀的长度等于字符集的长度,一个排列就完成了
            count++;
            if (count == k) {
                System.out.println(prefix);
                System.exit(0);
            }
        }
        //每次都从头扫描,只要该字符可用,我们就附加到前缀后面,前缀变长了
        for(int i=0;i<arr.length();i++){
            char ch=arr.charAt(i);
            //判断这个字符是否可用
            if(!prefix.contains(ch+"")){
                permutation(prefix+ch+"",arr);
            }
        }
    }

}

深度优先搜索 DFS

dfs 一条路走到黑

在这里插入图片描述

bfs 所有路口看一遍

在这里插入图片描述

题4:解数独

编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        char[][] board = new char[][]{
                {'5', '3', '.', '.', '7', '.', '.', '.', '.'},
                {'6', '.', '.', '1', '9', '5', '.', '.', '.'},
                {'.', '9', '8', '.', '.', '.', '.', '6', '.'},
                {'8', '.', '.', '.', '6', '.', '.', '.', '3'},
                {'4', '.', '.', '8', '.', '3', '.', '.', '1'},
                {'7', '.', '.', '.', '2', '.', '.', '.', '6'},
                {'.', '6', '.', '.', '.', '.', '2', '8', '.'},
                {'.', '.', '.', '4', '1', '9', '.', '.', '5'},
                {'.', '.', '.', '.', '8', '.', '.', '7', '9'}
        };
        dfs(board,0,0);
        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                System.out.print(board[i][j]+" ");
            }
            System.out.println();
        }

    }
    static boolean isfish=false;
    public  static boolean check(char[][]board,int x,int y,int i){
        //检查在board[x][y]填入i是否合法
        for(int j=0;j<9;j++){
            if(board[x][j]=='0'+i){
                return false;
            }
            if(board[j][y]=='0'+i){
                return false;
            }
        }
        //(x/3)代表第几个3组吗,(x/3+1)代表(x/3)的下一组
        for(int j=(x/3)*3;j<(x/3+1)*3;j++){
            for(int l=(y/3)*3;l<(y/3+1)*3;l++){
                if(board[j][l]=='0'+i){
                    return false;
                }
            }
        }
        return true;
    }
    public static void dfs(char[][]board,int x,int y){
        if(x==9){
            isfish=true;
            return;
        }
        if(board[x][y]=='.'){//当前元素没有数字
            for(int i=1;i<=9;i++){
                if(check(board,x,y,i)){
                    board[x][y]=(char)('0'+i);
                    dfs(board,x+((y+1)/9),(y+1)%9);//递归下一个空
                    if(isfish){
                        return;
                    }
                }
            }
            board[x][y]='.';//说明没有符合要求的
        }else{//有数字递归下一个空
            dfs(board,x+(y+1)/9,(y+1)%9);
        }
    }


}
题5:和恰好为k

给定整数序列a1,a2,…,an,判断是否可以从中选出若干数,使它们的和恰好为k.
1≤n≤20
-108≤ai≤108
-108≤k≤108
此题可以用DFS来做,也可以用二进制求子集的方法

public static void dfs(int []m, int k, int cur, ArrayList<Integer> ints){
    if(k==0){
        System.out.println("Yes "+ints);
        System.exit(0);
    }
    if(cur==m.length){
        return;
    }
    dfs(m,k,cur+1,ints);//不将元素加入的dfs
    ints.add(m[cur]);  //将元素加入的dfs
    int index=ints.size()-1;//加入元素的位置
    dfs(m,k-m[cur],cur+1,ints);//将元素加入的dfs
    ints.remove(index);//回溯
}
题6:水洼法

Descriptions:
由于近日阴雨连天,约翰的农场中中积水汇聚成一个个不同的池塘,农场可以用 N x M (1 <= N <= 100; 1 <= M <= 100) 的正方形来表示。农场中的每个格子可以用’W’或者是’.'来分别代表积水或者土地,约翰想知道他的农场中有多少池塘。池塘的定义:一片相互连通的积水。任何一个正方形格子被认为和与它相邻的8个格子相连。
给你约翰农场的航拍图,确定有多少池塘
Input
Line 1: N 和 M
Lines 2…N+1: M个字符一行,每个字符代表约翰的农场的土地情况。每个字符中间不包含空格
Output
Line 1: 池塘的数量
题目链接 https://vjudge.net/problem/POJ-2386
先找到一个有水的第一个点,进入DFS,并把它设置为无水,遍历这个点的八个方向,进行深搜,直到整个一片干燥了,退出DFS,count++,开始下一个池塘的寻找

import java.util.Scanner;

//水洼数
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int M = sc.nextInt();
        int N = sc.nextInt();
        int count = 0;
        char[][] a = new char[M][N];
        for (int i = 0; i < M; i++) {
            a[i] = sc.next().toCharArray();
        }
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < N; j++) {
                if (a[i][j] == 'W') {
                    dfs(a, i, j);
                    count++;
                }
            }
        }
        System.out.println(count);
    }

    public static void dfs(char[][] a, int i, int j) {
        a[i][j] = '.';//把池塘变成没水
        for (int k = -1; k < 2; k++) {
            for (int l = -1; l < 2; l++) {
                if (k == 0 && l == 0) {
                    continue; //为本身
                }
                if (i+ k >= 0 && i + k < a.length && j + l >= 0 && j + l <a[0].length) {
                    if (a[i+k][j+l] =='W') {
                        dfs(a, i + k, j + l);
                    }
                }
            }
        }
    }
}

回溯

递归调用代表开启一个分支,如果希望这个分支返回后某些数据恢复到分支开启前的状态以便重新开始,就要使用回溯技巧
比如
全排列的交换法,数独,部分和 都用到了回溯

剪枝

深搜时,如果已经明确从当前状态无论怎么转移都不会存在更优解,就应该中断接下来的继续搜索,这种方法称为剪枝
数独里面有剪枝
部分和里面有剪枝
在这里插入图片描述

题7:N皇后问题

在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。
你的任务是,对于给定的N,求出有多少种合法的放置方法。
Input
共有若干行,每行一个正整数N≤10,表示棋盘和皇后的数量;如果N=0,表示结束。
Output
共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        while(true){
             n=sc.nextInt();
             if(n==0){
                 break;
             }
             count=0;
            res=new int[n];
            dfs(0);
            System.out.println(count);
        }
    }
    static int count=0;
    static int n=0;
    static int []res;
    public static void dfs(int row){
        if(row==n){
            count++;
            return;
        }
        for(int col=0;col<n;col++) {
            boolean fc=true;
            for (int i = 0; i < row; i++) {
                if (res[i]==col||i+res[i]==col+row||i-res[i]==row-col) {
                    fc = false;
                    break;
                }
            }
            if (fc) {
                res[row] = col;
                dfs(row+1);
            }
        }
    }
}
题8:素数环

圆环由n个圆组成,如图所示。将自然数1、2,…,n分别放入每个圆,并且两个相邻圆中的数字总和应为质数。
注意:第一个圆的数目应始终为1。
输入值
n(0 <n <20)。
输出量
输出格式如下所示。每一行代表环中从顺时针和逆时针1开始的一系列圆圈编号。数字顺序必须满足上述要求。按字典顺序打印解决方案。您将编写一个完成上述过程的程序。在每种情况下都打印空白行。
在这里插入图片描述

import java.util.Scanner;

public class 素数环 {
    static  int count=1;
    private static boolean check(int m){//是质数为true
        if(m<2){
            return false;
        }
        for (int i = 2; i*i <=m ; i++) {
            if(m%i==0){
                return false;
            }
        }
        return true;
    }
    private static void dfs(int[] res, int k) {
        int n = res.length;
        if (k == n && check(res[n - 1] + res[0])) {//res已经填完最后一个,要检查头尾和是否为质数
            for (int num : res) {
                System.out.print(num + " ");
            }
            System.out.println();
        }else{
            for (int i = 2; i <= n; i++) {//检查2-n是否是可以
                boolean isAc = true;
                for (int j = 1; j < k; j++) {//i是否前面已经用过了
                    if (res[j] == i) {
                        isAc = false;
                        break;
                    }
                }
                if (isAc && !check(i + res[k - 1])) {//i是否和前面元素和是素数
                    isAc = false;
                }
                if (isAc) {//如果i符合要求
                    res[k] = i;//填入
                    dfs(res, k + 1);//继续填下一个
                    res[k] = 0;//回溯
                }
            }
        }

    }

    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        while (true){
            int n=sc.nextInt();
            int[] res=new int[n];
            res[0]=1;
            System.out.println("Case "+count+":");
            dfs(res,1);
            count++;
            System.out.println();
        }
    }
}
题9:困难的串

如果一个字符串包含两个相邻的重复子串,则称它为容易的串,其他串称为困难的串,
如:BB,ABCDACABCAB,ABCDABCD都是容易的,A,AB,ABA,D,DC,ABDAB,CBABCBA都是困难的。
输入正整数n,L,输出由前L个字符(大写英文字母)组成的,字典序第n小的困难的串。
例如,当L=3时,前7个困难的串分别为:
A,AB,ABA,ABAC,ABACA,ABACAB,ABACABA
n指定为4的话,输出ABAC

小结

有一类问题,有明确的递推形式,则比较容易用迭代形式实现,用递归也有较为明确的层数和宽度
如走楼梯,走方格,硬币表示,括号组合,子集,全排列
有一类问题,解的空间很大,要在所有可能性中找到答案,只能进行试探,尝试往前走一步,不行再回来,再往其他的分支走
即 dfs+回溯+剪枝
对这类问题的优化,越早减越好,但是很难
如素数环

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值