当前位置: 首页 > 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()实现状态回退,尝试所有可能

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

http://www.xdnf.cn/news/742519.html

相关文章:

  • tmux基本原理
  • OpenLayers 图形交互编辑
  • Redis最佳实践——安全与稳定性保障之访问控制详解
  • VMware-workstation安装教程--超详细(附带安装包)附带安装CentOS系统教程
  • 【Docker项目实战篇】Docker部署PDF查看器PdfDing
  • Maestro CLI云端测试以及github cl,bitrise原生cl的测试流程
  • Azure DevOps 管道部署系列之二IIS
  • 腾讯面试手撕题:返回行递增有序矩阵第k小的元素
  • 【教学类-36-10】20250531蝴蝶图案描边,最适合大小(一页1图1图、2图图案不同、2图图案相同对称)
  • C++ 重载(Overload)、重写(Override)、隐藏(Hiding) 的区别
  • LiquiGen流体导入UE
  • STM32 HAL库函数学习 CRC篇
  • Linux系统编程之共享内存
  • 在QT中,利用charts库绘制FFT图形
  • MAC软件游戏打开提示已损坏
  • MATLAB实战:机器学习分类回归示例
  • 【MFC】如何设置让exe的控制台不会跟着exe退出而退出
  • C++中指针常量和常量指针的区别
  • 【设计模式-4.6】行为型——状态模式
  • [蓝桥杯]拉马车
  • L56.【LeetCode题解】 电话号码的字母组合
  • 触发器与存储过程详解
  • Mybatis-Plus简单介绍
  • 鸿蒙HarmonyOS (React Native)的实战教程
  • Java后端技术栈问题排查实战:Spring Boot启动慢、Redis缓存击穿与Kafka消费堆积
  • 【Java学习笔记】内部类(重点)
  • 数据结构:时间复杂度(Time Complexity)和空间复杂度(Space Complexity)
  • Typescript学习教程,从入门到精通,TypeScript 配置管理与编译器详解(19)
  • Rust 配置解析`serde` + `toml`
  • 华为OD机试真题——找出两个整数数组中同时出现的整数(2025A卷:100分)Java/python/JavaScript/C++/C语言/GO六种最佳实现