离线,多次查询序列或区间第 k 大数(含区间修改)
需要满足:修改对判定答案的贡献互相独立
基础:查询数列中第 k 小数。
对于当前的所有询问,可以去猜测所有询问的答案都是 mid,然后去依次验证每个询问的答案应该是小于等于 mid 的还是大于 mid 的,并将询问分为两个部分(不大于/大于),对于每个部分继续二分
如果当前数列中小于等于 mid 的数有 t 个,则将询问划分后实际是在右区间询问第 k-t 小数
c++
struct Query {
int id, k; // 这个询问的编号, 这个询问的k
};
int ans[N]; // ans[i] 表示编号为i的询问的答案
int check(int x); // 返回原数列中小于等于x的数的个数
void solve(int l, int r, vector<Query> q)
// 请补全这个函数
{
int m = (l + r) / 2;
vector<Query> q1, q2; // 将被划到左侧的询问和右侧的询问
if (l == r) {
// ...
for (unsigned i = 0; i < q.size(); i++) ans[q[i].id] = l;
return;
}
// ...
for (unsigned i = 0; i < q.size(); i++)
if (q[i].k <= check(m))
q1.push_back(q[i]);
else
q[i].k -= check(m), q2.push_back(q[i]);
solve(l, m, q1), solve(m + 1, r, q2);
return;
}
如果查询区间第 k 小数呢
若询问区间内小于等于 m 的数有 t 个,询问的是区间内的 k 小数,则当 k≤t 时,答案应小于等于 m;否则,答案应大于 m
此处需记录一个区间小于等于指定数的数的数量,即单点加,求区间和,可用树状数组快速处理
在进一步递归之前,不仅将询问划分,将当前处理的数按值域范围划为两半
c++
struct Num {
int p, x;
}; // 位于数列中第 p 项的数的值为 x
struct Query {
int l, r, k, id;
}; // 一个编号为 id, 询问 [l,r] 中第 k 小数的询问
int ans[N];
void add(int p, int x); // 树状数组, 在 p 位置加上 x
int query(int p); // 树状数组, 求 [1,p] 的和
void clear(); // 树状数组, 清空
void solve(int l, int r, vector<Num> a, vector<Query> q)
// a中为给定数列中值在值域区间 [l,r] 中的数
{
int m = (l + r) / 2;
if (l == r) {
for (unsigned i = 0; i < q.size(); i++) ans[q[i].id] = l;
return;
}
vector<Num> a1, a2;
vector<Query> q1, q2;
for (unsigned i = 0; i < a.size(); i++)
if (a[i].x <= m)
a1.push_back(a[i]), add(a[i].p, 1);
else
a2.push_back(a[i]);
for (unsigned i = 0; i < q.size(); i++) {
int t = query(q[i].r) - query(q[i].l - 1);
if (q[i].k <= t)
q1.push_back(q[i]);
else
q[i].k -= t, q2.push_back(q[i]);
}
clear();
solve(l, m, a1, q1), solve(m + 1, r, a2, q2);
return;
}
进阶:给定一个数列,要支持单点修改,区间查第 k 小。
修改操作可以直接理解为从原数列中删去一个数再添加一个数
可将所有操作存于一个数组,用标识区分类型,依次处理每个操作
c++
struct Opt {
int x, y, k, type, id;
// 对于询问, type = 1, x, y 表示区间左右边界, k 表示询问第 k 小
// 对于修改, type = 0, x 表示修改位置, y 表示修改后的值,
// k 表示当前操作是插入(1)还是擦除(-1), 更新树状数组时使用.
// id 记录每个操作原先的编号, 因二分过程中操作顺序会被打散
};
Opt q[N], q1[N], q2[N];
// q 为所有操作,
// 二分过程中, 分到左边的操作存到 q1 中, 分到右边的操作存到 q2 中.
int ans[N];
void add(int p, int x);
int query(int p); // 树状数组函数, 含义见题3
void solve(int l, int r, int L, int R)
// 当前的值域范围为 [l,r], 处理的操作的区间为 [L,R]
{
if (l > r || L > R) return;
int cnt1 = 0, cnt2 = 0, m = (l + r) / 2;
// cnt1, cnt2 分别为分到左边, 分到右边的操作数
if (l == r) {
for (int i = L; i <= R; i++)
if (q[i].type == 1) ans[q[i].id] = l;
return;
}
for (int i = L; i <= R; i++)
if (q[i].type == 1) { // 是询问: 进行分类
int t = query(q[i].y) - query(q[i].x - 1);
if (q[i].k <= t)
q1[++cnt1] = q[i];
else
q[i].k -= t, q2[++cnt2] = q[i];
} else
// 是修改: 更新树状数组 & 分类
if (q[i].y <= m)
add(q[i].x, q[i].k), q1[++cnt1] = q[i];
else
q2[++cnt2] = q[i];
for (int i = 1; i <= cnt1; i++)
if (q1[i].type == 0) add(q1[i].x, -q1[i].k); // 清空树状数组
for (int i = 1; i <= cnt1; i++) q[L + i - 1] = q1[i];
for (int i = 1; i <= cnt2; i++)
q[L + cnt1 + i - 1] = q2[i]; // 将临时数组中的元素合并回原数组
solve(l, m, L, L + cnt1 - 1), solve(m + 1, r, L + cnt1, R);
return;
}
补充 main 函数,能过 P2617 Dynamic Rankings
c++
int n, m, cnt;
int main()
{
cin >> n >> m;
int l, r, k, x, y;
char op;
int cnt = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
q[cnt ++] = {i, a[i], 1, 0, 0};
}
for (int i = 1; i <= m; i++)
{
cin >> op;
if (op == 'Q')
{
cin >> l >> r >> k;
q[cnt ++] = {l, r, k, 1, i};
}
else
{
cin >> x >> y;
q[cnt ++] = {x, a[x], -1, 0, 0};
a[x] = y;
q[cnt ++] = {x, y, 1, 0, 0};
}
}
for (int i = 1; i <= m; i++) ans[i] = -1;
solve(0, 1e9, 0, cnt - 1);
for (int i = 1; i <= m; i++)
if (ans[i] > -1) cout << ans[i] << endl;
return 0;
}
区间修改的题:P3332 [ZJOI2013] K大数查询
1 l r c
:表示将 c 加入到编号在 [l,r] 内的集合中2 l r c
:表示查询编号在 [l,r] 内的集合的并集中,第 c 大的数是多少。
因为是区间插入,所以把树状数组换成线段树,然后把第 c 大变成第 c 小,套用模板就行
c++
void solve(int l, int r, int L, int R)
// 当前的值域范围为 [l,r], 处理的操作的区间为 [L,R]
{
if (l > r || L > R) return;
int cnt1 = 0, cnt2 = 0, m = (l + r) / 2;
// cnt1, cnt2 分别为分到左边, 分到右边的操作数
if (l == r) {
for (int i = L; i <= R; i++)
if (q[i].type == 2) ans[q[i].id] = l;
return;
}
for (int i = L; i <= R; i++)
if (q[i].type == 2) { // 是询问: 进行分类
LL t = query(1, q[i].l, q[i].r); // 线段树的查询
if (q[i].c <= t)
q1[++cnt1] = q[i];
else
q[i].c -= t, q2[++cnt2] = q[i];
} else
// 是修改: 更新线段树 & 分类
if (q[i].c <= m)
update(1, q[i].l, q[i].r, 1), q1[++cnt1] = q[i];
else
q2[++cnt2] = q[i];
for (int i = 1; i <= cnt1; i++)
if (q1[i].type == 1) update(1, q1[i].l, q1[i].r, -1); // 清空线段树
for (int i = 1; i <= cnt1; i++) q[L + i - 1] = q1[i];
for (int i = 1; i <= cnt2; i++)
q[L + cnt1 + i - 1] = q2[i]; // 将临时数组中的元素合并回原数组
solve(l, m, L, L + cnt1 - 1), solve(m + 1, r, L + cnt1, R);
return;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
scanf("%d%d%d%lld", &q[i].type, &q[i].l, &q[i].r, &q[i].c);
q[i].id = i;
if (q[i].type == 1) num[++ cnt] = q[i].c;
ans[i] = -1;
}
sort(num + 1, num + 1 + cnt);
cnt = unique(num + 1, num + 1 + cnt) - num - 1;
for (int i = 1; i <= m; i++)
if (q[i].type == 1)
{
q[i].c = lower_bound(num + 1, num + 1 + cnt, q[i].c) - num; // 离散化
q[i].c = cnt - q[i].c + 1; //这里将第k大转化为第k小,在最后输出中再换回来
}
build(1, 1, n);
solve(1, cnt, 1, m);
for (int i = 1; i <= m; i++)
if (ans[i] != -1) printf("%lld\n", num[cnt - ans[i] + 1]);
return 0;
}