今日算法(回溯算法)

题目描述

给定两个整数 n 和 k,返回范围 [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=1,path=[])
    • 循环 i 从 1 到 3(剪枝后 4 - 2 + 1 = 3)
    • i=1:
      • path = [1],进入第二层递归,start=2
      • 第二层循环 i 从 2 到 4
        • i=2:path=[1,2],长度为 2,加入 res,回溯后 path=[1]
        • i=3:path=[1,3],加入 res,回溯后 path=[1]
        • i=4:path=[1,4],加入 res,回溯后 path=[1]
      • 回溯,path=[]
    • i=2:
      • path=[2],进入第二层递归,start=3
      • 第二层循环 i 从 3 到 4
        • i=3:path=[2,3],加入 res,回溯后 path=[2]
        • i=4:path=[2,4],加入 res,回溯后 path=[2]
      • 回溯,path=[]
    • i=3:
      • path=[3],进入第二层递归,start=4
      • 第二层循环 i=4:path=[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() 个元素
    • 剩余可选元素从 i 到 n,共 n - i + 1 个
    • 当 n - i + 1 < need 时,无法凑齐 k 个元素,无需继续遍历
    • 例如 n=4, k=2,当 path.size()=1 时,need=1,i 最大为 4;当 path.size()=0 时,need=2,i 最大为 3(4-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 参数保证组合的递增性,避免重复;同时通过剪枝优化,减少无效的递归调用。这种方法是解决排列组合问题的通用模板,后续的子集、全排列等问题都可以基于这个框架修改。

相关推荐
用户979984807186 小时前
200 行代码写一个能跑的 AI Agent:不依赖任何框架,只靠 tool-calling 原理
算法
threerocks6 小时前
【Muse实战】X 流量作战室搭建保姆级教程 - 拥有你自己的运营团队
算法
GreenTea6 小时前
深度拆解 ScienceBuddy:如何用“双层递归自进化”构建高可靠科研 Agent Harness
前端·后端·算法
花椒技术6 小时前
2.46 秒生成 5 秒视频:拆解 H3 Max 的模型、推理栈与硬件协同
算法·音视频开发·视频编码
vivo互联网技术6 小时前
ART:妆容迁移框架,重新定义高保真妆容迁移 | ECCV 2026
人工智能·算法·图像识别
用户0441440924496 小时前
卫星信号模拟器里的四个坐标转换公式
算法
橘和柠6 小时前
阿里 open-code-review (AI代码审查工具)实战:安装、四层规则链、自定义规则格式与实测避坑
算法·面试
得物技术6 小时前
别再只卷向量检索了,得物交易搜索如何用“生成式”实现召回范式跃迁?
人工智能·算法·llm
罗西的思考6 小时前
[Agent Memory / 强化学习] MemPO源码学习笔记 ---(4)--- Rollout实现细节
人工智能·算法
罗西的思考7 小时前
机器人 / 物理 Agent Harness 综合分析与对比:从「更强的模型」到「更好的系统」
人工智能·算法·机器学习