系统学习算法:专题十 递归,搜索与回溯综合练习

根据前几个专题的学习,现在来一些比较综合的题进行练习

题目一:

思路:

根据之前写的那道求子集的题步骤一样,只是多了异或这一步,那我们完全可以先求出所有子集,然后再遍历子集,求得每一个子集异或的结果,进行相加

代码1:

class Solution {
    //所有子集异或的和
    public int sum=0;
    //结果集
    public List<List<Integer>> ret=new ArrayList<>();
    //其中一个子集
    public List<Integer> path=new ArrayList<>();
    public void dfs(int[] nums,int k){
        //添加子集
        ret.add(new ArrayList<>(path));
        for(int i=k;i<nums.length;i++){
            path.add(nums[i]);
            dfs(nums,i+1);
            //恢复现场
            path.remove(path.size()-1);
        }
    }
    public int subsetXORSum(int[] nums) {
        //先求出所有子集
        dfs(nums,0);
        //遍历每个子集
        for (List<Integer> x:ret){
            //该子集的异或和
            int ans=0;
            for (int a:x){
                ans^=a;
            }
            //加上该子集异或和
            sum+=ans;
        }
        return sum;
    }
}

 但是我们没必要用二维数组和一维数组求存储结果集和子集,因为这道题不需要我们返回这些,只需要求得每个子集的异或和,那我们完全可以在求子集的过程中就进行异或操作,减少时间复杂度和空间复杂度

其中一维数组恢复现场用的是remove,但我们异或就可以利用2个相同的数会消消乐来进行恢复现场

代码2(优化):

class Solution {
    public int sum=0;
    //该子集的异或和
    public int path=0;
    public void dfs(int[] nums,int k){
        //加上该子集的异或和
        sum+=path;
        for(int i=k;i<nums.length;i++){
            //异或该子集的元素
            path^=nums[i];
            dfs(nums,i+1);
            //恢复现场
            path^=nums[i];
        }
    }
    public int subsetXORSum(int[] nums) {
        dfs(nums,0);
        return sum;
    }
}

 题目二:

思路:

跟之前写的全排列I前面思路几乎一样,但多了重复数字这一前提,使得按照之前的方法会重复一些全排列

最简单的方法就是还是按照之前的写法写,但是最后用一个哈希set来去重即可,虽然时间复杂度和空间复杂度都会比较高,但是思路很简单,几乎照搬之前的代码

代码1:

class Solution {
    public List<List<Integer>> ret=new ArrayList<>();
    public List<Integer> path=new ArrayList<>();
    //哈希表和布尔数组一样的作用,可互换
    Map<Integer,Integer> hash=new HashMap<>();
    public void dfs(int[] nums){
        //如果排完了
        if(path.size()==nums.length){
            ret.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
            //如果查哈希表发现没用过
            if(hash.getOrDefault(i,0)==0){
                path.add(nums[i]);
                //更改为用过
                hash.put(i,1);
                dfs(nums);
                //恢复现场
                hash.put(i,0);
                path.remove(path.size()-1);
            }
        }
    }
    public List<List<Integer>> permuteUnique(int[] nums) {
        dfs(nums);
        //用哈希set去重
        Set<List<Integer>> set=new HashSet<>(ret);
        //去完重再转回二维数组
        ret=new ArrayList<>(set);
        return ret;
    }
}

虽然思路很简单,但是还是消耗比较大,不够优秀

所以就来思考如何不使用哈希set,在排列的时候就进行对应的剪枝,减少set的消耗

以[1,1,1,2]这个数组来举例

还是先画出决策树

从第一次选第一格的数字时,就发生了剪枝,那就是如果出现相同的数字,就要判断一下,如果前面用过该数字,后面相同的就不能使用了,但问题是数组不一定有序,所以我们要先排序

第一个前面没有数字,但第一个一定是不用剪枝的,所以把第一个也加入条件

但每次循环都从第一个开始,但第一个如果已经用过,那么就不能再用了,所以前提是没使用过的不用剪枝

综上有三个条件,没使用过&&(如果是第一个||如果前面的数字不等于后面的数字||两个数字相等用最前面没用过的),如果满足这个条件就进入递归,否则就不进入(剪枝)

当然也可以换成满足条件就剪枝,不满足条件就进入递归,只需要将上面的或者和并且进行互换,否定和肯定进行互换即可

代码2:

class Solution {
    List<List<Integer>> ret=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    boolean[] check;
    public void dfs(int[] nums){
        //如果排完了
        if(path.size()==nums.length){
            ret.add(new ArrayList<>(path));
            //剪枝
            return;
        }
        for(int i=0;i<nums.length;i++){
            //满足条件的就不剪枝
            if(check[i]==false&&(i==0||nums[i-1]!=nums[i]||check[i-1]==false)){
                path.add(nums[i]);
                //标记为使用过
                check[i]=true;
                dfs(nums);
                //恢复现场
                check[i]=false;
                path.remove(path.size()-1);
            }
        }
    }
    public List<List<Integer>> permuteUnique(int[] nums) {
        //先排序
        Arrays.sort(nums);
        check=new boolean[nums.length];
        dfs(nums);
        return ret;
    }
}

题目三:

思路:

老样子,先画决策树

跟之前的操作大同小异,只是这里如何建立映射关系,哈希表是一一映射,这里是一对多映射,可以创建一个字符串数组,每一个数字下标对应其所能表示的字符串

然后就按照之前全排列的步骤写就行

代码:

class Solution {
    //结果集
    List<String> ret =new ArrayList<>();
    //每一个排列
    StringBuffer sb=new StringBuffer();
    //创建映射关系
    String[] ss=new String[]{null,null,"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    public void dfs(String s,int k){
        //如果排完了
        if(sb.length()==s.length()){
            ret.add(sb.toString());
            return;
        }
        //找到对应映射的字符串
        String str=ss[s.charAt(k)-'0'];
        //遍历字符串
        for(int i=0;i<str.length();i++){
            //添加该字符串的第i个
            sb.append(str.charAt(i));
            //往后排列下一个
            dfs(s,k+1);
            //恢复现场
            sb.deleteCharAt(sb.length()-1);
        }
    }
    public List<String> letterCombinations(String digits) {
        //如果为空字符串
        if(digits.equals("")){
            return ret;
        }
        dfs(digits,0);
        return ret;
    }
}

注意这里判断是否是空字符串可以用digits.equals("")或者digits.length()==0来判断,不能用digits=="",因为这个是比较引用地址,肯定是false,编程语言还学了其他的比如python就容易写出来,不要犯语法错误

题目四:

思路:

可能第一反应是跟之前有一道题很像,就是选或者不选,比如第一个选“(”或者不选“(”, 选“)”或者不选“)”,按照这种思路来画决策树,就可以将所有括号组成排列完

但是又排列的太过了,题目要求是有效的括号组合,什么是有效的括号组合

 第一个显而易见,第二个证明也不难,就是不能出现“())(”这种情况,这种情况第二对的括号就反了

按照之前简单的选或者不选的决策树,有些无效的括号组合就添加进来了,所以我们就要进行剪枝

主要剪的就是左括号的数量必须大于等于右括号,换而言之就是添加右括号之前必须有待匹配的左括号存在

那么我们就需要两个全局变量left和right来记录左右括号的剩余数量,结束条件那就是当排列长度为括号对数的两倍时就停止

代码:

class Solution {
    //结果集
    List<String> ret=new ArrayList<>();
    //每一个排列
    StringBuffer sb=new StringBuffer();
    //左括号和右括号剩余数量
    int left=0;
    int right=0;
    public void dfs(int n){
        //如果排完了
        if(sb.length()==n*2){
            ret.add(sb.toString());
            return;
        }
        //如果还有左括号
        if(left>0){
            //加上左括号
            sb.append("(");
            //剩余数量减1
            left--;
            //排下一个空
            dfs(n);
            //恢复现场
            sb.deleteCharAt(sb.length()-1);
            left++;
        }
        //如果前面出现了左括号
        if(left<right){
            //加上右括号
            sb.append(")");
            right--;
            //排下一个空
            dfs(n);
            //恢复现场
            sb.deleteCharAt(sb.length()-1);
            right++;
        }
    }
    public List<String> generateParenthesis(int n) {
        //更新数量
        left=right=n;
        dfs(n);
        return ret;
    }
}

题目五:

思路:

 还是大同小异的全排列类型的题目,其中[2,1]和[1,2]是同一种,所以要进行剪枝,因此传参的时候要传遍历的位置

画决策树,然后就可以写代码了

代码:

class Solution {
    //结果集
    List<List<Integer>> ret = new ArrayList<>();
    //每一种排列
    List<Integer> path = new ArrayList<>();

    public void dfs(int n, int k, int cur) {
        //如果排完了
        if (path.size() == k) {
            ret.add(new ArrayList<>(path));
            return;
        }
        for (int i = cur; i <= n; i++) {
            path.add(i);
            dfs(n, k, i + 1);
            //恢复现场
            path.removeLast();  
        }
    }

    public List<List<Integer>> combine(int n, int k) {
        dfs(n, k, 1);
        return ret;
    }
}

题目六:

思路:

跟之前子集那道题大同小异,之前是选或者不选,这道题是加或者减

还是画决策树,然后写代码

 代码1(全局变量):

class Solution {
    //结果
    int count = 0;
    //和
    int sum = 0;
    public void dfs(int[] nums, int target, int cur) {
        //如果数字全部用完了并且符合目标和
        if (cur == nums.length && sum == target) {
            count++;
            return;
        }
        //如果还有数字没用完
        if (cur < nums.length) {
            //加
            sum += nums[cur];
            dfs(nums, target, cur + 1);
            sum -= nums[cur];  //恢复现场
            //减
            sum -= nums[cur];
            dfs(nums, target, cur + 1);
            sum += nums[cur];  //恢复现场
        }
    }

    public int findTargetSumWays(int[] nums, int target) {
        dfs(nums, target, 0);
        return count;
    }
}

这道题也可以利用方法传参的方式来自动恢复现场,将和以参数传递

代码2(方法传参):

class Solution {
    // 结果
    int count = 0;
    // 和
    int sum = 0;

    public void dfs(int[] nums, int target, int cur, int path) {
        // 如果数字全部用完了并且符合目标和
        if (cur == nums.length && path == target) {
            count++;
            return;
        }
        // 如果还有数字没用完
        if (cur < nums.length) {
            // 加
            dfs(nums, target, cur + 1, path + nums[cur]);
            // 减
            dfs(nums, target, cur + 1, path - nums[cur]);
        }
    }

    public int findTargetSumWays(int[] nums, int target) {
        dfs(nums, target, 0, 0);
        return count;
    }
}

这道题第二种方式时间会更快一点,但大部分都是忽略不计的,两种方法都行

题目七:

思路:

题意很简单,跟之前的也大同小异,如果按照之前的方法,那么会出现顺序不同但结果是同一个的情况,有一个很暴力无脑的方法 ,就是添加的时候排好序再添加,最后再用哈希set去重就行

代码(暴力):

class Solution {
    List<List<Integer>> ret=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public void dfs(int[] nums,int target,int sum){
        //如果加过了就不加了
        if(sum>target){
            return;
        }
        //如果刚刚好
        if(sum==target){
            //先排序再添加
            List<Integer> tmp=new ArrayList<>(path);
            Collections.sort(tmp);
            ret.add(tmp);
            return;
        }
        //进行添加
        for(int i=0;i<nums.length;i++){
            path.add(nums[i]);
            dfs(nums,target,sum+nums[i]);
            //恢复现场
            path.removeLast();
        }
    }
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates,target,0);
        //用哈希set去重
        Set<List<Integer>> set=new HashSet<>(ret);
        ret=new ArrayList<>(set);
        return ret;
    }
}

其中数组的排序是Arrays.sort(),而顺序表的排序是Collections.sort()

这种方法很好想,不需要花心思剪枝,但是时间空间复杂度肯定也不低

如果要剪枝的话也很容易,先画决策树

(注:第二层3+5等于8,不是等于5,画错了) 

 之前的题是不能重复选,所以数组位置都是加1,这道题可以重复选,所以位置不用加1

剪枝有两种情况,第一种就是数组位置之前的都剪掉,第二种就是和大于目标和时,直接return

代码1:

class Solution {
    List<List<Integer>> ret=new ArrayList<>();
    List<Integer> path=new ArrayList<>();
    public void dfs(int[] nums,int target,int sum,int cur){
        //如果加过了就不加了
        if(sum>target){
            return;
        }
        //如果刚刚好
        if(sum==target){
            ret.add(new ArrayList<>(path));
            return;
        }
        //进行添加
        for(int i=cur;i<nums.length;i++){
            path.add(nums[i]);
            dfs(nums,target,sum+nums[i],i);
            //恢复现场
            path.removeLast();
        }
    }
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates,target,0,0);
        return ret;
    }
}

决策树也不唯一,除此之外还有一种画法

那就是按照选多少个的思路来画决策树

比如从2开始,可以选0到4个2,因为第5个2就超了

然后在每个分支下,去选n个3,同理在0个2的基础上可以选0到2个3,以此类推

代码2:

class Solution {
    int aim;
    List<Integer> path;
    List<List<Integer>> ret;

    public List<List<Integer>> combinationSum(int[] nums, int target) {
        path = new ArrayList<>();
        ret = new ArrayList<>();
        aim = target;
        dfs(nums, 0, 0);
        return ret;
    }

    public void dfs(int[] nums, int pos, int sum) {
        if (sum == aim) {
            ret.add(new ArrayList<>(path));
            return;
        }
        if (sum > aim || pos == nums.length)
            return;
        // 枚举 nums[pos] 使⽤多少个
        for (int k = 0; k * nums[pos] + sum <= aim; k++) {
            if (k != 0)
                path.add(nums[pos]);
            dfs(nums, pos + 1, sum + k * nums[pos]);
        }

        // 恢复现场
        for (int k = 1; k * nums[pos] + sum <= aim; k++) {
            path.remove(path.size() - 1);
        }
    }
}

题目八:

思路:

其中数字不用变,如果字母的话就有两种选择,转大写或者小写

然后结束条件还是排的长度等于原字符串的长度就说明排完了,就添加到结果集

恢复现场就删除最后的元素即可

代码:

class Solution {
    List<String> ret = new ArrayList<>();
    StringBuffer path = new StringBuffer();
    public void dfs(String s, int cur) {
        //结束条件
        if (cur == s.length()) {
            String ss = path.toString();
            ret.add(ss);
            return;
        }
        char ch = s.charAt(cur);
        //如果当前字符是字母
        if (Character.isLetter(ch)) {
            //选择大写
            path.append(Character.toUpperCase(ch));
            dfs(s, cur + 1);
            path.deleteCharAt(path.length() - 1);
            //选择小写
            path.append(Character.toLowerCase(ch));
            dfs(s, cur + 1);
            path.deleteCharAt(path.length() - 1);
        }
        //如果是数字
        if (Character.isDigit(ch)) {
            path.append(ch);
            dfs(s, cur + 1);
            path.deleteCharAt(path.length() - 1);
        }
    }

    public List<String> letterCasePermutation(String s) {
        dfs(s, 0);
        return ret;
    }
}

题目九:

思路:

还是先画出决策树

两种情况要剪枝,第一种是之前用过的不能用,第二种是不满足题目优美的条件就剪掉,每次填入一个空,用Boolean类型的数组记录使用情况,这道题没说要记录所有路径情况,所以可以不用建立path数组,只用返回有多少种即可,每次回溯完要恢复现场

代码:

class Solution {
    //记录有多少种
    int count = 0;
    //记录数字使用情况
    boolean[] check;

    public void dfs(int n, int cur) {
        //如果当前全部排完了
        if (cur == n + 1) {
            count++;
            return;
        }
        //枚举1到n个数
        for (int j = 1; j <= n; j++) {
            //如果这个数没用过
            if (check[j] == false) {
                //如果这个数符合优美的条件
                if (j % cur == 0 || cur % j == 0) {
                    //修改使用情况
                    check[j] = true;
                    //去填下一个空
                    dfs(n, cur + 1);
                    //恢复现场
                    check[j] = false;
                }
            }
        }
    }

    public int countArrangement(int n) {
        //因为下标是从1开始,所以要加1
        check = new boolean[n + 1];
        //有n个数,从第1个空开始填
        dfs(n, 1);
        return count;
    }
}

题目十:

思路:

很经典的一道递归回溯的题,题意就是返回一个二维数组,一维数组记录每一种的解法,一维数组里的字符串表示放置情况,每一个字符串代表每一行的放置情况,Q代表有皇后,.表示空格

先画决策树

也就是从第0行开始,每个格子去尝试

剪枝有三种情况,第一种就是同列有皇后,第二种就是主对角线有皇后,第三种就是副对角线有皇后(因为我们是一行一行去枚举的,所以不包括同行有皇后的情况)

那么接下来就是如何实现剪枝

我们还是根据之前的方法,用boolean类型的数组去记录是否有皇后,如果false就代表没有皇后,这个位置可以放,true就代表有皇后,这个位置不能放

因为有三种剪枝情况,所以可以创建三个Boolean类型的数组

其中col记录列的情况,checkDig1记录主对角线,checkDig2记录副对角线

其中列很好写,有n列就创建长度为n的数组

主对角线如下

以(0,0)为原点,创建坐标系,很容易知道每条主对角线的斜率是1(因为是45度),根据关系可以写成表达式:y=kx+b,其中k=1,也就是y=x+b

然后进行移项,y-x=b,所以每一条主对角线上的点y-x都是一个定值,因此我们就可以用boolean数组的下标y-x来代表这条主对角线有没有皇后

但是当y<x时,会出现负数,而数组下标又没有负数,所以我们要对两边进行同加

其中负数最小的情况是当y=0,x=n-1(有n列,但是是从0开始,所以最大为n-1列)

此时y-x=-n+1,所以为了让所有情况都为正数,就可以加上n(当然也可以加上n-1,使得刚好为0,无非是浪不浪费一个格子而已,只要足够就行)

因此就用y-x+n来表示主对角线皇后的情况

同理副对角线

斜率为-1,经过移项得到x+y=b,因为是加法,所以不出现负数的情况,就不用同加了

最后确定三个数组长度 

列数组:有n列,所以长度为n

主对角线数组:当row=n-1,col=0,row-col+n=n-1-0+n=2*n-1,长度为2n刚好

(如果要做到极致的不浪费,那么就是row-col+n-1=2*n-2,长度为2n-1刚好)

副对角线数组:当row=n-1,col=n-1,row+col=n-1+n-1=2*n-2,所以长度2n足够

(如果要做到极致的不浪费,那么长度为2n-1刚好)

因此当这一个格子对应的三个Boolean数组都为false才能放皇后,这时进行修改记录情况,去找下一行,找完要恢复现场

结束条件就是当到了第n行时,就说明全都正确放完了,没到第n行的全部都失败了,此时将棋盘的放置情况添加即可

代码:

class Solution {
    //结果集
    List<List<String>> ret=new ArrayList<>();
    //记录是否有皇后,col:列,checkDig1:主对角线,checkDig2:副对角线
    boolean[] col,checkDig1,checkDig2;
    //模拟棋盘
    char[][] ch;
    public void dfs(int row,int n){
        //如果到了第n行
        if(row==n){
            //把这种解法全部放进去
            List<String> path=new ArrayList<>();         
            for(int i=0;i<n;i++){
                path.add(new String(ch[i]));
            }
            ret.add(path);
            return;
        }
        //遍历row行的第n格
        for(int i=0;i<n;i++){
            //如果这个格子没有发生皇后冲突
            if(col[i]==false&&checkDig1[row-i+n-1]==false&&checkDig2[row+i]==false){
                //可以放进去
                ch[row][i]='Q';
                //修改记录情况
                col[i]=checkDig1[row-i+n-1]=checkDig2[row+i]=true;
                //去下一行
                dfs(row+1,n);
                //恢复现场
                ch[row][i]='.';
                col[i]=checkDig1[row-i+n-1]=checkDig2[row+i]=false;                
            }
        }
    }
    public List<List<String>> solveNQueens(int n) {
        //有n列,所以长度为n足够
        col=new boolean[n];
        //当row=n-1,col=0,row-col+n-1=2*n-2,所以长度2n-1刚好
        checkDig1=new boolean[2*n-1];
        //当row=n-1,col=n-1,row+col=2*n-2,所以长度2n-1刚好
        checkDig2=new boolean[2*n-1];
        //模拟n*n的棋盘
        ch=new char[n][n];
        //初始化棋盘
        for(int i=0;i<n;i++){
            Arrays.fill(ch[i],'.');
        }
        //从第0行开始遍历,到第n行结束
        dfs(0,n);
        return ret;
    }
}

题目十一:

思路:

跟上一道的n皇后比较类似,都是棋盘类,但是n皇后剪枝主要是行,列,主副对角线,而数独是行,列,九宫格

所以行列大致类似的思路,而实现九宫格剪枝是主要问题

先给棋盘画出下标

 总共有9个九宫格,我们以每3行每3列作为一块,就变成0,1,2三“行”,和0,1,2三“列”

这样[0][0]就对应左上角的九宫格,以此类推

而0-2这三列/3都等于0,3-5这三列/3都等于1,6-8这三列/3都等于2,因此就可以通过/3来判断是哪一个九宫格了,列如此,反过来行也是如此

而之前n皇后只需要记录有没有一个就行,所以是一维数组

而数独还有记录是哪一个数字出现了,所以还要多一维

因此

行:[9][10](9行,每行有9个数,所以长度10就是1对应1下标,浪费了0这一小格,如果要做到极致的不浪费,也可以为[9][9],那么就是1对应0下标)

列:[9][10](9列,每列有9个数,所以长度为10就是1对应1下标,浪费了0这一小格,如果要做到极致的不浪费,也可以为[9][9],那么就是1对应0下标)

九宫格:[3][3][10](3行3列个九宫格,每个九宫格有9个数,所以长度为10就是1对应1,浪费了0这一小格,如果要做到极致的不浪费,也可以为[3][3][9],那么就是1对应0下标)

那么接下来就遍历整个棋盘,如果空的就不管,有数字就去三个数组里看看有没有出现重复的,有就直接返回false,没有就继续遍历,最后遍历完了还没有返回false,就说明是有效的数读,返回true

代码:

class Solution {
    //行
    boolean[][] row=new boolean[9][10];
    //列
    boolean[][] col=new boolean[9][10];
    //九宫格
    boolean[][][] grid=new boolean[3][3][10];

    public boolean isValidSudoku(char[][] board) {
        //遍历棋盘
        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                //如果不是空格,是数字
                if(board[i][j]!='.'){
                    //拿到这个格子的数字
                    int num=board[i][j]-'0';
                    //如果出现过了
                    if(row[i][num]||col[j][num]||grid[i/3][j/3][num]){
                        return false;
                    }else{//修改记录情况
                        row[i][num]=col[j][num]=grid[i/3][j/3][num]=true;
                    }
                }
            }
        }
        //说明是有效的数独
        return true;
    }
}

题目十二:

思路:

 跟上一题比较类似,只不过这道题需要我们自己将数独填完,其中需要进行决策和回溯,而决策的时候需要进行一些剪枝,其中剪枝就使用我们上一道题判断是否有效来实现

第一步先将原本棋盘上数字的出现情况进行修改,也就是初始化

第二步就去遍历棋盘,从第0行第0列开始,一行一行的去填

如果该行和该列的这一格是空的,就说明要填,我们就从1-9进行尝试,也就是决策,而有些数我们通过判断就可以排除掉,这时就进行了剪枝,然后往后进行遍历,其中如果是最后一列,就要换行,反之,就往下一列去遍历

而如果有数,就说明原本题目就已经填过了,我们不用修改,直接去填后面的

其中结束条件那就是到了第9行,说明整个棋盘都填完了,那么就结束了,可以返回

但其中有些决策是错误的,导致后面填不下去了,即无解了,说明前面填错了,我们要让之前填数的知道,所以要有返回值false,而如果接收到返回值false,就说明这里填错了,要重新填,那么就要恢复现场,将该格置空,并修改出现情况

其中有唯一一个正确解会到达结束条件,那么结束条件就要返回true,让前面的都知道这么填是对的,不用修改了,于是又一层一层返回true

最后就解出了数独

代码:

class Solution {
    boolean[][] row = new boolean[9][10];
    boolean[][] col = new boolean[9][10];
    boolean[][][] grid = new boolean[3][3][10];

    public boolean dfs(char[][] board, int i, int j) {
        //如果到了第9行,说明已经解完数独了
        if (i == 9) {
            return true;
        }
        //如果这格是空的
        if (board[i][j] == '.') {
            //尝试填数1-9
            for (int n = 1; n <= 9; n++) {
                //如果这个数填进去没有发生冲突
                if (row[i][n] == false && col[j][n] == false && grid[i / 3][j / 3][n] == false) {
                    //修改出现情况
                    row[i][n] = col[j][n] = grid[i / 3][j / 3][n] = true;
                    //填数
                    board[i][j] = (char) (n + '0');
                    //如果是该行的最后一列
                    if (j == 8) {
                        //去下一行继续填数,如果下一行也能够正确填完,说明之前的数没填错
                        if(dfs(board, i + 1, 0)){
                            //返回给上一次填数的时候,说明这里填对了
                            return true;
                        }else{//说明下一行出现填不下去的情况,说明前面填错了
                            //恢复现场
                            row[i][n] = col[j][n] = grid[i / 3][j / 3][n] = false;
                            board[i][j]='.';
                        }
                    } else {//如果不是该行的最后一列
                        //去填该行的下一列,如果下一列也能够正确填完,说明之前的数没填错
                        if(dfs(board, i, j + 1)){
                            //返回给上一次填数的时候,说明这里填对了
                            return true;
                        }else{//说明下一列出现填不下去的情况,说明前面填错了
                            //恢复现场
                            row[i][n] = col[j][n] = grid[i / 3][j / 3][n] = false;
                            board[i][j]='.';
                        }
                    }
                }
            }
            //出现无解的情况了,说明之前没填对,要告诉之前填数的决策是错的
            return false;
        } else {//如果是原本棋盘就有的数
            //如果是该行的最后一列
            if (j == 8) {
                //如果后面能够正确填数,说明之前没填错
                if(dfs(board, i + 1, 0)){
                    return true;
                }
                //后面填不下去了,说明之前填错了
                return false;
            } else {//如果不是该行的最后一列
                //如果后面能够正确填数,说明之前没填错
                if(dfs(board, i, j + 1)){
                    return true;
                }
                //后面填不下去了,说明之前填错了
                return false;
            }
        }
    }

    public void solveSudoku(char[][] board) {
        //先初始化原本棋盘数字出现情况
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    int num = board[i][j] - '0';
                    row[i][num] = col[j][num] = grid[i / 3][j / 3][num] = true;
                }
            }
        }
        //从第0行第0列开始
        dfs(board, 0, 0);
    }
}

题目十三:

思路:

这种属于矩阵内的搜索,类似于走迷宫,做决策的过程如下

 而画出决策树如下

所以这时候就需要先去遍历整个数组, 找到起点再去走,而起点也不止一个,需要找到正确的起点

然后走的方向是上下左右,去找字符串的下一个字符

因此我们的dfs功能是给定一个点,去找字符串包括cur位置之后的字符串

因此参数为dfs(char[][] board,int i,int j,int cur,String word),其中board是原数组,ij是坐标,cur是要找的字符串的cur位置,word是要找的字符串

其中上下左右会出现边界情况,因此需要作出界判断,没出界才能走

但是会有一个细节,那就是不能走重复的路

 因此我们需要一个记录数组,来记录走过的情况

那么结束条件就是当cur遍历完了字符串,就说明找完了,返回true,同时也要告诉上一次决策是对的,反之,要返回false,跟数独那个一个思想

但还有一个优化的点,那就是鸽巢原理,如果字符数组所有字符都没有字符串长的话,那么肯定是不能的,因此可以先判断一下

代码1:

class Solution {
    //原数组的行和列
    int row,col;
    //记录数组
    boolean[][] check;
    public boolean dfs(char[][] board,int i,int j,int cur,String word){
        //结束条件
        if(cur==word.length()){
            return true;
        }
        //如果当前是要找的字符
        if(board[i][j]==word.charAt(cur)&&check[i][j]==false){
            //标记该位置走过了
            check[i][j]=true;
            //往上
            if(i>0){
                if(dfs(board,i-1,j,cur+1,word)){
                    return true;
                }
            }
            //往下
            if(i+1<row){
                if(dfs(board,i+1,j,cur+1,word)){
                    return true;
                }                
            }
            //往右
            if(j+1<col){
                if(dfs(board,i,j+1,cur+1,word)){
                    return true;
                }                
            }
            //往左
            if(j>0){
                if(dfs(board,i,j-1,cur+1,word)){
                    return true;
                }                
            }
            //恢复现场
            check[i][j]=false;
            //说明此时无解,之前决策错了
            return false;
        }else{//说明当前不是要找的字符
            //告诉之前的决策错了
            return false;
        }
    }
    public boolean exist(char[][] board, String word) {
        //求原数组的行数和列数
        row=board.length;
        col=board[0].length;
        //鸽巢原理
        if(word.length()>row*col){
            return false;
        }
        //特判
        if(row==1&&col==1){
            if(board[0][0]==word.charAt(0)){
                return true;
            }
            return false;
        }
        //创建记录数组
        check=new boolean[row][col];
        //遍历数组找起点
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                //找到一个起点
                if(board[i][j]==word.charAt(0)){
                    if(dfs(board,i,j,0,word)){
                        //说明决策对了
                        return true;
                    }
                }
            }
        }
        //说明无解,找不到
        return false;
    }
}

但这里会出现字符数组只有一个的情况,那么起点就是终点,没有上下左右可以选,正常情况下认为是无解了,但其实这里是已经找完了,所以要特判一下

还有另外一个思路

刚刚那个dfs我们假设为给定一个点,将该点与字符串cur位置的字符进行比较

这回我们还可以假设为给定一个点,去上下左右找字符串cur位置的字符

然后还有上下左右的查找,我们之前是用四个dfs,比较冗杂,如果出现八个方向,那么要写八个dfs,非常多,因此有一个方式进行修改

我们可以用两个数组来存偏移量,并建立映射关系

 比如上图,dx[0]dy[0]就对应右,dx[1]dy[1]就对应左,如此类推,这样就可以用一个for循环来实现上下左右

代码2:

class Solution {
    int row, col;
    boolean[][] check;
    //上下左右映射关系
    int[] dx = { 0, 0, 1, -1 };
    int[] dy = { 1, -1, 0, 0 };

    public boolean dfs(char[][] board, int i, int j, int cur, String word) {
        //找完了
        if (cur == word.length()) {
            return true;
        }
        //上下左右去找
        for (int k = 0; k < 4; k++) {
            int x = dx[k], y = dy[k];
            //不出界&&没走过&&是要找的字符
            if (i + x < row && i + x >= 0 && j + y < col && j + y >= 0 && !check[i + x][j + y]
                    && board[i + x][j + y] == word.charAt(cur)) {
                //修改记录情况
                check[i + x][j + y] = true;
                if (dfs(board, i + x, j + y, cur + 1, word)) {//在当前位置的上下左右去找下一个字符
                    return true;
                }
                //恢复现场
                check[i + x][j + y] = false;
            }
        }
        //上下左右都没有,说明之前决策错了
        return false;
    }

    public boolean exist(char[][] board, String word) {
        row = board.length;
        col = board[0].length;
        //鸽巢原理
        if (row * col < word.length()) {
            return false;
        }
        check = new boolean[row][col];
        //遍历数组找起点
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                //找到一个起点
                if (board[i][j] == word.charAt(0)) {
                    //修改记录情况
                    check[i][j] = true;
                    //在当前位置的上下左右去找word的第二个字符
                    if (dfs(board, i, j, 1, word)) {
                        return true;
                    }
                    //恢复现场
                    check[i][j] = false;
                }
            }
        }
        //无解
        return false;
    }
}

题目十四:

思路:

跟上一道题差不多类型的,都是走迷宫,所以我们只需要先遍历数组,然后找起点,上下左右去寻找,因为也是不能走重复的路,所以需要使用一个记录数组

而这一道题主要独特的是不需要递归出口,按理来说大部分dfs一开始都有一个结束条件,这道题不需要,只需要在一开始更新结果就行,如果上下左右都不能走,那么也就自然return了

代码:

class Solution {
    //结果
    int max=0;
    //上下左右映射关系
    int[] dx={0,0,1,-1};
    int[] dy={1,-1,0,0};
    //记录数组
    boolean[][] check;
    int row,col;
    public void dfs(int[][] grid,int i,int j,int sum){
        //更新结果
        if(sum>max){
            max=sum;
        }
        //上下左右去找
        for(int k=0;k<4;k++){
            int x=dx[k],y=dy[k];
            if(i+x>=0&&i+x<row&&j+y>=0&&j+y<col&&check[i+x][j+y]==false&&grid[i+x][j+y]!=0){
                //修改记录情况
                check[i+x][j+y]=true;
                dfs(grid,i+x,j+y,sum+grid[i+x][j+y]);
                //恢复现场
                check[i+x][j+y]=false;
            }
        }
    }
    public int getMaximumGold(int[][] grid) {
        row=grid.length;
        col=grid[0].length;
        check=new boolean[row][col];
        //遍历数组找起点
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                //找到其中一个起点
                if(grid[i][j]!=0){
                    check[i][j]=true;
                    dfs(grid,i,j,grid[i][j]);
                    check[i][j]=false;
                }
            }
        }
        //返回结果
        return max;
    }
}

 题目十五:

 思路:

也还是走迷宫类型的题,还是先找到起点,然后去上下左右遍历,直到走到终点,这时就需要检查是否符合题目要求,是合法的走法

比较暴力的检查方法就是去遍历记录数组,如果除了-1之外都是true,说明都走过了,此时结果+1,反之则不加

还有一种方法要更好一点,那就是在遍历数组找起点的时候,顺便统计出从起点到终点不漏不重复的走需要多少步,然后dfs传参要多传一个当前步数,这样子到终点时只需要判断当前步数是否等于所需步数,即可知道是否是合法的走法

其他地方都与前两道走迷宫的题几乎相同

代码1:

class Solution {
    int row,col;
    int ret=0;
    int[] dx={0,0,1,-1};
    int[] dy={1,-1,0,0};
    boolean[][] check;
    //暴力检查是否是合法的走法
    public boolean checkright(int[][] grid,boolean[][] checkarr){
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                //如果是能走的但没走过
                if(grid[i][j]!=-1&&checkarr[i][j]==false){
                    //说明不是合法的
                    return false;
                }
            }
        }
        //说明合法
        return true;
    }
    public void dfs(int[][] grid,int i,int j){
        //如果走到终点了
        if(grid[i][j]==2){
            //如果是合法的
            if(checkright(grid,check)){
                //结果+1
                ret++;
            }
            return;
        }
        //上下左右
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<row&&y>=0&&y<col&&check[x][y]==false&&grid[x][y]!=-1){
                //修改记录情况
                check[x][y]=true;
                dfs(grid,x,y);
                //恢复现场
                check[x][y]=false;
            }
        }
    }
    public int uniquePathsIII(int[][] grid) {
        row=grid.length;
        col=grid[0].length;
        check=new boolean[row][col];
        //遍历数组找起点
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                if(grid[i][j]==1){
                    //修改记录情况
                    check[i][j]=true;
                    dfs(grid,i,j);
                    //返回结果
                    return ret;                    
                }
            }
        }
        //照顾编译器
        return ret;
    }
}

代码2(更优):

class Solution {
    int row,col;
    //结果
    int ret=0;
    //上下左右映射关系
    int[] dx={0,0,1,-1};
    int[] dy={1,-1,0,0};
    //记录数组
    boolean[][] check;
    //所需步数
    int step;
    public void dfs(int[][] grid,int i,int j,int count){
        //如果走到终点了
        if(grid[i][j]==2){
            //如果是合法的走法
            if(step==count){
                //结果+1
                ret++;
            }
            return;
        }
        //上下左右
        for(int k=0;k<4;k++){
            int x=i+dx[k],y=j+dy[k];
            if(x>=0&&x<row&&y>=0&&y<col&&check[x][y]==false&&grid[x][y]!=-1){
                //修改记录情况
                check[x][y]=true;
                dfs(grid,x,y,count+1);
                //恢复现场
                check[x][y]=false;
            }
        }
    }
    public int uniquePathsIII(int[][] grid) {
        row=grid.length;
        col=grid[0].length;
        check=new boolean[row][col];
        //起点的位置
        int bx=0,by=0;
        for(int i=0;i<row;i++){
            for(int j=0;j<col;j++){
                //统计需要的步数
                if(grid[i][j]==0){
                    step++;
                }else if(grid[i][j]==1){
                    //记录起点的位置并修改记录情况
                    check[i][j]=true;
                    bx=i;
                    by=j;                     
                }
            }
        }
        //加上起点和终点的两步
        step+=2;
        //从起点开始,当前已经走了起点,步数为1步
        dfs(grid,bx,by,1);
        return ret;
    }
}

总结:

至此所有的综合练习全部写完,应该对dfs有了更深刻的感受,其中重点是恢复现场,剪枝,画决策树,确定结束条件

以及走迷宫类型的题,可以用映射关系来记录上下左右,同理也可以记录八个方向

还有对角线和九宫格的写法

基本也就是暴搜,思路不算太难,主要要有代码实现能力,能将决策树的思路转化为代码

接下来还有floodfill和记忆化搜索两个小专题,学完就算大体地学完了递归,搜索和回溯这个大专题了,进行加油!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值