当前位置: 首页 > news >正文

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(); //3for(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 flagStringBuffer 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 - iSet<Integer> diagonals2 =  new HashSet<>(); //副对角线 row + imyFunction(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 = 01011000x-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

相关文章:

  • 《拖延心理学》:深度剖析与应对指南​
  • Java对象中的MarkWord
  • 解决电脑第一排按键功能失效的问题
  • 【Redis】分布式锁
  • 手动 + 自动双方案组合:Innocise 壁虎吸盘灵活适配多场景无损搬运需求
  • Redis中的set底层实现
  • 行为设计模式之State(状态)设计模式
  • ceil方法
  • WebStorm编辑器侧边栏
  • 40套精品大气黑金系列行业PPT模版分享
  • 【Bluedroid】蓝牙启动之核心模块(startProfiles )初始化与功能源码解析
  • gradle的 build时kaptDebugKotlin 处理数据库模块
  • Laravel 12 更新与之前版本结构变更清单
  • 4.查看、删除数据库
  • 第9章:Neo4j集群与高可用性
  • 基于docker的nocobase本地部署流程
  • 快速使用 Flutter 中的 SnackBar 和 Toast
  • SpringBoot学习day3-SpringBoot注解开发(新闻项目后段基础)
  • 【项目实训】【项目博客#07】HarmonySmartCodingSystem系统前端开发技术详解(5.12-6.15)
  • 工厂模式Factory Pattern
  • 企业网站建设方案 功能规划/百度外推排名代做
  • 南京公司做网站/百度学术论文官网入口
  • 苏州电子商务网站建设/怎么优化网站性能
  • 软件公司网站设计与制作/最好用的搜索引擎
  • 桂林疫情最新消息确诊19例/石家庄百度快照优化
  • 缪斯设计上海/seo搜索引擎优化内容