Leetcode 刷题记录 14 —— 回溯

本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C++语言,08及以后为Java语言。

01 全排列

在这里插入图片描述

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        
    }
}

预备知识

回溯法:一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化抛弃该解,即回溯并且再次尝试。

在这里插入图片描述

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> output = new ArrayList<>();
        for(int num : nums){
            output.add(num);
        }

        int n = nums.length;
        myFunction(n, result, output, 0);
        return result;
    }
	/**
     * 回溯方法,递归生成全排列
     * @param n 数组长度
     * @param output 当前排列的列表状态
     * @param res 存储所有排列的结果列表
     * @param first 当前固定元素的位置(从0开始)
     */
    public void myFunction(int n, List<List<Integer>> result, List<Integer> output, int first){
        if(first == n){
            result.add(new ArrayList<>(output));
            return;
        }

        for(int i=first; i<n; i++){
            Collections.swap(output, first, i);
            myFunction(n, result, output, first + 1);
            Collections.swap(output, first, i);
        }
    }
}

Collections是啥?

Collections 是 Java 标准库中 java.util 包下的一个工具类,提供了一系列静态方法,用来操作或返回集合(Collection)类型的对象,比如 ListSet 等。

常用功能包括:

  • 排序Collections.sort(List<T> list) 可以对列表进行排序
  • 交换元素Collections.swap(List<?> list, int i, int j) 用于交换列表中指定位置的两个元素
  • 查找Collections.max()Collections.min() 找最大值、最小值
  • 填充、复制、反转fill()copy()reverse()

02 子集

在这里插入图片描述

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        
    }
}

方法一:神奇二进制

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> output = new ArrayList<>();
        int n = nums.length;

        for(int mask=0; mask<(1<<n); mask++){ //遍历0~2^n-1之间的数
            output.clear();
            for(int i=0; i<n; i++){ //遍历0~n-1之间的位
                if((mask & (1<<i)) != 0){
                    output.add(nums[i]);
                }
            }
            result.add(new ArrayList<>(output));
        }

        return result;
    }
}

方法二:深度优先搜索

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> output = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        myFunction(0, nums);
        return result;
    }

    public void myFunction(int curr, int[] nums){
        if(curr == nums.length){
            result.add(new ArrayList<>(output));
            return;
        }

        output.add(nums[curr]);
        myFunction(curr+1, nums);
        output.remove(output.size()-1);
        myFunction(curr+1, nums);
    }
}

t.add(nums[cur]);是头添加还是尾添加?t.remove(t.size() - 1);是头删除还是尾删除?

t.add(nums[cur]);尾部添加,而 t.remove(t.size() - 1);尾部删除

List<List<Integer>> result = new ArrayList<>();默认访问权限是啥?public还是protected还是private

变量声明时如果不写访问修饰符,默认就是“包访问权限”(package-private),即默认访问权限。

output.remove(output.size()-1);为啥是output.size()-1而不是nums[curr]

  • output.size() - 1 是列表中最后一个元素的索引
  • output.remove(index) 是根据索引删除指定位置的元素。

为什么一般不用 output.remove(nums[curr])

考虑场景:

nums = [1, 2, 2]

递归过程中,output 可能是 [1, 2, 2]

  • 当想撤销回溯,删除最后一个2时,如果用 output.remove(nums[curr]),会删除列表中第一个2,而不是刚刚加进去的那个尾部元素。
  • 导致撤销操作不正确,破坏递归的状态维护。

03 电话号码的字母组合

在这里插入图片描述

在这里插入图片描述

class Solution {
    public List<String> letterCombinations(String digits) {
        //1.创建 List<String> 和 Map<Character, String>
        List<String> combinations = new ArrayList<>();
        if(digits.length() == 0){
            return combinations;
        }

        Map<Character, String> phoneMap = new HashMap<>(){{
            put('2', "abc");
            put('3', "def");
            put('4', "ghi");
            put('5', "jkl");
            put('6', "mno");
            put('7', "pqrs");
            put('8', "tuv");
            put('9', "wxyz");
        }};
        myFunction(combinations, phoneMap, digits, 0, new StringBuffer());

        return combinations;
    }

    public void myFunction(List<String> combinations, Map<Character, String> phoneMap, 
    String digits, int index, StringBuffer combination){
        //2.核心操作
        if(index == digits.length()){
            combinations.add(combination.toString());
        }else{
            //3.递归(回溯)
            char digit = digits.charAt(index); //'2'
            String letters = phoneMap.get(digit); //"abc"
            int count = letters.length(); //3
            for(int i=0; i<count; i++){
                combination.append(letters.charAt(i));
                myFunction(combinations, phoneMap, digits, index+1, combination);
                combination.deleteCharAt(index); //⭐
            }
        }
    }
}

new StringBuffer()啥意思?

new StringBuffer()表示创建一个空的、可以修改的字符串缓冲区对象。

在回溯算法中,利用StringBuffer可以高效地构建和修改当前的字符串组合,比如append添加字符,deleteCharAt删除字符,实现回溯过程中的“选择”和“撤销选择”。

Map<Character, String> phoneMap为啥要加双大括号,并写一堆put

内层的{{ ... }}是一个匿名内部类的实例初始化块

具体来说:

  • 第一对大括号{}是匿名内部类的定义体
  • 第二对大括号{}是匿名内部类的实例初始化块,放构造过程中要执行的代码

combination.toString()啥意思?

combination.toString() 的意思是将 combination 这个对象转换成一个普通的 String 类型,这一步是必要的,因为 combinationsList<String>,需要的是不可变的字符串。

04 组合总和

在这里插入图片描述

在这里插入图片描述

自己写的:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> output = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //1.创建
        /*
            List<List<Integer>> result = new ArrayList<>();
            List<Integer> output = new ArrayList<>();
        */
        myFunction(0, 0, target, candidates);
        return result;


        //2.核心步骤
        /*
            if(sum == target){
                result.add(new ArrayList<>(output));
            }
        */
        //3.递归(回溯)
        /*
            myFunction(int curr, int sum, int target, int[] candidates); //当前数组,当前位置
        */
    }

    public void myFunction(int curr, int sum, int target, int[] candidates){
        //特殊情况判断①
        if(curr == candidates.length){
            return;
        }

        //特殊情况判断②
        if(sum == target){
            result.add(new ArrayList<>(output));
            return;
        }

        if(sum + candidates[curr] <= target){
            output.add(candidates[curr]);
            myFunction(curr, sum + candidates[curr], target, candidates);
            output.remove(output.size() - 1);
        }
        myFunction(curr + 1, sum, target, candidates);
    }
}

05 括号总和

在这里插入图片描述

class Solution {
    List<String> result = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        //1.创建
        /*
            List<String> result = new ArrayList<>();
            StringBuffer output
        */
        myFunction(n, 0, 0, new StringBuffer());
        return result;

        //2.核心操作
        /*
            if(output.length() == n * 2){
                result.add(output.toString());
                return;
            }
        */


        //3.递归(回溯)
        /*
            n: 括号对数
            indexLeft: 左括号当前下标
            indexRight: 右括号当前下标
            output: 可变长度 String 字符串

            myFunction(int n, int indexLeft, int indexRight, StringBuffer output)
        */

    }

    public void myFunction(int n, int indexLeft, int indexRight, StringBuffer output){
        if(output.length() == n * 2){
            result.add(output.toString());
            return;
        }

        if(indexLeft < n){
            output.append("(");
            myFunction(n, indexLeft + 1, indexRight, output);
            output.deleteCharAt(output.length() - 1);
        }

        if(indexLeft > indexRight){
            output.append(")");
            myFunction(n, indexLeft, indexRight + 1, output);
            output.deleteCharAt(output.length() - 1);
        }
    }
}

06 单词搜索

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

自己写的:

class Solution {
    boolean flag = false;

    public boolean exist(char[][] board, String word) {
        //1.创建
        /*
            boolean flag
            StringBuffer output
        */
        myFunction(0, 0, board, word, new StringBuffer());
        return flag;

        //2.核心操作
        /*
            if(output == word){
                flag = true;
                return;
            }
        */


        //3.递归(回溯)
        /*
            myFunction(int row, int column, char[][] board, String word, StringBuffer output)
        */
    }

    public void myFunction(int row, int column, char[][] board, String word, StringBuffer output){
        if(output == word){
            flag = true;
            return;
        }

        output.append(board[row][column]);
        if(row+1 < board.length){
            myFunction(row+1, column, board, word);
        }
        if(column+1 < board[0].length){
            myFunction(row, column+1, board, word);
        }
        output.deleteCharAt(output.size()-1);
    }
}

参考答案:

class Solution {
    public boolean exist(char[][] board, String word) {
        int h = board.length, w = board[0].length;
        boolean[][] visited = new boolean[h][w];
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                boolean flag = check(board, visited, i, j, word, 0);
                if (flag) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean check(char[][] board, boolean[][] visited, int i, int j, String s, int k) {
        if (board[i][j] != s.charAt(k)) {
            return false;
        } else if (k == s.length() - 1) {
            return true;
        }
        visited[i][j] = true;
        int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
        boolean result = false;
        for (int[] dir : directions) {
            int newi = i + dir[0], newj = j + dir[1];
            if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
                if (!visited[newi][newj]) {
                    boolean flag = check(board, visited, newi, newj, s, k + 1);
                    if (flag) {
                        result = true;
                        break;
                    }
                }
            }
        }
        visited[i][j] = false;
        return result;
    }
}

07 分割回文串

在这里插入图片描述

class Solution {
    public List<List<String>> partition(String s) {
        
    }
}

Arrays.fill(f[i], true); 是什么意思?

  • f 是一个二维布尔数组,f[i] 表示二维数组 f 的第 i 行(也是一个布尔数组)。
  • Arrays.fill 是Java标准库中用于快速给数组填充值的方法。
  • Arrays.fill(f[i], true); 的意思是:将数组 f[i] 中的每个元素,都赋值为 true

② 为什么这样遍历?

for (int i = n - 1; i >= 0; --i) {
    for (int j = i + 1; j < n; ++j) {
        f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
    }
}

动态规划的状态转移是:

f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i+1][j-1];

也就是说:

子串[i, j]是否是回文,取决于:

  • s[i]s[j] 是否相等
  • 中间的子串 [i+1, j-1] 是否是回文(即 f[i+1][j-1]

因此,需要先保证f[i+1][j-1]已经被计算出来,才能正确计算f[i][j]

假如从前往后遍历 i,那f[i+1][j-1]还没有计算,dp就没法用。

③ ans.add(s.substring(i, j + 1));为什么是j+1而不是j?

这是因为Java中Stringsubstring方法的语法是:

s.substring(beginIndex, endIndex)
  • beginIndex 是子串起始索引(包含)。
  • endIndex 是子串结束索引(不包含)。

所以substring(i, j + 1)表示取得字符串从索引i开始,到j结束(包含位置j的字符),刚好是你想要的子串s[i..j]

ret.add(new ArrayList<>(ans)); 是什么是“深拷贝”?

  • 因为 ans 是递归过程中不断修改的同一个对象,它会随着回溯加入和删除元素。
  • 如果不复制,只把 ans 本身加入结果,后续修改会导致结果集中所有引用都变成最后的状态,结果错误。
  • 复制一份保证当前这个状态的路径是独立的,不会被后续递归修改。

什么是深拷贝?

  • 浅拷贝只复制引用,多个对象共享同一份内存数据(比如指向同一个列表),修改其中一个会影响所有。
  • 深拷贝则复制对象及其内部包含的数据,生成完全独立的对象。

dfs(s, j + 1);为什么新的递归起始位置是 j + 1

  • 当前选择的回文子串是 s[i..j](包含ij)。
  • 下一步需要找的是紧接着当前子串后面的部分,即以 j + 1 为起点的剩余字符串。
  • 因为回文分割要求子串连续且不重叠,每找到一个回文子串后,搜索区间从它的下一个位置开始。
class Solution {
    //1.创建
    boolean[][] f;
    List<List<String>> result = new ArrayList<>();
    List<String> output = new ArrayList<>();
    int n;

    public List<List<String>> partition(String s) {
        n = s.length();
        f = new boolean[n][n];
        for(int i=0; i<n; i++){
            Arrays.fill(f[i], true);
        }

        for(int i=n-1; i>=0; --i){
            for(int j=i+1; j<n; j++){
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i+1][j-1];
            }
        }

        myFunction(s, 0);
        return result;
    }

    public void myFunction(String s, int i){
        //2.核心操作
        if(i == s.length()){
            result.add(new ArrayList<>(output));
            return;
        }

        //3.递归(回溯)
        for(int j=i; j<n; ++j){
            if(f[i][j]){
                output.add(s.substring(i, j+1));
                myFunction(s, j+1);
                output.remove(output.size()-1);
            }
        }
    }
}

08 N 皇后

在这里插入图片描述

在这里插入图片描述

方法一:基于集合的回溯

class Solution {
    public List<List<String>> solveNQueens(int n) {
        //1.创建
        List<List<String>> result = new ArrayList<>();
        int[] queens = new int[n];
        Arrays.fill(queens, -1);

        Set<Integer> columns =  new HashSet<>(); //列
        Set<Integer> diagonals1 =  new HashSet<>(); //主对角线 row - i
        Set<Integer> diagonals2 =  new HashSet<>(); //副对角线 row + i

        myFunction(result, queens, n, 0, columns, diagonals1, diagonals2);
        return result;
    }

    public void myFunction(List<List<String>> result, int[] queens, int n, int row,
    Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2){
        //2.核心操作
        if(n == row){
            List<String> output = myFunction2(queens, n);
            result.add(output);
        }else{
            //遍历全列,寻找插入皇后的位置
            for(int i=0; i<n; i++){ 
                if(columns.contains(i)){
                    continue;
                }
                int diagonal1 = row - i;
                if(diagonals1.contains(diagonal1)){
                    continue;
                }
                int diagonal2 = row + i;
                if(diagonals2.contains(diagonal2)){
                    continue;
                }

                //3.递归(回溯)
                queens[row] = i;
                columns.add(i);
                diagonals1.add(diagonal1);
                diagonals2.add(diagonal2);

                myFunction(result, queens, n, row+1, columns, diagonals1, diagonals2);

                queens[row] = -1;
                columns.remove(i);
                diagonals1.remove(diagonal1);
                diagonals2.remove(diagonal2);
            }
        }
    }

    public List<String> myFunction2(int[] queens, int n){
        List<String> output = new ArrayList<>();
        for(int i=0; i<n; i++){
            char[] row = new char[n];
            Arrays.fill(row, '.');
            row[queens[i]] = 'Q';
            output.add(new String(row));
        }
        return output;
    }
}

方法二:基于位运算的回溯

① 为什么 x & (-x) 可以获得 x 的二进制表示中的最低位的 1 的位置?

  • x 是一个二进制数,如 01011000。
  • -xx 的补码表示,计算方式是对 x 取反后加 1,-x = ~x + 1,二进制中最低的那个 1 保持位置不变,之后的位变成了 0。

② 为什么 x & (x - 1) 可以将 x 的二进制表示中的最低位的 1 置成 0?

举例:

假设 x = 01011000,最低位的 1 是倒数第 4 位。

  • x = 01011000
  • x - 1 = 01010111(减 1 会把最低位的 1 减为 0,后面的 0 变成 1)

然后计算:

            x = 01011000
         x-1 = 01010111
x & (x - 1) = 01010000

可以看到,最低位的 1(第 4 位)被成功清除。

int availablePositions = ((1 << n) - 1) & (~(columns | diagonals1 | diagonals2));啥意思?

  • (1 << n) - 1:相当于让低n位变成1,高位是0。比如n=4时, (1 << 4) - 1 = 15,二进制是1111,生成一个低n位为1的掩码,限制后续的位运算结果不超过n位范围。
  • (columns | diagonals1 | diagonals2):这是把3个已有占用位置的状态进行“或运算”,得到所有被占用的位置集合。
  • ~(columns | diagonals1 | diagonals2):取反操作,表示所有没有被占用的位置,即可用位置。
  • &与前面((1 << n) - 1)进行“与运算”,保证结果只在低n位,防止高位的误差。

int column = Integer.bitCount(position - 1);啥意思?

  • position 是一个位掩码,且只有1个二进制位为1(我们通过availablePositions & (-availablePositions)拿到的最低位1),比如position = 00001000
  • position - 1 则是将最低的那个1位变成0,且该位右边的位全部置1,比如:
    position = 00001000 (第4位是1,代表第3列)
    position - 1 = 00000111
  • Integer.bitCount(x) 是Java内置函数,计算x的二进制表示中有多少个1。

举例:

position = 00001000 (二进制)position - 1 = 00000111 (二进制)bitCount(7) = 3,说明皇后要放在第3列(从0开始计数)。

columns | position 中的|啥意思?

它会对两个整数的二进制每一位进行“或”操作,只要对应位上有1,结果位就是1, 否则是0。

举例:

列(从右到左)位3位2位1位0
columns0010
position0100
columns | position0110
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值