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) {
        // 终止条件:当前组合长度等于k
        if (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()实现状态回退,尝试所有可能

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值