ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

「HEOI2016/TJOI2016」排序

2020-06-08 19:09:06  阅读:265  来源: 互联网

标签:rt return int 线段 son tag TJOI2016 HEOI2016 排序


「HEOI2016/TJOI2016」排序

题目大意

给定一个 \(1\) 到 \(n\) 的排列,每次可以对这个序列的一个区间进行升序/降序排序,求所有操作后第 \(q\) 个位置上的数字。

题解

大棒子,又学到了许多。

做法很多,这里大概讲一下主流的几种做法。

在线做法

  • 线段树合并&分裂

其实将一个区间升序或降序排序可以看作同一个操作——进行升序排序,打一个是否是升序排序的标记。

所以我们可以在每一个位置维护一棵权值线段树,当要将区间 \([l,r]\) 的数字排序时,取出这些位置所维护的权值线段树合并至某一特定位置即可。

但是我们不可能每次合并过后又暴力将其分裂回每个位置上,这样复杂度肯定是吃不消的。所以我们考虑每一次在上一次的基础上取出我们想要的区间进行合并。

我们假设合并时将线段树合并至区间的左端点。

我们可以维护一个 \(\texttt{set}\),代表现存的线段树左端点标号。

我们在 \(\texttt{set}\) 中去二分找到包含端点的那棵权值线段树,然后根据升序/降序的标记做一个分类讨论。

设这个端点为 \(k\)。

升序:将这棵权值线段树分裂成 \([l,k-1]\) 和 \([k,r]\) 两个部分,则这个端点所维护的线段树区间为 \([k,r]\)。

降序:将这棵权值线段树分裂成 \([l,r-k-1]\) 和 \([r-k,r]\) 两个部分,则这个端点所维护的线段树区间为 \([l,r-k-1]\)。这里一定要注意因为是降序所以处理有一定不同。

记得要分裂出来的两棵线段树标记须与原来的那棵线段树保持一致。

时间复杂度为 \(O((n+m)\log_2n)\)

代码真的很可读,就没啥注释了OWO。

/*---Author:HenryHuang---*/
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int son[maxn*30][2],rs[maxn*30],bac[maxn*30],sum[maxn*30],cnt,tot;
int rt[maxn],tag[maxn];
int newnode(){
	return cnt?bac[cnt--]:++tot;
}
void del(int u){
	bac[++cnt]=u,son[u][0]=son[u][1]=sum[u]=0;
}
void update(int &u,int l,int r,int pos,int val){
	if(!u) u=newnode();sum[u]+=val;
	if(l==r) return ;int mid=(l+r)>>1;
	if(pos<=mid) update(son[u][0],l,mid,pos,val);
	else update(son[u][1],mid+1,r,pos,val);
	return ;
}
void split(int x,int &y,int k){
	y=newnode();
	int v=sum[son[x][0]];
	if(k>v) split(son[x][1],son[y][1],k-v);
	else swap(son[x][1],son[y][1]);
	if(k<v) split(son[x][0],son[y][0],k);
	sum[y]=sum[x]-k,sum[x]=k;
	return ;
}
int merge(int x,int y){
	if(!x||!y) return x+y;
	sum[x]+=sum[y];
	son[x][0]=merge(son[x][0],son[y][0]);
	son[x][1]=merge(son[x][1],son[y][1]);
	del(y);
	return x;
}
set<int> s;
set<int> ::iterator id(int x){
	set<int> ::iterator it=s.lower_bound(x);
	if(*it==x) return it;
	int p=*it;//这就是r+1
	int l=*--it;//这是l
	if(!tag[l]) split(rt[l],rt[x],x-l),tag[x]=tag[l]=0;
	else{
		split(rt[l],rt[x],p-x),swap(rt[l],rt[x]),tag[l]=tag[x]=1;
	}
	return s.insert(x).first;
}
void solve(int l,int r){
	set<int> ::iterator L=id(l),R=id(r+1);
	for(set<int> ::iterator it=++L;it!=R;++it) rt[l]=merge(rt[l],rt[*it]);//合并
	s.erase(L,R);//全部删掉,因为都合并掉了
}
int query(int u,int l,int r){
	if(l==r) return l;
	int mid=(l+r)>>1;
	if(son[u][0]) return query(son[u][0],l,mid);
	else return query(son[u][1],mid+1,r);
}//最后查答案
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n,m;cin>>n>>m;
	s.insert(n+1);
	for(int i=1;i<=n;++i){
		int x;cin>>x;
		update(rt[i],1,n,x,1);
		s.insert(i);
	}
	for(int i=1;i<=m;++i){
		int opt,l,r;
		cin>>opt>>l>>r;
		solve(l,r);tag[l]=opt;
	}
	int q;cin>>q;
	id(q),id(q+1);
	cout<<query(rt[q],1,n)<<'\n';
	return 0;
}

  • 平衡树启发式合并&分裂

同理,只是将线段树换为 Splay/FHQ Treap。个人推荐FHQ Treap。(因为你不用额外徒增代码量,似乎复杂度也更对)

离线做法

  • 线段树+二分

如果对于一个排列,我们进行区间排序显然是不好维护的。

那么对于一个 \(\texttt{0/1}\) 序列呢?

我们只需要进行统计 \(0,1\) 的个数,然后进行区间赋值即可。

所以我们的问题就变成了区间赋值,区间求和

这个用一棵线段树可以直接维护。

所以我们考虑二分答案 \(x\),将 \(\ge x\) 的数设为 \(1\),其他数设为 \(0\)。

我们所需要的答案就是最后答案位置上的数字为 \(1\) 时的最大的 \(x\)。

最后的时间复杂度为 \(O(m\log_2^2n)\)。

标签:rt,return,int,线段,son,tag,TJOI2016,HEOI2016,排序
来源: https://www.cnblogs.com/HenryHuang-Never-Settle/p/solution-P2824.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有