呦呦呦,这不是 LIS 最长上升子序列吗,几日不见,数据范围这么大了。
题意
有 n n n 朵顺序排列的花,每朵花有高度 h i h_i hi 和价值 a i a_i ai,高度两两不同。选取一些花使得这些花的高度依次递增,求选取花最大的总价值
-
1 ≤ n ≤ 2 × 1 0 5 1\le n\le 2\times 10^5 1≤n≤2×105
-
1 ≤ h i ≤ n 1\le h_i\le n 1≤hi≤n 且 h i ≠ h j , ∀ i ≠ j h_i\ne h_j,\forall i\ne j hi=hj,∀i=j
-
1 ≤ a i ≤ 1 0 9 1\le a_i\le 10^9 1≤ai≤109
分析
这题类似最长上升子序列,但是价值的数据范围太大了啊,没法以价值为状态。
对于这个问题,我们还得老老实实从头想起。问一问自己:我们关心的是什么?
在取花的过程中,要保证高度是递增的。我们唯一需要考虑的就是之前取的那朵花高度是多少,然后当前的花要更高。
- 考虑一个朴素的 dp 状态:
d p ( i ) dp(i) dp(i) 表示以高度为 i i i 的花结尾的子序列的最大价值
-
转移公式:
d p ( i ) = max j = 1 i − 1 { d p ( j ) } + a i dp(i)=\max\limits_{j=1}^{i-1} \{dp(j) \}+a_i dp(i)=j=1maxi−1{dp(j)}+ai
这种朴素的 dp 时间复杂度为 O ( n 2 ) O(n^2) O(n2),会超时,我们的优化策略要通过观察转移公式来得到。 -
优化:观察转移公式可以发现:每个 dp 值都由这段 dp 值前连续的一段 dp 值中最大值转移而来。但是因为为花的高度是无序的,前缀 max 来维护最大值并不理想。考虑使用线段树维护区间最大值。这样每次转移进行区间查询和单点修改即可。
如果不了解线段树的同学可以看这篇博客线段树详解。
时间复杂度:状态 O ( n ) O(n) O(n),转移 O ( log n ) O(\log n) O(logn),总复杂度 O ( n log n ) O(n\log n) O(nlogn)。
空间复杂度: O ( n ) O(n) O(n)
代码
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
#define pii pair<int,int>
#define mp make_pair
#define F first
#define S second
using namespace std;
int n,h[200005];
ll a[200005],dp[200005],mx[800005],ans;
//以下到33行的部分都是维护区间最大值的线段树
void PushUp(int rt)//上推{mx[rt]=max(mx[rt<<1],mx[rt<<1|1]);}
void Update(int L,ll C,int l,int r,int rt)
{//单点修改
if (l==r){
mx[rt]=max(mx[rt],C);
return;
}
int mid=(l+r)>>1;
if (L<=mid) Update(L,C,l,mid,rt<<1);
else Update(L,C,mid+1,r,rt<<1|1);
PushUp(rt);
}
ll Query(int L,int R,int l,int r,int rt)
{//区间查询
if (L<=l&&r<=R)
return mx[rt];
int mid=(l+r)>>1;
ll res=0;
if (L<=mid) res=max(res,Query(L,R,l,mid,rt<<1));
if (R>mid) res=max(res,Query(L,R,mid+1,r,rt<<1|1));
return res;
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
cin>>h[i];
for(int i=0;i<n;i++)
cin>>a[i];
for(int i=0;i<n;i++)
{//转移同时维护线段树
dp[h[i]]=Query(0,h[i]-1,0,n,1)+a[i];
Update(h[i],dp[h[i]],0,n,1);
ans=max(ans,dp[h[i]]);
}
cout<<ans<<endl;
return 0;
}
我承认我有复制板子的成分。