本文首发公众号:小码A梦

回溯算法是一种常见的算法,常见用于解决排列组合、排列问题、搜索问题等算法,在一个搜索空间中寻找所有的可能的解。通过向分支不断尝试获取所有的解,然后找到合适的解,找完一个分支后再往回搜索。回溯算法通常使用递归的方式实现。

回溯本质是一种暴力搜索法,列出所有可能的解,然后找到合适的解。以 a、b、c的排列组合为例,画出全排列组合。

以上排列组合回溯步骤:

  • 列出所有可能存在的组合。
  • 分解组合,把问题分解多个阶段,每个阶段添加一个分叉。
  • 走完一个分叉,或者遇到不符合期望条件的时,就退回到上一个分叉。继续走其它没走的路。直到走完所有的路。
  • 回溯一半都是使用递归实现。

根据以上的步骤得出一个简单的回溯算法的模板:

  1. public Solution {
  2. List<List<Integer>> result;
  3. LinkedList<Integer> path;
  4. //记录那些元素被遍历过
  5. boolean[] used;
  6. private List<List<Integer>> permute(int[] nums) {
  7. result = new ArrayList<>();
  8. path = new LinkedList<>();
  9. used = new boolean[nums.length];
  10. permuteHelper(nums);
  11. return result;
  12. }
  13. private void permuteHelper(int[] nums) {
  14. if (递归终止条件) {
  15. result.add(new ArrayList<>(path));
  16. return;
  17. }
  18. //遍历各个元素
  19. for (int i = 0; i < nums.length; i++) {
  20. used[i] = true;
  21. //选择元素
  22. path.add(nums[i]);
  23. permuteHelper(nums);
  24. //移除元素
  25. path.removeLast();
  26. used[i] = false;
  27. }
  28. }
  29. }

以上代码使用递归,递归一般要设置一个终止条件,然后遍历整个元素,通过链表选择元素和移除元素。

LeetCode 题解

上面所说的,回溯主要解决一些排列组合、排列问题、搜索问题等问题,LeetCode 有很多类似的问题,这里选取了几个比较常见的题目。

  • 39 组合总和
  • 40 组合总和 II
  • 46 全排列
  • 47 全排列 II
  • 51 N皇后

39.组合总和(中等)

题目描述

解法

这是一个比较典型的排列组合问题,本题采用的是求总和,使用总和减去遍历的数据,最后得到结果为零,就是符合的组合。

  • 为了减少遍历次数,数组需要先排序。总数减的数据如果小于零,就不会在该分支继续遍历了。
  • 可以重复使用元素,每次都遍历一遍全部元素。
  • 减去分支结果之后,以新的结果,再创建分支做减法。
  • 递归遍历一直到结果为零和负数。
    • 为零,符合条件,记录数据,对应的分支遍历终止,继续遍历下一个分支。
    • 为负数,返回到上一个分支,继续遍历后面的分支。

最终代码:

  1. class Solution {
  2. List<List<Integer>> list = new ArrayList<>();
  3. int[] candidate;
  4. public List<List<Integer>> combinationSum(int[] candidates, int target) {
  5. Arrays.sort(candidates);
  6. candidate = candidates;
  7. recall(0,target,new LinkedList<>());
  8. return list;
  9. }
  10. private void recall(int start, int target, LinkedList<Integer> path) {
  11. if (target == 0) {
  12. list.add(new ArrayList<>(path));
  13. return;
  14. }
  15. for (int i = start; i <candidate.length ; i++) {
  16. int sub = target - candidate[i];
  17. if (sub < 0) {
  18. break;
  19. }
  20. path.add(candidate[i]);
  21. recall(i,sub,path);
  22. path.removeLast();
  23. }
  24. }
  25. }

recall 使用递归方法遍历分支,而使用链表的特性,记录遍历的节点,如果不符合要求就上一个分支回撤,同时链表移除最后一个结点。

40.组合总和II(中等)

解题思路

这题的解题思路和上面的组合总和是差不多的,唯一不同的是元素不能被重复遍历,使用一个变量记录遍历的起始值,遍历过的数据,下次往后一位开始遍历。

代码如下:

  1. class Solution {
  2. List<List<Integer>> list = new ArrayList<>();
  3. int[] candidate;
  4. public List<List<Integer>> combinationSum2(int[] candidates, int target) {
  5. Arrays.sort(candidates);
  6. candidate = candidates;
  7. recall(0,target,new LinkedList<>());
  8. return list;
  9. }
  10. private void recall(int start, int target, LinkedList<Integer> path) {
  11. if (target == 0) {
  12. list.add(new ArrayList<>(path));
  13. return;
  14. }
  15. for (int i = start; i <candidate.length ; i++) {
  16. //这里解决集合重复问题
  17. if (i > start && candidate[i] == candidate[i-1]) {
  18. continue;
  19. }
  20. int sub = target - candidate[i];
  21. if (sub < 0) {
  22. break;
  23. }
  24. path.add(candidate[i]);
  25. recall(i + 1,sub,path);
  26. path.removeLast();
  27. }
  28. }
  29. }

start 记录遍历的起始值,其他解题方法和上面的组合求和是类似的。题目还有一个要求是不能出现重复的组合,就需要判断 candidate[i] == candidate[i-1] 就忽略该数据,往后继续遍历。

46.全排列

解题思路

  • 每个元素都需要遍历一遍。
  • 遍历元素的时,遍历完第一数,继续遍历未遍历的数据。
  • 遍历结束后,返回上一个分叉。

代码整理如下:

  1. class Solution {
  2. List<List<Integer>> result = new ArrayList<>();
  3. LinkedList<Integer> path = new LinkedList<>();
  4. boolean[] used;
  5. public List<List<Integer>> permute(int[] nums) {
  6. if (nums.length == 0) {
  7. return result;
  8. }
  9. used = new boolean[nums.length];
  10. permuteHelper(nums);
  11. return result;
  12. }
  13. private void permuteHelper(int[] nums) {
  14. if (path.size() == nums.length) {
  15. result.add(new ArrayList<>(path));
  16. return;
  17. }
  18. for (int i = 0; i < nums.length; i++) {
  19. if (used[i]) {
  20. continue;
  21. }
  22. used[i] = true;
  23. path.add(nums[i]);
  24. permuteHelper(nums);
  25. path.removeLast();
  26. used[i] = false;
  27. }
  28. }
  29. }

使用 used 记录哪些数据遍历过,遍历过的数据不会遍历,其他也是使用递归搜索。

51.N皇后

题目描述

解题思路

N 皇后问题是一个经典的回溯算法问题,是面试比较常见的问题。在一个 n * n 的棋盘上,每个格子放入的元素后,查看是够有同行、同列、左上方以及右上方是否冲突,冲突就回溯,不冲突就继续往下遍历。

  • 初始化数组,默认初始值。
  • 每一行只能放一个 Q,不冲突后,再遍历下一列的数据(因为同一行不能冲突)。
  • 因为每一行只放一个 Q,所以不存在同行冲突。判断冲突就潘丹同一列、左上方以及右上方是否有冲突。
  • 遍历到最后一行时,记录符合条件的数据。
  1. class Solution {
  2. List<List<String>> res = new ArrayList<>();
  3. public List<List<String>> solveNQueens(int n) {
  4. // 初始化棋盘 "." 表示空,"Q"表示皇后,
  5. char[][] board = new char[n][n];
  6. for (char[] c : board) {
  7. Arrays.fill(c, '.');
  8. }
  9. backtrack(board, 0);
  10. return res;
  11. }
  12. private void backtrack(char[][] board, int row) {
  13. //终止条件
  14. if (row == board.length) {
  15. res.add(charToList(board));
  16. return;
  17. }
  18. //每一行列数(也就是长度)
  19. int n = board[row].length;
  20. for (int col = 0; col < n; col++) {
  21. //排除相互攻击的格子
  22. if (!isValid(board,row,col)) {
  23. continue;
  24. }
  25. //放入Q
  26. board[row][col] = 'Q';
  27. //进入下一行放皇后
  28. backtrack(board,row + 1);
  29. //撤销Q
  30. board[row][col] = '.';
  31. }
  32. }
  33. private boolean isValid(char[][] board, int row, int col) {
  34. int n = board.length;
  35. //检查列是否有皇后冲突
  36. for (int i = 0; i < n; i++) {
  37. if (board[i][col] == 'Q') {
  38. return false;
  39. }
  40. }
  41. //检查右上方是否有皇后冲突
  42. for (int i = row - 1,j = col + 1; i >= 0 && j < n; i--,j++) {
  43. if (board[i][j] == 'Q') {
  44. return false;
  45. }
  46. }
  47. //检查左上方是否有皇后冲突
  48. for (int i = row - 1,j = col - 1; i >= 0 && j >= 0; i--,j--) {
  49. if (board[i][j] == 'Q') {
  50. return false;
  51. }
  52. }
  53. return true;
  54. }
  55. public List<String> charToList(char[][] board) {
  56. List<String> list = new ArrayList<>();
  57. for (int i = 0; i < board.length; i++) {
  58. list.add(String.copyValueOf(board[i]));
  59. }
  60. return list;
  61. }
  62. }

总结

回溯算法尝试在问题的解空间中搜索可能的解,并在搜索过程中进行选择、撤销选择和终止搜索,直到找到解或确定无解为止。

  • 通常通过递归函数来实现回溯算法。
  • 在每一步,需要做出选择(选择一个分支)然后递归地探索该选择的结果。
  • 在递归返回后,需要撤销之前的选择,以便继续探索其他分支。
  • 使用条件语句或循环来控制选择的范围和条件。

图解 LeetCode 算法汇总——回溯的更多相关文章

  1. LeetCode算法训练-回溯 491.递增子序列 46.全排列 47.全排列 II

    欢迎关注个人公众号:爱喝可可牛奶 LeetCode算法训练-回溯 491.递增子序列 46.全排列 47.全排列 II LeetCode 491. 递增子序列 分析 找出并返回所有数组中不同的递增子序 ...

  2. LeetCode算法训练-回溯总结

    欢迎关注个人公众号:爱喝可可牛奶 LeetCode算法训练-回溯总结 适用问题 组合问题:N个数里面按一定规则找出k个数的集合 排列问题:N个数按一定规则全排列,有几种排列方式 切割问题:一个字符串按 ...

  3. LeetCode算法题目解答汇总(转自四火的唠叨)

    LeetCode算法题目解答汇总 本文转自<四火的唠叨> 只要不是特别忙或者特别不方便,最近一直保持着每天做几道算法题的规律,到后来随着难度的增加,每天做的题目越来越少.我的初衷就是练习, ...

  4. 近日LeetCode算法(记录)

    近日LeetCode算法 前言:最近刷了好多leetcode算法题,大家知道,程序=数据结构+算法,由此可见,算法真的是很重要的呢.闲话少谈,切入正题,来看看小编觉得有点意思的5题算法题吧... 1. ...

  5. ACM金牌选手讲解LeetCode算法《栈和队列的高级应用》

    大家好,我是编程熊,双非逆袭选手,字节跳动.旷视科技前员工,ACM金牌,保研985,<ACM金牌选手讲解LeetCode算法系列>作者. 上一篇文章讲解了<线性表>中的数组.链 ...

  6. LeetCode算法训练 93.复原IP地址 78.子集 90.子集II

    欢迎关注个人公众号:爱喝可可牛奶 LeetCode算法训练 93.复原IP地址 78.子集 90.子集II LeetCode 93. 复原 IP 地址 分析 字符串全部由数字组成,ipv4每一段数字不 ...

  7. 排序算法汇总(C/C++实现)

    前言:     本人自接触算法近2年以来,在不断学习中越多地发觉各种算法中的美妙.之所以在这方面过多的投入,主要还是基于自身对高级程序设计的热爱,对数学的沉迷.回想一下,先后也曾参加过ACM大大小小的 ...

  8. leetcode算法: Find Bottom Left Tree Value

    leetcode算法: Find Bottom Left Tree ValueGiven a binary tree, find the leftmost value in the last row ...

  9. LeetCode算法题-Subdomain Visit Count(Java实现)

    这是悦乐书的第320次更新,第341篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第189题(顺位题号是811).像"discuss.leetcode.com& ...

  10. LeetCode算法题-Number of Lines To Write String(Java实现)

    这是悦乐书的第319次更新,第340篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第188题(顺位题号是806).我们要将给定字符串S的字母从左到右写成行.每行最大宽度为 ...

随机推荐

  1. 自动化测试-基础知识—Bash基础

    Bash 在 Bash 中,美元符号 $ 可以用于引用变量或者表达式的值.Bash 中的变量并不需要事先声明,而是在第一次赋值时自动创建.基于这个特性,我们可以通过给变量名加上 $ 的方式来引用它的值 ...

  2. Java革命性ORM框架之快速上手的Jimmer

    Jimmer是一款革命性的ORM框架,它的目标是提供一个简单易用的API,帮助开发人员更加轻松地操作数据库.Jimmer使用了Java 8的新特性,如Lambda表达式和Stream API,使得代码 ...

  3. go 常用命令总结

    转载请注明出处: go build:编译包和依赖项,生成可执行文件.命令用于编译包和依赖项,生成可执行文件.当对Go程序进行修改后,需要使用go build命令重新编译程序,以生成新的可执行文件.该命 ...

  4. Python modbus_tk 库源码分析

    modbus_tk 源代码分析 前言 modbus_tcp 协议是工业项目中常见的一种基于 TCP/IP 协议的设备数据交互协议. 作为 TCP/IP 协议的上层协议,modbus_tcp 协议涉及到 ...

  5. 【LeetCode】Find Pivot Index #724 Rust Solution

    给定一个整数类型的数组 nums,请编写一个能够返回数组 "中心索引" 的方法.我们是这样定义数组 中心索引 的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和.如果数 ...

  6. 前端学习C语言 - 函数和关键字

    函数和关键字 本篇主要介绍:自定义函数.宏函数.字符串处理函数和关键字. 自定义函数 基本用法 实现一个 add() 函数.请看示例: #include <stdio.h> // 自定义函 ...

  7. 记一次.net加密神器 Eazfuscator.NET 2023.2 最新版 使用尝试

    很多人看到这个Eazfuscator.NET还不知是什么东东... 首先介绍下 什么是 Eazfuscator.NET? Eazfuscator.NET 是用于.NET平台的工业级混淆器. Eazfu ...

  8. .NetCore3.1+微服务架构技术栈

    目标 目标系统架构演变,单体-分布式-微服务-中台 微服务架构核心解决,横向对比1.0.2.0.3.0 践行微服务架构,全组件解读! 也谈中台 单体架构Monolithic 单体应用时代:应用程序就是 ...

  9. JVM GC配置指南

    本文旨在简明扼要说明各回收器调优参数,如有疏漏欢迎指正. 1.JDK版本 以下所有优化全部基于JDK8版本,强烈建议低版本升级到JDK8,并尽可能使用update_191以后版本. 2.如何选择垃圾回 ...

  10. 三级缓存---解决 Spring 循环依赖

    1. 循环依赖 1.1 什么是循环依赖 首先,什么是循环依赖?这个其实好理解,就是两个 Bean 互相依赖,类似下面这样: """ @Service public cla ...