珂朵莉樹小結

珂朵莉是世界上最幸福的女孩,不接受任何反駁

學習珂學?請左轉此處


一、珂朵莉樹簡介

珂朵莉樹是由某毒瘤在2017年的一場CF比賽中提出的數據結構,原名老司機樹(Old Driver Tree,ODT)。因爲第一個以珂朵莉樹爲正解的題目的背景以《末日時在作什麼?有沒有空?能夠來拯救嗎?》中的角色——珂朵莉爲主角,因此這個數據結構又被稱爲珂朵莉樹(Chtholly Tree)她主要處理區間修改、查詢問題,在數據隨機的前提下有着優秀的複雜度。node

說到區間維護,我就想起明年年初 咱們都會想到線段樹、樹狀數組、splay、分塊、莫隊等數據結構,這些數據結構貌似是無所不能的(畢竟一個不行用兩個),不管區間加減乘除、平方開方都能搞定。但若是咱們有這麼一題:(CF中的起源題)數組

【CF896C】 Willem, Chtholly and Seniorious

題意:寫個數據結構維護長度爲\(n\)的序列(\(n≤10^5\)),實現區間賦值、區間加上一個值、求區間第k小、求區間每一個數x次方之和模y的值的操做,數據隨機生成數據結構

咱們發現本題的第4個操做涉及每一個數字的相關操做,那麼線段樹、樹狀數組、分塊甚至splay是確定行不通的,莫隊雖然能夠維護4操做,但複雜度一樣難以承受(要對每一個入隊出隊的數取快速冪)。咱們須要一個更爲高效的數據結構來維護這些操做,她就是珂朵莉樹。less

二、珂朵莉樹的實現

珂朵莉樹基於std::set,咱們定義節點爲一段連續的值相同的區間,代碼以下:函數

struct node {
    int l, r;
    mutable ll val;
    int operator < (const node &a) const{
        return l < a.l;
    }
    node(int L, int R, ll Val) : l(L), r(R), val(Val) {}
    node(int L) : l(L) {}
};

其中函數node是構造函數,能夠理解爲定義變量時的初始化函數,具體用法請看下文代碼。學習

mutable修飾符意爲「可變的」,這樣咱們就能夠在本不支持直接修改的set中修改它的值辣優化

\(l\)\(r\)分別表明當前區間的左、右端點,\(val\)表示當前區間每一個數字的值。spa

可是咱們查詢的時候不能保證查詢的區間端點必定與這些節點的端點重合,若是採用分塊思想(邊角暴力)確定行不通(會退化成暴力),因此咱們要按需把節點分裂一下:code

#define sit set<node>::iterator
sit Split(int pos) {
    sit it = s.lower_bound(node(pos));
    if (it != s.end() && it->l == pos) return it;
    --it;
    int l = it->l, r = it->r;
    int val = it->val;
    s.erase(it);
    s.insert(node(l, pos - 1, val));
    return s.insert(node(pos, r, val)).first;
}

這段代碼所作的事就是把\(pos\)所在的節點(左右端點分別爲\(l\)\(r\))分紅 \([l, pos)\)\([pos, r]\) 兩塊,而後返回後者。返回值所用的是set::insert的返回值,這是一個pair對象(熟悉map的同窗應該能熟練運用),它的first是一個迭代器,即插入的東西在set中的迭代器。對象

這段代碼很簡單,應該不難打(bei)出(song)

接下來的addkthsum等操做都依賴於Split操做,具體作法就是把區間左右端點所在的節點分裂,使得修改&查詢區間能徹底對應起來,以後就是暴力地去搞啦qwq

參考代碼:

void Add(int l, int r, ll val) {//暴力枚舉
    sit it2 = Split(r + 1), it1 = Split(l);
    for (sit it = it1; it != it2; ++it) it->val += val;
}
ll Kth(int l, int r, int k) {//暴力排序
    sit it2 = Split(r + 1), it1 = Split(l);
    vector< pair<ll, int> > aa;
    aa.clear();
    for (sit it = it1; it != it2; ++it) aa.push_back(pair<ll, int>(it->val, it->r - it->l + 1));
    sort(aa.begin(), aa.end());
    for (int i = 0; i < aa.size(); ++i) {
        k -= aa[i].second;
        if (k <= 0) return aa[i].first;
    }
}
ll qpow(ll a, int x, ll y) {
    ll b = 1ll;
    a %= y;//不加這句話WA
    while (x) {
        if (x & 1) b = (b * a) % y;
        a = (a * a) % y;
        x >>= 1;
    }
    return b;
}
ll Query(int l, int r, int x, ll y) {//暴力枚舉+快速冪
    sit it2 = Split(r + 1), it1 = Split(l);
    ll res = 0;
    for (sit it = it1; it != it2; ++it) res = (res + (it->r - it->l + 1) * qpow(it->val, x, y)) % y;
    return res;
}

FAQ:

\(1.Q:\)爲何要Split(r + 1)
\(A:\)便於取出 \([l, r + 1)\) 的部分,即 \([l,r]\)

\(2.Q:\)爲何要先Split(r + 1),再Split(l)
\(A:\)由於若是先Split(l),返回的迭代器會位於所對應的區間以\(l\)爲左端點,此時若是\(r\)也在這個節點內,就會致使Split(l)返回的迭代器被erase掉,致使RE。

\(3.Q:\)這些操做裏面全是Split,複雜度理論上會退化成暴力(不斷分裂直到沒法再分),怎麼讓它不退化?

這便涉及到珂朵莉樹不可或缺的操做:\(Assign\)

Assign操做也很簡單,Split以後暴力刪點,而後加上一個表明當前區間的點便可。代碼以下:

void Assign(int l, int r, int val) {
    sit it2 = Split(r + 1), it1 = Split(l);
    s.erase(it1, it2);
    s.insert(node(l, r, val));
}

不難看出,這個操做把多個節點減成一個,但這樣就能使複雜度優化了嗎?

\(Assign\)的珂朵莉樹究竟有多快

因爲題目數據隨機,因此Assign操做不少,約佔全部操做的\(\frac{1}{4}\)。其餘全部操做都有兩個Split,咱們能夠用一下程序模擬一下珂朵莉樹在數據隨機狀況下節點的個數:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <set> 
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;
struct node {
    int l, r;
    mutable int val;
    int operator < (const node &a) const {
        return l < a.l;
    }
    node(int L, int R = -1, int Val = 0) : l(L), r(R), val(Val) {} 
};
set<node> s;
#define sit set<node>::iterator
sit Split(int pos) {
    sit it = s.lower_bound(node(pos));
    if (it != s.end() && it->l == pos) return it;
    --it;
    int l = it->l, r = it->r, val = it->val;
    s.erase(it);
    s.insert(node(l, pos - 1, val));
    return s.insert(node(pos, r, val)).first;
}
void Assign(int l, int r, int val) {
    sit it2 = Split(r + 1), it1 = Split(l);
    s.erase(it1, it2);
    s.insert(node(l, r, val));
}
int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n + 1; ++i) s.insert(node(i, i, 0));
    srand((unsigned)time(0));
    srand(rand());
    for (int t = 1; t <= n; ++t) {
        int a = rand() * rand() % n + 1, b = rand() * rand() % n + 1;
        if (a > b) swap(a, b);
        if (rand() % 4 == 0) {
            Assign(a, b, 0);
        }
        else Split(a), Split(b + 1);
    }
    printf("%d", s.size());
    return 0;
}

本人機子上實驗數據以下(\(n\)表示序列長度,\(f(n)\)表示節點個數)

\(n\) \(f(n)\)
10 7
100 24
1000 33
10000 47
100000 67
1000000 95

可見,加了Assign的珂朵莉樹在隨機數據下跑得飛快,節點數達到了\(\log\)級別。也就是說,珂朵莉樹的高效是由隨機分配的Assign保證的。若是一個題目沒有區間賦值操做或者有數據點沒有賦值操做,或者數據很不隨機,請不要把珂朵莉樹當正解打。

珂朵莉樹目前的應用還很狹窄,各位dalao仍是用她來騙分吧qwq

關於詳盡的複雜度證實,我數學太差證不出,這裏貼一下@Blaze dalao的證實:


例題代碼:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <set>
#include <vector>
#include <utility>
using namespace std;
#define ll long long

struct node {
    int l, r;
    mutable ll val;
    int operator < (const node &a) const{
        return l < a.l;
    }
    node(int L, int R, ll Val) : l(L), r(R), val(Val) {}
    node(int L) : l(L) {}
};

set<node> s;
#define sit set<node>::iterator
sit Split(int pos) {
    sit it = s.lower_bound(node(pos));
    if (it != s.end() && it->l == pos) return it;
    --it;
    int l = it->l, r = it->r;
    ll val = it->val;
    s.erase(it);
    s.insert(node(l, pos - 1, val));
    return s.insert(node(pos, r, val)).first;
}
sit Assign(int l, int r, ll val) {
    sit it2 = Split(r + 1), it1 = Split(l);
    s.erase(it1, it2);
    s.insert(node(l, r, val));
}
void Add(int l, int r, ll val) {
    sit it2 = Split(r + 1), it1 = Split(l);
    for (sit it = it1; it != it2; ++it) it->val += val;
}
ll Kth(int l, int r, int k) {
    sit it2 = Split(r + 1), it1 = Split(l);
    vector< pair<ll, int> > aa;
    aa.clear();
    for (sit it = it1; it != it2; ++it) aa.push_back(pair<ll, int>(it->val, it->r - it->l + 1));
    sort(aa.begin(), aa.end());
    for (int i = 0; i < aa.size(); ++i) {
        k -= aa[i].second;
        if (k <= 0) return aa[i].first;
    }
}
ll qpow(ll a, int x, ll y) {
    ll b = 1ll;
    a %= y;
    while (x) {
        if (x & 1) b = (b * a) % y;
        a = (a * a) % y;
        x >>= 1;
    }
    return b;
}
ll Query(int l, int r, int x, ll y) {
    sit it2 = Split(r + 1), it1 = Split(l);
    ll res = 0;
    for (sit it = it1; it != it2; ++it) res = (res + (it->r - it->l + 1) * qpow(it->val, x, y)) % y;
    return res;
}
int n, m, vmax;
ll seed;
int rnd() {
    int ret = (int)seed;
    seed = (seed * 7 + 13) % 1000000007;
    return ret; 
}
int main() {
    scanf("%d%d%lld%d", &n, &m, &seed, &vmax);
    for (int i = 1; i <= n; ++i) {
        int a = rnd() % vmax + 1;
        s.insert(node(i, i, (ll)a));
    } 
    s.insert(node(n + 1, n + 1, 0));
    for (int i = 1; i <= m; ++i) {
        int l, r, x, y;
        int op = rnd() % 4 + 1;
        l = rnd() % n + 1, r = rnd() % n + 1;
        if (l > r) swap(l, r);
        if (op == 3) x = rnd() % (r - l + 1) + 1;
        else x = rnd() % vmax + 1;
        if (op == 4) y = rnd() % vmax + 1;
        if (op == 1) Add(l, r, (ll)x);
        else if (op == 2) Assign(l, r, (ll)x);
        else if (op == 3) printf("%lld\n", Kth(l, r, x));
        else if (op == 4) printf("%lld\n", Query(l, r, x, (ll)y));
    }
    return 0;
}

題目:

珂朵莉樹的經典題目大都非正解(且正解大可能是線段樹),不過在有的題目中吊打正解

不過有些題目作起來仍是挺有意思的qwq

1.【Luogu P2787】語文1(chin1)- 理理思惟

標算線段樹(貌似?)

不過珂朵莉樹隨隨便便跑進最優解,這纔是重點。

2.【CF915E】Physical Education Lessons

同上,珂朵莉樹甩線段樹幾條街

3.【Luogu P2572】[SCOI2010] 序列操做

數據不純隨機,但珂朵莉樹也能無壓力跑過~~

4.【Luogu P4344】[SHOI2015] 腦洞治療儀

毒瘤題目,治療腦洞操做的時候注意題面的規則,否則RE65或75。其他沒什麼難度。

5.【Luogu P2146】[NOI2015] 軟件包管理器

樹剖+珂朵莉樹+O2,其實和線段樹同樣的作法qwq

完結撒花✿✿ヽ(°▽°)ノ✿

相關文章
相關標籤/搜索