排序

这篇博客介绍了如何使用动态开点权值线段树解决序列排序和局部排列的问题。动态开点权值线段树在处理值域过大但元素数量有限的情况时,能有效节省空间。文章详细讲解了权值线段树的构建、查询、合并和分裂等操作,并通过实例展示了如何处理区间升序或降序排列的需求,以及查询特定位置的数值。此外,还探讨了如何利用这种数据结构处理区间交集问题,实现高效的操作。
摘要由CSDN通过智能技术生成

排序

[P2824 HEOI2016/TJOI2016]排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意

​ 给你一个长度为 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 e 9 1e9 1e9了,不离散化显然我们是开不了这么大的空间的,但是一般给你的数的个数是在 1 e 5 1e5 1e5左右的,而线段树是一个满二叉树,如果我们开 1 e 9 1e9 1e9这么大的线段树的话,最终肯定会有很多没有用到的点,所以启示我们假定开了个这么大的线段树,每次用到的时候再去给他分配一个结点,由于每次单点修改只会访问 l o g n logn logn个结点,因此只会开 m l o g n mlogn mlogn个结点,这样空间就可以保证了,这就是动态开点了

​ 这时线段树需要记录他左右儿子的位置了,我们并不需要在结构体里记录当前维护的哪个区间,只需要在函传参的时候维护即可

以查找某个区间的逆序对为例:

struct Node {
    int son[2]; // 左儿子的位置, 右儿子的位置
    int sum; // 当前区间有多少个数
}tr[N * 20];
#define ls(x) (tr[x].son[0])
#define rs(x) (tr[x].son[1])

对于建树,我们直接对值域插入结点即可,如果没出现过就给他开点

void insert(int u, int l, int r, int x) {
    if (l == r) {
        tr[u].sum ++;
        return ;
    }

    int mid = l + r >> 1;
    if (x <= mid) insert((ls(u) ? ls(u) : ls(u) = ++idx), l, mid, x);
    else insert((rs(u) ? rs(u) : rs(u) = ++idx), mid + 1, r, x);
    pushup(u);
}

对于查询某个区间有多少个数

int query(int u, int l, int r, int ll, int rr) {
    if (ll <= l && r <= rr) return tr[u].sum;    
    int mid = l + r >> 1;
    int res = 0;
    if (ll <= mid && tr[ls(u)].sum) res = query(ls(u), l, mid, ll, rr);
    if (rr > mid && tr[rs(u)].sum) res += query(rs(u), mid + 1, r, ll, rr);
    return res;
}

那么权值线段树还可以干什么呢?

​ 例如求给定序列的第 k k k大数是什么,我们可以在树上二分,如果左边有 ≥ k \ge k k个数就从左边找,如果没有的话就递归到右边找第 k − c n t 左边有多少个数 k- cnt_{左边有多少个数} kcnt左边有多少个数个数。

​ 回归到这个题,我们可一个把他抽象成一个一个的连续段,每个连续段都是有序的,一开始每个位置都是一个连续段,对于一个连续段里的有哪些数我们可以开一个权值线段树来维护这个连续段,也就是一个连续的段对应了一个权值线段树。

​ 如果我们要将 [ l , r ] [l,r] [l,r]变成升序的,显然我们要将所有和 [ l , r ] [l,r] [l,r]有交集的段找出来然后对这些段进行操作,这里我们可以用一个 s e t set set来维护所有的段,然后每次二分查有交集的区间即可,即

typedef tuple<int, int, int, int> tup;// 开一个四元组分别表示 r, l, 根, 升序还是降序
set<tup> se;

这样每次我们搞好一个区间以后就可以直接将他插入 s e t set set

void add(int l, int r, int rt, int opt) {
	se.insert(make_tuple(r, l, rt, opt)); //以r为第一关键字方便后面找交集线段
}

​ 我们用权值线段树来维护一段不仅方便查找该段的第几个数是什么,并且将任意两个权值线段树合并也是非常方便的,因为它们的结构是一样的,假设我们要将树 b b b合并到 a a a上则只需

int merge(int a, int b) { // 合并两个树并返回根
	if (!a || !b) return a | b; // 某个为空的时候直接重合
	ls(a) = merge(ls(a), ls(b)), rs(a) = merge(rs(a), rs(b)); // 不空的时候,直接将左右合并
	pushup(a);
	return a;
}

我们也可以将一个权值线段树分裂成两个,其实就是将某个区间的信息剪切给另一个线段树,这里的裂开只需要将原来的那个区间换个父节点就相当于裂开了

void split(int &a, int &b, int l, int r, int ll, int rr) { // 将区间[l,r]裂出[ll,rr]且以b为父节点
	if (!a || ll <= l && r <= rr) { // 如果到头了,或者完全包含了就裂开
		b = a, a = 0;
		return ;
	}

	int mid = l + r >> 1, L = 0, R = 0; // 搞两个左右儿子是否可以裂开
	if (ll <= mid) split(ls(a), L, l, mid, ll, rr);
	if (rr > mid) split(rs(a), R, mid + 1, r, ll, rr);
	if (L || R) { // 当前区间裂开了某一部分我们就,新建一个结点来维护这个区间
		b = ++idx;
		ls(b) = L, rs(b) = R;
		pushup(a), pushup(b);
	}
}

还可以求出某个权值线段树从左到右第 k k k大的数是什么

int queryL(int u, int l, int r, int k) {
	if (l == r) return l;
	int mid = l + r >> 1;
	if (tr[ls(u)].sum >= k) return queryL(ls(u), l, mid, k);
	return queryL(rs(u), mid + 1, r, k - tr[ls(u)].sum);
}

也可以求出从右到左的第 k k k大的数是什么

int queryR(int u, int l, int r, int k) {
	if (l == r) return l;
	int mid = l + r >> 1;
	if (tr[rs(u)].sum >= k) return queryR(rs(u), mid + 1, r, k);
	return queryR(ls(u), l, mid, k - tr[rs(u)].sum);
}

上述两个操作均模拟二分即可

有了这些操作之后,我们以让 [ l , r ] [l,r] [l,r]有序且新建一个根为 r t rt rt为例,一定是有一些连续的段将 [ l , r ] [l,r] [l,r]覆盖住,因此我们写个循环,每次找到当前最靠左覆盖了 [ l , r ] [l,r] [l,r]的段,然后将它在 s e t set set里移除,假设这个段维护的是 [ L , R ] [L,R] [L,R]且根为 R t Rt Rt O p t = 1 升序, O p t = 2 降序 Opt=1升序,Opt=2降序 Opt=1升序,Opt=2降序 分为以下几种情况

  1. L ≤ l   r ≤ R L\le l \ r\le R Ll rR,即包含住了我们要排序的区间,从实际意义出发,我们需要将这个段分成三份,即 [ L , l − 1 ] , [ l , r ] , [ r + 1 , R ] [L,l-1],[l,r],[r+1,R] [L,l1],[l,r],[r+1,R],那么如何分呢,我们的 s p l i t split split是将某个树的某个区间给分出去,因此我们实际上是要将 R t Rt Rt这个树的前 l − 1 − L + 1 l-1-L+1 l1L+1个数所在的区间(这里是我们线段树所维护的值域区间)给分出去,和后 R − r − 1 + 1 R-r-1+1 Rr1+1个所在的区间给分出去,这里配合以下我们的 q u e r y L , q u e r y R queryL,queryR queryL,queryR即可。

  2. l ≤ L   R ≤ r l\le L \ R \le r lL Rr 这个直接和我们当前的区间合并即可

  3. L ≤ l   R < r L\le l\ R< r Ll R<r,切成两部分即可

  4. l ≤ L   r < R l\le L\ r< R lL r<R 切成两部分即可

    我们循环结束的条件就是处理完了 r r r了,也就是查到的区间 [ L , R ] [L,R] [L,R] L > r L>r L>r或者我们某次处理的区间 R ≥ r R\ge r Rr的时候直接结束即可

对于查 q q q这个位置的数是什么我们可以找到包含 q q q的段,然后查这个段对应的第几个数的值即可。

Code

#include <bits/stdc++.h>
#define x first
#define y second
#define debug(x) cout<<#x<<":"<<x<<endl;
using namespace std;
typedef long double ld;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
typedef unsigned long long ULL;
const int N = 1e5 + 10, M = 2 * N, INF = 0x3f3f3f3f, mod = 1e9 + 7;
const double eps = 1e-8, pi = acos(-1), inf = 1e20;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int h[N], e[M], ne[M], w[M], idx;
void add(int a, int b, int v = 0) {
	e[idx] = b, w[idx] = v, ne[idx] = h[a], h[a] = idx ++;
}
int n, m, k;
struct Node {
	int l, r, sum;
}tr[N * 100];
#define ls(x) (tr[x].l)
#define rs(x) (tr[x].r)

void pushup(int u) {
	tr[u].sum = tr[ls(u)].sum + tr[rs(u)].sum;
}
void split(int &a, int &b, int l, int r, int ll, int rr) { // 将区间[l,r]裂出[ll,rr]且以b为父节点
	if (!a || ll <= l && r <= rr) {
		b = a, a = 0;
		return ;
	}

	int mid = l + r >> 1, L = 0, R = 0;
	if (ll <= mid) split(ls(a), L, l, mid, ll, rr);
	if (rr > mid) split(rs(a), R, mid + 1, r, ll, rr);
	if (L || R) {
		b = ++idx;
		ls(b) = L, rs(b) = R;
		pushup(a), pushup(b);
	}
}
int merge(int a, int b) { // 合并两个树
	if (!a || !b) return a | b;
	ls(a) = merge(ls(a), ls(b)), rs(a) = merge(rs(a), rs(b));
	pushup(a);
	return a;
}
void insert(int u, int l, int r, int pos) {
	if (l == r) {
		tr[u].sum = 1;
		return ;
	}
	int mid = l + r >> 1;
	if (pos <= mid) insert(ls(u) = ++idx, l, mid, pos);
	else insert(rs(u) = ++idx, mid + 1, r, pos);
	pushup(u);
}
int queryL(int u, int l, int r, int k) {
	if (l == r) return l;
	int mid = l + r >> 1;
	if (tr[ls(u)].sum >= k) return queryL(ls(u), l, mid, k);
	return queryL(rs(u), mid + 1, r, k - tr[ls(u)].sum);
}
int queryR(int u, int l, int r, int k) {
	if (l == r) return l;
	int mid = l + r >> 1;
	if (tr[rs(u)].sum >= k) return queryR(rs(u), mid + 1, r, k);
	return queryR(ls(u), l, mid, k - tr[rs(u)].sum);
}

typedef tuple<int, int, int, int> tup;
set<tup> se;
void add(int l, int r, int rt, int opt) {
	se.insert(make_tuple(r, l, rt, opt));
}
int main() {
	ios::sync_with_stdio(false), cin.tie(0);
	cin >> n >> m;
	for (int i = 1; i <= n; i ++) {
		int x; cin >> x;
		int rt = ++idx;
		insert(rt, 1, n, x); // 开点插入
		add(i, i, rt, 1);
	}
	
	while (m --) {
		int opt, l, r;
		cin >> opt >> l >> r;
		opt ++;
		int rt = 0;
		while (1) {
			auto it = se.lower_bound(make_tuple(l, 0, 0, 0));
			if (it == se.end() || get<1>(*it) > r) break;
			tup x = *it;
			se.erase(it);
			int L = get<1>(x), R = get<0>(x), Rt = get<2>(x), Opt = get<3>(x);
			if (l <= L && R <= r) rt = merge(rt, Rt);
			else if (L < l && r < R) {
				int lrt, rrt;
				if (Opt == 1) {
					split(Rt, lrt, 1, n, 1, queryL(Rt, 1, n, l - L));
					split(Rt, rrt, 1, n, queryR(Rt, 1, n, R - r), n);
				}
				else {
					split(Rt, lrt, 1, n, queryR(Rt, 1, n, l - L), n);
					split(Rt, rrt, 1, n, 1, queryL(Rt, 1, n, R - r));
				}
				add(L, l - 1, lrt, Opt), add(r + 1, R, rrt, Opt);
				rt = Rt;
				break;
			}
			else if(R <= r) {
				int lrt;
				if (Opt == 1) split(Rt, lrt, 1, n, 1, queryL(Rt, 1, n, l - L));
				else split(Rt, lrt, 1, n, queryR(Rt, 1, n, l - L), n);
				add(L, l - 1, lrt, Opt);
				rt = merge(rt, Rt);
			}
			else if (L <= r) {
				int rrt;
				if (Opt == 1) split(Rt, rrt, 1, n, queryR(Rt, 1, n, R - r), n);
				else split(Rt, rrt, 1, n, 1, queryL(Rt, 1, n, R - r));
				add(r + 1, R, rrt, Opt);
				rt = merge(rt, Rt);
				break;
			}
		} 
		add(l, r, rt, opt);
	}

	int pos; cin >> pos;
	auto it = se.lower_bound(make_tuple(pos, 0, 0, 0));
	tup x = *it;
	int L = get<1>(x), R = get<0>(x), Rt = get<2>(x), Opt = get<3>(x);

	if (Opt == 1) cout << queryL(Rt, 1, n, pos - L + 1) << '\n';
	else cout << queryR(Rt, 1, n, pos - L + 1) << '\n'; 
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值