【Coel.做题笔记】【珂艾尔与珂朵莉】Chtholly Tree 珂朵莉树

题前碎语

我为什么要在期考前写玄学数据结构的笔记……
我是不是有点大病……
huige说我在越级做题,但是StudyingFather的题解里说学会珂朵莉树只要会用std::set(

题目简介

CF896C Willem, Chtholly and Seniorious
CodeForces原题传送门
洛谷传送门
题目背景就不抄了,想看的珂学家可以直接去看原题。

题目描述

实现一个支持以下操作的数据结构:

  • \(1\) \(l\) \(r\) \(x\) :将\([l,r]\) 区间所有数加上\(x\)
  • \(2\) \(l\) \(r\) \(x\) :将\([l,r]\) 区间所有数改成\(x\)
  • \(3\) \(l\) \(r\) \(x\) :输出将\([l,r]\) 区间从小到大排序后的第\(x\) 个数是的多少(即区间第\(x\) 小,数字大小相同算多次,保证 \(1\leq\) \(x\) \(\leq\) \(r-l+1\) )
  • \(4\) \(l\) \(r\) \(x\) \(y\) :输出\([l,r]\) 区间每个数字的\(x\) 次方的和模\(y\) 的值(即(\(\sum^r_{i=l}a_i^x\) ) \(\mod y\) )

输入格式

这道题目的输入格式比较特殊,需要选手通过\(seed\) 自己生成输入数据。
输入一行四个整数\(n,m,seed,v_{max}\)\(1\leq\) \(n,m\leq 10^{5}\) ,\(0\leq seed \leq 10^{9}+7\) \(,1\leq vmax \leq 10^{9}\)
其中\(n\) 表示数列长度,\(m\) 表示操作次数,后面两个用于生成输入数据。
题目给出用于生成数据的伪代码如下:

def rnd():

    ret = seed
    seed = (seed * 7 + 13) mod 1000000007
    return ret

for i = 1 to n:

    a[i] = (rnd() mod vmax) + 1

for i = 1 to m:

    op = (rnd() mod 4) + 1
    l = (rnd() mod n) + 1
    r = (rnd() mod n) + 1

    if (l > r): 
         swap(l, r)

    if (op == 3):
        x = (rnd() mod (r - l + 1)) + 1
    else:
        x = (rnd() mod vmax) + 1

    if (op == 4):
        y = (rnd() mod vmax) + 1

输出格式

对于每个操作3和4,输出一行仅一个数。


这题需要实现的就是大名鼎鼎的珂朵莉树——\(Chtholly\) \(Tree\)(又叫\(Old\)-\(Driver\) \(Tree\),简称\(ODT\))。
不过根据各位大佬的解释,珂朵莉树实际上不能算是一种数据结构,而是一种暴力解决问题的算法技巧。


不管这么多,直接开始正题。
珂朵莉树最出众的功能就是推倒(hso)操作,把一个区间全部修改为同一个数,也就是这题的操作2。
当然,我们首先得写一个结构体,把每个区间的左端点、右端点和数值存起来。

struct odt {
    ll left, right;
    mutable ll val;
 
    odt(ll left, ll right = 0, ll val = 0) : left(left), right(right), val(val) {}//初始化,当然强制转换也行
 
    inline bool operator<(const odt &x) const {
        return left < x.left;//按左端点排序
    }
};

注意到对val的定义多了个mutable关键字,这个在之后的操作里有很大作用,暂且卖个关子。
接下来用一个set把点给存起来进行初始化。伪代码中给出的数据用a[i]表示,这里直接插进set里就好了,不需要这个数组。

std::set< odt > S;
for (int i = 1; i <= n; i++)
        S.insert(odt(i, i, (rnd() % vmax) + 1));

很快,我们来到了珂朵莉树最为核心最为关键的一个函数:\(split\)!
(它是如此关键以至于我要用一个\(\LaTeX\)来强调)
这个函数的作用是把一个区间分割成\([left,pos-1]\)\([pos,right]\),这样才方便之后的合并;同时返回一个迭代器,代表后半段开头。
先定义一个迭代器表示不小于pos的点,然后分成以下几个情况看待:

  • 如果这个区间以pos为起点,那就不需要做任何事,直接返回这个迭代器;
  • 如果右端点比pos还要小,那就直接返回S.end();
  • 如果把pos包含上了,才需要把区间分成两半,顺便把原来那个区间删掉。
auto split(ll pos) {
    auto it = S.lower_bound(odt(pos));
 
    if (it != S.end() && (it -> left) == pos)//情况1
        return it;
 
    it--;
 
    if ((it -> right) < pos)//情况2
        return S.end();
 
    ll l = it -> left, r = it -> right, v = it -> val;//情况3
 
    S.erase(it);
    S.insert(odt(l, pos - 1, v));
 
    return S.insert(odt(pos, r, v)).first;
}

这里利用到了两个小技巧。
第一个是insert函数有返回值,是一个pair,并且这个pair的first就是我们需要的后半段开头的迭代器,所以直接返回。
第二个就是C++14的独门秘技——auto!!!
auto可以直接根据数据判定数据类型,这是一个很棒的东西。
试想一下,如果没有auto,写一个迭代器定义就只能是:
std::set< odt >::iterator
写起来又累又容易打错!
虽然我们可以用宏定义解决这个问题,但是auto就能轻松解决。
有人可能会问了:

auto不是C++11就有了吗?

确实,但是在C++11,auto不能作为函数返回值,这意味着我们的\(split\)只能回到那个冗长的样子。
更重要的是,在NOI-Linux 2.0里,C++14统一使用!!!
改革春风吹满地,用C++14的CCF真神气!
啊好像有点跑题了(


下一个要实现的就是推倒区间的操作——assign。
有了前面split的铺垫,这一步就灰常简单了~
定义两个迭代器代表区间左端点和右端点,用split把区间分解,然后插入推倒之后的新区间,并且用S.erase(it_l, it_r)删掉之前的区间。
需要注意的是,一定要先分解后半段再分解前半段,否则会访问到空区间导致RE。

void assign(ll l, ll r, ll v) {
    auto it_r = split(r + 1), it_l = split(l);
    S.erase(it_l, it_r);
    S.insert(odt(l, r, v));
}

剩下的几个操作也都很简单,快点过掉吧~


全部加上一个数:
定义端点-遍历-直接加,简单粗暴。

void add(ll l, ll r, ll v) {
    auto it_r = split(r + 1), it_l = split(l);
    for (auto it = it_l; it != it_r; it++)
        it->val += v;
}

前面提到的multable就是在这儿发挥作用的~
iterator是常量迭代器,正常情况下没法改数据,但是加上了multable就意味着常量也可以被强制修改,加数字就能轻松实现了~


查询区间第x小:
全部扔到一个vector里面排序,然后逐个相减,也很简单粗暴。
有的题解又定义一个结构体,其实用pair足矣:first代表数字,second代表相同数字的数量(有点像桶排序里的桶?)。
顺带一提,这个函数我一开始犯了各种各样的低级错误,还被huige说了一顿……qwq

ll rank(ll l, ll r, ll x) {
    std::vector< std::pair<ll, ll> > V;
    auto it_r = split(r + 1), it_l = split(l);
 
    for (auto it = it_l; it != it_r; it++)
        V.push_back(std::make_pair(it -> val, (it -> right) - (it -> left) + 1));
    
    std::sort(V.begin(), V.end());
 
    auto it = V.begin();
 
    while (it != V.end() && it -> second < x)
        x -= (it -> second),it++;
    return it -> first;
}

不知道在干什么的申必操作求乘方和取模,同样很暴力。
小提醒:做快速幂的时候要先把底数模一下,因为有可能在a*a%p的时候爆long long导致结果错误。

ll calc(ll l, ll r, ll x, ll y) {
    auto it_r = split(r + 1), it_l = split(l);
    ll ans = 0;
 
    for (auto it = it_l; it != it_r; ++it) 
        ans = (ans + qpow(it -> val, x, y) * ((it -> right) - (it -> left) + 1) % y) % y;
    
    return ans;
}

好啦,所有操作实现完成,是时候祭出珂朵莉树的完整代码了!

#include <set>
#include <vector>
#include <cstdio>
#include <iostream>
#include <algorithm>
 
typedef long long ll;
 
const ll mod = 1000000007;
 
ll n, m, seed, vmax;
 
struct odt {
    ll left, right;
    mutable ll val;
 
    odt(ll left, ll right = 0, ll val = 0) : left(left), right(right), val(val) {}
 
    inline bool operator<(const odt &x) const {
        return left < x.left;
    }
};
 
std::set< odt > S;
 
inline ll qpow(ll a, ll b, ll p) {
    ll ans = 1;
    a %= p;

    while (b) {
        if (b % 2 == 1)
            ans = ans * a % p;
        a = a * a % p;
        b >>= 1;
    }
 
    return ans;
}
 
auto split(ll pos) {
    auto it = S.lower_bound(odt(pos));
 
    if (it != S.end() && it -> left == pos)
        return it;
 
    it--;
 
    if (it -> right < pos)
        return S.end();
 
    ll l = it -> left, r = it -> right, v = it -> val;
 
    S.erase(it);
    S.insert(odt(l, pos - 1, v));
 
    return S.insert(odt(pos, r, v)).first;
}
 
void assign(ll l, ll r, ll v) {
    auto it_r = split(r + 1), it_l = split(l);
    S.erase(it_l, it_r);
    S.insert(odt(l, r, v));
}
 
void add(ll l, ll r, ll v) {
    auto it_r = split(r + 1), it_l = split(l);
    for (auto it = it_l; it != it_r; it++)
        it->val += v;
}
 
ll rank(ll l, ll r, ll x) {
    std::vector< std::pair<ll, ll> > V;
    auto it_r = split(r + 1), it_l = split(l);
 
    for (auto it = it_l; it != it_r; it++)
        V.push_back(std::make_pair(it -> val, (it -> right) - (it -> left) + 1));
    
    std::sort(V.begin(), V.end());
 
    auto it = V.begin();
 
    while (it != V.end() && it -> second < x)
        x -= (it -> second),it++;
    return it -> first;
    return -1;
}
 
ll calc(ll l, ll r, ll x, ll y) {
    auto it_r = split(r + 1), it_l = split(l);
    ll ans = 0;
 
    for (auto it = it_l; it != it_r; ++it) 
        ans = (ans + qpow(it -> val, x, y) * (it -> right - it -> left + 1) % y) % y;
    
    return ans;
}
 
inline ll rnd() {
    ll ans = seed;
    seed = (seed * 7 + 13) % mod;
    return ans;
}
 
int main() {
    scanf("%lld%lld%lld%lld", &n, &m, &seed, &vmax);
 
    for (int i = 1; i <= n; i++)
        S.insert(odt(i, i, (rnd() % vmax) + 1));
 
    for(int i = 1; i <= m; i++) {
        ll op = (rnd() % 4) + 1, l = (rnd() % n) + 1, r = (rnd() % n) + 1, x, y;
 
        if (l > r)
            std::swap(l, r);
 
        if (op == 3) 
            x = (rnd() % (r - l + 1)) + 1;
        else 
            x = (rnd() % vmax) + 1;
        
        if (op == 4) 
            y = (rnd() % vmax) + 1;//严格按照伪代码要求生成数据
 
        switch (op) {
            case 1 : add(l, r, x); break;
            case 2 : assign(l, r, x); break;
            case 3 : printf("%lld\n", rank(l, r, x)); break;
            case 4 : printf("%lld\n", calc(l, r, x, y)); break;
        }
    }
 
    return 0;
}

题后闲话

写完还要被老妈催去睡觉,烦耶……
过几天就要期考了,祝自己期考顺利ww

原文地址:https://www.cnblogs.com/Coel-Flannette/p/15808590.html