Acwing2306 K大数查询 题解
题面
传送门
有 \(N\) 个位置,\(M\) 个操作。每个位置可以同时存储多个数。
操作有两种,每次操作:
- 如果是
1 a b c
的形式,表示在第 \(a\) 个位置到第 \(b\) 个位置,每个位置加入一个数 \(c\)。 - 如果是
2 a b c
的形式,表示询问从第 \(a\) 个位置到第 \(b\) 个位置,第 \(c\) 大的数是多少。
\(1≤N,M≤50000\),所有数在int范围内,保证操作合法。
题解
我们很容易可以想到一种树套树的做法。先用一棵线段树维护位置,每个节点再用一个“集合”维护这段位置区间内的所有数。由于题目“每个位置加入一个数”的性质,平衡树不好操作,而权值线段树可以。先将数离散化,总共不超过50000个,配合动态开点,空间足够。权值线段树中“下标”为数字,“权值”为其在当前区间内出现的次数。
于是,1操作在外层树中区间修改,在内层树中单点修改,复杂度 \(O(\log^2{N})\)(实际不成立)。2操作可以采用二分,转化为判断大于 \(x\) 的在 [a,b] 区间内的数的个数。那么外层树区间为 [a,b],内层为 [x+1,+∞]。复杂度\(O(\log^3{N})\)。
然而,这种方法是错误的。错误的地方在于外层树的区间修改。
在线段树的基本操作中,区间修改原本是需要\(O(N)\)才能实现的。但我们引入了懒惰标记,其实际意义在于:这次的区间修改可能不被查询,留到需要用时再更新。另外,如果每次的懒惰标记不满足“加”的性质,复杂度仍然很高。举个例子,第一次向[1,n]区间每个位置加入1,[1,n]的懒惰标记为1,;第二次向[1,n]每个位置加入2,[1,n]的懒惰标记为1和2……\(M\)次操作后再查询,因为每次向下传递都需要\(O(M)\),复杂度为\(O(M\log{N})\)。而我们回忆最基础的区间求和,其懒惰标记可以相加,那么后面的懒惰标记会“继承”前面的懒惰标记,使得每次向下传递仅为\(O(1)\),故实现了\(O(\log{N})\)。另外,对于“将区间内所有数修改为\(x\)”这样的操作,后面的懒惰标记覆盖了前面的懒惰标记,我们也称其为满足“加”的性质。
在这题中,外层树的懒惰标记是“加入的数字”,显然不满足“加”的性质。
实际上,树套树的外层树一般可以单点修改而无法区间修改。因为每个外层树节点都套着一棵内层树。为什么我们要用内层树而不是普通的变量(如区间和sum,区间最值min,max)?还不是因为每个节点维护的信息不满足“加”的性质,所以不得不用内层树存储每次操作的信息!那么此时,也自然不能在外层树上使用懒惰标记了。
那么这题该如何解决呢?还是树套树,不过我们将原本的内层树换到外层。外层是权值线段树,每个节点再开一棵线段树,下标为位置,权值为出现次数。那么,1操作变成外层树单点修改,内层树区间修改,可以实现了。
线段树小技巧——标记持久化
话说这标记持久化,当初在学扫描线时也出现过。不过当时没有系统的概念,又是新学线段树,怕打起来和普通的懒惰标记混淆,就没有使用。现在才发现,打起来太香了……只需加入短短一行代码,不仅免去了pushdown,效率还快了很多(实测本题:普通懒惰标记6013ms,标记持久化3958ms)。
何为标记持久化?就是“不下传的懒惰标记”。普通懒惰标记的实际意义为:当前节点更新,但其子节点未更新,且在递归到当前节点时,节点内存真实值。而在使用了标记持久化后,节点内存的值为此节点及其所有子节点修改后的值。换句话说,没有算上其所有父节点(一定包含它)的修改操作。根据这种定义,显然不再需要pushdown,只需要pushup。
单点查询时,“加”(广义的加,可参考上文)上其所有父节点的懒惰标记,再加上它自己存的值即为答案;区间查询时,在递归过程中加上与此区间的交集部分。如在此题中,设此节点为 \(t[p]\),就是加上\(t[p].lazy\)乘上\(t[p]\)与要求区间的交集长度。可以手工推演验证正确性。
当然,如果标记持久化真的那么香,世界上就不会有懒惰标记了。它也有很大的局限性。以下:
- 在要维护的信息很多时,码量很大,也容易出错,不如普通的懒惰标记;
- 仅在“只插入不删除”或“对于所有删除的区间,一定能找到一段一模一样的区间在之前被插入”时适用,如扫描线。否则会很麻烦。
另外,关于此题的查询操作,复杂度为\(O(\log^3{N})\),还是有点大。我们可以不采用二分,而是类似二叉查找树的思想,对于每个节点,先看它的右儿子查询的值是否小于c。若小于,那么c减去查询得到的值,并递归其左儿子;否则递归其右儿子。最后走到L=R时,就是答案。复杂度\(O(\log^2{N})\)。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 5e4 + 5;
const int M = 2e7 + 5;
int n, m, opt[N], a[N], b[N], c[N];
int mp[N], mpt;
int L[N << 2], R[N << 2], RT[N << 2], idx;
struct SegmentTree {
int lc, rc, cnt, add;
} t[M];
int Match(int x) {
return lower_bound(mp + 1, mp + mpt + 1, x) - mp;
}
void Build(int l, int r, int p) {
L[p] = l; R[p] = r;
if (l == r) return ;
int mid = l + r >> 1;
Build(l, mid, p << 1); Build(mid + 1, r, p << 1 | 1);
}
int Intersection(int l1, int r1, int l2, int r2) {
return min(r1, r2) - max(l1, l2) + 1;
}
void Insert(int dl, int dr, int l, int r, int &p) {
if (!p) p = ++idx;
t[p].cnt += Intersection(l, r, dl, dr);
if (dl <= l && r <= dr) {
t[p].add++; return ;
}
int mid = l + r >> 1;
if (dl <= mid) Insert(dl, dr, l, mid, t[p].lc);
if (dr > mid) Insert(dl, dr, mid + 1, r, t[p].rc);
}
int Query(int dl, int dr, int l, int r, int &p) {
if (!p) p = ++idx;
if (dl <= l && r <= dr) return t[p].cnt;
int mid = l + r >> 1, res = t[p].add * Intersection(l, r, dl, dr);
if (dl <= mid) res += Query(dl, dr, l, mid, t[p].lc);
if (dr > mid) res += Query(dl, dr, mid + 1, r, t[p].rc);
return res;
}
void Change(int x, int dl, int dr, int p) {
Insert(dl, dr, 1, n, RT[p]);
if (L[p] == R[p]) return ;
int mid = L[p] + R[p] >> 1;
if (x <= mid) Change(x, dl, dr, p << 1);
else Change(x, dl, dr, p << 1 | 1);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d%d%d", &opt[i], &a[i], &b[i], &c[i]);
if (opt[i] == 1) mp[++mpt] = c[i];
}
sort(mp + 1, mp + mpt + 1);
mpt = unique(mp + 1, mp + mpt + 1) - (mp + 1);
Build(1, mpt, 1);
for (int i = 1; i <= m; i++) {
if (opt[i] == 1) {
c[i] = Match(c[i]);
Change(c[i], a[i], b[i], 1);
}
else {
int p = 1;
while (L[p] != R[p]) {
int s = Query(a[i], b[i], 1, n, RT[p << 1 | 1]);
if (s < c[i]) {
c[i] -= s; p = p << 1;
}
else p = p << 1 | 1;
}
printf("%d\n", mp[L[p]]);
}
}
return 0;
}