Y
K
N
U
F

题解 P3380【模板】树套树(莫队套分块)

link

莫队 值域分块

一点点前情提要:

我写完后本来以为这种写法非常好想且普遍,后来经某同学指点突然发现这道题目的题解区并没有这样的写法,故作为一点补充写作此题解。

正文:

首先要注意到两件事:

  1. 这道题所询问的东西非常有趣,如果不考虑区间的问题,我们可以使用权值线段树来做,这是显然的(所以莫队套权值线段树也可以做应该)。不过这里用的是一个类似的东西,它的名字是值域分块。
  2. 我们考虑到区间问题,那么伟大的莫队自然也就应该被想到,区间查询和单点修改,那不就是带修莫队么?

想到了这两个东西,我们就考虑怎么把他们拼起来。


算法介绍:

首先我们要使用值域分块这个东西,显然是要离散化的,这样子才能放下整个值域,这一步比较独立,就写在这里了。

\(c\) 为不同的数字的总数。离散化复杂度为 \(O(c \log c)\)


接下来我们先不管区间,看看值域分块的操作:

  1. 查询 \(k\) 的排名,直接根据 \(k\) 在离散后的值,对 \([0,k]\) 区间进行求和即可,分块后复杂度为 \(O(\sqrt c)\)
  2. 查询排名为 \(k\) 的值,我们可以从 \(0\) 位置开始按块往后累加,直到超过 \(k\) 再换成挨个数字的增加,复杂度为 \(O(\sqrt c)\)
  3. 修改,按照 \(k\) 离散后的值对这个值的次数以及这个值所在块的总次数都减一或加一就可以,复杂度为 \(O(1)\)
  4. 查询前驱,有两种方法,一种是我写的,利用前面写过的 \(1\)\(2\) 操作,先查询 \(k-1\) 的排名,然后查询这个排名上的数字就可以了。还有一种是先从 \(k\) 往前按每个数遍历完它的所在块,如果有数就直接返回,如果没有再按块往前遍历, 直到第一个值不为 \(0\) 的块,再对这个块内部遍历,遇到第一个不为 \(0\) 的数就返回。这两种写法复杂度都是 \(O(\sqrt c)\)
  5. 查询后继,与上一个操作类似,不再赘述。

值域分块的总复杂度为 \(O(m \sqrt{c})\)


然后我们就可以给它套上莫队了。

值域分块的问题在于只能考虑整体,就可以用莫队来维护区间,我们不妨就利用莫队让每一个区间成为一个暂时的“整体”。
实现的具体思路如下:

  • 既然我们要的是区间,那就可以简单的在改变莫队遍历到的 \(l\)\(r\) 时对值域分块进行操作 \(3\),这样子对于值域分块要考虑的就是整体问题了。
  • 其次是修改操作的问题,正如刚才所说,写成带修莫队就可以。这一步不算难,就不赘述了。

莫队的复杂度是 \(O(n^{\frac 5 3})\)


正确性证明:

综上所述,总复杂度为 \(O(m \sqrt c + n^{\frac 5 3})\)。显然足以通过此题目。
话说这两项大小差不多吧。


代码实现:

细节都在注释里面了,代码较长,耐心阅读。

// code by 樓影沫瞬_Hz17
#include <bits/extc++.h>
using namespace std;
const int N = 120010;
inline int in() { // 快读
    int n = 0, p = getchar_unlocked();
    while (p < '-')p = getchar_unlocked();
    bool f = p == '-' ? p = getchar_unlocked() : 0;
    do n = n * 10 + (p ^ 48), p = getchar_unlocked();
    while (isdigit(p));
    return f ? -n : n;
}
inline int in(int &a) { return a = in(); }
struct Query { // 莫队要用的结构体,存储问题
    int l, r, k, t, id, opt; // 比较特别的是要储存操作的类型 就是 opt
};
struct Change { // 带修莫队要用的结构体
    int pos, k;
};
Query q[N];
Change c[N]; 
int pos[N]/*对于莫队的所在块数组,对应的是原数组*/, 
    belong[N]/*值域分块的所在块数组,对应的是值域*/,
    a[N], b[N],/*代表原数组和离散化数组*/ 
    cnt[N], sum[5000], /*值域分块的单体计数和块计数*/
    l[5000], r[5000], /*值域分块的每个块的起始和结束*/
    cntc /*操作计数*/, cntq /*询问计数*/, cntv /*值域计数*/, 
    ans[N];/*就是答案*/
inline int kth(int k) { // 得到排名为 k 的数
    int i = 1;
    for (; k - sum[i] > 0; i++)
        k -= sum[i];
    for (int j = l[i];; j++) {
        k -= cnt[j];
        if (k <= 0) return j;
    }
}
inline int th(int s) { // 得到某个数的排名
    int res = 0, i = 1;
    for (; i < belong[s]; i++)
        res += sum[i];
    for (int j = l[i]; j <= s; j++)
        res += cnt[j];
    return res;
}
int n, m;
int main() {
    cntv = in(n), in(m);
    for (int i = 1; i <= n; i++) b[i] = a[i] = in(); 
    Query t1;
    Change t2;
    for (int i = 1; i <= m; i++) {
        in(t1.opt);
        switch (t1.opt) {
            case 3:
                in(t2.pos), b[++cntv] = t2.k = in();
                c[++cntc] = t2;
            break;
            default:
                in(t1.l), in(t1.r), in(t1.k), t1.t = cntc, t1.id = ++cntq;
                if (t1.opt != 2)b[++cntv] = t1.k;
                q[cntq] = t1;
            break;
        }
    }
    // 以上是读入
    sort(b + 1, b + 1 + cntv);
    cntv = unique(b + 1, b + 1 + cntv) - b - 1;
    for (int i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + 1 + cntv, a[i]) - b;
    for (int i = 1; i <= cntq; i++) if (q[i].opt != 2)q[i].k = lower_bound(b + 1, b + 1 + cntv, q[i].k) - b;
    for (int i = 1; i <= cntc; i++) c[i].k = lower_bound(b + 1, b + 1 + cntv, c[i].k) - b;
    // 简单好作离散化
    int len = sqrt(cntv), // 这个是值域分块的块长
        block = pow(n, 0.7), // 莫队块长 由于修改比较少其实再大点更好
        kl = (cntv - 1) / len + 1; // 值域分块块数
    for (int i = 1; i <= cntv; i++) pos[i] = (i - 1) / block + 1;
    sort(q + 1, q + 1 + cntq, [](const Query & a, const Query & b) { return pos[a.l] == pos[b.l] ? pos[a.r] == pos[b.r] ? a.t < b.t : a.r < b.r : a.l < b.l;});
    for (int i = 1; i <= kl; i++) {
        l[i] = (i - 1) * len + 1;
        r[i] = min(i*len, cntv);
        for (int j = l[i]; j <= r[i]; j++)
            belong[j] = i;
    }
    // 以上是两个块的预处理部分
    int l = 1, r = 0, t = 0, tem;
    for (int i = 1; i <= cntq; i++) {
        while (l < q[i].l) cnt[a[l]]--, sum[belong[a[l++]]]--;
        while (l > q[i].l) cnt[a[--l]]++, sum[belong[a[l]]]++;
        while (r > q[i].r) cnt[a[r]]--, sum[belong[a[r--]]]--;
        while (r < q[i].r) cnt[a[++r]]++, sum[belong[a[r]]]++;
        while (t < q[i].t) {
        ++t;
        if (q[i].l <= c[t].pos && c[t].pos <= q[i].r) {
                cnt[a[c[t].pos]]--, sum[belong[a[c[t].pos]]]--;
                cnt[c[t].k]++, sum[belong[c[t].k]]++;
            }
            swap(a[c[t].pos], c[t].k);
        }
        while (t > q[i].t) {
            if (q[i].l <= c[t].pos && c[t].pos <= q[i].r) {
                cnt[a[c[t].pos]]--, sum[belong[a[c[t].pos]]]--;
                cnt[c[t].k]++, sum[belong[c[t].k]]++;
            }
            swap(a[c[t].pos], c[t].k), t--;
        }
                // 以上是带修莫队操作
        switch (q[i].opt) {
            case 1:
                ans[q[i].id] = th(q[i].k - 1) + 1;
            break;
            case 2:
                ans[q[i].id] = b[kth(q[i].k)];
            break;
            case 4:
                tem = th(q[i].k - 1);
                if (tem) ans[q[i].id] = b[kth(tem)];
                else ans[q[i].id] = -2147483647;
            break;
            case 5:
                tem = th(q[i].k) + 1;
                if (tem <= (q[i].r - q[i].l + 1)) ans[q[i].id] = b[kth(tem)];
                else ans[q[i].id] = 2147483647;
            break;
        }
        // 以上是值域分块操作
    }
    for (int i = 1; i <= cntq; i++)
        cout << ans[i] << endl;
    // 输出答案说是
    return 0;
}
// 看到这里了点个赞么?
posted @ 2025-08-15 20:48  樓影沫瞬_Hz17  阅读(33)  评论(0)    收藏  举报