P2824 [HEOI2016/TJOI2016] 排序 题解

题目传送门

前置知识

  1. 线段树
  2. 二分答案

题意

给出一个 \(1\)\(n\) 的排列,现在对这个排列序列进行 \(m\) 次局部排序,排序分为两种:

  • 0 l r 表示将区间 \([l,r]\) 的数字升序排序
  • 1 l r 表示将区间 \([l,r]\) 的数字降序排序

最后询问第 \(q\) 位置上的数字。

解法

先来看一道类似的题目([AGC006D] Median Pyramid Hard),居然是一道3000 的AT题

可以发现,最终答案一定是一个 $ 1 $ 到 $ 2\times n-1$ 之间的数,然而我们并不知道这个数是什么,需要进行枚举。但是发现这个答案具有单调性,所以这里考虑二分答案。

二分最终的答案,对于原序列我们可以把大于等于mid的值在新数组里赋值为 $ 1 $ ,否则为 $ 0 $ 。如果最终最上层的值为 $ 1 $ 说明是合法的,因为它包括了最终解。

那我们到底是二分最大值还是最小值呢?

其实当 $ mid $ 越大时,新数组里 $ 1 $ 的个数就会更少,也就更难满足要求,所以我们要去搜当刚好满足要求的 $ mid $ 且 $ mid+1 $ 不能满足要求时 $ mid $ 的值。

(以下为AT_agc006d单独的解法,与本题无关,如果仅想看本题,请移步至后方正文处,本题仅会用到上面的相关思路)

那怎么判断新数组最终的结果是否为1呢?

举个例子:

当这一层是 $ 001 $ 时,上一层会是 $ 0 $ 。

当这一层是 $ 011 $ 时,上一层是 $ 1 $ 。

当这一层为 $ 1001011 $ 时,倒数第二层为 $ 00011 $ ,倒数第三层为 $ 001 $ ,第一层为 $ 0 $ 。

可以发现,当出现连续的两个相同的数时,这两个数往上走时一定能保存原值。

例如 $ b_i $ 为 $ 0 $ 且 $ b_i+1 $ 为 $ 0 $ ( $ b $ 为新数组),所以在 $ i-1 $ , $ i $ , $ i+1 $ 这个区间中至少存在两个 $ 0 $ ,因此这一个区间一定为 $ 0 $ ,而在 $ i $ , $ i+1 $ , $ i+2 $ 这个区间中也至少存在两个0,因此这个区间的值也为 $ 0 $ ,然后发现上一层同样出现了两个相邻的 $ 0 $ 。

那如果出现多个相邻且相同的值呢?

上面的第三个例子,同时存在 $ 00 $ 和 $ 11 $ ,但最终结果为 $ 0 $ 。

再举个例子:

当这一层为 $ 101001011 $ 时,倒数第二层为 $ 1000011 $ ,倒数第三层为 $ 00001 $ ,倒数第四层为 $ 000 $ ,第一层为 $ 0 $ 。

同样是存在 $ 11 $ 和 $ 00 $ ,为何最终答案却是 $ 0 $ 呢?

在同上面的话,如果这两个数同时为 $ 0 $ ,则涵盖这两个数的两个区间结果都是 $ 0 $ ,但当 $ i+1 $ 为边界或 $ i $ 为边界时,上一层只会出现一个 $ 0 $ ,因为只存在一个区间。

所以是看谁先靠近中间,谁就最后被挤掉,就去找最中间的那个相邻且相同的位置,返回它的值就行了。

但有个例外,如果这个数组没有两个相邻且相等的位置。

那就有两种情况了:

  1. 序列为 $ 0101010 $ ,那么无论这个序列长度为多少,结果必定为 $ 0 $ 。

  2. 序列为 $ 1010101 $ ,那么无论这个序列长度为多少,结果必定为 $ 1 $ 。

所以就判断一下第一个位置的值是多少就行了。

刚才说到了被挤掉,那为啥最接近中点的那个区间不会被挤掉呢?

随便再举个例子,序列 $ 00101 $ ,此时 $ 00 $ 已经在最边上了,然后我们会发现,这个序列不会存在其它的相同且相邻的序列,所以序列剩下的部分一定是 $ 1010101 $ ...

可以发现 $ 00101 $ ,区间 $ 001 $ 结果为 $ 0 $ ,而往下移一位,区间 $ 010 $ 结果为 $ 0 $ ,两个刚好又组成了一个新的 $ 00 $ 。所以最后一个区间不会被挤掉。

有没有可能出现两个相邻且相同的位置离中间距离相等且值不同的情况呢?

这种情况是不存在的,因为当它们距离相等时,它们之间一定只有奇数个空位,因此它们往中间走一格一定会有一个区间走到的格子与原来的位置上的值相同,从而构成另一个区间。

代码

#include<bits/stdc++.h>
#include<cstring>
using namespace std;
const int N=200005;
int n,a[N],b[N];
int read()//快读 
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
bool check(int w)
{
	int pos=0;
	for(int i=1;i<=2*n-1;i++)
	{
		if(a[i]>=w) b[i]=1;
		else b[i]=0;
	}//赋值新数组 
	int l=n-1,r=n+1;//从中间开始找相邻且相等的位置 
	while(l>=1&&r<=2*n-1)
	{
		if(b[r]==b[r-1]) return b[r];//找到了就返回 
		if(b[l]==b[l+1]) return b[l];
		l--,r++;//继续往下走 
	}
	if(b[1]==0)return false;//判断特殊情况 
	return true;
}
int main()
{
	n=read();
	for(int i=1;i<=2*n-1;i++) a[i]=read();
	int l=1,r=2*n-1,ans=n;//划定二分答案范围 
	while(l<=r)//二分答案 
	{
		int mid=(l+r)>>1;
		if(check(mid)) l=mid+1,ans=mid;
		else r=mid-1;
	}
	cout<<ans;
	return 0;
}

正文

回到本题,本题需要求出换位后某一位置上的值,如果这个序列只存在 $ 0 $ 和 $ 1 $ ,那么排序也就变得简单了,直接进行区间赋值。

具体来说就是已知 $ l $ 到 $ r $ 中有 $ cnt $ 个 $ 1 $ ,如果是升序排列,就将 $ l $ 到 $ r-cnt $ 赋值为 $ 0 $ , $ r-cnt+1 $ 到 $ r $ 赋值为 $ 1 $ 。反之,如果是降序排列,就将 $ l $ 到 $ l+cnt-1 $ 赋值为 $ 1 $ , $ l+cnt $ 到 $ r $ 赋值为 $ 0 $ 就行了。

所以线段树里维护三个东西,这个区间有多少个 $ 1 $ ,这个区间是否被全部赋值为 $ 0 $ 以及这个区间是否被全部赋值为 $ 1 $ (也就是 $ lazy $ 标记)。

最后每一次二分答案都需要重新建一次树,把树上所有的标记全部清除。

这里还需要注意一下,当这个区间全是 $ 0 $ 时, $ r-cnt+1 $ 会大于 $ r $ , $ l+cnt-1 $ 会小于 $ l $ ,有可能会造成线段树中 $ l $ 大于 $ r $ 的情况。所以这里需要特判一下,如果全是 $ 0 $ ,忽略这个操作,或者是在线段树上加一个越界的判断,否则就会喜提RE $ 4 $ 个点。

代码

#include<bits/stdc++.h>
using namespace std;
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
const int N=100005;
int a[N],n,m,pos;
struct segment_tree//线段树 
{
	int sum;
	bool c0,c1;
}s[N<<2];
struct quest//把所有操作记下来 
{
	bool op;
	int l,r;
}q[N];
int read()//快读 
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
void pushup(int rt)//上传 
{
	s[rt].sum=s[rt<<1].sum+s[rt<<1|1].sum;
}
void build(int l,int r,int rt,int w)//建树 
{
	s[rt].c0=s[rt].c1=s[rt].sum=0;//记得清空所有标记 
	if(l==r)
	{
		if(a[l]>=w) s[rt].sum=1;//赋值 
		return;
	}
	int mid=(l+r)>>1;
	build(lson,w);
	build(rson,w);
	pushup(rt);
}
void pushdown(int l,int r,int rt)//下传 
{
	if(s[rt].c0)//如果全部赋值为0 
	{
		s[rt<<1].c0=s[rt<<1|1].c0=1;
		s[rt<<1].c1=s[rt<<1|1].c1=0;
		s[rt<<1].sum=s[rt<<1|1].sum=0;
		s[rt].c0=0;
	}
	if(s[rt].c1)//如果全部赋值为1
	{
		int mid=(l+r)>>1;
		s[rt<<1].c0=s[rt<<1|1].c0=0;
		s[rt<<1].c1=s[rt<<1|1].c1=1;
		s[rt<<1].sum=mid-l+1;
		s[rt<<1|1].sum=r-mid;
		s[rt].c1=0;
	}
}
void update0(int l,int r,int rt,int L,int R)//区间赋值为0 
{
	if(L<=l&&R>=r)
	{
		s[rt].c0=1;
		s[rt].c1=0;
		s[rt].sum=0;
		return;
	}
	pushdown(l,r,rt);
	int mid=(l+r)>>1;
	if(L<=mid) update0(lson,L,R);
	if(R>mid) update0(rson,L,R);
	pushup(rt);
}
void update1(int l,int r,int rt,int L,int R)//区间赋值为1 
{
	if(L<=l&&R>=r)
	{
		s[rt].c0=0;
		s[rt].c1=1;
		s[rt].sum=r-l+1;
		return;
	}
	pushdown(l,r,rt);
	int mid=(l+r)>>1;
	if(L<=mid) update1(lson,L,R);
	if(R>mid) update1(rson,L,R);
	pushup(rt);
}
int query(int l,int r,int rt,int L,int R)//查询 
{
	if(L<=l&&R>=r) return s[rt].sum;
	pushdown(l,r,rt);
	int mid=(l+r)>>1,res=0;
	if(L<=mid) res+=query(lson,L,R);
	if(R>mid) res+=query(rson,L,R);
	return res; 
}
bool check(int w)//二分答案 
{
	build(1,n,1,w);//建树 
	for(int i=1;i<=m;i++)
	{
		int op=q[i].op,l=q[i].l,r=q[i].r;
		if(op==0)//如果是升序 
		{
			int k=query(1,n,1,l,r);
			if(k==0) continue;//越界特判 
			int x1=r-k+1;
			update0(1,n,1,l,x1-1);
			update1(1,n,1,x1,r);
		}
		else//如果是降序 
		{
			int k=query(1,n,1,l,r);
			if(k==0) continue;
			int x1=k+l-1;
			update1(1,n,1,l,x1);
			update0(1,n,1,x1+1,r);
		}
	}
	return query(1,n,1,pos,pos)==1;//判断pos的值是否为1 
}
int main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=m;i++) q[i].op=read(),q[i].l=read(),q[i].r=read();
	pos=read();
	int l=1,r=n,ans=0;//划定二分范围 
	while(l<=r)//二分答案 
	{
		int mid=(l+r)>>1;
		if(check(mid)) l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d",ans);
	return 0;
}
posted @ 2024-01-10 21:33  Twilight_star  阅读(41)  评论(0)    收藏  举报