可持久化线段树,原理分析,题目详解

零、写在前面

可持久化线段树学习起来难度并不大,如果学过可持久化Trie,了解怎样复用旧版本的信息,只通过少量空间维护信息的修改,其实很容易理解可持久化线段树的原理。

对于普通线段树,我们可能是维护序列,维护权值,但是可持久化之后,其实我们掌握的信息更多了,可以解决很多看起来只能暴力解决的问题。


一、可持久化线段树

1.1 可持久化线段树

支持访问历史版本的线段树(基于动态开点)。

暴力实现:每次修改或查询都创建一颗新的线段树,这样空间复杂度O(4n * n),显然不行。

例如数组初值:8 5 7 2

把版本0 位置4 修改为6

把版本1 位置2 修改为1

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但线段树之所以高效就在于每次修改涉及路径上的节点数目为O(log)级别, 也就是说,新版本的线段树和旧版本线段树有着大量的相同节点,如果我们和旧版本共用这些旧节点,只对被修改的节点开辟新结点。

这种策略下,前面的修改表示为:
在这里插入图片描述

这样我们的空间复杂度是否能够降低?时间复杂度如何?

空间复杂度:

  • 每次只修改 logn+1 个节点,故只增加修改的节点即可。
  • 初始建树需开 2n - 1个节点。n 次修改,每次修改最多开 logn + 1 个节点。所以节点总数为 2n + n(logn + 1) = n(logn + 3)

动态开点:

不再用 p * 2和 p * 2 + 1代表左右儿子,每个节点记录左右儿子的编号。

1.2 模板——静态区间第 k小

原题链接

P3834 【模板】可持久化线段树 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

就是给了你一个数组,没有修改操作,然后查询任意区间第k小。(普通莫队可过)

以这道题为例,来介绍一下可持久化线段树的实现代码

节点定义

  • 每一个版本都是一颗权值线段树,这里采用动态开点
struct Node {
    int l, r; // 左右儿子编号
    int sum;
} pool[N * 22];	// 节点内存池

// 动态开点
int newNode() {
    static int tot = 0;
    return tot ++;
}

O(n)建树

void build(int &p, int l, int r) {
    p = newNode();
    if (l == r) {
        return;
    }
    int m = (l + r) / 2;
    build(pool[p].l, l, m);
    build(pool[p].r, m + 1, r);
}

点修

  • 每次修改都会新开一个版本的线段树,所以 x = newNode();
  • y 是上一个版本线段树的节点
  • x 继承 y
  • 然后沿着路径修改,没有涉及到的节点和y 共用
  • 时间复杂度: O(logn)
void modify(int &x, int y, int l, int r, int v) {
    x = newNode();
    pool[x] = pool[y];
    ++ pool[x].sum;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    if (v <= m) {
        modify(pool[x].l, pool[y].l, l, m, v);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, v);
    }
}

区间第 k 小查询

  • 查询 [L, R] 的第k 小
  • 初始y 为 版本R 根节点,x 为版本L - 1 根节点
  • 那么递归过程中,对于当前区间 [l, r] 内 的元素数目就是 pool[y].sum - pool[x].sum(前缀和)
  • 我们根据 元素数目 和 k 的关系到对应区间查询即可
  • 时间复杂度:O(logn)
// kth
int query(int x, int y, int l, int r, int k) {
    if (l == r) {
        return l;
    }

    int m = (l + r) / 2;
    int sum = pool[pool[y].l].sum - pool[pool[x].l].sum;
    if (sum >= k) {
        return query(pool[x].l, pool[y].l, l, m, k);
    } else {
        return query(pool[x].r, pool[y].r, m + 1, r, k - sum);
    }
}

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 200000;

struct Node {
    int l, r;
    int sum;
} pool[N * 22];

int newNode() {
    static int tot = 0;
    return tot ++;
}

void build(int &p, int l, int r) {
    p = newNode();
    if (l == r) {
        return;
    }
    int m = (l + r) / 2;
    build(pool[p].l, l, m);
    build(pool[p].r, m + 1, r);
}

void modify(int &x, int y, int l, int r, int v) {
    x = newNode();
    pool[x] = pool[y];
    ++ pool[x].sum;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    if (v <= m) {
        modify(pool[x].l, pool[y].l, l, m, v);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, v);
    }
}

// kth
int query(int x, int y, int l, int r, int k) {
    if (l == r) {
        return l;
    }

    int m = (l + r) / 2;
    int sum = pool[pool[y].l].sum - pool[pool[x].l].sum;
    if (sum >= k) {
        return query(pool[x].l, pool[y].l, l, m, k);
    } else {
        return query(pool[x].r, pool[y].r, m + 1, r, k - sum);
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

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

    std::vector<int> p(a);
    std::ranges::sort(p);
    p.resize(std::unique(p.begin(), p.end()) - p.begin());

    for (int &x : a) {
        x = std::lower_bound(p.begin(), p.end(), x) - p.begin();
    }

    int U = p.size();

    std::vector<int> root(n + 1);
    build(root[0], 0, U - 1);

    for (int i = 1; i <= n; ++ i) {
        modify(root[i], root[i - 1], 0, U - 1, a[i - 1]);
    }

    while (m --) {
        int l, r, k;
        std::cin >> l >> r >> k;

        -- l, -- r;

        std::cout << p[query(root[l], root[r + 1], 0, U - 1, k)] << '\n';
    }

    return 0;
}

二、OJ练习

2.1 P1383 高级打字机

原题链接

P1383 高级打字机

思路分析

显然每次修改 / undo 都要新开一个版本,每个版本的线段树维护当前的文本序列,节点内存了区间字符数目,叶子节点保存了这个位置的字符。

对于修改操作:

  • 当前区间 [l, r],如果 [l, mid] 内有空位,即 sum < mid - l + 1,那么往左区间插入
  • 否则往有区间插入

对于undo操作:

很简单,copy 一份 若干个版本前的线段树即可(只需copy 根节点)

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 1E5;

struct Node {
    int sum;
    char ch;
    int l, r;
} pool[22 * N];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void pull(int p) {
    pool[p].sum = pool[pool[p].l].sum + pool[pool[p].r].sum;
}

void modify(int &x, int y, int l, int r, char ch) {
    x = newNode();
    pool[x] = pool[y];

    if (l == r) {
        pool[x].sum = 1;
        pool[x].ch = ch;
        return;
    }

    int m = (l + r) / 2;
    if (pool[x].sum < m - l + 1) {
        modify(pool[x].l, pool[y].l, l, m, ch);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, ch);
    }

    pull(x);
}

char query(int x, int l, int r, int k) {
    if (l == r) {
        return pool[x].ch;
    }

    int m = (l + r) / 2;

    if (pool[pool[x].l].sum >= k) {
        return query(pool[x].l, l, m, k);
    } else {
        return query(pool[x].r, m + 1, r, k - pool[pool[x].l].sum);
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n;
    std::cin >> n;

    std::vector<int> root{0};

    for (int i = 0; i < n; ++ i) {
        char op;
        std::cin >> op;

        switch (op) {
        case 'T': { 
            char ch;
            std::cin >> ch;
            root.emplace_back();
            modify(root.back(), root[root.size() - 2], 0, n - 1, ch);
            break;
        }

        case 'U': {
            int x;
            std::cin >> x;
            root.push_back(root[root.size() - x - 1]);
            break;
        }

        case 'Q': {
            int x;
            std::cin >> x;
            std::cout << query(root.back(), 0, n - 1, x) << '\n';
            break;
        }
        }
    }

    return 0;
}

2.2 P1972 [SDOI2009] HH的项链

原题链接

P1972 [SDOI2009] HH的项链

思路分析

本题非常经典,建议也写一下树状数组离线的做法,很有帮助。

和树状数组离线的处理方式类似,只不过可持久化seg的做法更为直观。

每个贝壳数组下标在前一个版本基础上新开一颗线段树。

第 i 个版本的线段树维护了每种贝壳在 [1, i] 中的最右位置

这样我们查询 [l, r] 内的贝壳种类,只需版本r 所有 >= l 位置的权值和即可。

因为版本r 线段树所有元素的最右位置不超过r,我们又限制搜索区间不会向左跨越,所以保证了正确性。

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 1E6;

struct Node {
    int sum;
    int l, r;
} pool[40 * N];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int k, int v) {
    x = newNode();
    pool[x] = pool[y];
    pool[x].sum += v;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    if (k <= m) {
        modify(pool[x].l, pool[y].l, l, m, k, v);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, k, v);
    }
}

int query(int x, int l, int r, int k) {
    if (l == r) {
        return pool[x].sum;
    }

    int m = (l + r) / 2;
    if (k <= m) {
        return query(pool[x].l, l, m, k) + pool[pool[x].r].sum;
    } else {
        return query(pool[x].r, m + 1, r, k);
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n;
    std::cin >> n;

    std::vector<int> root(n + 1);

    constexpr int M = 1E6;

    std::vector<int> last(M + 1, -1);
    for (int i = 1, t; i <= n; ++ i) {
        int x;
        std::cin >> x;

        if (~last[x]) {
            modify(t, root[i - 1], 0, n - 1, last[x], -1);
            modify(root[i], t, 0, n - 1, i - 1, 1);
        } else {
            modify(root[i], root[i - 1], 0, n - 1, i - 1, 1);
        }

        last[x] = i - 1;
    }

    int m;
    std::cin >> m;

    while (m --) {
        int l, r;
        std::cin >> l >> r;

        std::cout << query(root[r], 0, n - 1, l - 1) << '\n';
    }

    return 0;
}

2.3 P2468 [SDOI2010] 粟粟的书架

原题链接

P2468 [SDOI2010] 粟粟的书架

思路分析

注意本题数据范围,只有 R = 1的时候,列数 会达到5E5量级

对于R > 1的测试点,我们二分+二位前缀和可以轻松解决

对于R = 1的测试点,矩阵退化成了序列,我们考虑可持久化线段树

每个下标对应一个版本的权值线段树,即维护每种厚度的书的出现次数

那么查询的时候我们贪心的优先拿取厚的书,这样可以保证书的数量最少。

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 5E5;

struct Node {
    int sum;
    int siz;
    int l, r;
} pool[25 * N];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int k) {
    x = newNode();
    pool[x] = pool[y];
    pool[x].sum += k;
    ++ pool[x].siz;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    if (k <= m) {
        modify(pool[x].l, pool[y].l, l, m, k);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, k);
    }
}

int query(int x, int y, int l, int r, int k) {
    if (l == r) {
        return (k + l - 1) / l;
    }

    int s = pool[pool[x].r].sum - pool[pool[y].r].sum;
    int m = (l + r) / 2;
    if (s >= k) {
        return query(pool[x].r, pool[y].r, m + 1, r, k);
    } else {
        return query(pool[x].l, pool[y].l, l, m, k - s) + pool[pool[x].r].siz - pool[pool[y].r].siz;
    }
}

void work1(int n, int m, int q) {
    std::vector<int> a(m);
    for (int i = 0; i < m; ++ i) {
        std::cin >> a[i];
    }

    const int M = std::ranges::max(a);

    std::vector<int> root(m + 1);
    for (int i = 1; i <= m; ++ i) {
        root[i] = newNode();
        modify(root[i], root[i - 1], 1, M, a[i - 1]);
    }

    while (q --) {
        int x1, y1, x2, y2, H;
        std::cin >> x1 >> y1 >> x2 >> y2 >> H;

        if (pool[root[y2]].sum - pool[root[y1 - 1]].sum < H) {
            std::cout << "Poor QLW\n";
            continue;
        }

        std::cout << query(root[y2], root[y1 - 1], 1, M, H) << '\n';
    }

}


int psum[201][201][1001], psiz[201][201][1001];

void work2(int n, int m, int q) {
    std::vector<std::vector<int>> g(n, std::vector<int>(m));

    int M = 0;

    for (int i = 0; i < n; ++ i) {
        for (int j = 0; j < m; ++ j) {
            std::cin >> g[i][j];
            M = std::max(M, g[i][j]);
        }
    }

    for (int i = 0; i < n; ++ i) {
        for (int j = 0; j < m; ++ j) {
            for (int k = 1; k <= M; ++ k) {
                psum[i + 1][j + 1][k] = psum[i + 1][j][k] + psum[i][j + 1][k] - psum[i][j][k] + (g[i][j] >= k) * g[i][j];
                psiz[i + 1][j + 1][k] = psiz[i + 1][j][k] + psiz[i][j + 1][k] - psiz[i][j][k] + (g[i][j] >= k);
            }
        }
    }

    auto get = [&](int x1, int y1, int x2, int y2, int k, int t) -> int {
        -- x1, -- y1;

        if (t == 0) {
            return psum[x2][y2][k] - psum[x1][y2][k] - psum[x2][y1][k] + psum[x1][y1][k];
        } else {
            return psiz[x2][y2][k] - psiz[x1][y2][k] - psiz[x2][y1][k] + psiz[x1][y1][k];
        }
    };

    while (q --) {
        int x1, y1, x2, y2, H;
        std::cin >> x1 >> y1 >> x2 >> y2 >> H;

        int lo = 0, hi = M;

        while (lo < hi) {
            int x = (lo + hi + 1) / 2;

            if (get(x1, y1, x2, y2, x, 0) >= H) {
                lo = x;
            } else {
                hi = x - 1;
            }
        }

        if (lo == 0) {
            std::cout << "Poor QLW\n";
            continue;
        }

        std::cout << get(x1, y1, x2, y2, lo, 1) - (get(x1, y1, x2, y2, lo, 0) - H) / lo << '\n';
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m, q;
    std::cin >> n >> m >> q;

    if (n == 1) {
        work1(n, m, q);
    } else {
        work2(n, m, q);
    }

    return 0;
}

2.4 P2633 Count on a tree

原题链接

P2633 Count on a tree

思路分析

树上前缀和 + 可持久化seg

关于树上前缀和:树上前缀和详解-CSDN博客

我们对树做dfs,每个节点u以父亲节点p为旧版本开辟新版本线段树,线段树维护的是权值,即从根节点到 u 路径上点权出现次数

那么对于查询 u, v, k

我们怎样得知 元素 x 在u 到 v路径上的出现次数?根据树上前缀和:cnt(u, x) + cnt(v, x) - cnt(lca, x) - cnt(parent(lca), x)

但现在要查询第k小,我们类似于静态区间第k 小的方式在线段树上搜索即可:

  • 四树联查:u, v, lca(u, v), parent(lca(u, v))

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 1E5;

struct Node {
    int l, r;
    int sum;
} pool[N * 22];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int v) {
    x = newNode();
    pool[x] = pool[y];
    ++ pool[x].sum;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    if (v <= m) {
        modify(pool[x].l, pool[y].l, l, m, v);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, v);
    }
}

int query(int u, int v, int x, int y, int l, int r, int k) {
    if (l == r) {
        return l;
    }

    int s = pool[pool[v].l].sum + pool[pool[u].l].sum - pool[pool[x].l].sum - pool[pool[y].l].sum;
    int m = (l + r) / 2;

    if (k <= s) {
        return query(pool[u].l, pool[v].l, pool[x].l, pool[y].l, l, m, k);
    } else {
        return query(pool[u].r, pool[v].r, pool[x].r, pool[y].r, m + 1, r, k - s);
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

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

    std::vector<int> p(a);
    std::ranges::sort(p);
    p.resize(std::unique(p.begin(), p.end()) - p.begin());
    const int M = p.size();

    for (int &x : a) {
        x = std::ranges::lower_bound(p, x) - p.begin();
    }

    std::vector<std::vector<int>> adj(n);

    for (int i = 1; i < n; ++ i) {
        int u, v;
        std::cin >> u >> v;
        -- u, -- v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    constexpr int B = 18;
    std::vector<std::array<int, B>> f(n);
    std::vector<int> dep(n), root(n);

    auto dfs = [&](auto &&self, int u, int p) -> void{
        modify(root[u], ~p ? root[p] : 0, 0, M - 1, a[u]);

        for (int v : adj[u]) {
            if (v != p) {
                dep[v] = dep[u] + 1;
                f[v][0] = u;

                for (int i = 1; i < B; ++ i) {
                    f[v][i] = f[f[v][i - 1]][i - 1];
                }
                
                self(self, v, u);
            }
        }
    };

    dfs(dfs, 0, -1);

    auto LCA = [&](int u, int v) -> int {
        if (dep[u] < dep[v]) std::swap(u, v);
        for (int i = B - 1; ~i; -- i)
            if (dep[f[u][i]] >= dep[v])
                u = f[u][i];

        if (u == v)
            return u;

        for (int i = B - 1; ~i; -- i)
            if (f[u][i] != f[v][i]) {
                u = f[u][i];
                v = f[v][i];
            }

        return f[u][0];
    }; 

    int last = 0;

    while (m --) {
        int u, v, k;
        std::cin >> u >> v >> k;
        u ^= last;
        -- u, -- v;

        int lca = LCA(u, v);

        last = p[query(root[u], root[v], root[lca], lca ? root[f[lca][0]] : 0, 0, M - 1, k)];
        
        std::cout << last << '\n';
    }

    return 0;
}

2.5 P3302 [SDOI2013] 森林

原题链接

P3302 [SDOI2013] 森林

思路分析

和上一题类似,对于树上路径的第k小查询我们可以处理,但是这道题多了加边操作。

对于加边操作我们:启发式合并,即小树合到大树身上,同时dfs维护倍增数组,用于求lca

合并途中,维护小树节点新版本线段树。

合并部分:每次O(log)

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 8E4;

struct Node {
    int l, r;
    int sum;
} pool[N * 150];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int v) {
    x = newNode();

    pool[x] = pool[y];
    ++ pool[x].sum;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    if (v <= m) {
        modify(pool[x].l, pool[y].l, l, m, v);
    } else {
        modify(pool[x].r, pool[y].r, m + 1, r, v);
    }
}

int query(int u, int v, int x, int y, int l, int r, int k) {
    if (l == r) {
        return l;
    }

    int s = pool[pool[v].l].sum + pool[pool[u].l].sum - pool[pool[x].l].sum - pool[pool[y].l].sum;
    int m = (l + r) / 2;

    if (k <= s) {
        return query(pool[u].l, pool[v].l, pool[x].l, pool[y].l, l, m, k);
    } else {
        return query(pool[u].r, pool[v].r, pool[x].r, pool[y].r, m + 1, r, k - s);
    }
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int _;
    std::cin >> _;

    int n, m, t;
    std::cin >> n >> m >> t;

    std::vector<int> a(n + 1), root(n + 1);
    for (int i = 1; i <= n; ++ i) {
        std::cin >> a[i];
    }

    std::vector<int> p(a);
    std::ranges::sort(p);
    p.resize(std::unique(p.begin(), p.end()) - p.begin());
    const int M = p.size() - 1;

    for (int &x : a) {
        x = std::ranges::lower_bound(p, x) - p.begin();
    }

    std::vector<std::vector<int>> adj(n + 1);

    for (int i = 0; i < m; ++ i) {
        int u, v;
        std::cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    constexpr int B = 18;
    std::vector<std::array<int, B>> f(n + 1);
    std::vector<int> dep(n + 1), siz(n + 1), top(n + 1);

    auto dfs = [&](auto &&self, int u, int p, int t) -> void {
        top[u] = t;
        ++ siz[t];

        modify(root[u], root[p], 1, M, a[u]);

        f[u][0] = p;
        dep[u] = dep[p] + 1;
        for (int i = 1; i < B; ++ i) {
            f[u][i] = f[f[u][i - 1]][i - 1];
        }

        for (int v : adj[u]) {
            if (v == p) continue;
            self(self, v, u, t);
        }
    };

    auto LCA = [&](int u, int v) -> int {
        if (dep[u] < dep[v]) std::swap(u, v);

        for (int i = B - 1; ~i; -- i)
            if (dep[f[u][i]] >= dep[v])
                u = f[u][i];

        if (u == v)
            return u;

        for (int i = B - 1; ~i; -- i)
            if (f[u][i] != f[v][i]) {
                u = f[u][i];
                v = f[v][i];
            }

        return f[u][0];
    };

    for (int i = 1; i <= n; ++ i) {
        if (!top[i]) {
            dfs(dfs, i, 0, i);
        }
    }

    int last = 0;

    while (t --) {
        char op;
        std::cin >> op;

        if (op == 'Q') {
            int u, v, k;
            std::cin >> u >> v >> k;
            u ^= last;
            v ^= last;
            k ^= last;

            int lca = LCA(u, v);

            last = p[query(root[u], root[v], root[lca], root[f[lca][0]], 1, M, k)];

            std::cout << last << '\n';
        } else {
            int u, v;
            std::cin >> u >> v;
            u ^= last;
            v ^= last;

            adj[u].push_back(v);
            adj[v].push_back(u);
            if (siz[top[u]] > siz[top[v]]) {
                dfs(dfs, v, u, top[u]);
            } else {
                dfs(dfs, u, v, top[v]);
            }
        }
    }

    return 0;
}

2.6 P2839 [国家集训队] middle

原题链接

P2839 [国家集训队] middle

思路分析

本题中位数下标上取整

如果查询的是 [l, r] 内的中位数怎么做?

二分,中位数x满足 大于等于 x 的数目 - 小于x 的数目 ∈ [0, 1]

现在加上了左右端点的区间限制

我们考虑这么做:

将原数组排序(记得保存下标),遍历排序后的数组

每个数x开一个线段树,维护出现位置,x前面的数出现的位置权值为-1,剩下的其他为1,同时维护区间最大前后缀权值和

这样我们对于查询,仍然二分中位数

对于查询 a, b, c, d

只要 sum(b + 1, c - 1) + lsum(a, b) + rsum(c, d) >= 0,我们就右边界左移,否则左区间右移

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 2E4;
constexpr int inf = 1E9;

struct Info {
    int sum = 0;
    int lsum = -inf;
    int rsum = -inf;
};

Info operator+ (const Info &x, const Info &y) {
    return {
        x.sum + y.sum,
        std::max(x.lsum, x.sum + y.lsum),
        std::max(y.rsum, x.rsum + y.sum)
    }; 
}

struct Node {
    int l, r;
    Info info;
} pool[N * 25];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void pull(int p) {
    pool[p].info = pool[pool[p].l].info + pool[pool[p].r].info;    
}

void modify(int &x, int y, int l, int r, int k, int v) {
    if (k < l || k > r) {
        return;
    }

    x = newNode();
    
    pool[x] = pool[y];

    if (l == r) {
        pool[x].info = {v, v, v};
        return;
    }

    int m = (l + r) / 2;

    modify(pool[x].l, pool[y].l, l, m, k, v);
    modify(pool[x].r, pool[y].r, m + 1, r, k, v);

    pull(x);
}

Info rangeQuery(int p, int l, int r, int x, int y) {
    if (x > r || y < l) {
        return Info();
    }

    if (x <= l && r <= y) {
        return pool[p].info;
    }

    int m = (l + r) / 2;

    return rangeQuery(pool[p].l, l, m, x, y) + rangeQuery(pool[p].r, m + 1, r, x, y);
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n;
    std::cin >> n;

    std::vector<int> arr(n);

    for (int i = 0; i < n; ++ i) {
        std::cin >> arr[i];
    }

    std::vector<int> p(n);
    std::iota(p.begin(), p.end(), 0);
    std::ranges::sort(p, {}, [&](int x) {
        return arr[x];
    });

    std::vector<int> root(n + 1);
    
    auto build = [&](auto &&self, int &p, int l, int r){
        if (!p) {
            p = newNode();
        }
        if (l == r) {
            pool[p].info = {1, 1, 1};
            return;
        }

        int m = (l + r) / 2;
        
        self(self, pool[p].l, l, m);
        self(self, pool[p].r, m + 1, r);

        pull(p);
    };

    build(build, root[0], 0, n - 1);

    for (int i = 1; i <= n; ++ i) {
        modify(root[i], root[i - 1], 0, n - 1, p[i - 1], -1);
    }

    int q;
    std::cin >> q;

    int last = 0;

    while (q --) {
        std::array<int, 4> Q;
        std::cin >> Q[0] >> Q[1] >> Q[2] >> Q[3];

        for (int &x : Q) {
            x = (x + last) % n;
        }
        std::ranges::sort(Q);

        int a = Q[0], b = Q[1], c = Q[2], d = Q[3];

        int lo = 0, hi = n;

        while (lo < hi) {
            int x = (lo + hi + 1) / 2;

            int s = 0;
            if (b + 1 < c) {
                s += rangeQuery(root[x], 0, n - 1, b + 1, c - 1).sum;
            }

            s += rangeQuery(root[x], 0, n - 1, a, b).rsum;
            s += rangeQuery(root[x], 0, n - 1, c, d).lsum;

            if (s >= 0) {
                lo = x;
            } else {
                hi = x - 1;
            }
        }

        last = arr[p[lo]];
        std::cout << last << '\n';
    }

    return 0;
}

2.7 P3168 [CQOI2015] 任务查询系统

原题链接

P3168 [CQOI2015] 任务查询系统

思路分析

很简单,以时间为版本号维护可持久化线段树,每个线段树维护了当前时刻存在的任务优先级的出现次数

具体操作我们将输入区间按照开始时间分组,结束时间 + 1分组,类似差分的思想去建树即可

查询操作也非常简单,具体见代码。

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 1E5;

std::vector<int> o;

struct Node {
    int l, r;
    int cnt;
    i64 sum;
} pool[N * 40];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int k, int v) {
    if (l > k || r < k) {
        return;
    }

    x = newNode();
    pool[x] = pool[y];

    pool[x].cnt += v;
    pool[x].sum += v * o[k];
    
    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    modify(pool[x].l, pool[y].l, l, m, k, v);
    modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}

i64 rangeQuery(int p, int l, int r, int k) {
    if (pool[p].cnt <= k) {
        return pool[p].sum;
    }

    if (k <= 0) {
        return 0;
    }

    if (l == r) {
        return pool[p].sum / pool[p].cnt * k;
    }

    int m = (l + r) / 2;

    return rangeQuery(pool[p].l, l, m, k) + rangeQuery(pool[p].r, m + 1, r, k - pool[pool[p].l].cnt);
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int m, n;
    std::cin >> m >> n;

    std::vector<std::vector<int>> st(n + 1), ed(n + 1);
    std::vector<int> a(m);

    for (int i = 0; i < m; ++ i) {
        int s, e, p;
        std::cin >> s >> e >> p;

        -- s, -- e;

        st[s].push_back(i);
        ed[e + 1].push_back(i);
        
        a[i] = p;
        o.push_back(p);
    }

    std::ranges::sort(o);
    o.resize(std::unique(o.begin(), o.end()) - o.begin());

    int U = o.size();

    for (int &x : a) {
        x = std::ranges::lower_bound(o, x) - o.begin();
    }

    std::vector<int> root(n);

    for (int i = 0; i < n; ++ i) {
        root[i] = i > 0 ? root[i - 1] : 0;
        for (int x : st[i]) {
            modify(root[i], root[i], 0, U - 1, a[x], 1);
        }

        for (int x : ed[i]) {
            modify(root[i], root[i], 0, U - 1, a[x], -1);
        }

    }

    i64 last = 1;

    for (int i = 0; i < n; ++ i) {
        int t, x, y, z;
        std::cin >> t >> x >> y >> z;
        -- t;

        int k = 1 + (x * last % z + y) % z;

        std::cout << (last = rangeQuery(root[t], 0, U - 1, k)) << '\n';
    }

    return 0;
}

2.8 P3293 [SCOI2016] 美味

原题链接

P3293 [SCOI2016] 美味

思路分析

先看看能不能不用字典树做出这个前置题目:421. 数组中两个数的最大异或值 - 力扣(LeetCode)

其实就是试填法求两个数最大异或值的 加强版。

按位考虑:

当前考虑到第 i 位

如果 bi 当前位为 0

前面已经确定的位为msk

我们异或后如果该位为1,那么 aj + xi 该位为 1,即 aj + xi ∈ [msk | 1 << i, msk | ((1 << (i + 1)) - 1)]

bi 当前位为 1 的情况类似。

可持久化树每个版本对应原序列下标,然后每个版本是一个权值线段树。

AC代码

#include <bits/stdc++.h>

using i64 = long long;

constexpr int N = 2E5;

struct Node {
    int l, r;
    int sum;
} pool[N * 22];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int v) {
    if (l > v || r < v) {
        return;
    }

    x = newNode();
    pool[x] = pool[y];
    ++ pool[x].sum;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    modify(pool[x].l, pool[y].l, l, m, v);
    modify(pool[x].r, pool[y].r, m + 1, r, v);
}

int rangeQuery(int p, int q, int l, int r, int x, int y) {
    if (x > y) {
        return 0;
    }

    if (x > r || y < l) {
        return 0;
    }

    if (x <= l && r <= y) {
        return pool[p].sum - pool[q].sum;
    }

    int m = (l + r) / 2;

    return rangeQuery(pool[p].l, pool[q].l, l, m, x, y) + rangeQuery(pool[p].r, pool[q].r, m + 1, r, x, y);
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

    std::vector<int> a(n), root(n + 1);
    for (int i = 0; i < n; ++ i) {
        std::cin >> a[i];
    }

    const int M = std::ranges::max(a);

    for (int i = 0; i < n; ++ i) {
        modify(root[i + 1], root[i], 0, M, a[i]);
    }

    constexpr int B = 19;

    for (int i = 0; i < m; ++ i) {
        int b, x, l, r;
        std::cin >> b >> x >> l >> r;

        int msk = 0;

        for (int j = B - 1; j >= 0; -- j) {
            int lo = msk, hi = msk;

            if (!(b >> j & 1)) {
                lo |= 1 << j;
                hi |= 1 << j;
            }

            hi |= ((1 << j) - 1);

            lo -= x;
            hi -= x;

            if (rangeQuery(root[r], root[l - 1], 0, M, std::max(0, lo), std::min(hi, M))) {
                msk |= (!(b >> j & 1)) << j;
            } else {
                msk |= (b >> j & 1) << j;
            }
        }

        std::cout << (b ^ msk) << '\n';
    }

    return 0;
}

2.9 P3755 [CQOI2017] 老C的任务

原题链接

P3755 [CQOI2017] 老C的任务

思路分析

CDQ 分治的方法已经介绍过:CDQ分治详解,一维、二维、三维偏序-CSDN博客

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以x 坐标作为版本号,每个x 坐标开一个线段树,维护这条竖线上各个y 坐标的功率和

那么对于查询就很板了,就是在对应x 坐标区间的两个线段树在指定 y 区间内进行功率和的差分

AC代码

#include <bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;

constexpr int N = 1E5;

struct Node {
    int l, r;
    i64 sum;
} pool[N * 25];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int k, int v) {
    if (k < l || k > r) {
        return;
    }

    x = newNode();
    pool[x] = pool[y];
    pool[x].sum += v;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    modify(pool[x].l, pool[y].l, l, m, k, v);
    modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}

i64 rangeQuery(int p, int q, int l, int r, int x, int y) {
    if (l > r || x > y) {
        return 0;
    }
    if (y < l || x > r) {
        return 0;
    }

    if (x <= l && r <= y) {
        return pool[p].sum - pool[q].sum;
    }

    int m = (l + r) / 2;

    return rangeQuery(pool[p].l, pool[q].l, l, m, x, y) + rangeQuery(pool[p].r, pool[q].r, m + 1, r, x, y);
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

    std::vector<int> x(n), y(n), p(n);
    for (int i = 0; i < n; ++ i) {
        std::cin >> x[i] >> y[i] >> p[i];
    }

    auto dy = y;
    std::ranges::sort(dy);
    dy.resize(std::unique(dy.begin(), dy.end()) - dy.begin());
    for (int &v : y) {
        v = std::ranges::lower_bound(dy, v) - dy.begin();
    }

    const int M = dy.size();

    std::vector<int> o(n);
    std::iota(o.begin(), o.end(), 0);
    std::ranges::sort(o, {}, [&](int i){
        return x[i];
    });

    std::vector<int> root(n + 1);
    for (int i = 0; i < n; ++ i) {
        modify(root[i + 1], root[i], 0, M - 1, y[o[i]], p[o[i]]);
    }

    while (m --) {
        int x1, y1, x2, y2;
        std::cin >> x1 >> y1 >> x2 >> y2;

        int R = std::ranges::upper_bound(o, x2, {}, [&](int i){
            return x[i];
        }) - o.begin();

        int L = std::ranges::lower_bound(o, x1, {}, [&](int i){
            return x[i];
        }) - o.begin();

        int Y = std::ranges::upper_bound(dy, y2) - dy.begin() - 1;
        int X = std::ranges::lower_bound(dy, y1) - dy.begin();

        std::cout << rangeQuery(root[R], root[L], 0, M - 1, X, Y) << '\n';
    }
    
    return 0;
}

2.10 P3567 [POI2014] KUR-Couriers

原题链接

P3567 [POI2014] KUR-Couriers

思路分析

以下标为版本号开权值线段树

对于查询二分即可

AC代码

#include <bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;

constexpr int N = 5e5;

struct Node{
    int sum;
    int l, r;
} pool[30 * N];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int k) {
    if (k < l || k > r) {
        return;
    }

    x = newNode();
    pool[x] = pool[y];
    ++ pool[x].sum;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;

    modify(pool[x].l, pool[y].l, l, m, k);
    modify(pool[x].r, pool[y].r, m + 1, r, k);
}

int rangeQuery(int p, int q, int l, int r, int len) {
    if (pool[p].sum - pool[q].sum <= len / 2) {
        return 0;
    }

    if (l == r) {
        return l;
    }

    int m = (l + r) / 2;

    return std::max(rangeQuery(pool[p].l, pool[q].l, l, m, len), rangeQuery(pool[p].r, pool[q].r, m + 1, r, len));
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

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

    std::vector<int> root(n + 1);

    const int L = std::ranges::min(a);
    const int R = std::ranges::max(a);

    for (int i = 0; i < n; ++ i) {
        modify(root[i + 1], root[i], L, R, a[i]);
    }

    while (m --) {
        int l, r;
        std::cin >> l >> r;
        std::cout << rangeQuery(root[r], root[l - 1], L, R, r - l + 1) << '\n';
    }

    return 0;
}

2.11 P3899 [湖南集训] 更为厉害

原题链接

P3899 [湖南集训] 更为厉害

思路分析

可以线段树合并来做,该天写个线段树合并的博客。

对于查询 p,k

如果 b 为 p 的祖先,那么方案数就是 min(d[p] - 1, k) * (size[p] - 1)

如果 b 为 p 的后代,那么方案数为 所有距离合法的后代的 size - 1 的和

那么我们按dfs序开线段树,维护每个深度的权值和,因为子树内dfs序连续,第二部分贡献我们查询 dfn[p] 和 dfn[p] + size[p] - 1两个线段树关于指定深度区间的差分和即可。

AC代码

#include <bits/stdc++.h>

using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;

constexpr int N = 3E5;

struct Node{
    i64 sum;
    int l, r;    
} pool[30 * N];

int newNode() {
    static int tot = 0;
    return ++ tot;
}

void modify(int &x, int y, int l, int r, int k, int v) { 
    if (k < l || k > r) {
        return;
    }

    x = newNode();
    pool[x] = pool[y];
    pool[x].sum += v;

    if (l == r) {
        return;
    }

    int m = (l + r) / 2;
    modify(pool[x].l, pool[y].l, l, m, k, v);
    modify(pool[x].r, pool[y].r, m + 1, r, k, v);
}

i64 rangeQuery(int p, int q, int l, int r, int x, int y) {
    if (x > y || x > r || y < l) {
        return 0;
    }

    if (x <= l && r <= y) {
        return pool[p].sum - pool[q].sum;
    }

    int m = (l + r) / 2;

    return rangeQuery(pool[p].l, pool[q].l, l, m, x, y) + rangeQuery(pool[p].r, pool[q].r, m + 1, r, x, y);
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, q;
    std::cin >> n >> q;

    std::vector<std::vector<int>> adj(n);
    for (int i = 1; i < n; ++ i) {
        int u, v;
        std::cin >> u >> v;

        -- u, -- v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    std::vector<int> root(n), siz(n, 1), d(n), dfn(n), seq(n);

    int cur = 0;
    auto dfs = [&](auto &&self, int u, int p) -> void {
        dfn[u] = cur ++;
        seq[dfn[u]] = u;
        for (int v : adj[u]) {
            if (v == p) {
                continue;
            }

            d[v] = d[u] + 1;
            self(self, v, u);
            siz[u] += siz[v];
        }
    };

    dfs(dfs, 0, -1);

    const int M = std::ranges::max(d);

    for (int i = 0; i < n; ++ i) {
        modify(root[i], i > 0 ? root[i - 1] : 0, 0, M, d[seq[i]], siz[seq[i]] - 1);
    }

    while (q --) {
        int p, k;
        std::cin >> p >> k;
        -- p;

        std::cout << std::min(d[p], k) * (siz[p] - 1LL) + rangeQuery(root[dfn[p] + siz[p] - 1], root[dfn[p]], 0, M, d[p] + 1, std::min(M, d[p] + k)) << '\n';
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Equinox

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值