树状数组优化LIS问题
树状数组优化LIS问题
LIS即为最长上升子序列问题。学习动态规划问题(DP问题)中,其中有一个知识点叫最长上升子序列(longest increasing subsequence),也可以叫最长非降序子序列。
总所周知,LIS问题有贪心解法和DP解法。
贪心时间复杂度,DP时间复杂度。
本文将不讨论贪心的解法,因为一般是想不到怎么去做贪心的。
实际上,在面试和比赛时候更常见的是使用DP做法(更加直观和具有一般性。
『题目传送门』:洛谷P1020
题目简述:给出一个长度不超过100000的数列,其中的数每个是不大于50000的正整数,求这个数列的最长不降子序列(问一)以及将这个数列划分为n个不降子序列时,n的最小值(问二)。
(推导过程见Dilworth定理:偏序集的最少反链划分数等于最长链的长度)
DP平方复杂度#
『题目传送门』:洛谷P1020
因为空间是1e5,如果不优化空间开二维数组肯定空间爆炸()。
使用滚动数组思想优化空间后,对于最长上升子序列的状态转移方程是其中 并且,值得注意的是需要把每一次状态的初始值初始化为1(因为一个数的上升子序列就是1)
相同的,对于最长不上升子序列,只需要将这个过程反过来做即可,即从后面向前面做转移,方程是其中 并且,值得注意的是i的初始状态是从n开始的(从后往前做转移)。
// 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各个状态转移成本,其中转移方程的次数又叫做阶段数。
- 首先是转移方程的次数是,这个没有办法再优化了(至少要完成每一个转移)。
- 然后是转移的成本,分析转移方程可知,DP的LIS做法每一次都需要查找前面元素中可转移的最大值即a[j]<=a[i]中a[j]最大。这一部分可以转为有条件的在区间内搜索最大值,是一个变种的RMQ问题,即可使用ST表或者线段树或者树状数组来优化。
更加具体一点,树状数组(区间范围是从)。
维护的是:树状数组f[i]当前以i结尾的LIS长度的最大值。(关键是理解维护的是什么)
因此每一次转移时,我们即查询以中的最大值。(区间查询操作query(a[i] - 1)
)
然后这个最大值+1即为当前的以结尾的最大值,并且把这个最大值加入到树状数组中(插入单点操作add(a[i], q + 1);
)
//#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」