AcWing3662. 最大上升子序列和(线性DP + 树状数组优化 + 离散化处理)
- 一、问题
- 二、分析
- 1、DP过程
- (1)状态表示
- (2)状态转移
- 2、数据结构优化
- (1)树状数组维护最值
- (2)离散化
- (3)优化过程
- 三、代码
一、问题
二、分析
1、DP过程
这道题考察的DP模型是最长上升子序列的模型,如果对这个模型不了解的话,建议去看作者的这一篇文章:第四十三章 动态规划——最长单调序列模型
这里就是对这个模型稍作变形。这里要注意的是:最长的上升子序列的和不一定是最大的。
(1)状态表示
f [ i ] f[i] f[i]表示以第 i i i个元素为结尾的上升子序列的和的最大值。
(2)状态转移
状态转移也比较经典,这里直接给出方程再解释。
f
[
i
]
=
m
a
x
(
f
[
i
]
)
+
a
[
i
]
f[i] = max(f[i]) + a[i]
f[i]=max(f[i])+a[i]
直接去枚举
i
i
i之前的状态,但是为了保证我们的序列是单调递增的,我们转移的条件需要多一个
a
[
j
]
<
a
[
i
]
a[j]<a[i]
a[j]<a[i]。
上述操作的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)的。在这道题中,该复杂度会造成超时的错误,所以我们需要对这道题的做法进行优化。对于取最大值的操作我们常用的是单调队列优化,但是这道题随着我们
i
i
i的移动,我们的转移条件在不断地发生变化,所以无法使用单调队列来优化。
因此,我们只能换一种数据结构来优化了,对于维护一个区间的最值得操作,我们常用的是线段树和 s p l a y splay splay树。而我们这里只需要维护 1 1 1到 i − 1 i-1 i−1的最大值,也就是说维护的是前缀最大值,很明显,我们的前缀最大值是比区间最大值好维护的,所以我们这里可以使用一个相对于代码量较少的数据结构——树状数组。
2、数据结构优化
在讲解优化之前,我们先学习一下前置知识。
(1)树状数组维护最值
如果大家不懂树状数组的作用和代码的话,作者这里建议读者去看作者的文章:第五十六章 树状数组(一)
作者之前的文章中介绍的是如何利用树状数组去动态维护区间和。我们这里则只需要将加和的操作改成去最值即可,其余操作没有区别。
(2)离散化
离散化的本质其实就是将数组中的数据本身映射为下标。我们只需要去将读入的数据进行排序去重,这样就实现了一个数据与下标之间的对应关系。这样的话,当我们遍历数据的时候,只需要采用二分操作就能够在刚刚的映射关系中找到该元素所映射的下标。
如果还不理解的话,可以去看作者之前的文章:第五章:双指针与离散化的映射
(3)优化过程
我们现在的目的是通过较低的复杂度去快速地查询出在第 i i i个元素之前并且小于第 i i i个元素的元素 a [ j ] a[j] a[j],再通过 a [ j ] a[j] a[j]找到这个元素所对的状态 f [ j ] f[j] f[j],然后在这些满足条件的 f [ j ] f[j] f[j]中找到一个最大值。
在这个目的的驱使下,我们可以进行如下优化:
我们将原数组
a
a
a进行排序得到数组
b
b
b。那么我们
a
a
a数组中的元素在
b
b
b数组中就根据其大小关系得到了一个相对下标,我们根据这个相对下标建立树状数组,树状数组存储的数值就是状态值
f
f
f。
这里要注意的是,我们 f f f状态所对的下标是 a a a数组的下标,但我们却按照 b b b的大小顺序将其存储在了树状数组的特定位置。
上述操作有什么作用呢?
由于我们是按照元素的相对顺序建立的树状数组,所以对于树状数组的任意位置 k k k,这个 k k k前面所有存储的 f [ i ] f[i] f[i]所对的状态 a [ i ] a[i] a[i]都是小于 k k k位置所存状态对应的 a a a的。也就是说,在不考虑原数组 a a a的下标先后顺序的条件下,树状数组 k k k前面所存储的状态都是能够更新当前状态的。
现在的关键在于我们如何保证我们 k k k位置前所存储的状态在原数组 a a a中的下标位置都是小于当前状态在数组 a a a中所对下标的?
这个问题就比较好解决了,因为我们是动态维护的,所以对于 a a a数组中的任意下标 i i i,我们先找到其在 b b b数组中的相对下标 k k k,然后根据刚刚的分析,我们可以先查询树状数组 k k k之前的状态的最大值,查询后去更新当前状态,然后再把当前状态插入树状数组即可。这样做的目的在于我们所用的状态都是之前插入的,而我们是按照 a a a数组下标从小到大枚举的。所以已经插入的状态对应的 a a a数组下标必定是在待更新状态之前的。
离散化算法其实就是针对我们的 b b b数组的,因为 b b b数组存储的是数组元素的相对大小,相同元素的相对位置应该是一样的,所以对于重复元素我们需要去重。这个过程恰好和离散化是一致的,所以我们就可以认为这里用了离散化操作。
三、代码
#include<bits/stdc++.h>
#define endl '\n'
#define INF 0x3f3f3f3f
#define int long long
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N = 1e5 + 10;
int n;
int a[N], f[N], tre[N];
vector<int>xs;
int get(int x)
{
return lower_bound(xs.begin(), xs.end(), x) - xs.begin() + 1;
}
int lowbit(int x)
{
return x & -x;
}
void add(int pos, int x)
{
for(int i = pos; i <= n; i += lowbit(x))
{
tre[i] = max(tre[i], x);
}
}
int quary(int pos)
{
int res = 0;
for(int i = pos; i; i -= lowbit(i))
{
res = max(tre[i], res);
}
return res;
}
void solve()
{
cin >> n;
for(int i = 0; i < n; i ++ )
{
cin >> a[i];
xs.push_back(a[i]);
}
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end());
for(int i = 0; i < n; i ++ )
{
int k = get(a[i]);
f[i] = quary(k - 1) + a[i];
add(k, f[i]);
}
int ans = 0;
for(int i = 0; i < n; i ++ )
{
ans = max(f[i], ans);
}
cout << ans << endl;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
}