<think>题目:P2824 [HEOI2016/TJOI2016] 排序
题目大意:给出一个1到n的全排列,有m个操作,每个操作将区间[l, r]升序或降序排序。最后询问第q个位置上的数字。
常规思路:直接模拟排序操作会超时,因为m和n最大为10^5。
高效解法:二分答案 + 线段树(或类似数据结构)维护01序列。
思路:
1. 注意到最后只询问一个位置的值,考虑二分答案ans。
2. 假设当前二分的值为mid,将原数组中大于等于mid的值设为1,小于mid的值设为0。
3. 这样,排序操作就变成了将区间内的0和1进行排序:
- 升序排序:相当于将0全部放到前面,1全部放到后面。
- 降序排序:相当于将1全部放到前面,0全部放到后面。
4. 用线段树维护01序列,支持区间赋值和区间求和(即区间内1的个数)。
5. 对于每个操作[l, r]:
- 先查询该区间内1的个数(记为cnt)。
- 如果是升序排序,则将区间[l, r-cnt]赋值为0,将区间[r-cnt+1, r]赋值为1。
- 如果是降序排序,则将区间[l, l+cnt-1]赋值为1,将区间[l+cnt, r]赋值为0。
6. 操作完成后,检查位置q的值是否为1(注意:由于我们二分的是答案,如果位置q为1,说明答案至少为mid,否则小于mid)。
7. 不断二分,直到找到最大的mid,使得最后位置q的值为1,这个mid就是答案。
代码实现步骤:
- 二分框架:二分答案,范围是1到n。
- 构建线段树:支持区间赋值和区间求和。
- 对每个操作进行模拟(区间赋值操作)。
- 最后根据位置q的值调整二分边界。
注意:线段树需要支持区间覆盖(set)操作,并且要注意懒标记的处理。
代码示例:
```cpp
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 100005;
struct Node {
int sum; // 区间内1的个数
int set; // 懒标记:-1表示无操作,0表示置0,1表示置1
} tree[MAXN << 2];
struct Operation {
int op, l, r;
} op[MAXN];
int n, m, q;
int a[MAXN]; // 原数组
int b[MAXN]; // 用于01序列的数组
void pushUp(int rt) {
tree[rt].sum = tree[rt<<1].sum + tree[rt<<1|1].sum;
}
void pushDown(int rt, int l, int r) {
if (tree[rt].set != -1) {
int mid = (l + r) >> 1;
// 下传左子树
tree[rt<<1].set = tree[rt].set;
tree[rt<<1].sum = (mid - l + 1) * tree[rt].set;
// 下传右子树
tree[rt<<1|1].set = tree[rt].set;
tree[rt<<1|1].sum = (r - mid) * tree[rt].set;
// 清除标记
tree[rt].set = -1;
}
}
void build(int rt, int l, int r) {
tree[rt].set = -1;
if (l == r) {
tree[rt].sum = b[l];
return;
}
int mid = (l + r) >> 1;
build(rt<<1, l, mid);
build(rt<<1|1, mid+1, r);
pushUp(rt);
}
void update(int rt, int l, int r, int L, int R, int val) {
if (L <= l && r <= R) {
tree[rt].set = val;
tree[rt].sum = (r - l + 1) * val;
return;
}
pushDown(rt, l, r);
int mid = (l + r) >> 1;
if (L <= mid) update(rt<<1, l, mid, L, R, val);
if (R > mid) update(rt<<1|1, mid+1, r, L, R, val);
pushUp(rt);
}
int query(int rt, int l, int r, int L, int R) {
if (L <= l && r <= R) {
return tree[rt].sum;
}
pushDown(rt, l, r);
int mid = (l + r) >> 1;
int res = 0;
if (L <= mid) res += query(rt<<1, l, mid, L, R);
if (R > mid) res += query(rt<<1|1, mid+1, r, L, R);
return res;
}
// 检查二分值mid:构建01序列,然后模拟所有操作
bool check(int mid) {
// 构建01序列:>=mid为1,否则为0
for (int i = 1; i <= n; i++) {
b[i] = (a[i] >= mid);
}
// 构建线段树
build(1, 1, n);
// 模拟m个操作
for (int i = 0; i < m; i++) {
int l = op[i].l, r = op[i].r;
int cnt = query(1, 1, n, l, r); // 查询区间内1的个数
if (op[i].op == 0) { // 升序:0在前,1在后
if (cnt > 0) update(1, 1, n, r - cnt + 1, r, 1); // 后cnt个置1
if (r - l + 1 - cnt > 0) update(1, 1, n, l, r - cnt, 0); // 前面的置0
} else { // 降序:1在前,0在后
if (cnt > 0) update(1, 1, n, l, l + cnt - 1, 1); // 前cnt个置1
if (r - l + 1 - cnt > 0) update(1, 1, n, l + cnt, r, 0); // 后面的置0
}
}
// 查询位置q的值
return query(1, 1, n, q, q) == 1;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
for (int i = 0; i < m; i++) {
scanf("%d%d%d", &op[i].op, &op[i].l, &op[i].r);
}
scanf("%d", &q);
// 二分答案
int L = 1, R = n, ans = 0;
while (L <= R) {
int mid = (L + R) >> 1;
if (check(mid)) {
ans = mid;
L = mid + 1;
} else {
R = mid - 1;
}
}
printf("%d\n", ans);
return 0;
}
```
§§相关问题§§
1. 为什么二分答案的正确性成立?如何保证最终答案的正确性?
2. 线段树在本题中维护的信息是什么?懒标记的作用是什么?
3. 如果题目要求最后多个位置的值,如何修改算法?
4. 如何处理排序操作中的区间覆盖与线段树懒标记的冲突?
5. 是否有其他数据结构(如平衡树)可以替代线段树解决此题?</think>### 洛谷 P2824 "排序" C++ 题解
**题目大意**:给定一个 1~n 的排列,进行 m 次区间排序操作(升序或降序),最后询问第 q 个位置的值。
**核心算法**:二分答案 + 线段树维护 01 序列
**解题思路**:
1. **二分答案**:最终答案范围 [1, n],二分检查 mid 是否可能为答案
2. **01 序列转换**:将原数组转为 01 序列(≥mid 为 1,<mid 为 0)
3. **线段树操作**:
- 排序操作转化为区间赋值(升序:0 在前 1 在后;降序:1 在前 0 在后)
- 通过区间求和快速计算 1 的数量
4. **验证答案**:检查位置 q 在最终序列中是否为 1
**代码实现**:
```cpp
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 1e5 + 5;
struct Node {
int sum; // 区间内 1 的数量
int set; // 懒标记:-1 无操作,0 置 0,1 置 1
} tree[MAXN << 2];
struct Operation {
int op, l, r; // op=0 升序,op=1 降序
} ops[MAXN];
int n, m, q, pos;
int a[MAXN], b[MAXN];
// 线段树维护
void pushUp(int rt) {
tree[rt].sum = tree[rt << 1].sum + tree[rt << 1 | 1].sum;
}
void pushDown(int rt, int len) {
if (tree[rt].set != -1) {
int setVal = tree[rt].set;
tree[rt << 1].set = setVal;
tree[rt << 1 | 1].set = setVal;
tree[rt << 1].sum = setVal * (len - (len >> 1));
tree[rt << 1 | 1].sum = setVal * (len >> 1);
tree[rt].set = -1;
}
}
void build(int rt, int l, int r) {
tree[rt].set = -1;
if (l == r) {
tree[rt].sum = b[l];
return;
}
int mid = (l + r) >> 1;
build(rt << 1, l, mid);
build(rt << 1 | 1, mid + 1, r);
pushUp(rt);
}
void update(int rt, int L, int R, int setVal, int l, int r) {
if (L <= l && r <= R) {
tree[rt].set = setVal;
tree[rt].sum = setVal * (r - l + 1);
return;
}
pushDown(rt, r - l + 1);
int mid = (l + r) >> 1;
if (L <= mid) update(rt << 1, L, R, setVal, l, mid);
if (R > mid) update(rt << 1 | 1, L, R, setVal, mid + 1, r);
pushUp(rt);
}
int query(int rt, int L, int R, int l, int r) {
if (L <= l && r <= R) return tree[rt].sum;
pushDown(rt, r - l + 1);
int mid = (l + r) >> 1, res = 0;
if (L <= mid) res += query(rt << 1, L, R, l, mid);
if (R > mid) res += query(rt << 1 | 1, L, R, mid + 1, r);
return res;
}
bool check(int mid) {
// 构建 01 序列
for (int i = 1; i <= n; i++)
b[i] = (a[i] >= mid);
build(1, 1, n);
// 执行所有排序操作
for (int i = 0; i < m; i++) {
int l = ops[i].l, r = ops[i].r;
int cnt = query(1, l, r, 1, n); // 查询 1 的数量
if (ops[i].op == 0) { // 升序排序
if (r - cnt + 1 <= r) update(1, r - cnt + 1, r, 1, 1, n);
if (l <= r - cnt) update(1, l, r - cnt, 0, 1, n);
} else { // 降序排序
if (l <= l + cnt - 1) update(1, l, l + cnt - 1, 1, 1, n);
if (l + cnt <= r) update(1, l + cnt, r, 0, 1, n);
}
}
return query(1, q, q, 1, n) == 1; // 检查目标位置
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 0; i < m; i++)
scanf("%d%d%d", &ops[i].op, &ops[i].l, &ops[i].r);
scanf("%d", &q);
// 二分答案
int L = 1, R = n, ans;
while (L <= R) {
int mid = (L + R) >> 1;
if (check(mid)) {
ans = mid;
L = mid + 1;
} else {
R = mid - 1;
}
}
printf("%d\n", ans);
return 0;
}
```
**算法优势**:
- **时间复杂度**:O(m log² n)
- **空间复杂度**:O(n)
- **核心技巧**:将排序操作转化为 01 序列的区间赋值,避免直接排序的高复杂度