P12194 [NOISG 2025 Prelim] Snacks 题解

题目简析

给定长度为 \(n\) 的序列,\(q\) 次询问,把在 \([l,r]\) 内的元素覆盖为 \(x\)

求最初的和每次询问时元素之和。

思路解析

动态开点权值线段树

对于在区间的操作容易想到线段树,线段树支持区间查询、区间清零和单点更新。

用动态开点权值线段树维护值域上的元素个数和总和。

时间复杂度 \(O(q \log V)\),其中 \(V\) 是值域范围(\(0 \le V \le10^9\)),可以考虑离散化。

平衡树(FHQ-Treap)

使用 FHQ-Treap 维护值域。每个节点存储一个值 \(val\),该值出现的次数 \(cnt\),子树总和 \(sum\),子树总节点数 \(tot\)

对于每个查询:

  • 分裂出值在 \([l, r]\) 范围内的节点。

  • 删除这部分,合并剩余部分。

  • 计算被删除的节点数 \(cnt\) 和总和 \(sum\)

  • 插入 \(cnt\) 个新值 \(x\)

时间复杂度 \(O((n + q) \log n )\)

STL

使用 std::map 存储每个值及其出现次数,维护值域到出现次数的映射,这样我们可以快速查找、删除和插入值。

使用 lower_boundupper_bound 快速查找。

时间复杂度 $ O((n + q) \log n)$。

珂朵莉树

珂朵莉树特别适合有大量区间赋值操作,只需要考虑如何维护和就可以了。

直接操作值域点,利用 std::set 定位值域范围,合并相同值节点,这样,就可以 AC 了。

详见 OI WIKI 上的珂朵莉树

时间复杂度 \(O((n + q) \log D)\),其中 \(D \le n + q\)\(D\) 为不同值的数量。

代码实现

动态开点权值线段树实现:

#pragma G++ optimize("O3", "unroll-loops", "omit-frame-pointer", "inline")
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1000000000;
const int MAXNODE = 20000000;
struct Node {
    int lson, rson;
    long long sum;
    long long cnt;
    bool zero;
} tree[MAXNODE + 10];
int root, tot;

void push_up(int p) {
    tree[p].cnt = 0;
    tree[p].sum = 0;
    if (tree[p].lson) {
        tree[p].cnt += tree[tree[p].lson].cnt;
        tree[p].sum += tree[tree[p].lson].sum;
    }
    if (tree[p].rson) {
        tree[p].cnt += tree[tree[p].rson].cnt;
        tree[p].sum += tree[tree[p].rson].sum;
    }
}

void push_down(int p) {
    if (tree[p].zero) {
        if (tree[p].lson) {
            int ls = tree[p].lson;
            tree[ls].cnt = 0;
            tree[ls].sum = 0;
            tree[ls].zero = true;
        }
        if (tree[p].rson) {
            int rs = tree[p].rson;
            tree[rs].cnt = 0;
            tree[rs].sum = 0;
            tree[rs].zero = true;
        }
        tree[p].zero = false;
    }
}

void update_point(int& p, int l, int r, int pos, long long delta_cnt) {
    if (delta_cnt == 0)
        return;
    if (!p) {
        p = ++tot;
        tree[p].lson = tree[p].rson = 0;
        tree[p].sum = 0;
        tree[p].cnt = 0;
        tree[p].zero = false;
    }
    if (l == r) {
        tree[p].cnt += delta_cnt;
        tree[p].sum = tree[p].cnt * (long long)l;
        return;
    }
    push_down(p);
    int mid = (l + (long long)r) >> 1;
    if (pos <= mid) {
        update_point(tree[p].lson, l, mid, pos, delta_cnt);
    } else {
        update_point(tree[p].rson, mid + 1, r, pos, delta_cnt);
    }
    push_up(p);
}

void update_zero(int& p, int l, int r, int ql, int qr) {
    if (!p || tree[p].cnt == 0)
        return;
    if (ql <= l && r <= qr) {
        tree[p].cnt = 0;
        tree[p].sum = 0;
        tree[p].zero = true;
        return;
    }
    push_down(p);
    int mid = (l + (long long)r) >> 1;
    if (ql <= mid) {
        update_zero(tree[p].lson, l, mid, ql, qr);
    }
    if (qr > mid) {
        update_zero(tree[p].rson, mid + 1, r, ql, qr);
    }
    push_up(p);
}

pair<long long, long long> query(int p, int l, int r, int ql, int qr) {
    if (!p || tree[p].cnt == 0) {
        return {0, 0};
    }
    if (ql <= l && r <= qr) {
        return {tree[p].sum, tree[p].cnt};
    }
    push_down(p);
    int mid = (l + (long long)r) >> 1;
    pair<long long, long long> res = {0, 0};
    if (ql <= mid) {
        auto left_res = query(tree[p].lson, l, mid, ql, qr);
        res.first += left_res.first;
        res.second += left_res.second;
    }
    if (qr > mid) {
        auto right_res = query(tree[p].rson, mid + 1, r, ql, qr);
        res.first += right_res.first;
        res.second += right_res.second;
    }
    return res;
}
long long a[(int)(2e5 + 1)];
int main() {
    int n, q;
    scanf("%d %d", &n, &q);
    long long total = 0;
    for (int i = 0; i < n; i++) {
        scanf("%lld", &a[i]);
        total += a[i];
    }

    root = 0;
    tot = 0;
    for (int i = 0; i < n; i++) {
        update_point(root, 0, MAXV, a[i], 1);
    }

    printf("%lld\n", total);

    for (int j = 0; j < q; j++) {
        long long l, r, x;
        scanf("%lld %lld %lld", &l, &r, &x);
        auto res = query(root, 0, MAXV, l, r);
        long long remove_sum = res.first;
        long long remove_cnt = res.second;

        update_zero(root, 0, MAXV, l, r);
        if (remove_cnt > 0) {
            update_point(root, 0, MAXV, x, remove_cnt);
        }

        total = total - remove_sum + x * remove_cnt;
        printf("%lld\n", total);
    }

    return 0;
}

FHQ-Treap 实现:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 400010; // 最大节点数

struct Node {
    int l, r;       // 左右子节点索引
    ll val;         // 节点存储的值
    ll cnt;         // 该值出现的次数
    ll sum;         // 子树值总和(值×出现次数)
    ll tot;         // 子树节点出现次数总和
    int rnd;        // 随机优先级
} tree[MAXN];

int idx = 0;        // 节点池索引
int root = 0;       // 树根

// 创建新节点
int new_node(ll val, ll cnt) {
    if (cnt == 0) return 0;
    idx++;
    tree[idx].l = tree[idx].r = 0;
    tree[idx].val = val;
    tree[idx].cnt = cnt;
    tree[idx].sum = val * cnt;
    tree[idx].tot = cnt;
    tree[idx].rnd = rand();
    return idx;
}

// 更新节点信息
void push_up(int id) {
    if (!id) return;
    Node &t = tree[id];
    t.tot = tree[t.l].tot + tree[t.r].tot + t.cnt;
    t.sum = tree[t.l].sum + tree[t.r].sum + t.val * t.cnt;
}

// 按值分裂:将树id分裂为<=k的树x和>k的树y
void split(int id, ll k, int &x, int &y) {
    if (!id) {
        x = y = 0;
        return;
    }
    if (tree[id].val <= k) {
        x = id;
        split(tree[id].r, k, tree[id].r, y);
        push_up(x);
    } else {
        y = id;
        split(tree[id].l, k, x, tree[id].l);
        push_up(y);
    }
}

// 合并两棵树x和y(x的所有值<=y的所有值)
int merge(int x, int y) {
    if (!x || !y) return x | y;
    if (tree[x].rnd < tree[y].rnd) {
        tree[x].r = merge(tree[x].r, y);
        push_up(x);
        return x;
    } else {
        tree[y].l = merge(x, tree[y].l);
        push_up(y);
        return y;
    }
}

// 插入cnt个值为val的节点
void insert(ll val, ll cnt) {
    if (cnt == 0) return;
    int x, y, z;
    z = 0;
    split(root, val, x, y);      // 分裂为<=val和>val
    split(x, val - 1, x, z);    // 将<=val的再分裂为<val和=val
    if (z) {
        tree[z].cnt += cnt;
        tree[z].sum += val * cnt;
        tree[z].tot += cnt;
        push_up(z); // 更新节点z
    } else {
        z = new_node(val, cnt);
    }
    root = merge(merge(x, z), y); // 合并三部分
}

int main() {
    srand(time(0)); // 初始化随机种子
    int n, q;
    scanf("%d%d", &n, &q);
    root = idx = 0;

    // 读入初始数组
    for (int i = 0; i < n; i++) {
        ll a_val;
        scanf("%lld", &a_val);
        insert(a_val, 1); // 插入每个值
    }

    // 输出初始总和
    printf("%lld\n", tree[root].sum);

    // 处理每个查询
    for (int i = 0; i < q; i++) {
        ll l, r, x_val;
        scanf("%lld%lld%lld", &l, &r, &x_val);
        int left, mid, right, mid_right;

        // 第一次分裂:按l-1分裂
        split(root, l - 1, left, mid_right);
        // 第二次分裂:按r分裂
        split(mid_right, r, mid, right);

        // 合并左部分和右部分(删除中间部分)
        root = merge(left, right);

        ll del_cnt = 0, del_sum = 0;
        if (mid) {
            del_cnt = tree[mid].tot; // 被删除的节点数
            del_sum = tree[mid].sum; // 被删除节点的总和
        }

        // 插入新值
        insert(x_val, del_cnt);

        // 输出当前总和
        printf("%lld\n", tree[root].sum);
    }

    return 0;
}

std::map 实现

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, q;
    cin >> n >> q;
    map<ll, ll> m; // 值 -> 出现次数
    ll total_sum = 0;

    // 读入初始数组
    for (int i = 0; i < n; i++) {
        ll a_val;
        cin >> a_val;
        m[a_val]++; // 计数
        total_sum += a_val;
    }

    // 输出初始总和
    cout << total_sum << '\n';

    for (int i = 0; i < q; i++) {
        ll l, r, x;
        cin >> l >> r >> x;

        // 找到第一个大于等于 l 的节点
        auto it = m.lower_bound(l);
        ll cnt_total = 0; // 记录被删除的元素总数

        // 遍历区间 [l, r] 内的节点
        while (it != m.end() && it->first <= r) {
            // 记录当前节点的值和出现次数
            ll num = it->first;
            ll cnt = it->second;
            cnt_total += cnt;
            total_sum -= num * cnt; // 从总和中减去贡献
            
            // 删除当前节点,并获取下一个节点的迭代器
            it = m.erase(it);
        }

        // 如果存在被删除的元素,则将它们替换为 x
        if (cnt_total > 0) {
            m[x] += cnt_total; // 增加 x 的出现次数
            total_sum += x * cnt_total; // 更新总和
        }

        // 输出当前总和
        cout << total_sum << '\n';
    }

    return 0;
}

珂朵莉树实现:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 节点结构体:表示值域区间 [v, v]
struct Node {
    ll v;           // 元素值
    mutable ll cnt; // 该值出现的次数
    
    Node(ll v, ll cnt) : v(v), cnt(cnt) {}
    
    // 按值排序
    bool operator<(const Node& rhs) const {
        return v < rhs.v;
    }
};

class ChthollyTree {
private:
    set<Node> s;    // 存储所有值域节点
    ll total_sum;   // 当前总和

public:
    ChthollyTree() : total_sum(0) {}
    
    // 添加初始元素
    void add_element(ll value) {
        auto it = s.find(Node(value, 0));
        if (it != s.end()) {
            it->cnt++;
        } else {
            s.insert(Node(value, 1));
        }
        total_sum += value;
    }
    
    // 处理查询
    void query(ll l, ll r, ll x) {
        // 找到第一个 ≥ l 的节点
        auto it = s.lower_bound(Node(l, 0));
        vector<set<Node>::iterator> to_remove;
        ll cnt_total = 0;
        ll sum_remove = 0;
        
        // 收集要删除的节点(迭代器)
        while (it != s.end() && it->v <= r) {
            cnt_total += it->cnt;
            sum_remove += it->v * it->cnt;
            to_remove.push_back(it);
            it++;
        }
        
        // 删除节点
        for (auto iter : to_remove) {
            s.erase(iter);
        }
        
        // 更新总和
        total_sum = total_sum - sum_remove + x * cnt_total;
        
        // 插入新值
        if (cnt_total > 0) {
            auto it_x = s.find(Node(x, 0));
            if (it_x != s.end()) {
                it_x->cnt += cnt_total;
            } else {
                s.insert(Node(x, cnt_total));
            }
        }
    }
    
    // 获取当前总和
    ll get_sum() const {
        return total_sum;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int n, q;
    cin >> n >> q;
    ChthollyTree ct;
    
    // 读入初始数组
    for (int i = 0; i < n; i++) {
        ll a_val;
        cin >> a_val;
        ct.add_element(a_val);
    }
    
    // 输出初始总和
    cout << ct.get_sum() << '\n';
    
    // 处理查询
    for (int i = 0; i < q; i++) {
        ll l, r, x;
        cin >> l >> r >> x;
        ct.query(l, r, x);
        cout << ct.get_sum() << '\n';
    }
    
    return 0;
}

后记

  1. 其实我都只在维护桶。

  2. 笑点解析:实际消耗时间 STL < 珂朵莉树 < FHQ-Treap <= 动态开点权值线段树。

posted @ 2025-07-31 10:13  TangyixiaoQAQ  阅读(29)  评论(0)    收藏  举报