Description

给定一个长度为 \(n\) 的排列,有 \(m\) 次操作,每次选取一段局部进行升序或降序排序,问你一波操作后某个位置上的数字是几

Hint

\(1~\leq~n,~m~\leq~10^5\)

Solution

有两种做法,一种在线一种离线,这里把在线部分讲得更清楚点吧……

考虑离线算法,我们二分该位置上的答案,将大于该数的元素置为 \(1\),小于该数的元素置为 \(0\),然后模拟所有的排序并检验。由于使用线段树对 \(0/1\) 序列多次局部排序可以做到 \(O(m~\log n~+~n)\) 的复杂度,所以总复杂度为 \(O(m~\log^2 n)\)。

具体排序的做法为使用线段树维护当前区间有多少个 \(1\),不妨设为 \(x\) 个。如果对该区间升序排序,则将后面 \(x\) 个数置为 \(1\),剩下的置为 \(0\),否则将前面 \(x\) 个数置为 \(1\),其余置为 \(0\)。于是一次操作的复杂度为 \(O(\log n)\),于是进行 \(m\) 次操作的复杂度为 \(O(m~\log n)\)。

考虑在线做。发现对于任意的时刻我们都有一些区间是排好序的。那么现在我们的问题是每次排序操作后合并被排序操作覆盖的区间。考虑到对于两个序列的按序合并可以使用权值线段树轻松做到,我们对每个排好序的区间分别维护一棵权值线段树。然后用一个 set 维护这些区间。对于被该排序操作完全覆盖的区间,我们可以直接进行线段树合并,而对于区间两侧的被覆盖了一部分的两个区间,可以先分裂成被完全覆盖的区间和完全不被覆盖的区间再进行合并。例如被排序的区间是 \([l,~r]\),左侧被覆盖了一部分的区间为 \([l_0,~y_0]\),其中 \(l_0~<~l~<~r_0\),那么将区间 \([l_0,~r_0]\) 拆分成 \([l_0,~l - 1]~\bigcap~[l,~r_0]\) 两个区间,然后对 \([l,~r_0]\) 与其他被完全覆盖的区间进行合并即可。

考虑复杂度分析:

如果不考虑线段树分裂操作,我们对区间的操作实质上是将很多的小区间合并成至少一个大区间。考虑到对 \(n\) 个长度为 \(1\) 的小区间全部合并成一个大区间的复杂度为 \(O(n~\log n)\),于是 merge 部分的的总复杂度为 \(O(n~\log n)\)。考虑分裂,一次排序操作后会分裂出 \(O(1)\) 个新区间,于是 \(m\) 次操作后会分裂出 \(O(m)\) 个新区间,合并这 \(O(m)\) 个区间的复杂度仍为 \(O(m~\log n)\),考虑到每次分裂是严格 \(O(\log n)\) 的,且会进行 \(O(m)\) 次排序,所以分裂操作对复杂度的总贡献仍是 \(O(m~\log n)\)。于是总复杂度为 \(O((n+m)~\log n)\)。比上面的离线算法更优秀。同时这个算法可以解决查询任意多个位置上的数字,而不是仅仅一个。

Code

在分裂区间的时候有很多小细节需要注意……另外下面的代码存在一定的内存泄漏问题,不过总共被泄露的内存是 \(O((n+m)~\log n)\) 级别的,对空间复杂度不产生影响,是可以接受的。

其实是我不知道为什么回收空间会莫名其妙 RE

#include <cstdio>
#include <set>
#include <algorithm>
#ifdef ONLINE_JUDGE
#define freopen(a, b, c)
#endif typedef long long int ll; namespace IPT {
const int L = 1000000;
char buf[L], *front=buf, *end=buf;
char GetChar() {
if (front == end) {
end = buf + fread(front = buf, 1, L, stdin);
if (front == end) return -1;
}
return *(front++);
}
} template <typename T>
inline void qr(T &x) {
char ch = IPT::GetChar(), lst = ' ';
while ((ch > '9') || (ch < '0')) lst = ch, ch=IPT::GetChar();
while ((ch >= '0') && (ch <= '9')) x = (x << 1) + (x << 3) + (ch ^ 48), ch = IPT::GetChar();
if (lst == '-') x = -x;
} namespace OPT {
char buf[120];
} template <typename T>
inline void qw(T x, const char aft, const bool pt) {
if (x < 0) {x = -x, putchar('-');}
int top=0;
do {OPT::buf[++top] = static_cast<char>(x % 10 + '0');} while (x /= 10);
while (top) putchar(OPT::buf[top--]);
if (pt) putchar(aft);
} const int maxn = 100010; struct Tree {
Tree *ls, *rs;
int l, r, v; Tree() {
ls = rs = NULL;
l = r = v = 0;
} inline void pushup() {if (this->l != this->r) this->v = (this->ls ? this->ls->v : 0) + (this->rs ? this->rs->v : 0); else this->v = 1;}
}; struct OP {
int l, r;
bool up;
Tree *rot; inline bool operator<(const OP &_others) const {
return this->r < _others.r;
} OP(int _l = 0, int _r = 0, bool _up = 0, Tree *_rot = 0) {
l = _l; r = _r; up = _up; rot = _rot;
}
};
OP temp;
std::set<OP>s; int n, m;
int MU[maxn]; void split(int, bool);
void insert(Tree*, int, int, int);
void split(Tree*, Tree*, Tree*, int);
Tree* merge(Tree*, Tree*);
int query(Tree*, int); int main() {
freopen("data.in", "r", stdin);
freopen("my.out", "w", stdout);
qr(n); qr(m);
for (int i = 1; i <= n; ++i) {
auto _rot = new Tree; qr(MU[i]);
insert(_rot, 1, n, MU[i]); s.insert(OP(i, i, true, _rot));
}
for (int j = 1, a, b, c; j <= m; ++j) {
a = b = c = 0; qr(a); qr(b); qr(c);
split(b, true); split(c + 1, false);
auto l = s.lower_bound({0, b, true, NULL}), r = s.lower_bound({0, c + 1, true, NULL});
auto _tmp = *l;
for (auto i = s.erase(l); i != r; i = s.erase(i)) {
_tmp.rot = merge(_tmp.rot, (*i).rot);
}
_tmp.l = b; _tmp.r = c; _tmp.up = !a;
s.insert(_tmp);
}
int q = 0; qr(q);
auto _ans = s.lower_bound({0, q, true, NULL}); auto ans = *_ans;
int k = ans.up ? q - ans.l + 1 : ans.r - q + 1;
qw(query(ans.rot, k), '\n', true);
return 0;
} int query(Tree *u, int k) {
if (u->l == u->r) return u->l;
if (!u->ls) return query(u->rs, k);
if (u->ls->v >= k) return query(u->ls, k);
return query(u->rs, k - u->ls->v);
} Tree* merge(Tree *u, Tree *v) {
if (!u) return v; else if (!v) return u;
u->v += v->v;
u->ls = merge(u->ls, v->ls);
u->rs = merge(u->rs, v->rs);
return u;
} void insert(Tree *u, int l, int r, int v) {
++u->v;
if ((u->l = l) == (u->r = r)) return;
int mid = (l + r) >> 1;
if (v <= mid) insert(u->ls = new Tree, l, mid, v);
else insert(u->rs = new Tree, mid + 1, r, v);
} void split(int x, bool isfront) {
auto k = s.lower_bound({0, x, true, NULL}); if (k == s.end()) return; auto t = *k;
if (t.l == x) return;
s.erase(k);
int _k = (isfront ? t.r - x + 1: x - t.l), len = t.r - t.l + 1;
if (t.up == isfront) {
Tree *_rot = new Tree;
_k = len - _k;
if (!_k) {
s.insert(t); return;
}
split(t.rot, t.rot, _rot, _k);
if (!t.up) {
_k = len - _k; std::swap(_rot, t.rot);
}
s.insert({t.l, t.l + _k - 1, t.up, t.rot});
s.insert({t.l + _k, t.r, t.up, _rot});
} else {
Tree *_rot = new Tree;
if (!_k) {
s.insert(t); return;
}
split(t.rot, t.rot, _rot, _k);
if (!t.up) {
std::swap(t.rot, _rot); _k = len -_k;
}
s.insert({t.l, t.l + _k - 1, t.up, t.rot});
s.insert({t.l + _k, t.r, t.up, _rot});
}
} void split(Tree *u, Tree *l, Tree *r, int k) {
l->l = r->l = u->l; r->r = l->r = u->r;
if (!u->ls) split(u->rs, l->rs ? l->rs : l->rs = new Tree, r->rs ? r->rs : r->rs = new Tree, k);
else if (k == u->ls->v) {
l->ls = u->ls; r->rs = u->rs; l->rs = NULL; r->ls = NULL;
} else if (k < u->ls->v) {
split(u->ls, l->ls ? l->ls : l->ls = new Tree, r->ls ? r->ls : r->ls = new Tree, k);
r->rs = u->rs; l->rs = NULL;
} else {
split(u->rs, l->rs ? l->rs : l->rs = new Tree, r->rs ? r->rs : r->rs = new Tree, k - u->ls->v);
l->ls = u->ls; r->ls = NULL;
}
l->pushup(); r->pushup();
}

Summary

1、将 \(n\) 个长度为 \(1\) 的小区间合并为一个大区间的复杂度为 \(O(n~\log n)\),理由是这样的开销显然不大于将这些区间顺次插入一棵线段树。

2、合并两个有序序列可以使用合并权值线段树来做到均摊复杂度 \(O(\log n)\)。

【线段树合并】【P2824】 [HEOI2016/TJOI2016]排序的更多相关文章

  1. [Luogu P2824] [HEOI2016/TJOI2016]排序 (线段树+二分答案)

    题面 传送门:https://www.luogu.org/problemnew/show/P2824 Solution 这题极其巧妙. 首先,如果直接做m次排序,显然会T得起飞. 注意一点:我们只需要 ...

  2. 洛谷 P2824 [HEOI2016/TJOI2016]排序 解题报告

    P2824 [HEOI2016/TJOI2016]排序 题意: 有一个长度为\(n\)的1-n的排列\(m\)次操作 \((0,l,r)\)表示序列从\(l\)到\(r\)降序 \((1,l,r)\) ...

  3. 洛谷 P2824 [HEOI2016/TJOI2016]排序 (线段树合并)

    (另外:题解中有一种思路很高妙而且看上去可以适用一些其他情况的离线方法) 线段树合并&复杂度的简单说明:https://blog.csdn.net/zawedx/article/details ...

  4. 洛谷P2824 [HEOI2016/TJOI2016]排序(线段树)

    传送门 这题的思路好清奇 因为只有一次查询,我们考虑二分这个值为多少 将原序列转化为一个$01$序列,如果原序列上的值大于$mid$则为$1$否则为$0$ 那么排序就可以用线段树优化,设该区间内$1$ ...

  5. 洛谷$P2824\ [HEOI2016/TJOI2016]$ 排序 线段树+二分

    正解:线段树+二分 解题报告: 传送门$QwQ$ 昂着题好神噢我$jio$得$QwQQQQQ$,,, 开始看到长得很像之前考试题的亚子,,,然后仔细康康发现不一样昂$kk$,就这里范围是$[1,n]$ ...

  6. [洛谷P2824][HEOI2016/TJOI2016]排序

    题目大意:一个全排列,两种操作: 1. $0\;l\;r:$把$[l,r]$升序排序2. $1\;l\;r:$把$[l,r]$降序排序 最后询问第$k$位是什么 题解:二分答案,把比这个数大的赋成$1 ...

  7. luogu P2824 [HEOI2016/TJOI2016]排序

    题目描述 在2016年,佳媛姐姐喜欢上了数字序列.因而他经常研究关于序列的一些奇奇怪怪的问题,现在他在研究一个难题,需要你来帮助他.这个难题是这样子的:给出一个1到n的全排列,现在对这个全排列序列进行 ...

  8. 「Luogu P2824 [HEOI2016/TJOI2016]排序」

    一道十分神奇的线段树题,做法十分的有趣. 前置芝士 线段树:一个十分基础的数据结构,在这道题中起了至关重要的作用. 一种基于01串的神奇的二分思想:在模拟赛中出现了这道题,可以先去做一下,这样可能有助 ...

  9. P2824 [HEOI2016/TJOI2016]排序

    题面 这是一道非常巧妙的线段树的题 我们会发现维护\(1 \sim n\)的序列非常困难,但如果我们维护\(01\)序列的的顺序,就非常容易了 但是我们怎么能把这道题变成维护\(01\)序列的顺序呢? ...

  10. Luogu P2824 [HEOI2016/TJOI2016]排序 线段树+脑子

    只会两个$log$的$qwq$ 我们二分答案:设答案为$ans$,则我们把$a[i]<=ans$全部设成$0$,把$a[i]>ans$全部设成$1$,扔到线段树里,这样区间排序(升序)就是 ...

随机推荐

  1. bitcoin PoW原理及区块创建过程

    bitcoin PoW原理及区块创建过程 PoW 为了在点对点的基础上实现一个分布式时间戳服务器,我们需要使用PoW(Proof of Work)系统来达成共识.PoW过程就是寻找一个目标值的过程,当 ...

  2. 线程_synchronized_volatile_ReentranLock

    线程:cpu同时执行多个任务 synchonized   代码块,对象,类 同步方法和非同步方法可以同时执行同步方法可以调用同步方法(重入)脏读:之同步写,不同步读死锁的demo  一个线程先对A加锁 ...

  3. Django_用户权限管理rbac

    组成部分 1.初始化权限:login视图initial_permission,把权限信息放入session.initial_permission函数生成权限列表.菜单列表 2.中间件验证权限:在第一次 ...

  4. [BUAA OO]第二次博客作业

    第五次作业 这次作业是电梯系列作业的终极版,要求是使用多线程实现三部电梯的运行.这次作业的难点在于第一次运用多线程技术,对于线程中的行为并不了解,以及电梯功能的实现(如果之前作业采取的是扫描指令队列预 ...

  5. IO文件的读取,以及写入文件内容

    package zxc; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.Fi ...

  6. 我是一名IT小小鸟

    我是一只it小小鸟 书中介绍了it界大牛们大学期间的学习方法和对未来的职业规划,相比他们,自我感觉相距甚远,对这学科的热情程度也远远比不上他们. 就拿目前数据结构这门高深的课程,应通过更多的课外扩展来 ...

  7. 软工网络15团队作业8——Beta阶段敏捷冲刺(用户使用调查报告)

    一.项目概述 1.项目名称 考研必背 2.项目简介 微信小程序,帮助考研学生记忆单词. 3.项目预期达到目标 用户无需下载app,仅通过微信小程序就可以达到背单词的目的,并且能够制定背单词的计划. 4 ...

  8. sublime很常用快捷方式演示

                    来自为知笔记(Wiz)

  9. Python实现客观赋权法

    本文从阐述Python实现客观赋权法的四种方式: 一. 熵权法 二. 因子分析权数法(FAM) 三. 主成分分析权数法(PCA) 四. 独立性权系数法 Python实现客观赋权法,在进行赋权前,先导入 ...

  10. 面试问题总结二(技术能力-PHP)----Ⅱ

    20.支付功能的实现? 答:在线支付一般来说有两种实现方式,一种是调用各个银行提供的接口,另一种是使用第三方集成好的支付功能,两种方式各有优劣.对于第三方支付来说会需要提交企业5证来验证,还会有部分手 ...