从分块到线段树(一)

序列分块

在线回答区间询问


序列分块

个东西从左到右排成一行,编号 ,就是一个序列

选择一个正整数 ,把这个序列每 个分成一(block)。称 块长

序列里编号 的元素所在的块的编号是

数列分块入门 1

给你一个长为 的整数序列 。处理 个操作。操作有两类:

  • 0 l r c:把 每个都加上
  • 1 l r c:询问 的值。(忽略
限制
  • 的初始值,,每次修改过后的 都在 long long 范围内。

,对序列 进行分块。

如果一次区间加操作会把第 块里的每个数都加上 ,那么我们并不去修改这个块里数,而是把这个操作记录下来。

具体地,我们对第 块维护一个变量 add[i],表示区间加操作对这一块整体加的数的总和。

如果一次区间加操作只涉及第 块里的一部分元素,那么我们就去逐个修改相关的元素。
注意到这样的块在区间两端,最多两个,需要修改的元素至多 个。

个数的真实值就是 a[j] + add[j / B]

处理区间加操作的时间是 ,回答询问的时间是

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    vector<long long> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;
    vector<long long> add(NB);
    int q = n;
    while (q--) {
        int type, l, r; long long c;
        cin >> type >> l >> r >> c;
        if (type == 0) {
            --l;
            int lb = (l + B - 1) / B;
            int rb = r / B;
            if (lb > rb)
                for (int i = l; i < r; i++) {
                    a[i] += c;
                }
            else {
                for (int i = l; i < lb * B; i++)
                    a[i] += c;
                for (int i = lb; i < rb; i++)
                    add[i] += c;
                for (int i = rb * B; i < r; i++)
                    a[i] += c;
            }
        } else {
            r--;
            cout << a[r] + add[r / B] << '\n';
        }
    }
}
  • 序列下标总是从 开始。
  • 总是用左闭右开的方式表示区间。
  • NB 是块数。

l = r =
lb = rb =

对于区间 整块边块

一般的,[lb, rb) 是整块,lb - 1 和 rb 是边块。

线段树:多层次分块

center

对于区间 来说, 是整块,但是它们的父节点不是整块(而是边块),我们把这样的整块称为极大整块

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    vector<long long> a(2 * n);
    for (int i = 0; i < n; i++)
        cin >> a[i + n];

    int q = n;
    while (q--) {
        int type, l, r; long long c;
        cin >> type >> l >> r >> c;
        if (type == 0) {
            l--;
            l += n;
            r += n;
            while (l < r) {
                if (l & 1) a[l++] += c;
                if (r & 1) a[--r] += c;
                l /= 2;
                r /= 2;
            }
        } else {
            r--;
            r += n;
            long long ans = 0;
            while (r > 0) {
                ans += a[r];
                r /= 2;
            }
            cout << ans << '\n';
        }
    }
}
  • 处理区间加操作的时间:
  • 回答询问的时间:

数列分块入门 2

给你一个长为 的整数序列 。处理 个操作。操作有两类:

  • 0 l r c:把 每个都加上
  • 1 l r c:询问 中小于 的数的个数。
限制
  • 的初始值, 在 int 范围内。

分块

  • 对每个块 ,维护对块 的整体加的操作所加的数的总和 add[i]
  • 维护一个长为 的数组 a
    对于每个 都有 等于 a[j] + add[j / B]

为了回答 中小于 的数的个数,

  • 每个块 ,维护块内元素对应的那些 a[j]有序序列 sorted[i]

区间加

  • 对于整块 ,更新 add[i]
  • 对于边块 ,首先更新其中被修改的元素 对应的 a[j],然后重建 的有序列表 sorted[i](重新排序)。

时间:

区间查询

  • 对于整块 ,在 sorted[i] 里做二分查找。
  • 对于边块,逐个检查涉及的元素。

时间:

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n; cin >> n;
    vector<long long> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];
    
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;
    
    vector<vector<long long>> b(NB); // b 即 sorted
    vector<long long> add(NB);

    auto build = [&](int i) {
        int l = i * B, r = min(l + B, n);
        b[i].assign(a.begin() + l, a.begin() + r);
        sort(b[i].begin(), b[i].end());
    };

    for (int i = 0; i < NB; i++)
        build(i);
    int q = n;
    while (q--) {
        int type, l, r; long long c;
        cin >> type >> l >> r >> c;
        l--;
        int lb = (l + B - 1) / B;
        int rb = r / B;
        if (type == 0) {
            if (lb > rb) {
                for (int i = l; i < r; i++)
                    a[i] += c;
                build(lb - 1);
            } else {
                for (int i = l; i < lb * B; i++)
                    a[i] += c;
                for (int i = rb * B; i < r; i++)
                    a[i] += c;
                for (int i = lb; i < rb; i++)
                    add[i] += c;
                if (l != lb * B)
                    build(lb - 1);
                if (r != rb * B)
                    build(rb);
            }
        } else {
            c *= c;
            int ans = 0;
            if (lb > rb) {
                for (int i = l; i < r; i++)
                    ans += (a[i] + add[lb - 1]) < c;
            } else {
                for (int i = l; i < lb * B; i++)
                    ans += (a[i] + add[lb - 1]) < c;
                for (int i = rb * B; i < r; i++)
                    ans += (a[i] + add[rb]) < c;
                for (int i = lb; i < rb; i++)
                    ans += lower_bound(b[i].begin(), b[i].end(), c - add[i]) - b[i].begin();
            }
            cout << ans << '\n';
        }
    }
}

一个优化

在处理区间加时,把边块重新排序,可以通过归并来实现。

对于边块来说,被加上 的是其中的一段元素。修改之后,那一段仍是有序的。边块里没被修改的那些元素也是有序的。

数列分块入门 3

给你一个长为 的整数序列 。处理 个操作。操作有两类:

  • 0 l r c:把 每个都加上
  • 1 l r c:询问 中小于 的最大的数。若不存在小于 的数,输出 -1。
限制
  • 的初始值,,每次操作后的 都在 int 范围内。

解法与《数列分块入门 2》类似。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n; cin >> n;
    vector<long long> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];
    
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;

    vector<vector<long long>> b(NB);
    vector<long long> add(NB);

    auto build = [&](int i) {
        int l = i * B, r = min(l + B, n);
        b[i].assign(a.begin() + l, a.begin() + r);
        sort(b[i].begin(), b[i].end());
    };

    for (int i = 0; i < NB; i++)
        build(i);
    int q = n;
    while (q--) {
        int type, l, r, c;
        cin >> type >> l >> r >> c;
        l--;
        int lb = (l + B - 1) / B;
        int rb = r / B;
        if (type == 0) {
            if (lb > rb) {
                for (int i = l; i < r; i++)
                    a[i] += c;
                build(lb - 1);
            } else {
                for (int i = l; i < lb * B; i++)
                    a[i] += c;
                for (int i = rb * B; i < r; i++)
                    a[i] += c;
                for (int i = lb; i < rb; i++)
                    add[i] += c;
                if (l != lb * B)
                    build(lb - 1);
                if (r != rb * B)
                    build(rb);
            }
        } else {
            long long ans = LLONG_MIN;
            if (lb > rb) {
                for (int i = l; i < r; i++)
                    if (a[i] + add[lb - 1] < c)
                        ans = max(ans, a[i] + add[lb - 1]);
            } else {
                for (int i = l; i < lb * B; i++)
                    if (a[i] + add[lb - 1] < c)
                        ans = max(ans, a[i] + add[lb - 1]);
                for (int i = rb * B; i < r; i++) {
                    if (a[i] + add[rb] < c)
                        ans = max(ans, a[i] + add[rb]);
                }
                for (int i = lb; i < rb; i++) {
                    auto it = lower_bound(b[i].begin(), b[i].end(), c - add[i]);
                    if (it != b[i].begin())
                        ans = max(ans, *prev(it) + add[i]);
                }
            }
            if (ans == LLONG_MIN)
                ans = -1;
            cout << ans << '\n';
        }
    }
}

数列分块入门 4

给你一个长为 的整数序列 。处理 个操作,操作有两类:

  • 0 l r c:把 每个都加上
  • 1 l r c:输出 除以 的余数。保证
限制
  • 的初始值,,每次操作后的 都在 int 范围内。

分块

  • 对每个块 ,维护对块 的整体加的操作所加的数的总和 add[i]
  • 维护一个长为 的数组 a
    对于每个 都有 等于 a[j] + add[j / B]
  • 对每个块 ,维护块内元素对应的那些 a[j],维护 sum[i]

区间加

  • 对于整块 ,更新 add[i]
  • 对于边块 ,更新其中被修改的元素 对应的 a[j],更新 sum[i]

时间:

查询区间和

  • 对于整块 ,其中元素的和等于 sum[i] + add[i] * B
  • 对于边块,把涉及的元素逐个加起来。

时间:

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    vector<long long> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];
    
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;

    vector<long long> add(NB);
    vector<long long> sum(NB);
    for (int i = 0; i < n; i++)
        sum[i / B] += a[i];
    int q = n;
    while (q--) {
        int type, l, r, c;
        cin >> type >> l >> r >> c;
        l--;
        int lb = (l + B - 1) / B;
        int rb = r / B;
        if (type == 0) {
            if (lb > rb) {
                for (int i = l; i < r; i++) {
                    a[i] += c;
                    sum[lb - 1] += c;
                }
            } else {
                for (int i = l; i < lb * B; i++) {
                    a[i] += c;
                    sum[lb - 1] += c;
                }
                for (int i = rb * B; i < r; i++) {
                    a[i] += c;
                    sum[rb] += c;
                }
                for (int i = lb; i < rb; i++)
                    add[i] += c;
            }
        } else {
            long long ans = 0;
            if (lb > rb)
                for (int i = l; i < r; i++)
                    ans += a[i] + add[lb - 1];
            else {
                for (int i = l; i < lb * B; i++)
                    ans += a[i] + add[lb - 1];
                for (int i = rb * B; i < r; i++)
                    ans += a[i] + add[rb];
                for (int i = lb; i < rb; i++)
                    ans += sum[i] + add[i] * B;
            }
            ans %= c + 1;
            if (ans < 0) ans += c + 1;
            cout << ans << '\n';
        }
    }
}

用线段树处理「区间加,查询区间和」

center

对于每个块 ,维护两个数据

  • 标记:块 被整体加的数的总和 add[i]
  • :块 里的数之和 sum[i]

center

  1. 按从上到下的顺序,下传边块()的标记 add[j]
  2. 更新极大整块()的标记和值。
  3. 按从下到上的顺序,更新边块的值。

查询 的和

center

  1. 按从上到下的顺序,下传边块()的标记。
  2. 查询极大整块()的值。

找边块

center

对于区间 来说,边块是

  • 的那些不全在自己右边的祖先,即
  • 的那些不全在自己右边的祖先,即

center

节点(也就是块) 的某个祖先 不全在 右边。也就是说从节点 走到祖先 不全是往右上走的。也就是从说从 ,节点编号不是一直除以 的。

center

的第 个祖先,那么 等于 i >> k

走到祖先 不全是往右上走的,

  • 也就是说 的二进制写法的末尾没有 个连续的零。
  • 也就是说 i >> k << k != i

线段树有多少层

center

  • 长为 的序列的线段树有 层。如果不算最后一层,有 层。
  • 最上面那一层可能用不到。
  • 是正整数 的二进制写法的位数。计算方法
    • 32 - __builtin_clz(n) clz:count leading zero
    • bit_width((unsigned) n) since C++20

用线段树处理「区间加,查询区间和」

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n; cin >> n;

    vector<long long> add(2 * n), sum(2 * n);

    for (int i = 0; i < n; i++)
        cin >> sum[i + n];
    for (int i = n - 1; i > 0; i--)
        sum[i] = sum[2 * i] + sum[2 * i + 1];

    int LOG = bit_width((unsigned) n);

    auto apply = [&](int p, long long c, int len) {
        add[p] += c;
        sum[p] += c * len;
    };

    auto update = [&](int p) {
        sum[p] = sum[p * 2] + sum[p * 2 + 1];
    };

    auto push = [&](int p, int len) {
        apply(p * 2, add[p], len / 2);
        apply(p * 2 + 1, add[p], len / 2);
        add[p] = 0;
    };
    int q = n;
    while (q--) {
        int type, l, r, c;
        cin >> type >> l >> r >> c;
        l--;
        l += n; r += n;
        if (type == 0) {
            for (int i = LOG - 1; i >= 1; i--) {
                if (l >> i << i != l) push(l >> i, 1 << i);
                if (r >> i << i != r) push(r >> i, 1 << i);
            }
            {
                int l2 = l, r2 = r;
                int len = 1;
                while (l < r) {
                    if (l & 1) apply(l++, c, len);
                    if (r & 1) apply(--r, c, len);
                    l >>= 1; r >>= 1;
                    len *= 2;
                }
                l = l2; r = r2;
            }
            for (int i = 1; i < LOG; i++) {
                if (l >> i << i != l) update(l >> i);
                if (r >> i << i != r) update(r >> i);
            }
        } else {
            for (int i = LOG - 1; i >= 1; i--) {
                if (l >> i << i != l) push(l >> i, 1 << i);
                if (r >> i << i != r) push(r >> i, 1 << i);
            }
            long long ans = 0;
            while (l < r) {
                if (l & 1) ans += sum[l++];
                if (r & 1) ans += sum[--r];
                l >>= 1; r >>= 1;
            }
            ans %= c + 1;
            if (ans < 0) ans += c + 1;
            cout << ans << '\n';
        }
    }
}

数列分块入门 5

给你一个长为 的整数序列 。处理 个操作,操作有两类:

  • 0 l r c:把 每个都开平方。也就是把 变成 )。
  • 1 l r c:输出
限制

开平方,开 5 次平方之后就会得到

int x = INT_MAX;
while (x > 1) {
    x = sqrt(x);
    cout << x << '\n';
}

输出

46340
215
14
3
1

分块

  • 对每个块
    • 维护一个列表 big[i],是这一块中大于 的元素的下标。
    • 维护其中的元素之和 sum[i]
  • 维护一个长为 的数组 a,即序列

区间开平方

  • 对于整块 ,枚举列表 big[i] 中的下标 ,把 开平方。若 变成 ,则把 big[i] 中删除。
  • 对于边块 ,把涉及的元素 开平方。即使 从大于 变成 ,也不把 big[i] 中删除。

一个元素在整块里被开平方至多 5 次。

查询区间和

  • 对于整块 ,其中元素的和等于 sum[i]
  • 对于边块,把涉及的元素逐个加起来。

时间:

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    vector<long long> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];
    
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;

    vector<vector<int>> big(NB);
    vector<long long> sum(NB);
    for (int i = 0; i < n; i++) {
        sum[i / B] += a[i];
        if (a[i] > 1)
            big[i / B].push_back(i);
    }

    auto update = [&](int i) {
        int x = (int) sqrt(a[i]);
        sum[i / B] += x - a[i];
        a[i] = x;
    };
    int q = n;
    while (q--) {
        int type, l, r, c;
        cin >> type >> l >> r >> c;
        if (l > r) swap(l, r);
        l--;
        int lb = (l + B - 1) / B;
        int rb = r / B;
        if (type == 0) {
            if (lb > rb)
                for (int i = l; i < r; i++) update(i);
            else {
                for (int i = l; i < lb * B; i++) update(i);
                for (int i = rb * B; i < r; i++) update(i);
                for (int i = lb; i < rb; i++)
                    for (int j = 0; j < (int) big[i].size(); ) {
                        update(big[i][j]);
                        if (a[big[i][j]] == 1) {
                            swap(big[i][j], big[i].back());
                            big[i].pop_back();
                        } else {
                            j++;
                        }
                    }
            }
        } else {
            long long ans = 0;
            if (lb > rb)
                for (int i = l; i < r; i++) ans += a[i];
            else {
                for (int i = l; i < lb * B; i++) ans += a[i];
                for (int i = rb * B; i < r; i++) ans += a[i];
                for (int i = lb; i < rb; i++) ans += sum[i];
            }
            cout << ans << '\n';
        }
    }
}

数列分块入门 6

给你一个长为 的整数序列 。处理 个操作,操作有两类:

  • 0 l r:在 之前插入
  • 1 c:询问 的值。
限制
  • 在 int 范围内。
  • 操作时序列 的长度。
  • 测试数据随机生成:先随机等概率地生成类型(0 或 1),其余询问参数在所有合法的值中随机等概率地抽取。

序列 的最终长度的期望值是

我们取块长 ,对初始的序列 分块。
把每一块存储在一个 vector<int> 里。

在位置 插入元素

  1. 找到 应该插入到哪一块。
  2. 插入到那一块,用 vector::insert()
  3. 如果那一块的长度达到 ,就把它分成两个长为 的块。

这个数据结构叫作块状链表

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n; cin >> n;
    
    int B = 333;
    int NB = (n + B - 1) / B;

    vector<vector<int>> b(NB);

    for (int i = 0; i < n; i++) {
        int x; cin >> x;
        b[i / B].push_back(x);
    }
    int q = n;
    while (q--) {
        int type; cin >> type;
        if (type == 0) {
            int p, x; cin >> p >> x;
            p--;
            for (int i = 0; i < (int) b.size(); i++) {
                if (p <= (int) b[i].size()) {
                    b[i].insert(b[i].begin() + p, x);
                    if ((int) b[i].size() == 2 * B) {
                        vector<int> t(b[i].begin() + B, b[i].end());
                        b.insert(b.begin() + i + 1, t);
                        b[i].resize(B);
                    }
                    break;
                }
                p -= (int) b[i].size();
            }
        } else {
            int p; cin >> p;
            p--;
            for (int i = 0; i < (int) b.size(); i++) {
                if (p < (int) b[i].size()) {
                    cout << b[i][p] << '\n';
                    break;
                }
                p -= (int) b[i].size();
            }
        }
    }
}

数列分块入门 7

给你一个长为 的整数序列 。处理 个操作,操作有三类:

  • 0 l r c:把 每个都加
  • 1 l r c:把 每个都乘
  • 1 l r c:询问 的值模 (忽略 )。
限制
  • 在 int 范围内。

分块

对每个块 ,我们维护

  • 标记:对它的整体修改操作 tag[i]

tag[i] = ,表示对块 里的每个数先乘以 ,再加上

整体加 可表示为 ,整体乘 可表示为

先做 再做 ,合起来可表示为
这叫作操作的复合合成

另外我们维护一个数组 a,表示序列 ,但不需要实时更新。

区间修改

对于整块 ,更新 tag[i]

对于边块 ,把 tag[i] 下传到数组 a,然后逐个修改当前块内涉及的元素。

单点查询

tag[i / B] 作用到 a[i],就得到当前的

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    const int mod = 10007;
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        a[i] %= mod;
        if (a[i] < 0) a[i] += mod;
    }
    struct F {
        int mul = 1;
        int add = 0;
    };

    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;
    vector<F> lz(NB); // lz 是 lazy 的简写


    auto push = [&](int ib) {
        for (int i = ib * B; i < min(n, (ib + 1) * B); i++)
            a[i] = (a[i] * lz[ib].mul + lz[ib].add) % mod;
        lz[ib] = {1, 0};
    };

    int q = n;
    while (q--) {
        int type, l, r, c;
        cin >> type >> l >> r >> c;
        if (type == 2) {
            r--;
            int i = r / B;
            cout << (a[r] * lz[i].mul + lz[i].add) % mod << '\n';
        } else {
            l--;
            int lb = (l + B - 1) / B;
            int rb = r / B;
            c %= mod;
            if (c < 0) c += mod;
            F f;
            if (type == 0) f = {1, c};
            else f = {c, 0};
            if (lb > rb) {
                push(lb - 1);
                for (int i = l; i < r; i++) {
                    a[i] = (a[i] * f.mul + f.add) % mod;
                }
            } else {
                if (l < lb * B) {
                    push(lb - 1);
                    for (int i = l; i < lb * B; i++)
                        a[i] = (a[i] * f.mul + f.add) % mod;
                }
                if (rb * B < r) {
                    push(rb);
                    for (int i = rb * B; i < r; i++)
                        a[i] = (a[i] * f.mul + f.add) % mod;
                }
                for (int i = lb; i < rb; i++) {
                    lz[i].mul = (lz[i].mul * f.mul) % mod;
                    lz[i].add = (lz[i].add * f.mul + f.add) % mod;
                }
            }
        }
    }
}

用线段树处理「区间修改,单点查询」

《数列分块入门 1》是「区间加,单点查询」,这题的修改操作和区间加的区别在于:

  • 加是不讲顺序的而先乘再加是讲顺序的。

例如,“先加 3 再乘 2” 和“先乘 2 再加 3”是不一样的。

复合的结果是 ,而 复合的结果是

我们把不讲顺序的操作称为可交换的,讲顺序的操作称为不可交换的

区间修改

  1. 按从上往下的顺序下传边块的标记。
  2. 把当前操作作用在极大整块上:更新它的标记。

单点查询

从下到上的顺序把 所属的块的标记作用在 的初始值上,结果就是 的当前值。

struct F { 
    int mul = 1;
    int add = 0;
};

F composite(F x, F y) { // 先 y 后 x
    return {x.mul * y.mul % mod, (x.mul * y.add + x.add) % mod};
};

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n; cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        a[i] %= mod;
    }

    vector<F> lz(2 * n);

    auto apply = [&](int i, F f) {
        lz[i] = composite(f, lz[i]);
    };
    auto push = [&](int i) { // 下传标记
        apply(2 * i, lz[i]);
        apply(2 * i + 1, lz[i]);
        lz[i] = {1, 0};
    };

    int LOG = 31 - __builtin_clz(n); // log2(n) 向下取整
    int q = n;
    while (q--) {
        int type, l, r, c;
        cin >> type >> l >> r >> c;
        if (type == 2) {
            int v = a[r - 1];
            int p = r - 1 + n;
            while (p > 0) {
                v = (lz[p].mul * v + lz[p].add) % mod;
                p /= 2;
            }
            if (v < 0) v += mod;
            cout << v << '\n';
        } else {
            c %= mod;
            F f;
            if (type == 0) f = {1, c};
            else f = {c, 0};
            l--;
            l += n; r += n;
            for (int i = LOG; i >= 1; i--) {
                if (l >> i << i != l)
                    push(l >> i);
                if (r >> i << i != r)
                    push(r >> i);
            }
            while (l < r) {
                if (l & 1) apply(l++, f);
                if (r & 1) apply(--r, f);
                l /= 2; r /= 2;
            }
        }
    }
}

数列分块入门 8

给你一个长为 的整数序列 。处理 个操作

  • l r c:先输出 中有多少个 ,然后把 都改为
限制
  • 在 int 范围内。

分块

对每个块 ,维护两个值

  • mark[i]:bool 值,块 最近一次经历的整体赋值有没有下传。
  • val[i]:当 mark[i]true 时,它表示块 被整体赋的值。

区间操作

  • 对于边块 ,先下传标记,然后逐个检查、修改涉及的项。
  • 对于整块 ,如果它有标记,看 val[i] 是否等于 ,然后把 val[i] 改为 ;如果它没有标记,逐个检查每一项,然后打上标记。

每一个块在第一次作整块之后就有了标记,此后它失去标记当且仅当它作边块。
所有块作边块的总次数不超过 ,所以

  • 所有块作为没有标记的整块的总次数不超过
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];
    
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;
    
    vector<bool> mark(NB);
    vector<int> val(NB);

    auto push = [&](int ib) {
        if (mark[ib]) {
            for (int i = ib * B; i < (ib + 1) * B; i++) {
                a[i] = val[ib];
            }
            mark[ib] = false;
        }
    };

    int q = n;
    while (q--) {
        int l, r, c;
        cin >> l >> r >> c;
        l--;
        int lb = (l + B - 1) / B;
        int rb = r / B;
        int ans = 0;
        if (lb > rb) {
            push(lb - 1);
            for (int i = l; i < r; i++) {
                ans += a[i] == c;
                a[i] = c;
            }
        } else {
            if (l < lb * B) {
                push(lb - 1);
                for (int i = l; i < lb * B; i++) {
                    ans += a[i] == c;
                    a[i] = c;
                }
            }
            if (rb * B < r) {
                push(rb);
                for (int i = rb * B; i < r; i++) {
                    ans += a[i] == c;
                    a[i] = c;
                }
            }
            for (int i = lb; i < rb; i++) {
                if (mark[i]) {
                    ans += (val[i] == c) * B;
                } else {
                    for (int j = i * B; j < (i + 1) * B; j++)
                        ans += a[j] == c;
                }
                mark[i] = true;
                val[i] = c;
            }
        }
        cout << ans << '\n';
    }
}

区间推平

「把 都改为 」这样的操作,也被称为区间推平

处理区间推平操作的另一个办法是维护同值的连续段

  • 用一个 map<下标类型,值类型> 来表示一个序列,对于每个同值的连续段,只存这一段的第一项的下标和值。
  • 例如,序列 被表示为 (下标从零开始)。我们把下标 称为端点

这种表示序列的方法,也可看作一种分块:把每个同值的连续段作为一块。

一次区间推平操作的影响:边块缩短,整块消失,新增一个块。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    map<int,int> a;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    int q = n;
    while (q--) {
        int l, r, c;
        cin >> l >> r >> c;
        --l;
        auto lptr = a.insert({l, prev(a.upper_bound(l))->second}).first;
        auto rptr = a.insert({r, prev(a.upper_bound(r))->second}).first;
        int ans = 0;
        for (auto it = lptr; it != rptr; ++it) {
            auto [i, v] = *it;
            if (v == c) {
                ans += next(it)->first - i;
            }
        }
        a.erase(lptr, rptr);
        a[l] = c;
        cout << ans << '\n';
    }
}

推平区间

  1. 查询 的值,把 设置为端点。
  2. 查询区间
  3. 删除区间 上的端点。
  4. a[l] = c;
    (把 设置为端点,并把 置为
    也可以写 a.insert({l, c});。)
auto lptr = a.insert({l, prev(a.upper_bound(l))->second}).first;
  • prev(a.upper_bound(l))->second 的值。
  • a.insert({l, al}) 尝试把 {l, al} 插入 map。
    返回一个 pair<iterator, bool>,.second 表示插入是否成功,.first 表示指向 l 对应的 pair 的迭代器(相当于指针)。

One Occurrence

https://codeforces.com/contest/1000/problem/F

给你一个长为 的正整数序列 。回答 个询问:

  • l r:输出一个在 中恰好出现一次的数。如果多个这样的数,任意输出一个。若没有这样的数,输出 0

prev[i]:在 左边第一个等于 的项的下标。
若不存在这样的项,置 prev[i] 为

next[i]:在 右边第一个等于 的项的下标。
若不存在这样的项,置 next[i] 为

上是唯一的,相当于说 prev[i] < l ≤ i ≤ r < next[i]。

分块

f[i]:第 块内,满足 prev[j] < 且 next[j] 最大的那个下标

数列分块入门 9

给你一个长为 的整数序列 。回答 个询问

  • l r:输出 的众数。若有多个,输出最小的那个。
限制
  • 在 int 范围内。

为了突出重点以及便于讲述,以下我们讨论如何求出 的一个众数,不管它是不是最小的众数。

分块

,把序列 分成 个块。
(不是 块,最后剩下的不到 项不管。)

预处理

int NB = n / B;
vector<vector<int>> mode(NB, vector<int>(NB)); 
vector<vector<int>> f(NB, vector<int>(NB));

mode[i][j]:从第 块到第 块这一段的众数。
f[i][j]:从第 块到第 块这一段的众数的出现次数。

查询区间 的众数

如果区间 的众数不是 mode[i][j],那么它一定在边块里出现过。

我们检查边块里的数在区间 上出现的次数是不是更多

对于区间 中落在左边块里的一项 ,我们检查

  • 在区间 上的出现次数是否大于 f[i][j]

为此,我们做一些预处理。

预处理 1

对序列 的元素的值进行压缩。例:。我们把这个操作称为坐标压缩

设压缩过后 的元素的取值范围是

预处理 2

vector<vector<int>> pos(N);
vector<int> rank(n);
  • pos[x] 在序列 中出现的位置()。
  • rank[i] 在所有跟它相同的数中排第几?即 pos[a[i]] 里排第几?

例:pos[1]{4, 5, 8}(下标从 0 开始),
rank{0, 0, 1, 0, 0, 1, 0, 0, 2, 1}

坐标压缩

vector<int> compress(vector<int>& a) {
    vector<pair<int,int>> vi;
    for (int i = 0; i < (int) a.size(); i++)
        vi.push_back({a[i], i});

    sort(vi.begin(), vi.end());

    vector<int> b;
    for (auto [v, i] : vi) {
        if (b.empty() || b.back() < v)
            b.push_back(a[i]);
        a[i] = (int) b.size() - 1;
    }
    return b; // 排序去重之后的 a
}

计算 pos 和 rank

vector<vector<int>> pos(N);
vector<int> rank(n);
for (int i = 0; i < n; i++) {
    rank[i] = (int) pos[a[i]].size();
    pos[a[i]].push_back(i);
}

利用 pos 和 rank

在区间 上的出现次数大于 」可以表述为

pos[a[k]].size() > rank[k] + 10 &&
    pos[a[k]][rank[k] + 10] < r

检查边块里的数出现次数是否更多

int lb = (l + B - 1) / B, rb = r / B;
if (lb > rb) {
    int v = mode[lb][rb - 1];
    int mx = f[lb][rb - 1];
    // 检查左边块里的数
    for (int i = l; i < lb * B; i++) {
        int j = rank[i] + mx;
        while (j < pos[a[i]].size() && pos[a[i]][j] < r)
            j++;
        if (mx < j - rank[i]) {
            mx = j - rank[i];
            v = a[i];
        }
    }
    // 检查右边块里的数
    for (int i = r - 1; i >= rb * B; i--) {
        int j = rank[i] - mx;
        while (j >= 0 && pos[a[i]][j] >= l)
            j--;
        if (mx < rank[i] - j) {
            mx = rank[i] - j;
            v = a[i];
        }
    }
}

j++;j--; 执行的总次数不超过区间 落在左右边块里的部分的长度之和。

完整代码

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n; cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++)
        cin >> a[i];

    vector<int> val = compress(a); //坐标压缩
    int N = (int) val.size();

    int B = (int) sqrt(n);
    int NB = n / B;

    vector<vector<pair<int,int>>> mode(NB, vector<pair<int,int>>(NB));

    for (int i = 0; i < NB; i++) {
        int mx = 0, v = -1;
        vector<int> cnt(N);
        for (int j = i; j < NB; j++) {
            for (int k = j * B; k < (j + 1) * B; k++) {
                ++cnt[a[k]];
                if (cnt[a[k]] > mx || (cnt[a[k]] == mx && a[k] < v)) {
                    mx = cnt[a[k]];
                    v = a[k];
                }
            }
            mode[i][j] = {v, mx};
        }
    }

    vector<vector<int>> pos(N);
    vector<int> rank(n);
    for (int i = 0; i < n; i++) {
        rank[i] = (int) pos[a[i]].size();
        pos[a[i]].push_back(i);
    }
    int q = n;
    while (q--) {
        int l, r;
        cin >> l >> r;
        --l;

        int lb = (l + B - 1) / B, rb = r / B;
        int v = -1, mx = 0;

        if (lb >= rb) {
            for (int i = l; i < r; i++) {
                int j = rank[i] + mx;
                while (j < (int) pos[a[i]].size() && pos[a[i]][j] < r)
                    j++;
                if (mx < j - rank[i]) {
                    mx = j - rank[i];
                    v = a[i];
                } else if (a[i] < v && j - 1 < (int) pos[a[i]].size() && pos[a[i]][j - 1] < r) {
                    v = a[i];
                }
            }
        } else {
            v = mode[lb][rb - 1].first;
            mx = mode[lb][rb - 1].second;
            for (int i = l; i < lb * B; i++) {
                int j = rank[i] + mx;
                while (j < (int) pos[a[i]].size() && pos[a[i]][j] < r)
                    j++;
                if (mx < j - rank[i]) {
                    mx = j - rank[i];
                    v = a[i];
                } else if (a[i] < v && j - 1 < (int) pos[a[i]].size() && pos[a[i]][j - 1] < r) {
                    v = a[i];
                }
            }
            for (int i = rb * B; i < r; i++) {
                int j = rank[i] - mx;
                while (j >= 0 && pos[a[i]][j] >= l)
                    j--;
                if (mx < rank[i] - j) {
                    mx = rank[i] - j;
                    v = a[i];
                } else if (a[i] < v && j + 1 >= 0 && pos[a[i]][j + 1] >= l) {
                    v = a[i];
                }
            }
        }
        cout << val[v] << '\n';
    }
}

Marisa is happy

https://marisaoj.com/problem/652

给你一个长为 的序列 ,最初每个 都等于 ,又给你一个 的排列 个询问:

  • 0 l r x:给 每个都加上
  • 1 l r x:给 每个都加上
  • 2 l r:求
  • 3 l r:求

其实我们有两个序列,,一个是另一个重新排列。

操作是对二者的区间加和求区间和。

困难来自于,对某个序列的区间加,也会改变另一个序列。再后者当中,被改变的那些元素的位置不是连续的,而可能相当分散。

下面我们说一个很妙的解法。

分块

,对序列 进行分块。

进行分块,其实也就是对下标序列 进行分块。

:下标 中有多少个落在序列 的第 块里。

:下标 中多少个落在序列 的第 块里。

add1[i]:序列 的第 块被整体加的数的总和。

add2[i]:序列 的第 块被整体加的数的总和。

这样,序列 的第 块受到的整体加操作对序列 的区间 的和的贡献就是

用上面的办法,我们能够处理区间加操作对的整块修改。
剩下的就是区间加操作对边块的修改。这一部分逐个元素修改就可以了。

sum1[i]:序列 的第 块的元素和,只计每次区间加操作时边块的贡献。

sum2[i]:序列 的第 块的元素和,只计每次区间加操作时边块的贡献。

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n, q;
    cin >> n >> q;
    vector<int> p(n), ip(n);
    for (int i = 0; i < n; i++) {
        cin >> p[i];
        p[i]--;
        ip[p[i]] = i;
    }
    int B = (int) sqrt(n);
    int NB = (n + B - 1) / B;
    vector<vector<int>> s(NB, vector<int>(n + 1));
    vector<vector<int>> t(NB, vector<int>(n + 1));

    for (int i = 0; i < NB; i++) {
        int l = i * B, r = min(l + B, n);
        // 计算前缀和
        for (int j = 0; j < n; j++)
            s[i][j + 1] += s[i][j] + (l <= ip[j] && ip[j] < r);
        for (int j = 0; j < n; j++)
            t[i][j + 1] = t[i][j] + (l <= p[j] && p[j] < r);
    }
    vector<long long> add1(NB), add2(NB), sum1(NB), sum2(NB);
    vector<long long> a1(n), a2(n);
    auto work0 = [&](int l, int r, int x) {
        for (int i = l; i < r; i++) {
            a1[i] += x;
            sum1[i / B] += x;
            a2[ip[i]] += x;
            sum2[ip[i] / B] += x;
        }
    };

    auto work1 = [&](int l, int r, int x) {
        for (int i = l; i < r; i++) {
            a2[i] += x;
            sum2[i / B] += x;
            a1[p[i]] += x;
            sum1[p[i] / B] += x;
        }
    };
    auto query2 = [&](int l, int r) {
        long long ans = 0;
        for (int i = l; i < r; i++)
            ans += a1[i] + add1[i / B];
        return ans;
    };

    auto query3 = [&](int l, int r) {
        long long ans = 0;
        for (int i = l; i < r; i++)
            ans += a2[i] + add2[i / B];
        return ans;
    };
    while (q--) {
        int type, l, r, x;
        cin >> type >> l >> r;
        if (type < 2) cin >> x;
        l--;
        int lb = (l + B - 1) / B, rb = r / B;
        long long ans = 0;

        if (type == 0) {
            if (lb > rb) {
                work0(l, r, x);
            }
            else {
                work0(l, lb * B, x);
                work0(rb * B, r, x);
                for (int i = lb; i < rb; i++)
                    add1[i] += x;
            }
        } else if (type == 1) {
            if (lb > rb) {
                work1(l, r, x);
            } else {
                work1(l, lb * B, x);
                work1(rb * B, r, x);
                for (int i = lb; i < rb; i++)
                    add2[i] += x;
            }
        }
        else if (type == 2) {
            if (lb > rb) ans = query2(l, r);
            else {
                ans = query2(l, lb * B) + query2(rb * B, r);
                for (int i = lb; i < rb; i++)
                    ans += sum1[i] + add1[i] * B;
            }
            for (int i = 0; i < NB; i++)
                ans += add2[i] * (s[i][r] - s[i][l]);
        } else {
            if (lb > rb) ans += query3(l, r);
            else {
                ans = query3(l, lb * B) + query3(rb * B, r);
                for (int i = lb; i < rb; i++)
                    ans += sum2[i] + add2[i] * B;
            }
            for (int i = 0; i < NB; i++)
                ans += add1[i] * (t[i][r] - t[i][l]);
        }
        if (type >= 2) cout << ans << '\n';
    }
}

Yuno loves sqrt technology I

https://www.luogu.com.cn/problem/P5046

给你一个 排列 。回答 个询问

  • l r:求 的逆序数。

强制在线。每次询问的 等于输入的 异或上一次询问的答案。
对于第一次询问,上一次询问的答案视作

时限:750 毫秒。

分块

center

记号 :组列 的逆序数。

如上图所示,我们有

预处理

的项数。

的项数。