算法入门---回溯算法(C++实现)

回溯算法是解决组合优化问题的利器,本文将从基础概念讲起,逐步深入,通过经典例题带你掌握回溯算法的核心思想和实现技巧。

什么是回溯算法?

回溯算法(Backtracking)是一种通过探索所有可能的候选解来找出所有解的算法。当候选解被确认不是一个可行解(或者不是最优解)时,回溯算法会撤销上一步的操作,尝试其他的选择。

回溯法通常用于解决以下类型的问题:

  • 组合问题(如全排列、子集)

  • 约束满足问题(如N皇后、数独)

  • 切割问题(如分割回文串)

  • 棋盘问题(如骑士巡游)

回溯算法的核心思想

回溯算法的核心在于"试错"思想:从一条路往前走,能进则进,不能进则退回来,换一条路再试。这种思想可以用树形结构表示:

  1. 路径:已经做出的选择

  2. 选择列表:当前可以做的选择

  3. 结束条件:到达决策树底层,无法再做选择

回溯算法的通用框架

void backtracking(参数) {
    if (满足结束条件) {
        存放结果;
        return;
    }
    
    for (选择 : 本层选择列表) {
        处理当前选择;
        backtracking(路径,选择列表);  // 递归
        撤销选择;  // 回溯关键步骤
    }
}

经典例题1:全排列问题

问题描述

给定一个不含重复数字的数组 nums,返回其所有可能的全排列。

示例:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

解题思路

  1. 使用一个布尔数组 used 记录每个元素是否被使用过

  2. 递归过程中,将未使用的元素加入当前路径

  3. 当路径长度等于数组长度时,保存结果

  4. 递归结束后撤销选择,回溯到上一步

C++代码实现

#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> path;
        vector<bool> used(nums.size(), false);
        backtracking(nums, res, path, used);
        return res;
    }

private:
    void backtracking(vector<int>& nums, vector<vector<int>>& res, 
                     vector<int>& path, vector<bool>& used) {
        // 终止条件:路径长度等于数组长度
        if (path.size() == nums.size()) {
            res.push_back(path);
            return;
        }
        
        for (int i = 0; i < nums.size(); i++) {
            if (used[i]) continue;  // 跳过已使用的元素
            
            // 做选择
            used[i] = true;
            path.push_back(nums[i]);
            
            // 递归进入下一层决策树
            backtracking(nums, res, path, used);
            
            // 撤销选择(回溯)
            path.pop_back();
            used[i] = false;
        }
    }
};

// 测试代码
int main() {
    Solution solution;
    vector<int> nums = {1, 2, 3};
    vector<vector<int>> res = solution.permute(nums);
    
    cout << "全排列结果:" << endl;
    for (auto& vec : res) {
        for (int num : vec) {
            cout << num << " ";
        }
        cout << endl;
    }
    return 0;
}

复杂度分析

  • 时间复杂度:O(n × n!),其中 n 为数组长度

  • 空间复杂度:O(n),递归调用栈的最大深度为 n

经典例题2:N皇后问题

问题描述

在 n×n 的棋盘上放置 n 个皇后,使其不能互相攻击(任意两个皇后不能处于同一行、同一列或同一斜线上)。返回所有解决方案。

示例(4皇后问题):

输入:4
输出:[
 [".Q..", 
  "...Q",
  "Q...",
  "..Q."],
  
 ["..Q.", 
  "Q...",
  "...Q",
  ".Q.."]
]

解题思路

  1. 按行放置皇后,避免行冲突

  2. 检查当前列是否已被占用

  3. 检查两条对角线(左上到右下,右上到左下)是否已被占用

  4. 使用递归尝试每一行的所有列位置

C++代码实现

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> res;
        vector<string> board(n, string(n, '.'));  // 初始化棋盘
        backtracking(0, n, board, res);
        return res;
    }

private:
    void backtracking(int row, int n, vector<string>& board, 
                     vector<vector<string>>& res) {
        // 终止条件:所有行都放置完毕
        if (row == n) {
            res.push_back(board);
            return;
        }
        
        for (int col = 0; col < n; col++) {
            if (isValid(row, col, board, n)) {
                board[row][col] = 'Q';  // 放置皇后
                backtracking(row + 1, n, board, res);  // 递归下一行
                board[row][col] = '.';  // 回溯,撤销皇后
            }
        }
    }
    
    bool isValid(int row, int col, vector<string>& board, int n) {
        // 检查同一列
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q') return false;
        }
        
        // 检查左上对角线
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') return false;
        }
        
        // 检查右上对角线
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') return false;
        }
        
        return true;
    }
};

// 测试代码
int main() {
    Solution solution;
    int n = 4;
    vector<vector<string>> res = solution.solveNQueens(n);
    
    cout << n << "皇后问题的解:" << endl;
    for (int i = 0; i < res.size(); i++) {
        cout << "解法 " << i + 1 << ":" << endl;
        for (string row : res[i]) {
            cout << row << endl;
        }
        cout << endl;
    }
    return 0;
}

复杂度分析

  • 时间复杂度:O(n!),最坏情况下需要尝试所有排列组合

  • 空间复杂度:O(n²),存储棋盘状态

回溯算法优化:剪枝技巧

剪枝是回溯算法中最重要的优化手段,通过提前排除不可能的解,减少递归次数:

  1. 可行性剪枝:在递归前检查当前选择是否满足约束条件

  2. 最优性剪枝:当当前路径已不可能优于已有最优解时终止

  3. 对称性剪枝:避免重复计算对称解

剪枝示例(全排列问题优化)

// 优化:减少used数组的访问次数
void backtracking(vector<int>& nums, vector<vector<int>>& res, 
                 vector<int>& path, vector<bool>& used) {
    if (path.size() == nums.size()) {
        res.push_back(path);
        return;
    }
    
    // 使用局部变量避免多次访问used
    for (int i = 0; i < nums.size(); i++) {
        if (!used[i]) {  // 只处理未使用的元素
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, res, path, used);
            path.pop_back();
            used[i] = false;
        }
    }
}

常见问题及解决方法

避免重复解:

  • 排序数组后跳过相同元素

  • 使用集合记录已访问路径

提高效率:

  • 使用引用传递减少拷贝开销

  • 尽量使用基本类型而非容器

处理大型问题:

  • 迭代加深搜索

  • 双向回溯

练习题

  1. 组合问题(LeetCode 77

  2. 子集问题(LeetCode 78

  3. 组合总和(LeetCode 39

  4. 分割回文串(LeetCode 131

  5. 解数独(LeetCode 37

总结

回溯算法是解决组合优化问题的强大工具,核心在于:

  • 递归 + 回溯的基本框架

  • 剪枝优化提高效率

  • 合理的数据结构记录状态

掌握回溯算法需要大量练习,建议从简单问题开始,逐步理解回溯的思维模式。记住:回溯的本质是暴力搜索的优化,通过剪枝减少不必要的计算。

学习建议:每解决一个问题后,尝试画出其决策树,理解回溯的过程,这对掌握算法非常有帮助!


附录:回溯算法模板(C++)

void backtrack(参数) {
    if (终止条件) {
        保存结果;
        return;
    }
    
    for (选择 : 本层选择集合) {
        // 可选:剪枝操作
        if (不满足条件) continue;
        
        处理当前选择;
        backtrack(新参数); // 递归
        撤销选择; // 回溯
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jay_515

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值