P2824 【[HEOI2016/TJOI2016]排序】【线段树分裂】+【珂朵莉树】详解

36 篇文章 0 订阅
20 篇文章 0 订阅

戳这里,离线做法
给定一个长度为 N N N的数组,对 M M M个区间 l ∼ r l \sim r lr 进行排序
求最后 p p p 位置的值
排序分(正序和倒序两种)

分析

上面给的链接有离线的做法,算是一个比较套路的做法,用线段树维护 01 01 01序列
可以在 l o g n logn logn 的时间内实现排序,所以通过二分最终答案可以得到值

但是这题强制要求在线呢?
这样的话,我们需要使用数据结构维护这些数的状态

如何维护信息

我们思考这样一个过程:
初始我们有 N N N个区间,都是有序的(每个数自成一个区间)
一旦将区间 l ∼ r l \sim r lr正序排序
那么从 l l l, l + 1 , . . . , r − 1 , r l+1,...,r-1,r l+1,...,r1,r每个区间都可能不是之前的数了
既然我们看这些长度为 1 1 1的区间暂时得不到结果,我们就将这些区间合并起来看,假如我们能够在很快的时间内将这些数据合并起来,我们知道了区间长度 r − l + 1 r-l+1 rl+1,又知道了这些数据合并后的信息,我们岂不是就知道了任意位置 x x x 的值,比如 l l l 位置就是合并后的最小值 r r r 位置就是合并后的最大值,中间的某个位置就是区间的第 K K K

所以我们要维护的信息就是一颗权值线段树
由于线段树能够合并!我们的信息也满足短时间( l o g n logn logn)处理完成了


维护的过程

我们继续看,举个确切的例子
给了个数组(这里默认顺序从小到大排序)

5 3 8 2 7 6 1 4
初始区间情况为,每个数自成一个集合

[5] [3] [8] [2] [7] [6] [1] [4]

对区间 [ 2 , 6 ] [2, 6] [2,6] 排序,权值线段树合并后为(如果不会合并的可以看link

[5] [2, 3, 6, 7, 8] [1] [4]

如果再对区间 [ 7 , 8 ] [7, 8] [7,8] 排序,合并后为

[5] [2, 3, 6, 7, 8] [1, 4]

到这一步仍然没啥问题,很好的解决了排序的问题
细心的你一定发现了,我们上面排序的区间都没有交集,所以很自然的合并了
但是如果我们要对区间 [ 2 , 4 ] [2, 4] [2,4] 进行排序呢?
此时发现,区间 [ 2 , 4 ] [2, 4] [2,4]已经不是由单个小区间组成的了,我们不能直接像上面一样合并区间内的所有小区间
但是我们的思路就是合并啊,这样才能快速得到某个位置的值,我们怎么创造条件让它合并呢?
既然我们可以合并,那么我们是不是也能分裂?
现在考虑分裂!将我们想要的区间从原区间剥离!
我们想要 [ 2 , 4 ] [2, 4] [2,4] 区间,该区间在现在的区间 [ 2 , 6 ] [2, 6] [2,6]中,我们要从 [ 2 , 6 ] [2,6] [2,6]区间中中拿出前三个数(区间 [ 2 , 4 ] [2,4] [2,4]长度为 3 3 3
在权值线段树中,前三个数就是左子树中的三个数
所以我们构建一颗新的树,来存剥离的信息,假设存在了新的树 p p p上,原树存在了下标为 5 5 5 的根所在的树上( [ 2 , 6 ] [2,6] [2,6]分裂后区间为 [ 2 , 4 ] 和 [ 5 , 6 ] [2,4]和[5,6] [2,4][5,6]),这里可以容易观察到,我们的信息可以存在区间左端点为下标的根所在树上,所以 [ 2 , 4 ] [2,4] [2,4]信息存在下标为 2 2 2的位置,这里只是细节,我们继续讨论过程

分裂后,我们得到了两个小区间,我们想要的区间也是由若干个一整个的区间构成,此时就能执行合并了(这里想要的区间是 [ 2 , 4 ] [2,4] [2,4],分裂后想要的区间刚好由一个 [ 2 , 4 ] [2,4] [2,4]一整个区间组成)
最后合并的结果为

[5] [2, 3, 6] [7, 8] [1, 4]

假如我们要对区间 [ 3 , 7 ] [3, 7] [3,7] 排序呢
我们按照上面的思路,发现想要的区间 [ 3 , 7 ] [3,7] [3,7]

  • 区间 [ 2 , 4 ] [2, 4] [2,4]的一部分
  • 区间 [ 5 , 6 ] [5,6] [5,6]的一整个
  • 区间 [ 7 , 8 ] [7,8] [7,8]的一部分组成

我们目的是要让排序区间,由若干个一整个的区间构成
对于区间 [ 2 , 4 ] [2,4] [2,4]的一部分,我们根据上面的操作,分裂成 [ 2 , 2 ] [2,2] [2,2] [ 3 , 4 ] [3,4] [3,4]
对于一整个区间 [ 5 , 6 ] [5,6] [5,6]不分裂
对于区间 [ 7 , 8 ] [7,8] [7,8]的那一部分,同样分裂成 [ 7 , 7 ] [7,7] [7,7] [ 8 , 8 ] [8,8] [8,8]
最终我们得到的排序区间 [ 3 , 7 ] [3,7] [3,7]
[ 3 , 4 ] [3,4] [3,4], [ 5 , 6 ] [5,6] [5,6], [ 7 , 7 ] [7,7] [7,7]组成,都是一整个的
所以我们执行合并,完成了排序操作,结果为

[5] [2] [1, 3, 6, 7, 8] [4]

对于倒序排序,影响的只会在分裂的时候,取前 K K K个还是取后 K K K个的抉择


区间是怎么组成的?

届时排序问题已经解决完了,还剩下如何判断我想要的区间是否要分裂?
我们将某个区间合并后,这些原来的某些点的信息都集合在了某棵树上,这棵树上面说了,存在新区间(排序区间)的左端点为根的树上
所以我们要维护,这些点的信息在哪棵树上,方便判断是否需要分裂

这里介绍一个小清新数据结构,珂朵莉树
在数据随机的情况下,支持区间 s e t set set的操作可以用珂朵莉树在极小常数下完成
这里贴一个珂朵莉树的介绍 这里是链接,现在没有将来可能会有

因为珂朵莉树取一段区间的时候,也需要分裂,所以刚好在珂朵莉树分裂的时候
将维护信息的权值线段树分裂完,合并珂朵莉树的时候,合并权值线段树
珂朵莉树额外记录一个信息,表示正序还是倒序,也就是用于权值线段树分裂的时候取前 K K K个还是后 K K K个,此时问题解决完毕

我们要找到第 p o s pos pos个位置是什么树,参考上面的分裂操作
我们将区间按 [ 1 , p o s − 1 ] [ p o s , N ] [1,pos-1][pos,N] [1,pos1][pos,N]先分裂,之后将区间 [ p o s , N ] [pos,N] [pos,N]分裂为 [ p o s , p o s ] [pos,pos] [pos,pos] [ p o s + 1 , N ] [pos+1,N] [pos+1,N]
这样我们直接查管理存储 p o s pos pos位置信息的权值线段树就能得到这个答案了

代码

//P2824 
/*
  @Author: YooQ
*/
#include <bits/stdc++.h>
using namespace std;
#define sc scanf
#define pr printf
#define ll long long
#define FILE_OUT freopen("out", "w", stdout);
#define FILE_IN freopen("in", "r", stdin);
#define debug(x) cout << #x << ": " << x << "\n";
#define AC 0
#define WA 1
#define INF 0x3f3f3f3f
const ll MAX_N = 5e6+5;
const ll MOD = 1e9+7;
int N, M, K;

struct Seg {
	struct Tr {
		int k, l, r;
	}tr[MAX_N];
	int root[MAX_N];
	int rcnt = 0;
	int indx = 0;
	int rabi[MAX_N+10];
	int tt = 0;
	
	int mk() {
		if (tt) {
			return rabi[tt--];
		}
		return ++indx;
	}
	
	void del(int rt) {
		if (tt >= MAX_N) return;
		tr[rt].k = tr[rt].l = tr[rt].r = 0;
		rabi[++tt] = rt;
	}
	
	void push_up(int rt) {
		tr[rt].k = tr[tr[rt].l].k + tr[tr[rt].r].k;
	}
	
	void update(int& rt, int l, int r, int x, int k) {
		if (!rt) rt = mk();
		if (l == r) {
			tr[rt].k += k;
			return;
		}
		int mid = l + ((r-l)>>1);
		if (x <= mid) update(tr[rt].l, l, mid, x, k);
		if (x  > mid) update(tr[rt].r, mid+1, r, x, k);
		push_up(rt);
	}
	
	int merge(int x, int y, int l, int r) {
		if (!x || !y) return x | y;
		if (l == r) {
			tr[x].k += tr[y].k;
			return x;
		}
		int mid = l + ((r-l)>>1);
		tr[x].l = merge(tr[x].l, tr[y].l, l, mid);
		tr[x].r = merge(tr[x].r, tr[y].r, mid+1, r);
		del(y);
		push_up(x);
		return x;
	}
	
	void splitPre(int x, int& y, int l, int r, int k) {
		if (!x || k == tr[x].k) return;
		if (!y) y = mk();
		if (l == r) {
			tr[y].k = tr[x].k - k;
			tr[x].k = k;
			return;
		}
		int mid = l + ((r-l)>>1);
		int lsum = tr[tr[x].l].k;
		if (lsum >= k) swap(tr[x].r, tr[y].r), splitPre(tr[x].l, tr[y].l, l, mid, k);
		else splitPre(tr[x].r, tr[y].r, mid+1, r, k - lsum);
		push_up(x);
		push_up(y);
	}
	
	void splitBack(int x, int& y, int l, int r, int k) {
		if (!x || k == tr[x].k) return;
		if (!y) y = mk();
		if (l == r) {
			tr[y].k = tr[x].k - k;
			tr[x].k = k;
			return;
		}
		int mid = l + ((r-l)>>1);
		int rsum = tr[tr[x].r].k;
		if (rsum >= k) swap(tr[x].l, tr[y].l), splitBack(tr[x].r, tr[y].r, mid+1, r, k);
		else splitBack(tr[x].l, tr[y].l, l, mid, k - rsum);
		push_up(x);
		push_up(y);
	}
	
	int query(int rt, int l, int r) {
		if (l == r) return l;
		int mid = l + ((r-l)>>1);
		if (tr[tr[rt].l].k) return query(tr[rt].l, l, mid);
		else return query(tr[rt].r, mid+1, r);
	}
}seg;

struct Node{
	int l, r;
	mutable int k;
	
	Node(int a = -1, int b = -1, int c = -1) {
		l = a, r = b, k = c;
	}
	
	bool operator < (const Node& B) const {
		return l < B.l;
	}
};


typedef set<Node>::iterator sit;

struct ODT {
	
	set<Node> st;
	
	sit split(int pos) {
		sit it = st.lower_bound(Node(pos));
		if (it != st.end() && it->l == pos) return it;
		--it;
		Node tmp = *it;
		if (tmp.k==0) {
			seg.splitPre(seg.root[tmp.l], seg.root[pos], 1, N, pos-tmp.l);
		} else {
			seg.splitBack(seg.root[tmp.l], seg.root[pos], 1, N, pos-tmp.l);
		}
		st.erase(it);
		st.insert(Node(tmp.l, pos-1, tmp.k));
		return st.insert(Node(pos, tmp.r, tmp.k)).first;
	}
	
	void insert(Node t) {
		st.insert(t);
	}
	
}odt;

int arr[MAX_N];

void solve(){
	sc("%d%d", &N, &M);
	odt.insert({N+1, N+1, 0});
	
	for (int i = 1; i <= N; ++i) {
		sc("%d", &arr[i]);
		seg.update(seg.root[i], 1, N, arr[i], 1);
		odt.insert({i, i, 0});
	}
	int opt, l, r;
	for (int i = 1; i <= M; ++i) {
		sc("%d%d%d", &opt, &l, &r);
		sit rx = odt.split(r+1);
		sit lx = odt.split(l);
		lx->k = opt;
		for (sit i = ++lx;i!=rx;++i) {
			seg.root[l] = seg.merge(seg.root[l], seg.root[i->l], 1, N);
			seg.root[i->l] = 0;
		}
		odt.st.erase(lx, rx);
	}
	int x;
	sc("%d", &x);
	odt.split(x+1);
	odt.split(x);
	pr("%d\n", seg.query(seg.root[x], 1, N));
}


signed main()
{
	#ifndef ONLINE_JUDGE
	//FILE_IN
	FILE_OUT
	#endif
	int T = 1;//cin >> T;
	while (T--) solve();

	return AC;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hexrt

客官,请不要给我小费!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值