作为
OI
竞赛中最重要的几个算法之二,线段树和二分总可以完成一些让你无法想象的事情。今天要讲的例题就是一例……
[Problem] \color{blue}{\texttt{[Problem]}} [Problem]
[Solution] \color{blue}{\texttt{[Solution]}} [Solution]
咋一看,这道题跟二分和线段树是一点关系都没有对吧。但是,这庞大的数据规模让我们能想到的算法几乎都失效了(至少对于我是这样)。
一个很显然的事情是:这道题的答案是有单调性(可二分性)的。即如果最终的答案 ≥ a \geq a ≥a,那么它一定是 ≥ a − 1 \geq a-1 ≥a−1 的(显然吧)。
既然只有一个查询,那我们在这里浪费多一点时间其实是没有什么问题的。
有因为这道题有可二分性(单调性),我们可以二分答案。
具体地,二分答案 mid \texttt{mid} mid,检验最后位置 p p p 上的数(为了方便,我们把它记为 t t t)是否 ≥ mid \geq \texttt{mid} ≥mid。
如何检验呢?首先,因为我们只关心 t t t 是否 ≥ mid \geq \texttt{mid} ≥mid,所以我们可以在一开始就把所有 ≥ mid \geq \texttt{mid} ≥mid 的数记为 1 1 1,其它记为 0 0 0,排好序后检验 t t t 是否为 1 1 1 即可。
如何排序?肯定不能每次操作都用个 sort
,这个数据就是 O ( n ) O(n) O(n) 的桶排都会超时。怎么办办呢?
我们发现,因为我们已经把这个数组变成了一个 01 01 01 数组,所以我们的排序可以变得方便一些。
具体地,如果将区间 [ l , r ] [l,r] [l,r] 从小到大排序,那么我们就把所有的 0 0 0 放在区间的开头,所有的 1 1 1 放在区间的结尾即可(我们已经不需要管它原本是什么数了,反正排好序后, 0 0 0 肯定都在区间开头, 1 1 1 肯定都在区间结尾)。从大到小排序同理,不过 01 01 01 位置要反过来。
再进一步地,我们可以发现,因为区间内只有 0 0 0 和 1 1 1,所以排序操作可以转化为区间求和区间赋值操作(区间求和:求出区间内有多少个 0 0 0 和 1 1 1;区间赋值:就是把区间开头或结尾赋值为同一个数,相当于排序)。这个东西可以用线段树高效地完成。
举个例子,比如我们现在 mid = 7 \texttt{mid}=7 mid=7,原数组为 [ 3 , 6 , 10 , 7 , 5 , 9 , 0 ] [3,6,10,7,5,9,0] [3,6,10,7,5,9,0],于是我们把原数组改为 [ 0 , 0 , 1 , 1 , 0 , 1 , 0 ] [0,0,1,1,0,1,0] [0,0,1,1,0,1,0]。现在我们要将区间 [ 2 , 6 ] [2,6] [2,6] 从小到大排序(即区间 [ 0 , 1 , 1 , 0 , 1 ] [0,1,1,0,1] [0,1,1,0,1]),我们只需查询出区间的和 3 3 3(它代表区间一共有 3 3 3 个 1 1 1),然后把开头两位赋值为 0 0 0,结尾三位赋值为 1 1 1,即可完成排序。
时间复杂度: O ( m × log n × log max i ∈ { 1 , 2 ⋯ n } { a i } ) O\left (m \times \log n \times \log \max\limits_{i \in \left \{1,2 \cdots n \right \}} \left \{ a_i \right \} \right ) O(m×logn×logi∈{1,2⋯n}max{ai})。
[code] \color{blue}{\texttt{[code]}} [code]
const int N=1e5+100;
int a[N],sum[N<<2],add[N<<2];
inline void pushup(int o){
sum[o]=sum[o<<1]+sum[o<<1|1];
}
inline void pushdown(int o,int l,int r){
int tag=add[o];add[o]=-1;
add[o<<1]=add[o<<1|1]=tag;
register int mid=(l+r)>>1;
sum[o<<1]=tag*(mid-l+1);
sum[o<<1|1]=tag*(r-mid);
}
void build(int o,int l,int r,int x){
if (l==r){
sum[o]=a[l]>=x;
add[o]=-1;
return;
}
register int mid=(l+r)>>1;
build(o<<1,l,mid,x);
build(o<<1|1,mid+1,r,x);
pushup(o);add[o]=-1;return;
}//建立线段树
void change(int o,int l,int r,int p,int q,int x){
if (p<=l&&r<=q){
sum[o]=x*(r-l+1);
add[o]=x;return;
}
if (l>q||r<p) return;
register int mid=(l+r)>>1;
if (add[o]!=-1) pushdown(o,l,r);
change(o<<1,l,mid,p,q,x);
change(o<<1|1,mid+1,r,p,q,x);
pushup(o);return;
}//区间赋值
int query(int o,int l,int r,int p,int q){
if (l>q||r<p) return 0;
if (p<=l&&r<=q) return sum[o];
register int ans=0,mid=(l+r)>>1;
if (add[o]!=-1) pushdown(o,l,r);
ans+=query(o<<1,l,mid,p,q);
ans+=query(o<<1|1,mid+1,r,p,q);
return ans;
}//区间求和
int opt[N],L[N],R[N],n,m,q;
inline bool check(int mid){
build(1,1,n,mid);
for(int i=1;i<=m;i++){
int cnt=query(1,1,n,L[i],R[i]);
if (opt[i]==0){
change(1,1,n,R[i]-cnt+1,R[i],1);
change(1,1,n,L[i],R[i]-cnt,0);
}
else{
change(1,1,n,L[i],L[i]+cnt-1,1);
change(1,1,n,L[i]+cnt,R[i],0);
}//注意一下我们要操作的区间
}
return query(1,1,n,q,q);
}
int i,l,r,mid,ans;
int main(){
n=read();m=read();
for(i=1;i<=n;i++)
a[i]=read();
for(i=1;i<=m;i++){
opt[i]=read();
L[i]=read();
R[i]=read();
}
q=read();l=1;r=n;
while (l<=r){
mid=(l+r)>>1;
if (check(mid)){
ans=mid;//记录可行解
l=mid+1;
}
else r=mid-1;
}//二分答案
printf("%d",ans);
return 0;
}
read() 函数就是快读函数。