[洛谷P2824]排序

题目

传送门 to luogu

题目描述
2016 2016 2016 年,佳媛姐姐喜欢上了数字序列。因而 T a \tt{Ta} Ta 经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题,需要你来帮助他。

这个难题是这样子的:给出一个 1 1 1 n n n 的全排列,现在对这个全排列序列进行 m m m 次局部排序,排序分为两种:

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

排序后询问第 q q q 位置上的数字。

数据范围与约定
对于全部的数据, 1 ≤ q , l , r ≤ n , m ≤ 1 0 5 1\le q,l,r\le n,m\le 10^5 1q,l,rn,m105 j ∈ { 0 , 1 } j\in\{0,1\} j{0,1}

思路壹

考虑二分答案,只检查答案是否为一个不小于 x x x 的数。

那么我们很容易发现,对于两个数字 i , j    ( i , j ⩾ x ) i,j\;(i,j\geqslant x) i,j(i,jx),二者的效果是相同的,可以统一用 1 1 1 表示。类似的,比 x x x 小的数字,可以统一用 0 0 0 表示,问题转化为结果的第 q q q 位是否为 1 1 1,那么两个数字 1 1 1 是否需要交换,就无足轻重了。

0 / 1 0/1 0/1 序列排序,可以用线段树统计 1 1 1 的数量,然后进行区间赋值。时间复杂度 O ( n log ⁡ 2 n ) \mathcal O(n\log^2 n) O(nlog2n)

代码壹

#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
inline int readint(){
	int a = 0, f = 1; char c = getchar();
	for(; c<'0' or c>'9'; c=getchar())
		if(c == '-') f = -1;
	for(; '0'<=c and c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

const int MaxN = 100000;
int n, m, __BOUND, a[MaxN], q;
class SegmentTree{
	struct Node{
		int l, r, cnt;
	}node[MaxN<<2];
	void modifyNode(int pos,int v){
		node[pos].cnt = (node[pos].r-node[pos].l+1)*v;
	}
	void pushUp(int pos){
		node[pos].cnt = node[pos<<1].cnt+node[pos<<1|1].cnt;
	}
	void pushDown(int pos){
		if(node[pos].cnt != node[pos].r-node[pos].l+1 and node[pos].cnt)
			return ;
		int v = node[pos].cnt/(node[pos].r-node[pos].l+1);
		modifyNode(pos<<1,v), modifyNode(pos<<1|1,v);
	}
public:
	void buildTree(int l=1,int r=n,int pos=1){
		node[pos].l = l, node[pos].r = r;
		if(l != r){
			buildTree(l,(l+r)>>1,pos<<1);
			buildTree((l+r)/2+1,r,pos<<1|1);
			pushUp(pos);
		}else
			node[pos].cnt = (a[l] >= __BOUND);
	}
	int count(int l,int r,int pos=1){
		if(l <= node[pos].l and node[pos].r <= r)
			return node[pos].cnt;
		pushDown(pos);
		int mid = (node[pos].l+node[pos].r)>>1;
		if(r <= mid) return count(l,r,pos<<1);
		if(l > mid) return count(l,r,pos<<1|1);
		return count(l,r,pos<<1)+count(l,r,pos<<1|1);
	}
	void modifyRange(int l,int r,int v,int pos=1){
		if(l <= node[pos].l and node[pos].r <= r)
			return modifyNode(pos,v);
		pushDown(pos);
		int mid = (node[pos].l+node[pos].r)>>1;
		if(l <= mid) modifyRange(l,r,v,pos<<1);
		if(r > mid) modifyRange(l,r,v,pos<<1|1);
		pushUp(pos);
	}
	void SORT(int l,int r,int d){
		int ppl = count(l,r);
		if(not ppl or ppl == r-l+1) return ;
		if(d == 1){ // dropping
			modifyRange(l,l+ppl-1,1);
			modifyRange(l+ppl,r,0);
		}else{ // rising
			modifyRange(r-ppl+1,r,1);
			modifyRange(l,r-ppl,0);
		}
	}
}ppl;

struct Command{
	int TYPE, L, R;
	void input(){
		TYPE = readint();
		L = readint(), R = readint();
	}
}zxy[MaxN];
void init(){
	n = readint(), m = readint();
	for(int i=1; i<=n; ++i)
		a[i] = readint();
	for(int i=0; i<m; ++i)
		zxy[i].input();
	q = readint();
}

bool check(int mid){
	__BOUND = mid; ppl.buildTree();
	for(int i=0; i<m; ++i)
		ppl.SORT(zxy[i].L,zxy[i].R,zxy[i].TYPE);
	return ppl.count(q,q) == 1;
}
void solve(){
	int L = 1, R = n, mid;
	while(L != R){
		mid = (L+R+1)>>1;
		if(check(mid)) L = mid;
		else R = mid-1;
	}
	printf("%d\n",L);
}

int main(){
	init();
	solve();
	return 0;
}

思路贰

不断对序列的一部分进行排序,无论如何,你总觉得它是 努力朝着有序的方向进行

更具体地说,序列中肯定会有很多个子区间,是已经有序的——我们的操作就是在创造这种子区间啊。每次操作,最多会让边缘的两个子区间被拦腰截断,中间的子区间却全部合并为一。所以子区间的数量变化是 O ( n + m ) \mathcal O(n+m) O(n+m) 的!

我们接下来只需要实现:有序数列合并(或者说是集合的合并)与分裂(将前 k k k 小提取出来)。

一个最 n a i v e \rm naive naive 的想法是,用平衡树维护。然而它的合并似乎与 s i z e size size 有关?复杂度会不会假啊?

请出我们的主角,线段树合并。显然权值线段树可以维护集合,也可以提取前 k k k 小。更重要的是,它的时间复杂度很容易证明!

经典的势函数——节点数量。合并时 O ( 1 ) \mathcal O(1) O(1) 的代价降低 1 1 1 的势能;一次分裂是 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 复杂度与 log ⁡ n \log n logn 的势能增加,总复杂度 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn)

更恐怖的是,它甚至支持在线多次查询……听说外层套上平衡树,还可以支持查询区间信息……

代码贰

#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(int x){
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}
inline int ABS(const int &x){
	return x < 0 ? -x : x;
}

const int MaxN = 200005;
int n;
namespace SgTree{
	const int MaxM = MaxN*40;
	int ch[MaxM][2], v[MaxM], cntNode;
	void insert(int qid,int &o,int l=1,int r=n){
		if(!o) o = ++ cntNode;
		++ v[o]; if(l == r) return ;
		if(qid <= ((l+r)>>1))
			insert(qid,ch[o][0],l,(l+r)>>1);
		else insert(qid,ch[o][1],(l+r)/2+1,r);
	}
	void merge(int &o,const int &fr,int l=1,int r=n){
		if(!o || !fr) return void(o ^= fr);
		v[o] += v[fr]; if(l == r) return ;
		merge(ch[o][0],ch[fr][0],l,(l+r)>>1);
		merge(ch[o][1],ch[fr][1],(l+r)/2+1,r);
	}
	void split(int o,int &x,int &y,int k,int l=1,int r=n){
		if(!o) return void(x = y = 0);
		y = ++ cntNode, x = o;
		v[y] = v[o]-k; v[x] = k; // goal
		if(k > v[ch[o][0]]) k -= v[ch[o][0]],
			split(ch[o][1],ch[x][1],ch[y][1],k);
		else{
			swap(ch[x][1],ch[y][1]);
			if(k != v[ch[o][0]])
			split(ch[o][0],ch[x][0],ch[y][0],k);
		}
	}
	int kthElement(const int &o,int k,int l=1,int r=n){
		if(l == r) return l;
		if(k <= v[ch[o][0]])
			return kthElement(ch[o][0],k,l,(l+r)/2);
		else k -= v[ch[o][0]];
		return kthElement(ch[o][1],k,(l+r)/2+1,r);
	}
}
using namespace SgTree;

map<int,int> mp; // positive: increasing
map<int,int>::iterator posAt(int r){
	auto it = mp.lower_bound(r+1);
	int nxt = it->first; -- it;
	if(it->first == r) return it;
	int pre = it->first, &o = mp[pre];
	if(o > 0) split(o,o,mp[r],r-pre);
	else{
		split(-o,mp[r],o,nxt-r);
		o = -o, mp[r] = -mp[r];
	}
	return mp.find(r);
}

int main(){
	n = readint();
	int m = readint();
	for(int i=1; i<=n; ++i)
		insert(readint(),mp[i]);
	mp[n+1] = 0; // avoid end()
	for(int opt,l; m; --m){
		opt = readint();
		auto L = posAt(l = readint());
		auto R = posAt(readint()+1);
		int o = ABS(L->second);
		for(auto i=L; (++i)!=R; )
			merge(o,ABS(i->second));
		mp.erase(++L,R); // (L,R)
		mp[l] = (1-2*opt)*o;
	}
	int q = readint(); posAt(q+1);
	auto ans = posAt(q);
	int o = ans->second;
	writeint(kthElement(ABS(o),1));
	putchar('\n');
	return 0;
}

后记

最近一直在想,为啥线段树合并就可做呢?本质上线段树不也是平衡树的一种吗?

一种解释是,线段树通过非叶子节点,把树的形态固定了下来,于是就可以快速合并。非叶子节点撑起了线段树的结构,缺点就是舍弃了 任意有偏序关系的元素的在线插入 能力。当然它也失去了一些复杂的区间操作能力,比如翻转。

也就是说整数域是线段树恒优咯?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值