##[$P2824$ [$HEOI2016$/$TJOI2016$]排序](https://www.luogu.com.cn/problem/P2824) ### 一、题目大意 给一个序列, 两种操作, 一种是将$[l, r]$里所有数升序排列, 一种是降序排列。 所有操作完了之后, 问你$a[k]$等于多少。 ### 二、解题思路 由于将一个普通序列排序很慢,需要$nlogn$的时间,可以转化为对$01$序列排序。 #### $01$序列排序 **二分+$01$序列+中位数的一道引入例题:** [$AGC006D$ $Median$ $Pyramid$ $Hard$](https://www.cnblogs.com/littlehb/p/17013055.html) 先考虑简单问题: 如何将一个$01$序列排序? 算法复杂度:$O(logn)$
如上面这样一个$01$序列,灰色为$1$,白色为$0$,只要查询出区间的和,将最后的这几个覆盖为$1$,前面覆盖为$0$ (此为升序,降序同理), 即可完成排序。 **使用线段树来维护** * 查询一段区间内的$1$的个数记为$c$ * 如果是降序($1$在前,$0$在后), 将$[l,l+c−1]$更改为$1$,将$[l+c,r]$更改为$0$ * 如果是升序($0$在前,$1$在后), 将$[l,r−cnt]$更改为$0$, 将$[r−c+1,r]$更改为$1$ #### 单调性的证明和理解 如果把划定一个标准数$mid$,同时,将大于等于$mid$的设置为$1$,小于$mid$的设置为$0$,那么 **$01$序列排序结果** 对照 **正常排序的结果**,会发现: ![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2022/12/7bcc54c6b0d95fdec04d6a166e18161e.png) * 正常排序结果大于$mid$的位置上,$01$序列的排序结果都是$1$ * 正常排序结果小于$mid$的位置上,$01$序列的排序结果都是$0$ * 如果$q$这个位置最终结果是$5$,我们现在枚举的$mid$是$4$的话,则$5>4$,所以,此位置最终是标识的$1$,同时,还有余量,就是等于$4$的也标识为了$1$,也就是$1$的数量标识多了 * 如果$q$这个位置最终结果是$5$,我们现在枚举的$mid$是$5$的话,则$5=5$,所以,此位置最终是标识的$1$,整个结果中会有两个数字为$1$,也就是正常排序$5,6$所在的位置上是$1$,$q$这个位置上标识成了$1$ * 如果$q$这个位置最终结果是$5$,我们现在枚举的$mid$是$6$的话,则于$5<6$,所以,此位置最终是标识的$0$,整个结果中只会有一个数字为$1$,也就是正常排序$6$所在的位置上是$1$,$q$这个位置上标识成了$0$。 * 由于给定的数据是一个全排列,所以不断的判断区间,可以找出最终的准确解 #### 时间复杂度 $O(M\log^2n)$.(二分为$O(\log_2n)$,每一次$check$需要$O(Mlogn)$ ### 三、实现代码 ```cpp {.line-numbers} #include #include #include #include using namespace std; const int N = 2e5 + 5; int n, m, q; //输入的指令序列 struct Sort { int op, l, r; } s[N]; int a[N]; //原始数组 struct Node { int l, r; int tag; // 0:升序 1:降序 2:初始值 lazy tag:懒标记 ,不想每次都做一遍,不查询不想做 int sum; //大于目标值的个数 } tr[N << 2]; //管辖区间长度 int len(int u) { return tr[u].r - tr[u].l + 1; } //向父节点更新信息,写成Node &的方式目前看来是最合理的办法 void pushup(Node &c, Node &a, Node &b) { c.sum = a.sum + b.sum; } //更新lazy tag标识 void pushdown(int u) { if (tr[u].tag == 2) return; //如果没有下传标识,啥也不干 tr[u << 1].sum = len(u << 1) * tr[u].tag; //左儿子区间内所有数字都要加上tr[u].tag,sum值为累加 tr[u << 1 | 1].sum = len(u << 1 | 1) * tr[u].tag; //右儿子区间内所有数字都要加上tr[u].tag,sum值为累加 tr[u << 1].tag = tr[u << 1 | 1].tag = tr[u].tag; //左右儿子都修改标识为tag,一次只更新一层 tr[u].tag = 2; //标识已处理 } //构建线段树,x:当前两分取到的值,用于建立线段树时判断每个叶子的初始值是1还是0 // a[l]>=x tr[u].sum=1 // a[l]= x; //大于等于x的都设置为1,小于x的设置为0 return; } //递归构建左右子树 int mid = (l + r) >> 1; build(u << 1, l, mid, x), build(u << 1 | 1, mid + 1, r, x); //向上更新统计信息 pushup(tr[u], tr[u << 1], tr[u << 1 | 1]); } //查询区间内数字1的个数 int query(int u, int l, int r) { if (l > tr[u].r || r < tr[u].l) return 0; //不在范围内的返回0 if (l <= tr[u].l && r >= tr[u].r) return tr[u].sum; //整个区间命中,返回统计信息 // 分裂前要记得 lazy tag下传 pushdown(u); //分裂查询 左儿子+右儿子 return query(u << 1, l, r) + query(u << 1 | 1, l, r); } //区间修改 void modify(int u, int l, int r, int c) { if (l > r) return; //特判边界,防止越界 //如果命中区间,对区间的lazy tag和sum进行计算修改 if (l <= tr[u].l && r >= tr[u].r) { tr[u].tag = c; tr[u].sum = c * len(u); return; } //没有命中区间,需要递归向左右儿子传递修改消息 pushdown(u); //修改左区间 if (l <= tr[u << 1].r) modify(u << 1, l, r, c); //修改右区间 if (r >= tr[u << 1 | 1].l) modify(u << 1 | 1, l, r, c); //因为子区间内容修改,需要向父节点更新统计信息 pushup(tr[u], tr[u << 1], tr[u << 1 | 1]); } bool check(int x) { build(1, 1, n, x); //每次全新构建线段树 for (int i = 1; i <= m; i++) { //枚举每个排序动作 int l = s[i].l, r = s[i].r, op = s[i].op; // 0:升序,1:降序 int c = query(1, l, r); //查询l,r之间数字1的个数,也是大于等于x的个数 //算法本质:忽略数字的正实值,只记录大小关系,大于等于记录为1,小于记录为0 if (op) //降序 modify(1, l, l + c - 1, 1), modify(1, l + c, r, 0); //比x大的个数是c个,如果是降序,[l,l+c-1]修改为1,表示区间都大于等于x,[l+c,r]修改为0,表示这区间小于x else //升序 modify(1, l, r - c, 0), modify(1, r - c + 1, r, 1); //比x大的个数是c个,如果是升序,[l,r-c]修改为0,表示区间[l,r-c]都比x小,后面[r-c+1,r]都大于等于x } //按上面的操作序列要求,都模拟了一遍后,如果q这个位置上的数位是1,表示操作没有出现矛盾 return query(1, q, q) == 1; } int main() { //加快读入 ios::sync_with_stdio(false), cin.tie(0); cin >> n >> m; for (int i = 1; i <= n; i++) cin >> a[i]; //第二行为 n 个整数,表示 1 到 n 的一个排列 for (int i = 1; i <= m; i++) cin >> s[i].op >> s[i].l >> s[i].r; //记录排序的动作与范围 cin >> q; //查询第q个位置上的数字是多少 int l = 1, r = n; //开始二分,因为原始序列的数字,是从[1,n]的不重复序列排列,所以有二分时上下限就是决定好的[1,n] while (l <= r) { int mid = (l + r) >> 1; //来尝试位置q上的数字是多大,假设为mid if (check(mid)) //此位置的值大于等于mid l = mid + 1; //那么继续尝试l=mid+1,看看结果向右半区间走,也就是再大一点是不是可以 else r = mid - 1; //向左半区间走,看看再小一点是不是可以 } printf("%d\n", l - 1); return 0; } ```