当前位置: 首页 > news >正文

leetcode77.组合:回溯算法中for循环与状态回退的逻辑艺术

一、题目深度解析与组合问题本质

题目描述

给定两个整数nk,要求从1到n的整数中选取k个不同的数,返回所有可能的组合。例如,当n=4,k=2时,所有组合为[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]。题目要求:

  • 组合中的数字按升序排列
  • 不同组合之间按字典序排列
  • 不能有重复组合

组合问题的核心特性

组合问题的本质是在n个元素中选取k个元素的子集问题,具有以下特点:

  1. 无序性:组合不考虑元素顺序(如[1,2]和[2,1]视为同一组合)
  2. 唯一性:每个元素只能使用一次
  3. 有序输出:结果需按升序排列,便于比较和处理

回溯算法是解决组合问题的经典方法,其核心在于通过"选择-回退"的递归逻辑,系统地枚举所有可能的组合。

二、回溯解法的核心实现与逻辑框架

完整回溯代码实现

class Solution {List<Integer> temp = new LinkedList<>();  // 存储当前组合List<List<Integer>> res = new ArrayList<>();  // 存储所有结果组合public List<List<Integer>> combine(int n, int k) {backTracking(n, k, 1);  // 从1开始回溯return res;}public void backTracking(int n, int k, int s) {// 终止条件:当前组合长度等于kif (temp.size() == k) {res.add(new ArrayList<>(temp));  // 保存当前组合的副本return;}// 核心循环:从s到n-(k-temp.size())+1选择数字for (int i = s; i <= n - (k - temp.size()) + 1; i++) {temp.add(i);  // 选择当前数字backTracking(n, k, i + 1);  // 递归选择下一个数字(i+1保证升序)temp.removeLast();  // 回溯:撤销当前选择}return;}
}

核心设计解析:

  1. 数据结构设计

    • temp:使用LinkedList存储当前正在构建的组合,支持高效的尾部添加和删除
    • res:使用ArrayList存储所有结果组合,便于批量操作
  2. 回溯函数参数

    • n:总数字范围(1到n)
    • k:需要选取的数字个数
    • s:当前选择的起始位置(避免重复组合)
  3. 终止条件

    • temp.size() == k时,说明已选满k个数字,将当前组合添加到结果集
  4. 循环边界优化

    • i <= n - (k - temp.size()) + 1:动态计算循环上界,避免无效搜索

三、核心问题解析:回溯逻辑与循环边界

1. 回溯算法的核心流程

回溯三部曲:
  1. 选择:在当前可选范围内选择一个数字加入组合
  2. 递归:继续在剩余数字中选择下一个数字
  3. 回退:撤销当前选择,尝试其他可能
代码中的体现:
temp.add(i);         // 选择
backTracking(..., i+1); // 递归
temp.removeLast();   // 回退

2. for循环边界的数学推导

常规循环写法:
for (int i = s; i <= n; i++) { ... }

这种写法会导致无效搜索(如剩余需要选m个数字时,当前数字之后必须至少有m个数字)。

优化后的边界条件:
i <= n - (k - temp.size()) + 1
  • 设当前已选t个数字(t = temp.size()),还需选m = k - t个数字
  • 为保证后续能选够m个数字,当前数字i必须满足:i + m - 1 <= n
  • 推导得:i <= n - m + 1 = n - (k - t) + 1
示例说明:

n=4, k=2, temp.size()=1时,还需选1个数字:

  • 边界条件:i <= 4 - 1 + 1 = 4
  • 可选数字:i从当前s到4,符合实际需求

四、回溯流程深度模拟:以n=4,k=2为例

递归调用树:

backTracking(4,2,1)
├─ i=1: temp=[1]
│  └─ backTracking(4,2,2)
│    ├─ i=2: temp=[1,2] → 加入res
│    ├─ i=3: temp=[1,3] → 加入res
│    └─ i=4: temp=[1,4] → 加入res
├─ i=2: temp=[2]
│  └─ backTracking(4,2,3)
│    ├─ i=3: temp=[2,3] → 加入res
│    └─ i=4: temp=[2,4] → 加入res
└─ i=3: temp=[3]└─ backTracking(4,2,4)└─ i=4: temp=[3,4] → 加入res

详细步骤:

  1. 初始调用backTracking(4,2,1)

    • temp为空,进入循环,i从1开始
  2. i=1时

    • temp.add(1) → temp=[1]
    • 递归调用backTracking(4,2,2)
    • 在该递归中,i从2开始,依次选择2、3、4,形成[1,2]、[1,3]、[1,4]
  3. i=2时

    • temp.add(2) → temp=[2]
    • 递归调用backTracking(4,2,3)
    • 形成[2,3]、[2,4]
  4. i=3时

    • temp.add(3) → temp=[3]
    • 递归调用backTracking(4,2,4)
    • 形成[3,4]

五、算法复杂度分析

1. 时间复杂度

  • O(C(n,k)×k)
    • 组合数:C(n,k)为最终结果数量
    • 每个组合需要O(k)时间复制到结果集
  • 优化后的循环减少了无效搜索,但最坏情况下仍需遍历所有可能组合

2. 空间复杂度

  • O(k):递归栈深度最大为k(每层递归代表一个数字选择)
  • temp列表长度最多为k,res空间为O(C(n,k)×k)

六、核心技术点总结:回溯算法的三大要素

1. 状态定义

  • 当前组合:用temp列表记录正在构建的组合
  • 结果集:用res列表存储所有有效组合
  • 选择起点:用s参数避免重复组合

2. 递归逻辑

  • 终止条件:当组合长度达到k时保存结果
  • 递归方向:每次选择后,下一次选择从i+1开始
  • 回退操作:用removeLast()撤销选择

3. 剪枝优化

  • 循环边界计算:通过数学推导减少无效搜索
  • 顺序控制:从s开始选择,保证组合升序且唯一

七、常见误区与优化建议

1. 重复组合问题

  • 误区:未使用s参数控制选择起点
    for (int i = 1; i <= n; i++) { ... } // 错误,会产生[1,2]和[2,1]等重复组合
    
  • 正确做法:每次从s开始选择,且s=i+1

2. 结果集复制错误

  • 误区:直接添加temp到res
    res.add(temp); // 错误,后续修改会影响已保存的组合
    
  • 正确做法:添加副本new ArrayList<>(temp)

3. 优化建议:位运算实现

// 位运算解法(仅作示意)
List<List<Integer>> res = new ArrayList<>();
for (int mask = 1; mask < (1 << n); mask++) {if (Integer.bitCount(mask) == k) {List<Integer> combo = new ArrayList<>();for (int i = 0; i < n; i++) {if ((mask & (1 << i)) != 0) {combo.add(i + 1);}}res.add(combo);}
}
  • 优势:代码更简洁,时间复杂度相同
  • 劣势:无法处理n较大的情况(如n=30时,1<<30超出整数范围)

八、总结:回溯算法是组合问题的系统枚举之道

本算法通过回溯法系统地枚举所有可能的组合,核心在于:

  1. 状态管理:用temp和res维护当前组合和结果集
  2. 递归控制:通过s参数避免重复,用循环边界优化搜索
  3. 回退机制:通过removeLast()实现状态回退,尝试所有可能

理解回溯算法的关键在于把握"选择-递归-回退"的循环逻辑,以及如何通过参数设计避免重复计算。这种方法不仅适用于组合问题,还可扩展到排列、子集等多种组合优化问题,是算法设计中处理枚举类问题的核心技术之一。

相关文章:

  • LeetCode - 206. 反转链表
  • 软件性能之CPU
  • leetcode hot100刷题日记——30.两数之和
  • 设计模式——单例设计模式(创建型)
  • 【MFC】如何设置让exe的控制台不会跟着exe退出而退出
  • 【KWDB 创作者计划】_探秘浪潮KWDB数据库:从时间索引到前沿技术
  • C++ 重载(Overload)、重写(Override)、隐藏(Hiding) 的区别
  • 【Hot 100】121. 买卖股票的最佳时机
  • acwing刷题
  • 江科大IIC读取MPU6050hal库实现
  • 在Windows本地部署Dify详细操作
  • Linux入门(十二)服务管理
  • 建筑兔零基础人工智能自学记录101|Transformer(1)-14
  • LG P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III Solution
  • 若依框架-定制化服务搭建
  • 信息安全之什么是公钥密码
  • 大模型应用开发之预训练
  • vue3的watch用法
  • 产品规格书写作结构、规范(编写指南)
  • 力扣热题100之翻转二叉树
  • 南京优化网站/制作小程序的软件
  • 学网站开发应该学什么软件/哈尔滨seo推广优化
  • 英文成品网站模板下载/线上营销
  • 深圳哪家做网站/成都seo
  • 广州网站建设出名 乐云践新/网站推广计划
  • 南通通州区城乡建设局网站/常用搜索引擎有哪些