云计算百科
云计算领域专业知识百科平台

Leetcode回溯算法part2

93. 复原 IP 地址

整体思路(先不看代码)

题目要求:

给定一个只包含数字的字符串 s,返回所有可能的合法 IP 地址。

什么是“合法 IP”?

一个 IP 地址必须满足:

  • 由 4 段组成,用 . 分隔

  • 每一段:

    • 数值在 0 ~ 255

    • 不能有前导 0(除非就是 "0")

  • 例如:

    • "25525511135" → "255.255.11.135"、"255.255.111.35"


    为什么这是回溯问题?

    因为题目要求的是:

    • 枚举所有可能的切割方式

    • 并且每一种切割方式都要判断是否合法

    👉 一看到:

    “切割字符串 + 所有可能方案”

    就要立刻想到:

    回溯(切割问题)


    把问题抽象成一棵树(核心理解)

    可以把问题理解为:

    • 在字符串中 放 3 个点

    • 把字符串切成 4 段

    例如:"25525511135"

    第一刀 第二刀 第三刀
    ↓ ↓ ↓
    255 | 255 | 11 | 135

    树的含义

    回溯概念本题含义
    树的深度 已经放了几个点(n)
    每一层 决定当前这一段的结尾
    每个节点 一个合法的 IP 段
    一条路径 一个完整 IP 地址

    👉 当 已经放了 3 个点,剩下的那一段必须一次性判断是否合法。


    整体解法拆解(两部分)

    ✅ 第一部分:合法性判断(isValid)

    这是本题的**“规则引擎”**,负责判断:

    s[start..end] 能不能作为一个 IP 段


    ✅ 第二部分:回溯切割字符串

    • 用回溯尝试在不同位置插入 .

    • 每插入一次,就相当于确定了一段

    • 插入 3 个点后,检查最后一段

    class Solution {
    public:

    vector<string>result;

    bool isValid(const string&s,int start,int end){
    if(start>end)return false;
    if(s[start]=='0' && start != end){
    return false;
    }
    int num = 0;
    for(int i = start;i<=end;i++){
    if(s[i]>'9'||s[i]<'0')return false;
    num = num*10+(s[i]-'0');
    if(num>255)return false;
    }
    return true;
    }

    void backtracking(string& s ,int n ,int startIndex){
    if(n==3){
    if(isValid(s,startIndex,s.size()-1)){
    result.push_back(s);
    }
    return ;
    }
    for(int i = startIndex;i<s.size();i++){
    if(isValid(s,startIndex,i)){
    s.insert(s.begin()+i+1,'.');
    n++;
    backtracking(s,n,i+2);
    n–;
    s.erase(s.begin()+i+1);
    }
    }
    }

    vector<string> restoreIpAddresses(string s) {
    result.clear();
    if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了
    backtracking(s, 0, 0);
    return result;
    }
    };

    78. 子集

    整体思路(先不看代码)

    题目要求的是:

    给定一个数组 nums,返回它的所有子集(幂集)。

    例如:[1,2]

    [] [1] [2] [1,2]

    子集问题的最大特点

    • 不要求固定长度

    • 每个元素:选 or 不选

    • 所有可能情况都要

    👉 一句话判断:

    “每个元素都有选 / 不选两种状态” → 子集问题 → 回溯


    二、把子集问题抽象成一棵树(非常重要)

    以 nums = [1,2,3] 为例:

    []
    [1]
    [2]
    [1,2]

    但在代码实现中,我们通常不显式写“选 / 不选”两条分支,而是用:

    for 循环 + startIndex,自然枚举“选哪些”


    三、子集 vs 组合(核心认知)

    这是很多人第一次学子集时最容易混的点👇

    组合问题(如 combine)

    • 只有 path.size() == k 才收集结果

    • 有明确“终止条件”

    子集问题(本题)

    每一个节点,都是一个合法子集

    也就是说:

    • 空集是子集

    • 任何中间状态都是子集

    • 不需要等到“叶子节点”

    👉 这就是你代码里这句的真正含义:

    result.push_back(sub);


    四、回溯三部曲(对应本题)

    ✅ ① 回溯函数参数

    void backtracking(vector<int>& nums, int startIndex)

    • nums:原数组

    • startIndex:下一次可选择元素的起点

    👉 startIndex 的作用依然是: 防止出现 [1,2] 和 [2,1] 这种重复子集


    ✅ ② 终止条件(子集的“特殊点”)

    if (startIndex >= nums.size()) return;

    这里的终止条件非常弱,原因是:

    • 不靠终止条件收集结果

    • 收集结果发生在“进入函数的第一行”

    只要 startIndex 越界,就没法继续选了,直接返回即可。


    ✅ ③ 单层搜索逻辑

    for (int i = startIndex; i < nums.size(); i++) {
    sub.push_back(nums[i]);
    backtracking(nums, i + 1);
    sub.pop_back();
    }

    含义:

    • 在当前层,从 startIndex 开始

    • 依次尝试把每个元素加入子集

    • 然后递归向下扩展

    class Solution {
    public:
    vector<vector<int>>result;
    vector<int>sub;

    void backtracking(vector<int>&nums,int startIndex){
    result.push_back(sub);
    if(startIndex>=nums.size())return;
    for(int i= startIndex;i<nums.size();i++){
    sub.push_back(nums[i]);
    backtracking(nums,i+1);
    sub.pop_back();
    }

    }

    vector<vector<int>> subsets(vector<int>& nums) {
    result.clear();
    sub.clear();
    backtracking(nums, 0);
    return result;
    }
    };

    491. 非递减子序列

    整体思路(先不看代码)

    题目要求:

    给定一个整数数组 nums,找出所有递增子序列(长度 ≥ 2),子序列不要求连续,但结果不能重复。

    关键词拆解:

    • 子序列:不要求连续 → 回溯

    • 递增:有顺序约束

    • 所有方案:不是求个数,是求列表

    • 不能重复:这是难点

    👉 一句话判断:

    这是“子集类回溯 + 顺序约束 + 去重”的综合题

    把问题抽象成一棵树(非常关键)

    以 nums = [4,6,7,7] 为例:

    []
    / | \\
    [4] [6] [7]
    / \\ | |
    [4,6] [4,7] [6,7] [7,7]
    | |
    [4,6,7] [4,7,7]

    树结构里的限制

    1️⃣ 不能下降

    • 后选的数必须 ≥ 前一个数

    2️⃣ 同一层不能选相同的数

    • 否则会产生重复子序列

    👉 难点不在“回溯”,而在**“同层去重”**


    这道题的三大核心约束

    ✅ 1️⃣ 子序列 ≠ 子集

    • 子集:元素选 or 不选

    • 子序列:要保持原数组的相对顺序

    所以:

    • 只能从 startIndex 往后选

    • 不能回头


    ✅ 2️⃣ 递增约束(纵向约束)

    nums[i] >= path.back()

    如果当前数比路径最后一个小:

    • 这条分支直接非法

    • 必须剪掉


    ✅ 3️⃣ 去重约束(横向约束,最容易错)

    同一层递归中,相同的数字只能用一次

    否则会出现重复结果,比如:

    [7](选第一个 7) [7](选第二个 7)


    回溯三部曲(对应本题)


    ✅ ① 回溯函数参数

    void backtracking(vector<int>& nums, int startIndex)

    • startIndex:保证子序列顺序

    • 不需要 target / sum

    • 状态主要靠 path


    ✅ ② 收集结果的时机(非常重要)

    if (path.size() > 1) { result.push_back(path); }

    含义:

    • 只要当前路径长度 ≥ 2

    • 就是一个合法递增子序列

    • 不需要等到叶子节点

    👉 这一点和「子集问题」非常像


    ✅ ③ 单层搜索逻辑(核心)

    • 从 startIndex 开始遍历

    • 同时满足:

      • 递增条件

      • 同层去重条件

    class Solution {
    public:
    vector<vector<int>>result;
    vector<int>path;

    void backtracking(vector<int>& nums,int startIndex){
    if(path.size()>1){
    result.push_back(path);
    }
    int set[201]={0};
    for(int i = startIndex;i<nums.size();i++){
    if ((!path.empty() && nums[i] < path.back())
    || set[nums[i] + 100] == 1) {
    continue;
    }
    path.push_back(nums[i]);
    set[nums[i] + 100] = 1;
    backtracking(nums,i+1);
    path.pop_back();
    }
    }

    vector<vector<int>> findSubsequences(vector<int>& nums) {
    result.clear();
    path.clear();
    backtracking(nums,0);
    return result;
    }
    };

    46. 全排列

    整体思路(先不看代码)

    题目要求:

    给定一个数组 nums,返回它的所有全排列。

    例如:[1,2,3]

    [1,2,3]
    [1,3,2]
    [2,1,3]
    [2,3,1]
    [3,1,2]
    [3,2,1]

    排列问题的“本质特征”

    顺序敏感 ❗ 这和前面的题有本质区别:

    • [1,2] 和 [2,1] 是 两个不同解

    • 每一个位置都要尝试所有还没用过的数

    👉 一句话判断:

    “顺序会产生不同结果” → 排列 → 一定要用 used 数组


    把排列问题抽象成一棵树(非常关键)

    以 nums = [1,2,3] 为例:

    []
    / | \\
    [1] [2] [3]
    / \\ / \\ / \\
    [1,2] [1,3] [2,1] [2,3] [3,1] [3,2]
    | | | | | |
    [1,2,3][1,3,2][2,1,3][2,3,1][3,1,2][3,2,1]

    树结构含义

    回溯概念本题含义
    树的深度 path.size()
    每一层 决定“当前第几个位置放谁”
    每一层可选 所有还没用过的元素
    叶子节点 path.size() == nums.size()

    回溯三部曲(对应本题)


    ✅ ① 回溯函数参数

    void backtracking(vector<int>& nums, vector<bool>& used)

    为什么需要 used?

    因为在排列中:

    每个数字只能用一次,但每一层都要从 0 开始遍历

    这和组合 / 子集 完全不同。


    ✅ ② 终止条件(什么时候收集结果)

    if (path.size() == nums.size()) { result.push_back(path); }

    含义:

    • 当前路径长度等于数组长度

    • 说明每个位置都已经放了一个数

    • 得到一个完整排列


    ✅ ③ 单层搜索逻辑(排列的核心)

    for (int i = 0; i < nums.size(); i++) {

    注意这里:

    • 不是从 startIndex

    • 而是 每一层都从 0 开始

    👉 因为排列的每一个位置,都可以放任何还没用过的数。

    class Solution {
    public:
    vector<vector<int>>result;
    vector<int>path;
    void backtracking(vector<int>&nums,vector<bool>&used){
    if(path.size()==nums.size()){
    result.push_back(path);
    return;
    }

    for(int i = 0 ; i<nums.size();i++){
    if(used[i]==true)continue;
    used[i]=true;
    path.push_back(nums[i]);
    backtracking(nums,used);
    used[i]=false;
    path.pop_back();
    }

    }

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

    47. 全排列 II

    整体思路(先不看代码)

    题目要求:

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

    例如:

    nums = [1,1,2]

    合法结果:
    [1,1,2]
    [1,2,1]
    [2,1,1]

    非法(重复):

    [1,1,2](用第一个1)
    [1,1,2](用第二个1)

    为什么“普通排列 + used[]”不够?

    在 permute(无重复) 里:

    if (used[i]) continue;

    只能保证:

    • 一个元素在一条排列里只用一次

    ❌ 但挡不住这种情况:

    nums = [1,1,2]

    第一层:
    选 nums[0] = 1 → path = [1]
    选 nums[1] = 1 → path = [1] (数值一样,但下标不同)

    👉 下标不同,但值相同 👉 会生成一模一样的排列

    所以: 排列去重 ≠ 防止重复使用元素 而是:

    防止在同一层,用“相同的值”开头

    排列去重的核心思想(一句话版)

    相同的数字,必须“按顺序”使用 前一个没用,后一个不能用

    这句话就是下面这行代码的数学含义:

    if (i > 0 && nums[i] == nums[i – 1] && used[i – 1] == false)

    if (
    i > 0
    && nums[i] == nums[i – 1]
    && used[i – 1] == false
    ) {
    continue;
    }

    翻译成一句人话:

    如果当前数字和前一个数字相同, 且前一个数字在当前树枝中还没被用过, 那说明我正站在“同一层”, 这个分支一定会产生重复排列,必须跳过。

    class Solution {
    private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int>& nums, vector<bool>& used) {
    // 此时说明找到了一组
    if (path.size() == nums.size()) {
    result.push_back(path);
    return;
    }
    for (int i = 0; i < nums.size(); i++) {
    // used[i – 1] == true,说明同一树枝nums[i – 1]使用过
    // used[i – 1] == false,说明同一树层nums[i – 1]使用过
    // 如果同一树层nums[i – 1]使用过则直接跳过
    if (i > 0 && nums[i] == nums[i – 1] && used[i – 1] == false) {
    continue;
    }
    if (used[i] == false) {
    used[i] = true;
    path.push_back(nums[i]);
    backtracking(nums, used);
    path.pop_back();
    used[i] = false;
    }
    }
    }
    public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
    result.clear();
    path.clear();
    sort(nums.begin(), nums.end()); // 排序
    vector<bool> used(nums.size(), false);
    backtracking(nums, used);
    return result;
    }
    };

    51. N 皇后

    整体思路(先不看代码)

    题目要求:

    在一个 n × n 的棋盘上,放置 n 个皇后,使得它们互不攻击,返回所有不同的摆放方案。

    皇后的攻击规则

    一个皇后会攻击:

  • 同一列

  • 左上 ↖ 右下 ↘ 对角线

  • 右上 ↗ 左下 ↙ 对角线

  • 👉 行不用检查,因为我们保证每一行只放一个皇后

    N 皇后 = 典型“棋盘型回溯”

    这类题的固定套路是:

    一行一行放皇后,每一行选择一个合法的列

    也就是说:

    • 树的深度 = 行号 row

    • 每一层的选择 = 这一行皇后放在哪一列 col

    • 一个完整解 = 成功放完第 n 行


    把问题抽象成一棵树(关键理解)

    以 n = 4 为例:

    第 0 行:col = 0 / 1 / 2 / 3

    第 1 行:在合法列中选

    第 2 行:继续选

    第 3 行:继续选

    row == n → 得到一个解

    👉 每一层只关心“这一行皇后能放在哪”


    整体解法拆成两部分

    ✅ 第一部分:合法性判断 isValid

    判断:

    在 (row, col) 放皇后,是否会和前面已经放的皇后冲突

    ⚠️ 只需要检查 row 之前的行 因为后面的行还没放。


    ✅ 第二部分:回溯放皇后

    • 从第 0 行开始

    • 每一行尝试所有列

    • 放得下就递归到下一行

    • 放不下就换列

    • 行号到 n,收集结果

    class Solution {
    public:
    vector<vector<string>> result;
    bool isValid(int row,int col,vector<string>& chessboard,int n){
    for(int i=0;i<row;i++){
    if(chessboard[i][col]=='Q')return false;
    }
    for(int i = row-1,j=col-1;i>=0 && j>=0;i–,j–){
    if(chessboard[i][j]=='Q')return false;
    }
    for(int i = row-1,j=col+1;i>=0 && j<n;i–,j++){
    if(chessboard[i][j]=='Q')return false;
    }
    return true;
    }
    void backtracking(int n,int row,vector<string>&chessboard){
    if(row == n){
    result.push_back(chessboard);
    return;
    }

    for(int col = 0; col<n;col++){
    if(isValid(row,col,chessboard,n)){
    chessboard[row][col]='Q';
    backtracking(n,row+1,chessboard);
    chessboard[row][col]='.';
    }
    }
    }

    vector<vector<string>> solveNQueens(int n) {
    result.clear();
    vector<string>chessboard(n,string(n,'.'));
    backtracking(n,0,chessboard);
    return result;
    }
    };

    37. 解数独

    整体思路(先不看代码)

    题目要求:

    给定一个 9×9 的数独棋盘(部分已填),填满整个棋盘,使其满足数独规则。

    数独规则(三个约束)

    对每个数字 '1'~'9':

  • 同一行不能重复

  • 同一列不能重复

  • 同一个 3×3 宫格不能重复


  • 数独 = 棋盘回溯 + “找到一个解就停”

    这是数独和 N 皇后最大的不同:

    题目要不要所有解
    N 皇后 要所有方案
    数独 只要一个解

    👉 所以数独的回溯函数 必须返回 bool 👉 一旦找到合法解,立刻停止搜索


    整体回溯策略(非常关键)

    核心思想一句话:

    每次找一个空格 '.',尝试填 1~9,能填就继续,填不下就回退


    搜索顺序是这样的:

  • 从左到右、从上到下扫描棋盘

  • 找到第一个 '.'

  • 尝试填 '1'~'9'

  • 如果合法,递归填下一个空格

  • 如果后面失败,撤销当前填写,换下一个数字

  • 回溯函数 backtracking 详解

    bool backtracking(vector<vector<char>>& board)

    👉 返回值含义非常重要:

    • true:已经找到一个完整合法解

    • false:当前路径不行,需要回溯


    ① 找“当前要填的格子”

    for (int i = 0; i < board.size(); i++)

    { for (int j = 0; j < board.size(); j++)

    { if (board[i][j] == '.') {

    含义:

    • 按顺序扫描棋盘

    • 找到第一个空格

    • 只处理这一个空格

    ⚠️ 注意: 一旦找到 '.',后面的格子暂时不管

    ② 尝试填 '1' ~ '9'

    for (char k = '1'; k <= '9'; k++) {

    每一个 k,都是一个“选择”。


    ③ 合法性判断(剪枝)

    if (isValid(i, j, k, board)) {

    • 不合法 → 直接跳过

    • 合法 → 尝试填入


    ④ 做选择 + 递归

    board[i][j] = k; if (backtracking(board)) return true; board[i][j] = '.';

    这是数独回溯的灵魂三行:

    发生了什么?
  • 尝试填 k

  • 递归填后续空格

  • 如果后面成功:

    • 直接一路 return true

    • 整个搜索终止

  • 如果后面失败:

    • 回溯,撤销 k

    • 尝试下一个数字


  • ⑤ 为什么这里要 return false?

    return false;

    位置很关键,在这里:

    if (board[i][j] == '.') { … return false; }

    含义是:

    这个空格,1~9 全部试过了,都不行 那说明之前的某一步选错了,必须回溯


    ⑥ 什么时候返回 true?

    return true;

    在函数最后:

    return true;

    含义:

    • 扫描完整个棋盘

    • 没有找到任何 '.'

    • 说明整个棋盘已经合法填满

    • 找到一个解 ✔


    五、合法性判断 isValid 逐段讲解

    bool isValid(int row, int col, char k, vector<vector<char>>& board)


    ① 检查行

    for (int i = 0; i < 9; i++) { if (board[row][i] == k) return false; }


    ② 检查列

    for (int j = 0; j < 9; j++) { if (board[j][col] == k) return false; }


    ③ 检查 3×3 宫格

    int startrow = (row / 3) * 3;
    int startcol = (col / 3) * 3;

    for (int i = startrow; i < startrow + 3; i++) {
    for (int j = startcol; j < startcol + 3; j++) {
    if (board[i][j] == k) return false;
    }
    }

    👉 (row/3)*3 是定位宫格左上角的标准技巧


    六、数独 vs N 皇后(本质差异)

    这是非常重要的对比:

    维度N 皇后数独
    搜索目标 所有解 任意一个解
    回溯返回值 void bool
    搜索顺序 按行 按空格
    剪枝依据 攻击规则 行 / 列 / 宫
    找到解后 继续 立刻停止

    class Solution {
    public:
    bool backtracking(vector<vector<char>>& board){
    for(int i = 0;i<board.size();i++){
    for(int j=0;j<board.size();j++){
    if(board[i][j]=='.'){
    for(char k='1';k<='9';k++){
    if (isValid(i, j, k, board)) {
    board[i][j] = k; // 放置k
    if (backtracking(board)) return true; // 如果找到合适一组立刻返回
    board[i][j] = '.'; // 回溯,撤销k
    }
    }
    return false;
    }
    }
    }
    return true;
    }

    bool isValid(int row ,int col,char k,vector<vector<char>>&board){
    for(int i=0;i<9;i++){
    if(board[row][i]==k)return false;
    }
    for(int j = 0;j<9;j++){
    if(board[j][col]==k)return false;
    }
    int startrow = (row/3)*3;
    int statrcol = (col/3)*3;
    for(int i=startrow;i<startrow + 3;i++){
    for(int j=statrcol;j<statrcol+3;j++){
    if(board[i][j]==k)return false;
    }
    }
    return true;
    }

    void solveSudoku(vector<vector<char>>& board) {
    backtracking(board);
    }
    };

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Leetcode回溯算法part2
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!