前言
在做 6. 小E的怪物挑战 时,重新从一个新的角度去理解了LIS问题(最长上升子序列问题),并使用了树状数组去优化,达到了和二分优化相同的时间复杂度,并可以运用到其他更多的题目上去,在这里分享给大家。
我们首先回顾一下这个题目。
最长上升子序列II
题目描述
给定一个长度为 N 的数列 Ai,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N 。
第二行包含 N 个整数,第 i 个整数为 Ai, 表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围 1 ≤ N ≤ 100000, −10^9 ≤ Ai ≤ 10^9。
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
树状数组做法(二维偏序)
大部分人理解该问题基本都是用的二分的方法,这里提供从偏序问题的角度解决LIS问题的基本思路,并简述二维偏序问题。
在讲述这个方法之前,我们先来简述一个概念:偏序。
偏序
简单地说,偏序就是满足【自反性】【反对称性】【传递性】的一种元素之间的关系。
举个例子:如果a,b,c是一个偏序集中的元素。
- 自反性:
a≤a显然成立 - 反对称性: 若
a≤b且b≤a,那么a=b - 传递性: 若
a≤b且b≤c,那么a≤c显然成立
很显然,我们有理数上的小于等于关系就是一个典型的偏序关系,我们也将其称之为一维偏序。
在算法竞赛里,我们主要需要针对传递性进行考虑。
二维偏序
在刚刚一维偏序的例子里,我们可以发现a,b,c三个元素的偏序关系,实际上就是实数轴上的三点 a,b,c 的相对位置关系,越小的数越靠左,越大的数越靠右。
知道一维偏序实际上是数轴上的位置关系以后,我们现在来了解二维偏序。
若<a1,b1>,<a2,b2>都是偏序,且(a1,b1)和(a2,b2)分别是两个点对,则该问题就是二位偏序。
具体一点来说:如果我们在平面上有两点(x1,y1)和(x2,y2),其中x1和x2之间有横轴上的位置关系,y1和y2有纵轴上的位置关系,这两点的关系我们就称之为二维偏序。如果(x1,y1)在(x2,y2)左下角,点1就同时满足了x和y轴两个偏序中均小于点2。
在一维偏序中,a,b两个元素只有:a在b左侧,重合,a在b右侧3种情况。 在二维偏序中,共有左上,上,右上,左,重合,右,左下,下,右下共9种情况。
回到本题
考虑最朴素的DP方式:
dp[i] = max(dp[j]) + 1,其中 j < i 且 a[j] < a[i]。
我们发现,如果我们将下标i,j看成纵轴,a[i],a[j]看作横轴,将其画在平面上得到两点(a[i],i)和(a[j],j),我们发现,动态规划的转移方程,实际上是要我们在点(i,a[i])的左下角区域的所有(a[j],j)中,找到一个最大的dp[j]进行转移。
我们考虑将问题离线化,将pair(a[i],i)排序后,每次查询前,先单点修改,将所有a[j] < a[i]点的dp[j] 值写入线段树/树状数组的 j 下标处,然后我们区间查询线段树/树状数组中下标 1~i-1 中的最大值,即为我们要找的dp[j]。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 2e5+5;
int n;
int tr[N];
PII a[N];
int ans[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int pos) {
while(pos <= n) {
tr[pos] = max(tr[pos], x);
pos += lowbit(x);
}
}
int query(int pos) {
int res = 0;
while(pos) {
res = max(tr[pos], res);
pos -= lowbit(x);
}
return res;
}
int main() {
cin >> n;
for(int i=1;i<=n;i++) {
cin >> a[i].first;
a[i].second = -i;
}
sort(a+1,a+1+n);
int res = 0;
for(int i=1;i<=n;i++) {
auto [x, idx] = a[i];
idx = -idx;
ans[idx] = query(idx) + 1;
add(ans[idx], idx);
res = max(res, ans[idx]);
}
cout << res;
return 0;
}