rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • 回溯法

  • 算法定义
  • 算法思路
  • 算法复杂度
  • 优点和缺点
  • 应用场景
  • Java 代码实现
  • 代码解析
  • 资料

回溯法

算法定义

回溯法(Backtracking Paradigm)是一种通过探索所有可能的候选解来找出所有(或一个)有效解的算法。它采用试错的思想,当探索到某一步发现当前选择不能得到有效解时,就回溯到上一步,尝试其他选择,直到找到有效的解或探索完所有可能的选择。

算法思路

  1. 定义问题的解空间,确定解的组织结构
  2. 采用深度优先搜索(DFS)的方式搜索解空间
  3. 在搜索过程中使用剪枝函数(约束函数和限界函数)避免无效搜索
  4. 当找到完整解或确定当前路径无法得到解时,回溯到上一层继续搜索

回溯法的核心思想可概括为:"向前走,碰壁就回头"。

算法复杂度

  • 时间复杂度:
    • 最好情况:O (n),在搜索早期就找到解
    • 最坏情况:O (k^n) 或 O (n!),取决于问题的解空间大小(k 为每步的选择数,n 为问题规模)
    • 平均情况:通常介于最好和最坏情况之间,受剪枝效果影响较大
  • 空间复杂度:
    • 通常为 O (n),主要用于存储递归栈和当前路径信息,n 为问题的深度

优点和缺点

  • 优点:
    • 可以找出所有可能的解,适合求解组合优化问题
    • 通过剪枝可以大幅减少搜索空间,提高效率
    • 实现思路直观,易于理解和编码
  • 缺点:
    • 时间复杂度较高,在解空间较大时效率低下
    • 可能会进行大量重复计算
    • 不适合处理规模过大的问题

应用场景

  • 组合问题:如子集、组合总和等
  • 排列问题:如全排列、N 皇后问题
  • 路径问题:如迷宫求解、最短路径
  • 棋盘问题:如八皇后、数独
  • 图论问题:如哈密顿回路、图的着色
  • 背包问题:0-1 背包的回溯解法

Java 代码实现

import java.util.*;

public class BacktrackingExamples {

    // 1. 基础实现:子集问题
    public static List<List<Integer>> subsetsBasic(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrackBasic(nums, 0, new ArrayList<>(), result);
        return result;
    }
    
    private static void backtrackBasic(int[] nums, int start, List<Integer> path, List<List<Integer>> result) {
        // 将当前路径添加到结果集
        result.add(new ArrayList<>(path));
        
        // 探索所有可能的选择
        for (int i = start; i < nums.length; i++) {
            // 做选择
            path.add(nums[i]);
            // 递归探索
            backtrackBasic(nums, i + 1, path, result);
            // 撤销选择(回溯)
            path.remove(path.size() - 1);
        }
    }
    
    // 2. 带剪枝的实现:组合总和(找出所有和为目标值的组合)
    public static List<List<Integer>> combinationSumWithPruning(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        // 排序以便剪枝
        Arrays.sort(candidates);
        backtrackWithPruning(candidates, target, 0, new ArrayList<>(), result);
        return result;
    }
    
    private static void backtrackWithPruning(int[] candidates, int target, int start, 
                                           List<Integer> path, List<List<Integer>> result) {
        // 找到符合条件的组合
        if (target == 0) {
            result.add(new ArrayList<>(path));
            return;
        }
        
        for (int i = start; i < candidates.length; i++) {
            // 剪枝:如果当前数字大于剩余目标值,后续数字更大,无需考虑
            if (candidates[i] > target) {
                break;
            }
            
            // 做选择
            path.add(candidates[i]);
            // 递归探索(允许重复使用同一元素,所以start仍为i)
            backtrackWithPruning(candidates, target - candidates[i], i, path, result);
            // 撤销选择(回溯)
            path.remove(path.size() - 1);
        }
    }
    
    // 3. N皇后问题优化实现
    public static List<List<String>> solveNQueens(int n) {
        List<List<String>> result = new ArrayList<>();
        // 优化:使用数组记录皇后位置,col[i]表示第i行皇后所在的列
        int[] queens = new int[n];
        Arrays.fill(queens, -1);
        // 优化:使用集合记录已占用的列和对角线,加速判断
        Set<Integer> columns = new HashSet<>();
        Set<Integer> diagonals1 = new HashSet<>(); // 行-列 为常数的对角线
        Set<Integer> diagonals2 = new HashSet<>(); // 行+列 为常数的对角线
        
        backtrackQueens(n, 0, queens, columns, diagonals1, diagonals2, result);
        return result;
    }
    
    private static void backtrackQueens(int n, int row, int[] queens, 
                                      Set<Integer> columns, Set<Integer> diagonals1, 
                                      Set<Integer> diagonals2, List<List<String>> result) {
        // 找到一个有效解
        if (row == n) {
            result.add(generateBoard(queens, n));
            return;
        }
        
        for (int col = 0; col < n; col++) {
            int diagonal1 = row - col;
            int diagonal2 = row + col;
            
            // 剪枝:如果当前位置不合法(同列或同对角线已有皇后)
            if (columns.contains(col) || diagonals1.contains(diagonal1) || diagonals2.contains(diagonal2)) {
                continue;
            }
            
            // 做选择
            queens[row] = col;
            columns.add(col);
            diagonals1.add(diagonal1);
            diagonals2.add(diagonal2);
            
            // 递归探索下一行
            backtrackQueens(n, row + 1, queens, columns, diagonals1, diagonals2, result);
            
            // 撤销选择(回溯)
            queens[row] = -1;
            columns.remove(col);
            diagonals1.remove(diagonal1);
            diagonals2.remove(diagonal2);
        }
    }
    
    // 辅助方法:生成N皇后问题的棋盘表示
    private static List<String> generateBoard(int[] queens, int n) {
        List<String> board = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            char[] row = new char[n];
            Arrays.fill(row, '.');
            row[queens[i]] = 'Q';
            board.add(new String(row));
        }
        return board;
    }
    
    // 测试
    public static void main(String[] args) {
        // 测试子集问题
        int[] nums = {1, 2, 3};
        System.out.println("子集问题结果:");
        List<List<Integer>> subsets = subsetsBasic(nums);
        for (List<Integer> subset : subsets) {
            System.out.println(subset);
        }
        
        // 测试组合总和问题
        int[] candidates = {2, 3, 6, 7};
        int target = 7;
        System.out.println("\n组合总和问题结果:");
        List<List<Integer>> combinations = combinationSumWithPruning(candidates, target);
        for (List<Integer> combination : combinations) {
            System.out.println(combination);
        }
        
        // 测试N皇后问题
        int n = 4;
        System.out.println("\n" + n + "皇后问题结果:");
        List<List<String>> queens = solveNQueens(n);
        for (List<String> board : queens) {
            for (String row : board) {
                System.out.println(row);
            }
            System.out.println();
        }
    }
}

代码解析

上面的代码实现了三种不同的回溯算法写法,展示了不同的优化思路:

  1. 基础实现(子集问题):
    • 典型的回溯框架,包含 "做选择 - 递归探索 - 撤销选择" 三个步骤
    • 使用 start 参数避免重复的子集(如 [1,2] 和 [2,1] 视为同一子集)
  2. 带剪枝的实现(组合总和):
    • 先对数组排序,使得可以提前终止无效的搜索路径
    • 当当前元素大于剩余目标值时,直接 break,因为后续元素更大,不可能得到有效解
    • 剪枝是回溯法优化的核心手段,能显著提高算法效率
  3. N 皇后问题优化实现:
    • 使用数组记录皇后位置,避免使用二维数组,节省空间
    • 使用集合记录已占用的列和对角线,将皇后位置合法性判断从 O (n) 优化到 O (1)
    • 这种空间换时间的策略是回溯法中常用的优化手段

回溯法的关键在于合理设计解空间结构和有效的剪枝策略。对于不同的问题,需要根据其特点设计针对性的剪枝条件,以提高算法效率。在实现时,也要注意选择合适的数据结构来记录中间状态,平衡时间和空间开销。

资料

回溯法套路模板 刷通leetcode https://zhuanlan.zhihu.com/p/112926891

八皇后问题
https://juejin.im/post/5accdb236fb9a028bb195562

解决八皇后问题,可以分为两个层面:

1.找出第一种正确摆放方式,也就是深度优先遍历。

2.找出全部的正确摆放方式,也就是广度优先遍历。

最近更新:: 2025/9/27 00:43
Contributors: luokaiwen