news 2026/5/1 0:25:20

别再死记硬背LIS了!PTA这道列车调度题教你用set玩转最长上升子序列

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再死记硬背LIS了!PTA这道列车调度题教你用set玩转最长上升子序列

用STL set优雅解决最长上升子序列问题:从列车调度到算法优化

在算法竞赛和编程面试中,最长上升子序列(LIS)问题是一个经典且高频出现的题目。传统解法通常采用动态规划(DP)实现,时间复杂度为O(n²),这在处理大规模数据时往往力不从心。而今天我们要探讨的是一种更为优雅高效的解法——利用STL中的set容器,将时间复杂度优化到O(n log n),同时代码量大幅减少。

1. 问题背景与直观理解

想象一个火车调度场景:多列火车需要按照编号递减的顺序通过出口。每条轨道上的列车编号必须严格递减,我们需要计算最少需要多少条平行轨道才能完成调度任务。

这个问题看似是调度问题,实则暗藏玄机——它本质上等价于求原序列的最长上升子序列长度。让我们通过一个具体例子来理解:

给定列车编号序列:8, 4, 2, 5, 3, 9, 1, 6, 7

最优调度方案可能如下:

  • 轨道1:8 → 4 → 2
  • 轨道2:5 → 3 → 1
  • 轨道3:9 → 6
  • 轨道4:7

这里最少需要4条轨道,正好对应原序列的最长上升子序列长度:2, 3, 6, 7(还有其他可能的LIS)。

为什么最少轨道数等于LIS长度?

因为每条轨道上的列车编号必须递减,相当于在原始序列中选取若干个递减子序列来覆盖所有元素。根据Dilworth定理,这种划分的最小数量等于序列中最长上升子序列的长度。

2. 传统DP解法的局限性

在介绍set解法前,我们先回顾传统的动态规划解法:

int lengthOfLIS(vector<int>& nums) { vector<int> dp(nums.size(), 1); int res = 1; for(int i = 1; i < nums.size(); i++) { for(int j = 0; j < i; j++) { if(nums[j] < nums[i]) { dp[i] = max(dp[i], dp[j]+1); } } res = max(res, dp[i]); } return res; }

这种解法虽然直观,但有明显缺点:

  • 时间复杂度O(n²),当n=10^5时,计算量达到10^10,无法在合理时间内完成
  • 代码相对冗长,需要维护dp数组和双重循环
  • 难以直接获取LIS的具体内容,只能得到长度

3. 基于set的优化解法

STL中的set容器基于红黑树实现,具有自动排序和快速查找的特性。我们可以利用它来高效维护一个"潜在LIS的末尾值集合"。

核心思路:

  1. 维护一个set,其中每个元素代表一个潜在上升子序列的末尾值
  2. 对于新元素x,在set中查找第一个≥x的元素
    • 如果找到,用x替换它(贪心策略:使末尾尽可能小)
    • 如果没找到,将x插入set(意味着开始一个新的潜在序列)
  3. 最终set的大小就是LIS的长度

实现代码极其简洁:

#include <iostream> #include <set> using namespace std; int main() { int n, x; cin >> n; set<int> s; for(int i = 0; i < n; i++) { cin >> x; auto it = s.lower_bound(x); if(it != s.end()) s.erase(it); s.insert(x); } cout << s.size() << endl; return 0; }

算法分析:

  • 每次插入/删除操作的时间复杂度为O(log k),其中k是当前set的大小
  • 总体时间复杂度为O(n log n),完美处理n=10^5的情况
  • 空间复杂度O(n),仅需存储当前活跃的序列末尾

4. 算法原理深度解析

为什么这种方法能正确计算LIS长度?关键在于理解set中每个元素的含义:

  • set中的每个元素x代表存在一个长度为k的上升子序列,其末尾元素为x
  • set始终保持严格递增,这保证了每个x确实对应不同长度的序列
  • 使用lower_bound查找并替换的操作,确保了我们可以"延长"适当的序列

贪心策略的数学基础:

  1. 替换而非跳过:当我们用更小的值替换set中的元素时,并没有减少任何序列的长度,只是为后续更大的数字创造了更多可能性。

  2. 维护最小末尾:始终保持每个长度对应的序列末尾尽可能小,这样后续数字有更大机会延长某个序列。

  3. 单调性保证:set的自动排序特性恰好满足了我们需要维护"每个长度对应最小末尾"的需求。

5. 与vector实现的对比

另一种常见的优化解法是使用vector配合二分查找:

int lengthOfLIS(vector<int>& nums) { vector<int> tails; for(int num : nums) { auto it = lower_bound(tails.begin(), tails.end(), num); if(it == tails.end()) { tails.push_back(num); } else { *it = num; } } return tails.size(); }

set与vector实现的比较:

特性set实现vector实现
时间复杂度O(n log n)O(n log n)
空间复杂度O(n)O(n)
代码简洁度更简洁稍复杂
可读性较高需要理解二分查找
扩展性直接支持动态插入删除需要手动维护有序性
适用场景需要频繁查询/修改一次性计算LIS

对于大多数情况,set实现更为推荐,因为它:

  1. 代码更简洁直观
  2. 自动处理元素的排序和唯一性
  3. 更容易理解和记忆

6. 实际应用与变种问题

这种基于set的LIS解法不仅适用于列车调度问题,还能解决许多变种:

  1. 最长不下降子序列:只需将lower_bound改为upper_bound
  2. 最长递减子序列:可以反转比较逻辑,或预处理取负数
  3. 带权LIS问题:需要结合线段树等数据结构进行扩展
  4. 二维LIS问题:先对一维排序,再在另一维上应用LIS算法

实际工程中的应用场景:

  • 版本控制系统中的文件差异比较
  • 生物信息学中的DNA序列比对
  • 金融领域中的最长增长趋势分析
  • 任务调度中的依赖关系处理

7. 性能优化与注意事项

虽然set解法已经很高效,但在极端情况下还可以进一步优化:

  1. 使用unordered_set预检查:对于大量重复元素,可以先去重
  2. 自定义比较函数:对于复杂数据结构,可以自定义set的比较逻辑
  3. 内存预分配:如果知道大致规模,可以预先reserve空间

常见陷阱与调试技巧:

  • 确保输入序列没有负数(或正确处理负数情况)
  • 注意边界条件:空序列、单元素序列、全相同序列
  • 在竞赛中,注意PTA等平台可能禁用某些头文件
  • 多打印中间结果验证算法正确性
// 调试版:打印中间过程 for(int num : nums) { cout << "Processing: " << num << endl; auto it = s.lower_bound(num); if(it != s.end()) { cout << "Replace " << *it << " with " << num << endl; s.erase(it); } else { cout << "Insert new " << num << endl; } s.insert(num); cout << "Current set: "; for(int x : s) cout << x << " "; cout << endl; }

8. 从算法到思维的提升

这种set解法不仅是一种技巧,更体现了算法设计的精妙之处:

  1. 问题转化能力:将看似复杂的调度问题转化为经典算法问题
  2. 数据结构选择:根据问题特性选择最适合的STL容器
  3. 贪心思维应用:通过局部最优达到全局最优
  4. 数学建模能力:理解Dilworth定理等数学基础

掌握这种解法后,你会发现许多看似不同的问题其实共享相似的解决模式。例如:

  • 会议室安排问题
  • 任务调度问题
  • 文件存储优化
  • 网络数据包排序

在最近的PTA天梯赛和LeetCode周赛中,这类问题频繁出现。例如LeetCode 300(LIS)、354(俄罗斯套娃)、673(LIS个数)等,都可以用类似的思路解决。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 0:21:08

3分钟掌握APK Installer:Windows上安装Android应用的终极方案

3分钟掌握APK Installer&#xff1a;Windows上安装Android应用的终极方案 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 你是否曾想在Windows电脑上直接运行手机应用&…

作者头像 李华
网站建设 2026/5/1 0:18:29

终极指南:如何一键将任何网站完整保存到本地

终极指南&#xff1a;如何一键将任何网站完整保存到本地 【免费下载链接】WebSite-Downloader 项目地址: https://gitcode.com/gh_mirrors/web/WebSite-Downloader 想象一下&#xff0c;你花了几个月时间精心整理的在线学习资料突然消失不见&#xff0c;或者你在旅行途…

作者头像 李华
网站建设 2026/5/1 0:17:17

通过curl命令快速测试Taotoken的OpenAI兼容接口

通过curl命令快速测试Taotoken的OpenAI兼容接口 1. 准备工作 在开始测试之前&#xff0c;请确保您已具备以下条件&#xff1a; 有效的Taotoken API Key&#xff08;可在控制台创建&#xff09;目标模型ID&#xff08;可在模型广场查看&#xff0c;例如claude-sonnet-4-6&…

作者头像 李华
网站建设 2026/5/1 0:12:20

驾驭工程效率:模块化工具箱如何标准化开发运维实践

1. 项目概述&#xff1a;一个工程师的“瑞士军刀”工具箱最近在GitHub上看到一个挺有意思的项目&#xff0c;叫nnabuuu/harness-engineering-toolkit。光看名字&#xff0c;harness这个词就挺有味道的&#xff0c;它既有“利用、驾驭”的意思&#xff0c;也指代“线束、装备”。…

作者头像 李华
网站建设 2026/5/1 0:10:55

如何用Blender处理MMD模型:MMD Tools插件的完整解决方案

如何用Blender处理MMD模型&#xff1a;MMD Tools插件的完整解决方案 【免费下载链接】blender_mmd_tools MMD Tools is a blender addon for importing/exporting Models and Motions of MikuMikuDance. 项目地址: https://gitcode.com/gh_mirrors/bl/blender_mmd_tools …

作者头像 李华