leetcode回溯算法(491.非递减子序列)

本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。

所以不能使用之前几道题的去重逻辑!

用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:

cpp 复制代码
class Solution {
private:
    // 存储所有递增子序列的结果
    vector<vector<int>> result;
    // 存储当前递归路径上的递增子序列
    vector<int> path;
    
    // 回溯函数,寻找递增子序列
    // nums: 输入数组
    // startIndex: 当前递归开始搜索的索引位置
    void backtracking(vector<int>& nums, int startIndex) {
        // 如果当前路径长度大于1(至少两个元素),说明找到了一个有效子序列
        if (path.size() > 1) {
            // 将当前路径添加到结果集中
            result.push_back(path);
            // 注意:这里不能加return,因为即使找到了一个子序列,还可以继续添加更多元素
            // 形成更长的递增子序列
        }
        
        // 使用unordered_set对本层(当前递归深度)的元素进行去重
        // 避免在同一递归层级选择相同的数字,防止重复子序列
        unordered_set<int> uset;

        if(startIndex == nums.size())  return;  / /这一行代码用来做终止条件
        // 其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了。
        
        // 从startIndex开始遍历数组
        for (int i = startIndex; i < nums.size(); i++) {
            // 条件判断:如果以下两个条件之一满足,则跳过当前数字
            // 1. 当前路径非空且当前数字小于路径最后一个数字(不是递增)
            // 2. 当前数字已经在本层中使用过(去重)
            if ((!path.empty() && nums[i] < path.back())
                    || uset.find(nums[i]) != uset.end()) {
                continue; // 跳过当前数字,继续下一轮循环
            }
            
            // 记录当前数字在本层中已使用,本层后面不能再使用相同的数字
            uset.insert(nums[i]);
            
            // 选择当前数字,加入路径
            path.push_back(nums[i]);
            
            // 递归调用,从下一个位置继续搜索
            // i+1保证每个数字在子序列中最多使用一次(因为子序列要保序)
            backtracking(nums, i + 1);
            
            // 回溯:撤销选择,移除最后加入的数字
            // 尝试其他可能性
            path.pop_back();
            // 注意:uset不需要回溯,因为它是局部变量,只在当前递归层有效
            // 每次递归调用都会创建新的uset
        }
    }
    
public:
    // 主函数:寻找所有递增子序列
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        // 清空结果集和路径(确保多次调用时状态正确)
        result.clear();
        path.clear();
        
        // 从索引0开始回溯搜索
        backtracking(nums, 0);
        
        // 返回所有找到的递增子序列
        return result;
    }
};

为什么不能用 vector<int> uset; 来替代 unordered_set<int> uset

**(1) 使用 unordered_set 查找:平均 O(1) 时间复杂度

(2) 使用 vector 查找:需要遍历,O(n) 时间复杂度**

在这个算法中,我们需要 快速判断当前元素是否在本层已经使用过

  • unordered_set:哈希表,专门用于快速查找和去重

  • vector:顺序容器,没有内置的去重机制

cpp 复制代码
unordered_set<int> uset;

for (int i = startIndex; i < nums.size(); i++) {
    // 查找:O(1) 平均时间复杂度
    if (uset.find(nums[i]) != uset.end()) {
        continue;
    }
    uset.insert(nums[i]);  // 插入:O(1)
}
cpp 复制代码
vector<int> uset;

for (int i = startIndex; i < nums.size(); i++) {
    // 每次都需要遍历整个 vector 来检查是否已存在
    bool exists = false;
    for (int num : uset) {  // O(n) 查找
        if (num == nums[i]) {
            exists = true;
            break;
        }
    }
    if (exists) continue;
    
    uset.push_back(nums[i]);  // 插入:O(1)
}

虽然 unordered_setvector 使用更多内存(哈希表的开销),但:

  • 每一层的 uset 在函数返回时都会被销毁

  • 查找的时间复杂度优势对于回溯算法更重要

cpp 复制代码
uset.find(nums[i]) != uset.end()

1. uset.find(nums[i])

  • find()unordered_set 的成员函数

  • 在集合中查找值为 nums[i] 的元素

  • 返回值:一个迭代器(iterator)

    • 如果找到元素:返回指向该元素的迭代器

    • 如果没找到:返回 uset.end() 迭代器

2. uset.end()

  • end() 是容器的成员函数

  • 返回指向容器"末尾之后"的迭代器(不是最后一个元素,而是最后一个元素的下一个位置)

  • 用作"未找到"的标记值

3. != 比较运算符

  • 比较两个迭代器是否指向同一位置

  • 如果 find() 返回的迭代器 不等于 end(),说明找到了元素

  • 如果 等于 end(),说明没找到

数组 [4, 7, 6, 7] 为例,详细分析代码每一步的执行过程

初始状态

复制代码
nums = [4, 7, 6, 7]
result = []
path = []

第一步:findSubsequences(nums) 主函数

复制代码
result.clear();   // result = []
path.clear();     // path = []
backtracking(nums, 0); 

第二步:调用 backtracking(nums, 0)

cpp 复制代码
void backtracking(vector<int>& nums, int startIndex) {
     // 如果当前路径长度大于1(至少两个元素),说明找到了一个有效子序列
     if (path.size() > 1) {
     // 将当前路径添加到结果集中
         result.push_back(path);
         // 注意:这里不能加return,因为即使找到了一个子序列,还可以继续添加更多元素
         // 形成更长的递增子序列
     }    
     // 使用unordered_set对本层(当前递归深度)的元素进行去重
     // 避免在同一递归层级选择相同的数字,防止重复子序列
     unordered_set<int> uset;
复制代码
传入的参数:startIndex = 0
if条件判断不满足
创建 uset = {} (空集合)
cpp 复制代码
for (int i = startIndex; i < nums.size(); i++) {
    // 条件判断:如果以下两个条件之一满足,则跳过当前数字
    // 1. 当前路径非空且当前数字小于路径最后一个数字(不是递增)
    // 2. 当前数字已经在本层中使用过(去重)
    if ((!path.empty() && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()) {
        continue; // 跳过当前数字,继续下一轮循环
    }
            
    // 记录当前数字在本层中已使用,本层后面不能再使用相同的数字
    uset.insert(nums[i]);
            
    // 选择当前数字,加入路径
    path.push_back(nums[i]);
            
    // 递归调用,从下一个位置继续搜索
    // i+1保证每个数字在子序列中最多使用一次(因为子序列要保序)
    backtracking(nums, i + 1);
            
    // 回溯:撤销选择,移除最后加入的数字
    // 尝试其他可能性
    path.pop_back();
    // 注意:uset不需要回溯,因为它是局部变量,只在当前递归层有效
    // 每次递归调用都会创建新的uset
}
复制代码
for循环,i = 0,nums[0] = 4
if条件判断不满足
uset.insert(4);   // uset = {4}
path.push_back(4);   // path = [4]
backtracking(nums, 1); 

第三步:调用 backtracking(nums, 1)

复制代码
传入的参数:startIndex = 1
建新的 uset = {} (局部变量)
for循环(i从1到3)i = 1,nums[1] = 7
if条件判断:
// !path.empty() = true
// nums[1] = 7,path.back() = 4,7 >= 4   满足递增条件true
// uset.find(7) = end(),说明7还不在集合中 false
uset.insert(7);  // uset = {7}
path.push_back(7);  // path = [4, 7]
backtracking(nums, 2); 

第四步:调用 backtracking(nums, 2)

复制代码
传入的参数:startIndex = 2
path.size() = 2 > 1,添加结果:
result.push_back([4, 7]);  // result = [[4, 7]]
创建新的 uset = {} 
for循环(i从2到3)i = 2, nums[2] = 6
if条件判断:
// !path.empty() = true
// nums[2] = 6,path.back() = 7,6 < 7 不满足递增!   nums[i] < path.back() 为true
条件成立,执行 if语句continue

i++ ->  i=3 ,nums[3] = 7
if条件判断:
// !path.empty() = true
// nums[3] = 7,path.back() = 7,7 >= 7 递增 , 所以为false
!path.empty() && nums[i] < path.back()  为 false
// uset.find(7) = end(),不在集合中
// 两个条件都不成立,不执行if语句,继续执行下一行代码
uset.insert(7);  // uset = {7}
path.push_back(7);  // path = [4, 7, 7]
backtracking(nums, 4); 

第五步:调用 backtracking(nums, 4)

复制代码
传入的参数:startIndex = 4
if判断:path.size() = 3 > 1,添加结果:
result.push_back([4, 7, 7]);  // result = [[4, 7], [4, 7, 7]]
创建新的 uset = {}

for 循环条件:i = 4 < 4 不成立,直接结束本次调用

第六步:回到调用 backtracking(nums, 2) 的剩余部分

复制代码
path.pop_back();  // path = [4, 7]
// i 循环结束(已经到3,下一个是4,超过数组长度)

第七步:回到调用 backtracking(nums, 1) 的剩余部分

复制代码
path.pop_back();  // path = [4]
// 继续循环:i = 2,nums[2] = 6

for循环, i=2
if条件判断:
// !path.empty() = true
// nums[2] = 6,path.back() = 4,6 >= 4 递增   false
// uset.find(6) = end(),不在集合中(注意:这是第二次调用的uset,当前是{7})
// 两个条件都不成立,继续执行

uset.insert(6);  // uset = {7, 6}
path.push_back(6);  // path = [4, 6]
backtracking(nums, 3);  

.........

.........

.........

相关推荐
陳10302 小时前
C++:二叉搜索树
开发语言·数据结构·c++
睡一觉就好了。2 小时前
排序--直接排序,希尔排序
数据结构·算法·排序算法
_pinnacle_2 小时前
多维回报与多维价值矢量化预测的PPO算法
神经网络·算法·强化学习·ppo·多维价值预测
Yzzz-F2 小时前
P3842 [TJOI2007] 线段
算法
YuTaoShao2 小时前
【LeetCode 每日一题】1984. 学生分数的最小差值
算法·leetcode·排序算法
Aurora@Hui2 小时前
FactorAnalysisTool 因子分析工具
人工智能·算法·机器学习
wen__xvn2 小时前
基础算法集训第06天:计数排序
数据结构·算法·leetcode
(; ̄ェ ̄)。2 小时前
机器学校入门(十三)C4.5 决策树,CART决策树
算法·决策树·机器学习
Ll13045252982 小时前
Leetcode哈希表篇
算法·leetcode·散列表