今日算法(回溯算法)

题目描述

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

  • 组合:从 n 个元素中选 k 个,不考虑顺序 (即 [1,2][2,1] 视为同一个组合,只保留一个)
  • 可以按任何顺序返回答案

核心思路:回溯法(DFS)

组合问题的本质是从有序序列中按顺序选元素,避免重复,用回溯法可以暴力且高效地枚举所有可能。

核心逻辑

  1. 路径保存 :用一个临时列表 path 保存当前正在构建的组合
  2. 递归终止条件 :当 path.size() == k 时,说明找到了一个有效组合,将其加入结果集
  3. 选择与回溯
    • start 位置开始选择元素(避免选到前面的元素导致重复)
    • 选择当前元素,加入 path
    • 递归进入下一层,从 i+1 位置继续选择
    • 回溯:将当前元素从 path 中移除,尝试下一个元素

完整代码实现(C++)

cpp

复制代码
class Solution {
public:
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res; // 存储所有结果
        vector<int> path;        // 存储当前正在构建的组合
        backtrack(n, k, 1, path, res);
        return res;
    }

private:
    // start: 本轮可以选择的起始位置(避免重复)
    void backtrack(int n, int k, int start, vector<int>& path, vector<vector<int>>& res) {
        // 1. 递归终止条件:路径长度等于k,找到一个有效组合
        if (path.size() == k) {
            res.push_back(path);
            return;
        }

        // 2. 剪枝优化:剩余元素不足时,无需继续遍历
        // 还需要选 (k - path.size()) 个元素,因此 i 最大只能到 n - (k - path.size()) + 1
        for (int i = start; i <= n - (k - path.size()) + 1; ++i) {
            // 3. 选择当前元素
            path.push_back(i);
            // 4. 递归:下一轮从 i+1 开始选择(避免重复)
            backtrack(n, k, i + 1, path, res);
            // 5. 回溯:撤销选择,尝试下一个元素
            path.pop_back();
        }
    }
};

详细执行流程解析

以示例 n=4, k=2 为例,模拟回溯过程:

初始状态

  • res = []path = []start = 1

递归过程

  1. 第一层递归(start=1path=[]
    • 循环 i13(剪枝后 4 - 2 + 1 = 3
    • i=1
      • path = [1],进入第二层递归,start=2
      • 第二层循环 i24
        • i=2path=[1,2],长度为 2,加入 res,回溯后 path=[1]
        • i=3path=[1,3],加入 res,回溯后 path=[1]
        • i=4path=[1,4],加入 res,回溯后 path=[1]
      • 回溯,path=[]
    • i=2
      • path=[2],进入第二层递归,start=3
      • 第二层循环 i34
        • i=3path=[2,3],加入 res,回溯后 path=[2]
        • i=4path=[2,4],加入 res,回溯后 path=[2]
      • 回溯,path=[]
    • i=3
      • path=[3],进入第二层递归,start=4
      • 第二层循环 i=4path=[3,4],加入 res,回溯后 path=[3]
      • 回溯,path=[]

最终结果

res 中包含所有 C(4,2)=6 个组合:[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]],与示例输出一致。


关键细节与易错点

  1. start 参数的作用 每次递归从 start 位置开始选择,保证组合中的元素是递增的,避免出现 [2,1] 这种重复组合。

  2. 剪枝优化 循环条件 i <= n - (k - path.size()) + 1 是关键优化:

    • 还需要选 need = k - path.size() 个元素
    • 剩余可选元素从 in,共 n - i + 1
    • n - i + 1 < need 时,无法凑齐 k 个元素,无需继续遍历
    • 例如 n=4, k=2,当 path.size()=1 时,need=1i 最大为 4;当 path.size()=0 时,need=2i 最大为 34-2+1=3),避免了无效的 i=4 循环。
  3. 回溯操作的顺序 必须先 path.push_back(i),再递归,最后 path.pop_back(),否则会导致 path 状态混乱。

  4. 组合与排列的区别 排列问题不需要 start 参数,每次都从 1 开始选,会出现重复顺序;组合问题必须用 start 保证递增,避免重复。


复杂度分析

  • 时间复杂度 :\(O(C(n, k) \times k)\)
    • 组合数 \(C(n, k)\) 是所有有效组合的数量
    • 每个组合需要复制一次到结果集,时间复杂度为 \(O(k)\)
  • 空间复杂度 :\(O(k)\)
    • 递归调用栈的深度等于 k,临时路径 path 的最大长度也为 k,不包含结果集的额外空间。

拓展:迭代法实现(可选)

也可以用迭代法实现组合生成,核心思想是不断扩展已有的组合:

cpp

复制代码
class Solution {
public:
    vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> res;
        // 初始化:生成所有长度为1的组合
        for (int i = 1; i <= n; ++i) {
            res.push_back({i});
        }

        // 扩展组合长度到k
        for (int len = 2; len <= k; ++len) {
            vector<vector<int>> temp;
            for (auto& comb : res) {
                int last = comb.back();
                // 从last+1开始扩展,保证递增
                for (int i = last + 1; i <= n; ++i) {
                    temp.push_back(comb);
                    temp.back().push_back(i);
                }
            }
            res = move(temp);
        }

        return res;
    }
};

总结

组合问题的标准解法是带剪枝的回溯法 ,核心是通过 start 参数保证组合的递增性,避免重复;同时通过剪枝优化,减少无效的递归调用。这种方法是解决排列组合问题的通用模板,后续的子集、全排列等问题都可以基于这个框架修改。

相关推荐
毅炼6 小时前
今日LeetCode 摸鱼打卡
java·算法·leetcode
m0_629494736 小时前
LeetCode 热题 100-----28. 两数相加
数据结构·算法·leetcode·链表
菜菜的顾清寒6 小时前
力扣HOT100(25)环形链表
算法·leetcode·链表
学不懂飞行器6 小时前
【2024电赛H题硬核解析】自动行驶小车满分对策:多路灰度循迹与陀螺仪“交替盲走”融合算法(附源码)
stm32·单片机·嵌入式硬件·算法·电赛
机器学习之心6 小时前
大跨度拱桥施工智能优化:基于改进RBF神经网络与多目标算法的工程实践
人工智能·神经网络·算法·大跨度拱桥施工智能优化
Deep-w6 小时前
【MATLAB】基于 MATLAB/Simulink 的无刷直流电机(BLDC)转速控制模糊 PID 算法
开发语言·算法·matlab
皮卡祺q7 小时前
【算法-0】背包问题(三维+二维)
java·javascript·算法
葫三生7 小时前
《论三生原理》对《周易》《道德经》的一次根本性重写?
人工智能·算法·计算机视觉·区块链·量子计算
一路往蓝-Anbo7 小时前
第五章:如何对 HAL 库本身进行单元测试?
网络·数据结构·stm32·单片机·嵌入式硬件·单元测试·tdd