回溯算法简单理解

news/2025/2/6 9:41:13 标签: 算法, c++

在这里插入图片描述

leecode每日一题

回溯算法是一种通过试错来解决问题的算法,当发现当前路径无法得到正确解时,会回溯到上一步尝试其他可能。它特别适合解决 组合问题、排列问题、子集问题、棋盘类问题 等。以下是详细解析和C++实现:


一、回溯算法核心思想

“选择 → 探索 → 撤销” 的循环过程:

  1. 路径:已做出的选择
  2. 选择列表:当前可以做的选择
  3. 结束条件:到达决策树底层时的终止条件

二、算法框架模板

void backtrack(路径, 选择列表) {
    if (满足结束条件) {
        记录结果;
        return;
    }
    
    for (选择 : 选择列表) {
        做选择;
        backtrack(新路径, 新选择列表);
        撤销选择;
    }
}

三、经典问题示例

1. 全排列问题(无重复数字)

问题:给定数组 [1,2,3],返回所有可能的排列。

vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> res;
    vector<int> path;
    vector<bool> used(nums.size(), false);
    backtrack(nums, path, used, res);
    return res;
}

void backtrack(vector<int>& nums, vector<int>& path, vector<bool>& used, vector<vector<int>>& res) {
    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]);
        backtrack(nums, path, used, res);
        path.pop_back();        // 撤销选择
        used[i] = false;
    }
}

2. 组合问题(无重复元素,可重复选取)

问题:从 [2,3,5] 中选和为 8 的组合,如 [2,2,2,2]。

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> res;
    vector<int> path;
    sort(candidates.begin(), candidates.end());  // 排序以方便剪枝
    backtrack(candidates, target, 0, path, res);
    return res;
}

void backtrack(vector<int>& nums, int remain, int start, vector<int>& path, vector<vector<int>>& res) {
    if (remain < 0) return;
    if (remain == 0) {
        res.push_back(path);
        return;
    }
    
    for (int i = start; i < nums.size(); ++i) {
        if (nums[i] > remain) break;  // 剪枝:后续元素更大,无需尝试
        path.push_back(nums[i]);
        backtrack(nums, remain - nums[i], i, path, res); // 允许重复,仍从i开始
        path.pop_back();
    }
}

3. N皇后问题

问题:在N×N棋盘放置皇后,使其不能互相攻击。

vector<vector<string>> solveNQueens(int n) {
    vector<vector<string>> res;
    vector<string> board(n, string(n, '.'));
    backtrack(board, 0, res);
    return res;
}

void backtrack(vector<string>& board, int row, vector<vector<string>>& res) {
    if (row == board.size()) {
        res.push_back(board);
        return;
    }
    
    int n = board[row].size();
    for (int col = 0; col < n; ++col) {
        if (!isValid(board, row, col)) continue;
        board[row][col] = 'Q';      // 放置皇后
        backtrack(board, row + 1, res);
        board[row][col] = '.';      // 撤销
    }
}

bool isValid(vector<string>& board, int row, int col) {
    // 检查列
    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<board.size(); --i, ++j)
        if (board[i][j] == 'Q') return false;
    return true;
}

四、关键技巧

  1. 剪枝优化:通过排序提前终止无效分支(如组合问题)
  2. 去重处理:使用排序 + i > start && nums[i] == nums[i-1] 跳过重复元素
  3. 状态保存:使用used数组或索引start避免重复使用元素

五、时间复杂度分析

回溯算法通常时间复杂度较高(指数级),但通过剪枝可显著优化。例如:

  • 全排列问题:O(n×n!)
  • 组合问题:O(2^n)

六、练习题目

  1. 全排列II(含重复数字)
  2. 子集(无重复元素)
  3. 组合总和II(每个数字只能用一次)
  4. 解数独

掌握回溯算法需要理解其递归树结构,并熟练应用剪枝技巧来优化效率。建议从模板入手,逐步理解不同问题的变种处理方式。

AC代码

class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        // 对数组进行排序,方便后续去重
        sort(nums.begin(), nums.end());
        vector<int> combination;
        vector<vector<int>> combinations;
        // 调用回溯函数生成子集
        backtrack(0, nums, combination, combinations);
        return combinations;
    }

    void backtrack(int index, const vector<int>& nums, vector<int>& combination, vector<vector<int>>& combinations) {
        // 将当前组合加入结果集
        combinations.push_back(combination);
        for (int i = index; i < (int)nums.size(); ++i) {
            // 跳过重复元素
            if (i > index && nums[i] == nums[i - 1]) continue;
            // 选择当前元素
            combination.push_back(nums[i]);
            // 递归生成子集
            backtrack(i + 1, nums, combination, combinations);
            // 回溯,撤销选择
            combination.pop_back();
        }
    }
};

http://www.niftyadmin.cn/n/5842835.html

相关文章

rust安装笔记

安装笔记 安装加速cargo 国内源nightly版本安装其他目标将现有项目迁移到新版本升级 安装加速 export RUSTUP_UPDATE_ROOT"https://mirrors.ustc.edu.cn/rust-static/rustup" export RUSTUP_DIST_SERVERhttps://mirrors.tuna.tsinghua.edu.cn/rustup curl --proto h…

DeepSeek R1技术报告关键解析(8/10):DeepSeek-R1 的“aha 时刻”,AI 自主学习的新突破

1. 什么是 AI 的“aha 时刻”&#xff1f; 在强化学习过程中&#xff0c;AI 的推理能力并不是线性增长的&#xff0c;而是会经历一些关键的“顿悟”时刻&#xff0c;研究人员将其称为“aha 时刻”。 这是 AI 在训练过程中突然学会了一种新的推理方式&#xff0c;或者能够主动…

嵌入式八股文面试题(一)C语言部分

1. 变量/函数的声明和定义的区别&#xff1f; &#xff08;1&#xff09;变量 定义不仅告知编译器变量的类型和名字&#xff0c;还会分配内存空间。 int x 10; // 定义并初始化x int x; //同样是定义 声明只是告诉编译器变量的名字和类型&#xff0c;但并不为它分配内存空间…

从qml端发送坐标点到c++端。使用上下文把c++的对象暴露到qml端,然后直接调用c++的函数

coordinatereceiver.h #include <QObject> #include <QPointF> #include <QVector> #include <QDebug> // 定义全局变量来存储坐标点 //QVector<QPointF> globalCoordinates;class CoordinateReceiver : public QObject {Q_OBJECT public:expli…

react关于手搓antd pro面包屑的经验(写的不好请见谅)

我们先上代码&#xff0c;代码里面都有注释&#xff0c;我是单独写了一个组件&#xff0c;方便使用&#xff0c;在其他页面引入就行了 还使用了官方的Breadcrumb组件 import React, { useEffect, useState } from react; import { Breadcrumb, Button } from antd; import { …

双亲委派(jvm)

1.双亲委派 在 Java 中&#xff0c;双薪委派通常是指双亲委派模型&#xff0c;它是 Java 类加载器的一种工作模式&#xff0c;用于确保类加载的安全性和一致性。以下是其相关介绍&#xff1a; 定义与作用 定义&#xff1a;双亲委派模型要求除了顶层的启动类加载器外&#xf…

SAP HCM 读取特定0014信息类型(特定月份)数据

导读 0014信息类型:0014是HCM的周期性维护数据&#xff0c;也就是说默认我维护的周期时间很长&#xff0c;在一段时间内不需要维护&#xff0c;减少维护的工作量&#xff0c;今天遇到一个朋友问的问题&#xff0c;0014信息类型能读取特定月份的数据&#xff0c;例如我需要维护…

ES6 变量解构赋值总结

1. 数组的解构赋值 1.1 基本用法 // 基本数组解构 const [a, b, c] [1, 2, 3]; console.log(a); // 1 console.log(b); // 2 console.log(c); // 3// 跳过某些值 const [x, , y] [1, 2, 3]; console.log(x); // 1 console.log(y); // 3// 解构剩余元素 const [first, ...re…