树状数组优化LIS问题

树状数组优化LIS问题

LIS即为最长上升子序列问题。学习动态规划问题(DP问题)中,其中有一个知识点叫最长上升子序列(longest increasing subsequence),也可以叫最长非降序子序列。

总所周知,LIS问题有贪心解法和DP解法。
贪心时间复杂度O(n),DP时间复杂度O(n2)
本文将不讨论贪心的解法,因为一般是想不到怎么去做贪心的
实际上,在面试和比赛时候更常见的是使用DP做法(更加直观和具有一般性。

『题目传送门』:洛谷P1020
题目简述:给出一个长度不超过100000的数列,其中的数每个是不大于50000的正整数,求这个数列的最长不降子序列(问一)以及将这个数列划分为n个不降子序列时,n的最小值(问二)。
(推导过程见Dilworth定理:偏序集的最少反链划分数等于最长链的长度

DP平方复杂度#

『题目传送门』:洛谷P1020
因为空间是1e5,如果不优化空间开二维数组肯定空间爆炸((1e5)2)。
使用滚动数组思想优化空间后,对于最长上升子序列的状态转移方程是f[i]=max(f[j])其中 j<i并且a[j]<a[i],值得注意的是需要把每一次状态的初始值初始化为1(因为一个数的上升子序列就是1)
相同的,对于最长不上升子序列,只需要将这个过程反过来做即可,即从后面向前面做转移,方程是f[i]=max(f[j])其中 i<j并且a[j]<=a[i],值得注意的是i的初始状态是从n开始的(从后往前做转移)。

Copy
// Author: oceanlvr #include <bits/stdc++.h> using namespace std; typedef long long ll; const int inf = 0x3f3f3f3f; const int maxn = 1e5 + 10; int n; int res1 = -inf, res2 = -inf; int a[maxn]; int f[maxn]; int main() { while (cin >> a[++n]) ; n -= 1; //最长不上升子序列 for (int i = n; i >= 1; i--) { f[i] = 1; for (int j = n; j > i; j--) { // i <-j if (a[i] >= a[j]) { f[i] = max(f[i], f[j] + 1); } } } for (int i = 1; i <= n; i++) res1 = max(res1, f[i]); //最长上升子序列 for (int i = 1; i <= n; i++) { f[i] = 1; for (int j = 1; j < i; j++) { if (a[j] < a[i]) { // j->i f[i] = max(f[i], f[j] + 1); } } } for (int i = 1; i <= n; i++) res2 = max(res2, f[i]); cout << res1 << endl << res2 << endl; return 0; }

树状数组维护查询nlgn复杂度#

『题目传送门』:洛谷P1020

不出意外上面的写法只能过10个测试点。题目给出提示:nlgn的写法能够给出200分
那么如何达到nlgn呢?我们来分析下:
我们知道DP的时间复杂度是转移的次数x各个状态转移成本,其中转移方程的次数又叫做阶段数。

  • 首先是转移方程的次数是n,这个没有办法再优化了(至少要完成每一个转移)。
  • 然后是转移的成本,分析转移方程可知,DP的LIS做法每一次都需要查找前面元素中可转移的最大值即a[j]<=a[i]中a[j]最大。这一部分可以转为有条件的在区间内搜索最大值,是一个变种的RMQ问题,即可使用ST表或者线段树或者树状数组来优化。

更加具体一点,树状数组f(区间范围是从1>max(a[i]))。
维护的是:树状数组f[i]当前以i结尾的LIS长度的最大值。(关键是理解维护的是什么)
因此每一次转移时,我们即查询以[1,a[i]1]中的最大值。(区间查询操作query(a[i] - 1)
然后这个最大值+1即为当前的以a[i]结尾的最大值,并且把这个最大值加入到树状数组中(插入单点操作add(a[i], q + 1);

手动过一遍样例。

Copy
//#define judge // Author: oceanlvr #include <bits/stdc++.h> using namespace std; const int inf = 0x3f3f3f3f; const int ninf = 0xcfcfcfcf; static int faster_iostream = []() { std::ios::sync_with_stdio(false); std::cin.tie(NULL); return 0; }(); const int N = 1e5 + 10; int maxn = ninf; int n; int res1 = -inf, res2 = -inf; int a[N]; int f[N]; int lowbit(int x) { return x & -x; } void add(int x, int c) { for (int i = x; i <= maxn; i += lowbit(i)) f[i] = max(f[i], c); } int query(int x) { int res = 0; //求以小于等于x的数为结尾的最长不上升子序列的长度的最大值 for (int i = x; i; i -= lowbit(i)) res = max(res, f[i]); return res; } int main() { /*使用树状数组f来维护信息 维护的是:当前以i结尾的最大的LIS长度 每次查询的时间复杂度是log(max(a[i])) 即a[i]的值域的对数 */ while (cin >> a[++n]) maxn = max(a[n], maxn); n -= 1; //最长不上升子序列 /* 求以a[i]结尾的最大的不上升子序列的长度 等效于从后向前->求a[i]结尾的最长不上升子序列 1 3 4 4 5 最长上升 1 3 4 5 最长不上升 4 4 (从后向前看,求最长不上升) 所以对于这个问题只需要维护一个树状数组即可 一次从后向前做(最长不上升子序列) 一次从前往后做(最长上升子序列) 每次查询都是问 $以a[i]结尾目前的最长子序列的长度$ 即树状数组维护的是长度,并且其定义域是a[i]的值域 */ for (int i = n; i >= 1; i--) { int q = query(a[i]); add(a[i], q + 1); res1 = max(res1, q + 1); } memset(f, 0, sizeof f); // memset一下 //最长上升子序列 for (int i = 1; i <= n; i++) { int q = query(a[i] - 1); // 找到[1,a[i]-1]中的最大值 add(a[i], q + 1); //这个最大值即是有效的转移 加入到树状数组中去 res2 = max(res2, q + 1); } cout << res1 << endl << res2 << endl; return 0; }
posted @   AdaMeta730  阅读(1492)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示
CONTENTS