浅谈区间最值操作与历史最值问题
浅谈树状数组与线段树:https://www.cnblogs.com/AKMer/p/9946944.html
区间最值问题
对于线段树上每个结点,我们维护最大值,严格次大值,区间和,最大值个数即可。对于修改操作,分为三种情况讨论:
1、如果当前结点的最大值小于等于\(a\)的话,直接退出,因为取\(max\)操作不会对当前区间造成任何影响。
2、如果当前结点的最大值大于\(a\)但是次大值小于等于\(a\),所以最大值都会被改成\(a\),其它值不受影响。给当前点打上一个区间与\(a\)取\(min\)的标记和区间然后减去最大值个数乘以最大值与\(a\)的差值。
3、如果当前结点的次大值大于\(a\),那么就暴力递归它的两个子树继续做。
时间复杂度证明:
首先第一种和第二种情况都是\(O(1)\)的,我们不用考虑。那么对于第三种情况,怎么证明时间复杂度呢?
这个时候,吉如一线段树最重要的思想来了。
不要试着证明每次的复杂度多么多么优秀,总复杂度总会给你一个满意的答案。
我们记录\(fake\)为当前线段树的权值,\(fake\)记录的是所有结点的权值种数和。一开始最多是\(nlogn\)的。
对于第三种情况,意味着当前结点的最大值和次大值都将消失不见,被\(a\)取而代之。也就是说,你递归了多少个结点,\(fake\)的权值就至少会降低多少。\(fake\)最少可以降到\(2n\)(每个结点的权值都只有一种),所以总复杂度是\(O(nlogn)\)的。
于是乎,吉如一线段树处理这种问题的复杂度就是\(O(nlogn)\)的。
那么假如加上加标记呢?我们考虑一下,每次区间加最后终止的结点一共会有\(logn\)个,所以这会使\(fake\)的权值增加\(mlogn\),最终复杂度还是可以接受的。
时间复杂度:\(O((n+m)logn)\)
空间复杂度:\(O(n)\)
代码如下:
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e6+5;
int n,m;
int a[maxn];
int read() {
int x=0,f=1;char ch=getchar();
for(;ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
for(;ch>='0'&&ch<='9';ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct segment_tree {
ll sum[maxn<<2];
int mx[maxn<<2],se[maxn<<2];
int tag[maxn<<2],cnt[maxn<<2];
void update(int p) {
sum[p]=sum[p<<1]+sum[p<<1|1];
mx[p]=max(mx[p<<1],mx[p<<1|1]);
cnt[p]=(mx[p]==mx[p<<1])*cnt[p<<1];
cnt[p]+=(mx[p]==mx[p<<1|1])*cnt[p<<1|1];
if(mx[p]==mx[p<<1])se[p]=se[p<<1];
else se[p]=mx[p<<1];
if(mx[p]==mx[p<<1|1])se[p]=max(se[p],se[p<<1|1]);
else se[p]=max(se[p],mx[p<<1|1]);
}
void build(int p,int l,int r) {
tag[p]=-1;
if(l==r) {
sum[p]=mx[p]=a[l];
se[p]=-1,cnt[p]=1;
return;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
update(p);
}
void solve(int p,int limit) {
if(mx[p]<=limit)return;
if(se[p]<limit&&limit<mx[p]) {
sum[p]-=1ll*(mx[p]-limit)*cnt[p];
mx[p]=limit,tag[p]=limit;return;
}
solve(p<<1,limit),solve(p<<1|1,limit);
update(p);
}
void push_down(int p) {
if(~tag[p]) {
solve(p<<1,tag[p]);
solve(p<<1|1,tag[p]);
tag[p]=-1;
}
}
void Min(int p,int l,int r,int L,int R,int v) {
if(L<=l&&r<=R) {solve(p,v);return;}
int mid=(l+r)>>1;push_down(p);
if(L<=mid)Min(p<<1,l,mid,L,R,v);
if(R>mid)Min(p<<1|1,mid+1,r,L,R,v);
update(p);
}
int queryMx(int p,int l,int r,int L,int R) {
if(L<=l&&r<=R)return mx[p];
int mid=(l+r)>>1,res=0;push_down(p);
if(L<=mid)res=max(res,queryMx(p<<1,l,mid,L,R));
if(R>mid)res=max(res,queryMx(p<<1|1,mid+1,r,L,R));
return res;
}
ll querySum(int p,int l,int r,int L,int R) {
if(L<=l&&r<=R)return sum[p];
int mid=(l+r)>>1;ll res=0;
push_down(p);
if(L<=mid)res+=querySum(p<<1,l,mid,L,R);
if(R>mid)res+=querySum(p<<1|1,mid+1,r,L,R);
return res;
}
}T;
int main() {
int Test=read();
while(Test--) {
n=read(),m=read();
for(int i=1;i<=n;i++)
a[i]=read();
T.build(1,1,n);
for(int i=1;i<=m;i++) {
int opt=read(),l=read(),r=read();
if(!opt) {
int v=read();
T.Min(1,1,n,l,r,v);
}
if(opt==1)printf("%d\n",T.queryMx(1,1,n,l,r));
if(opt==2)printf("%lld\n",T.querySum(1,1,n,l,r));
}
}
return 0;
}
历史最大值
这个东西不好讲。。。
一般都是一个数组\(a\)一个数组\(b\),\(b_i\)记录\(a_i\)曾经到达过的最大/最小值或者是每次操作完之后\(a_i\)的和。然后在\(a\)上操作来操作去,冷不丁的问你\(b\)数组的区间最大/最小值或者是区间和。
这种问题一般分为四类(第二类和第四类如果我碰见了果断暴力分走起)
第一类:可以用延迟标记处理的问题
这类问题一般用延迟标记可以解决,不过要记录标记从上一次下传标记到现在这一次的最值。维护的东西比较多,但是并不会涉及什么骚操作,对合并标记功法要求很高。
第二类:无法用延迟标记处理的问题
都说了不能用延迟标记处理了就弃了吧~根本不会
第三类:无区间最值操作的区间问题
一般都要用到辅助数组,转化成传统的问题。
第四类:有区间最值操作的区间问题
无区间最值操作都不会了,有区间最值还玩屁~根本不会
参考资料:国家集训队2016论文集——吉如一《区间最值操作与历史最值问题》
竞赛是灵活的,所以我觉得吉司机线段树要考也不会变到哪去,只要掌握这种以全局来分析问题复杂度的思想,就没问题了。这就是我不学历史最值问题的理由