2020.08.11日常总结——线段树和二分产生的强大合力

作为 OI 竞赛中最重要的几个算法之二,线段树和二分总可以完成一些让你无法想象的事情。今天要讲的例题就是一例……

luogu   P2824   [HEOI2016/TJOI2016]排序 \color{green}{\texttt{luogu P2824 [HEOI2016/TJOI2016]排序}} luogu P2824 [HEOI2016/TJOI2016]排序

[Problem] \color{blue}{\texttt{[Problem]}} [Problem]

  • 给你一个数 n n n 和一个 n n n 个数的序列。
  • m m m 个指令:把区间 [ l , r ] [l,r] [l,r] 从小到大或从大到小排序。
  • 最后查询一次,给定一个数 p p p,问当前位置 p p p 上的数是多少。
  • 1 ≤ p ≤ n ≤ 1 × 1 0 5 , 1 ≤ m ≤ 1 × 1 0 5 1 \leq p \leq n \leq 1 \times 10^5,1 \leq m \leq 1 \times 10^5 1pn1×105,1m1×105

[Solution] \color{blue}{\texttt{[Solution]}} [Solution]

咋一看,这道题跟二分和线段树是一点关系都没有对吧。但是,这庞大的数据规模让我们能想到的算法几乎都失效了(至少对于我是这样)。

一个很显然的事情是:这道题的答案是有单调性(可二分性)的。即如果最终的答案 ≥ a \geq a a,那么它一定是 ≥ a − 1 \geq a-1 a1 的(显然吧)。

既然只有一个查询,那我们在这里浪费多一点时间其实是没有什么问题的。

有因为这道题有可二分性(单调性),我们可以二分答案。

具体地,二分答案 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,2n}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() 函数就是快读函数。

你可能感兴趣的:(线段树,二分答案,思维题)