欢迎光临
我们一直在努力

【算法随笔】P13983 数列分块入门 8

洛谷P13983 数列分块入门 8 题目背景 洛谷的数列分块入门系列的测试数据范围和原题有不同。


题目描述 给出一个长为 n 的数列,以及 n 个操作,操作涉及区间询问等于一个数 c 的元素,并将这个区间的所有元素改为 c。


输入格式 第一行输入一个数字 n。

第二行输入 n 个数字,第 i 个数字为

a

i

a_i

ai,以空格隔开。

接下来输入 n 行询问,每行输入三个数字 l、r、c,以空格隔开。

表示先查询位于 [l,r] 的数字有多少个是 c,再把位于 [l,r] 的数字都改为 c。


输出格式 对于每次询问,输出一行一个数字表示答案。


题解

这道题非常适合使用珂朵莉树(Chtholly Tree,又称 ODT – Old Driver Tree)来解决。

方法解析

珂朵莉树是一种基于 std::set 维护区间的数据结构。它将值相同的连续一段元素合并成一个区间(节点)进行维护。对于一般的区间操作,如果操作中包含高频的区间推平(区间赋值)操作,珂朵莉树能展现出极高的效率。

这道题的每一个操作恰好都是:先查询区间内具有某个值的元素个数,然后立刻把这个区间的所有元素全部修改为这个值(即典型的“区间推平”)。

时间复杂度分析:

  • 每次操作最多引入

    2

    2

    2 个新的区间切割点(分别在

    l

    l

    l

    r

    +

    1

    r+1

    r+1 处)。

  • 在处理了最多

    O

    (

    n

    )

    O(n)

    O(n) 次切割后,每次的推平操作会把

    [

    l

    ,

    r

    ]

    [l, r]

    [l,r] 内的所有散乱区间全部删除,替换为仅仅

    1

    1

    1 个大区间。

  • 因为新产生的区间总数是

    O

    (

    n

    )

    O(n)

    O(n) 级别的,每个区间被加入 set 和从 set 中删除最多各一次,所以整体的均摊时间复杂度被严格保证在

    O

    (

    n

    log

    n

    )

    O(n \\log n)

    O(nlogn),足以在时限内轻松通过

    n

    =

    300000

    n=300000

    n=300000 的数据。

  • C++ 完整代码

    #include <iostream>
    #include <set>

    using namespace std;

    // 定义珂朵莉树的节点
    struct Node {
    int l, r;
    mutable long long v; // mutable 允许在 set 迭代器中修改值,虽然本题直接erase再insert
    Node(int l, int r = 1, long long v = 0) : l(l), r(r), v(v) {}
    // set 根据左端点排序
    bool operator<(const Node& o) const {
    return l < o.l;
    }
    };

    set<Node> odt;
    int n;

    // 分裂操作:将包含 pos 的区间 [l, r] 分裂为 [l, pos-1] 和 [pos, r]
    // 并返回指向 [pos, r] 的迭代器
    set<Node>::iterator split(int pos) {
    if (pos > n) return odt.end(); // 超出边界返回 end()

    auto it = odt.lower_bound(Node(pos));
    if (it != odt.end() && it->l == pos) {
    return it; // 如果已经存在以 pos 为左端点的区间,直接返回
    }

    it; // 否则 pos 必然被包含在它前一个区间内部
    int l = it->l, r = it->r;
    long long v = it->v;

    // 删除原区间,插入两个分裂后的区间
    odt.erase(it);
    odt.insert(Node(l, pos 1, v));
    return odt.insert(Node(pos, r, v)).first;
    }

    int main() {
    // 开启 Fast IO
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    if (!(cin >> n)) return 0;

    // 初始输入压缩连续相同元素,优化初始化耗时
    long long prev_a = 2e18; // 使用超出数据范围的 dummy 值
    int prev_l = 1;

    for (int i = 1; i <= n; ++i) {
    long long a;
    cin >> a;
    if (i == 1) {
    prev_a = a;
    prev_l = 1;
    } else if (a != prev_a) {
    odt.insert(Node(prev_l, i 1, prev_a));
    prev_a = a;
    prev_l = i;
    }
    }
    if (n >= 1) {
    odt.insert(Node(prev_l, n, prev_a));
    }

    // 处理每次询问与修改
    for (int i = 0; i < n; ++i) {
    int l, r;
    long long c;
    cin >> l >> r >> c;

    // 核心:必须先 split 右端点 (r + 1),再 split 左端点 (l)
    // 顺序不能反,否则 split 右端点可能会导致左端点的迭代器失效
    auto itr = split(r + 1);
    auto itl = split(l);

    int count = 0;
    // 统计查询区间内值为 c 的元素个数
    for (auto it = itl; it != itr; ++it) {
    if (it->v == c) {
    count += (it->r it->l + 1);
    }
    }

    // 区间推平(赋新值):删除散段,插入整段
    odt.erase(itl, itr);
    odt.insert(Node(l, r, c));

    cout << count << "\\n";
    }

    return 0;
    }

    细节补充说明

  • 为什么要先 split(r+1) 再 split(l)? 在向集合请求分离时,如果先 split(l) 会返回一个迭代器,接下来做 split(r+1) 可能会因为恰巧在同一个大块内部而把包含

    l

    l

    l 的区间直接从 set 中 erase 删除掉,导致最初取得的

    l

    l

    l 的迭代器失效进而引发内存段错误(RE)。按倒序分裂(先右后左)就能安全规避此问题。

  • 数据类型界限: 题目给出元素的值在

    2

    31

    -2^{31}

    231

    2

    31

    1

    2^{31}-1

    2311 内,这其实恰好填满标准有符号 32位整型(int)的范围。但为确保计算和逻辑绝对安全避免溢出(以及设定不可能达到的界限哨兵值 2e18),代码采用的 long long 存储元素值。由于只计个数,count 最多为 300,000 不会越过 int,因此统计个数放心使用 int。

  • 赞(0)
    未经允许不得转载:171主机测评 » 【算法随笔】P13983 数列分块入门 8
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址