1. 程式人生 > 其它 >珂朵莉樹(超詳解!)

珂朵莉樹(超詳解!)

緒言:研究珂朵莉樹的原因

總是看到好多大佬在研究這個十分二次元化的東西(對於我這種男性 OIer 是這個感覺),所以想深入瞭解一下。

珂朵莉樹的起源

珂朵莉樹原名老司機樹(Old Driver Tree,ODT),是一種基於std::set的暴力資料結構,由2017年一場CF比賽中提出的資料結構,因為題目背景主角是《末日時在做什麼?有沒有空?可以來拯救嗎?》的主角珂朵莉,因此該資料結構被稱為珂朵莉樹。

應用

解決各種線段樹無法完成的操作。

注意珂朵莉樹保持複雜度主要依靠assign操作,所以題目中必須有區間賦值。

還有很重要的一點:資料需純隨機。

什麼時候用珂朵莉樹

關鍵操作:推平一段區間,使一整段區間內的東西變得一樣。保證資料隨機。

n個數,m次操作。 $n,m\leq10^5$

操作:

  • 區間加

  • 區間賦值

  • 區間第k小

  • 求區間冪次和

  • 資料隨機,時限2s。

構造

用一個帶結構體的集合(std::set)維護序列

集合中的每個元素有左端點,右端點,值

下面展示該結構體的構造:

struct Node{
	int l, r;
	mutable int val;
	Node(int a = -1, int b = -1, int c = 0){
		l = a, r = b, val = c;
	}
	bool operator < (const Node &a){
		return l < a.l;
	}
};

//mutale,意為可變的,即不論在哪裡都是可修改的,用於突破C++帶const函式的限制。

Split

set::iterator split(int pos)

將原來含有pos的區間分為[l,pos)和[pos,r]兩段。

返回一個std::set的迭代器,指向[pos,r]

可能有些抽象,詳細解如下:

split函式的作用就是查詢set中第一個左端點不小於pos的結點,如果找到的結點的左端點等於pos便直接返回指向該結點的迭代器,如果不是,說明pos包含在前一個結點所表示的區間之間,此時便直接刪除包含pos的結點,然後以pos為分界點,將此結點分裂成兩份,分別插入set中,並返回指向後一個分裂結點的迭代器。

首先我們假設 set 中有三個node結點,這三個結點所表示的區間長度為14 ,如下圖:

不妨以提取區間 [10,12] 為例詳細展開((躁動的讀者)誒,說好的查詢10到12呢,怎麼下面扯了一堆13?別急,後續將會揭曉 ):

如果我們要查詢序列第13個位置,首先執行si it = s.lower_bound(node(13)); 此時it將成為一個指向第三個結點的迭代器,為什麼是第三個結點,而不是第二個結點呢,因為lower_bound這個函式獲取的是第一個左端點l不小於13的結點,所以it是指向第三個結點的。

然後執行判斷語句,發現第三個結點的左端點不是13,不滿足條件,說明13必包含在前一個結點中,繼續向下執行,讓it指向前一個結點:

先將該結點的資訊儲存下來:int l = 10, r = 13, val = 2
然後直接刪除該結點

以pos為分界點,將被刪除的結點分裂為 [l,pos-1] ,[pos,r]這兩塊,並返回指向[pos,r]這個區間的迭代器,事實上return s.insert(node(pos,r,val)).first; ,便做到了插入[pos,r]這端區間,並返回指向它的迭代器,有一個insert函式返回值為pair型別,其中pair的第一個元素就是元素插入位置的迭代器。

至此13位置已經分裂完成,然後是查詢第10個位置,查詢步驟同上,但是10號點滿足if語句,便直接返回了

由上述步驟,為了提取區間 [10,12],我們執行了兩次 split ,一次為split(13),一次為split(10),並獲得了兩個迭代器,一個指向第二結點,一個指向第三結點。

為什麼要先分裂右端點,然後再分裂左端點呢?

因為如果先分裂左端點,返回的迭代器會位於所對應的區間以 l 為左端點,此時如果r也在這個節點內,就會導致分裂左端點返回的迭代器被 erase 掉,導致 RE

結合問題1和問題2 ,獲取區間迭代器時,務必寫成如下格式 si itr = split(r+1), itl = split(l); 起名無所謂,按自己的習慣就好

程式碼

set<Node>::iterator split(int pos){
	set<Node>::iterator it = st.lower_bound(Node(pos));
	if (it != st.end() && it->l == pos) return it;
	--it; Node tmp = *it; st.erase(it);
	st.insert(Node(tmp.l, pos - 1, tmp.val));
	return st.insert(Node(pos, tmp.r, tmp.val)).first; //first return iterator
}

Assign

注意:以後在使用split分裂區間的時候,請先右後左

區間賦值操作,也是珂樹維持其複雜度的關鍵函式

很暴力的思想,既然剛剛我們寫了一個split,那麼就要把它用起來。
首先split出l並記返回值為itl,然後split出r+1並記返回值為itr,顯然我們要操作的區間為[itl,itr),那麼我們將[itl,itr)刪除(std::set.erase(itl, itr)),再插入一個節點Node,其l為l,r為r,val為賦值的val

我們注意到因為這個操作, [itl,itr)中的所有節點合併為了一個節點,大大降低了集合的元素數量,因此調整了我們的複雜度

void assign(int l, int r, long long val){
	set<Node>::iterator itr = split(r + 1), itl = split(l);
    st.erase(itl, itr);
    st.insert((Node){l, r, val});
}
//將一個區間全部改為某個值。

其他操作

通用方法是split出l,split出r+1,然後直接暴力掃描這段區間內的所有節點執行需要的操作

例如我們的區間和查詢:

long long querySum(int l, int r){
    set<Node>::iterator itr = split(r + 1), itl = split(l); long long res = 0;
    for (set<Node>::iterator it = itl; it != itr; ++it)
        res += (it->r - it->l + 1) * it->val;
    return res;
}

例如我們的區間加:

void add(int l, int r, long long val){
    set<Node>::iterator itr = split(r + 1), itl = split(l);
    for (set<Node>::iterator it = itl; it != itr; ++it)
        it->val += val;
}

例如我們的區間第k小:

前置需要

algorithm庫中的std::sort(快速排序)

std::map(方便起見使用其中的pair),std::vector(方便起見)

還是splitlsplitr+1,然後將每個節點的值和個數(即r-l+1)組成一個pair(注意為了排序,將值放在第一關鍵字),將pair加入一個vector

vector排序

vectorbegin開始掃描,不停的使k減去vector當前項的第二關鍵字,若 $k\leq0$,返回當前項的第一關鍵字。

long long queryKth(int l, int r, int k){
    vector< pair<int, int> > vec(0);
    set<Node>::iterator itr = split(r + 1), itl = split(l);
    for (set<Node>::iterator it = itl; it != itr; ++it)
        vec.push_back(make_pair(it->val, it->r - it->l + 1));
    sort(vec.begin(), vec.end());
    for (vector< pair<int, int> >::iterator it = vec.begin(); it != vec.end(); ++it)
        if ((k -= it->second) <= 0) return it->first;
    return -1; //note:if there are negative numbers, return another impossible number.
}

求區間所有數的x次方的和模y的值

//快速冪取模
ll qpow(ll a,int b,ll m){
    ll t = 1ll;
    a %= m;
    while(b){
        if(b&1) t= (t*a)%m;
        a = (a*a)%m;
        b>>=1;
    }
    return t;
}
//提取區間,暴力運算
ll query(int l,int r,int x,int y){
    si itr=split(r+1),itl=split(l);
    ll res(0);
    for(si it=itl;it!=itr;++it)
        res=(res+(it->r-it->l+1)*qpow(it->val,x,y))%y;
    return res;
}

珂朵莉樹程式碼樣例

#include <cstdio>
#include <vector>
#include <algorithm>
#include <set>
#include <map>

using namespace std;

//build
struct Node{
    int l, r;
    mutable long long val;
    Node(int a = -1, int b = -1, long long c = 0){
        l = a, r = b, val = c;
    }
    bool operator < (const Node &a) const{
        return l < a.l;
    }
};

set<Node> st;

//modify
set<Node>::iterator split(int pos){
    set<Node>::iterator it = st.lower_bound(Node(pos));
    if (it != st.end() && it->l == pos) return it;
    --it; Node tmp = *it; st.erase(it);
    st.insert(Node(tmp.l, pos - 1, tmp.val));
    return st.insert(Node(pos, tmp.r, tmp.val)).first; //first return iterator
}

void assign(int l, int r, long long val){
	set<Node>::iterator itr = split(r + 1), itl = split(l);
    st.erase(itl, itr);
    st.insert((Node){l, r, val});
}

void add(int l, int r, long long val){
    set<Node>::iterator itr = split(r + 1), itl = split(l);
    for (set<Node>::iterator it = itl; it != itr; ++it)
        it->val += val;
}

//query
long long querySum(int l, int r){
    set<Node>::iterator itr = split(r + 1), itl = split(l); long long res = 0;
    for (set<Node>::iterator it = itl; it != itr; ++it)
        res += (it->r - it->l + 1) * it->val;
    return res;
}

long long queryKth(int l, int r, int k){
    vector< pair<long long, int> > vec(0);
    set<Node>::iterator itr = split(r + 1), itl = split(l);
    for (set<Node>::iterator it = itl; it != itr; ++it)
        vec.push_back(make_pair(it->val, it->r - it->l + 1));
    sort(vec.begin(), vec.end());
    for (vector< pair<long long, int> >::iterator it = vec.begin(); it != vec.end(); ++it)
        if ((k -= it->second) <= 0) return it->first;
    return -1; //note:if there are negative numbers, return another impossible number.
}

int main(){
	
    return 0;
}

例題詳解CF896C

一說起區間維護問題,我們就能想到線段樹,主席樹,樹狀陣列,Splay,分塊,莫隊等資料結構,但是讀完題目我們發現,第四個操作涉及每個數字的相關操作,上面提到的結構只有莫隊可以做到,但是複雜度太高,我們需要一個更加高效的資料結構 珂朵莉來維護這些操作。

#include <cstdio>
#include <vector>
#include <algorithm>
#include <set>
#include <map>

using namespace std;

long long read(){
    long long x = 0; int zf = 1; char ch = ' ';
    while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
    if (ch == '-') zf = -1, ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar(); return x * zf;
}

namespace Qpow{
	long long pow(long long a, long long b, long long mod){
		if (!a) return 0;
		long long res = 1; a %= mod;
		for ( ; b; (a *= a) %= mod, b >>= 1ll)
			if (b & 1) (res *= a) %= mod;;
		return res;
	}
};

//build
struct Node{
    int l, r;
    mutable long long val;
    Node(int a = -1, int b = -1, long long c = 0){
        l = a, r = b, val = c;
    }
    bool operator < (const Node &a) const{
        return l < a.l;
    }
};

set<Node> st;

//modify
set<Node>::iterator split(int pos){
    set<Node>::iterator it = st.lower_bound(Node(pos));
    if (it != st.end() && it->l == pos) return it;
    --it; Node tmp = *it; st.erase(it);
    st.insert(Node(tmp.l, pos - 1, tmp.val));
    return st.insert(Node(pos, tmp.r, tmp.val)).first; //first return iterator
}

void assign(int l, int r, long long val){
	set<Node>::iterator itr = split(r + 1), itl = split(l);
    st.erase(itl, itr);
    st.insert((Node){l, r, val});
}

void add(int l, int r, long long val){
    set<Node>::iterator itr = split(r + 1), itl = split(l);
    for (set<Node>::iterator it = itl; it != itr; ++it)
        it->val += val;
}

//query
long long querySum(int l, int r){
    set<Node>::iterator itr = split(r + 1), itl = split(l); long long res = 0;
    for (set<Node>::iterator it = itl; it != itr; ++it)
        res += (it->r - it->l + 1) * it->val;
    return res;
}

long long querySumWithPow(int l, int r, long long x, long long mod){
    set<Node>::iterator itr = split(r + 1), itl = split(l); long long res = 0;
    for (set<Node>::iterator it = itl; it != itr; ++it)
        (res += (it->r - it->l + 1) * Qpow::pow(it->val, x, mod)) %= mod;
    return res;
}

long long queryKth(int l, int r, int k){
    vector< pair<long long, int> > vec(0);
    set<Node>::iterator itr = split(r + 1), itl = split(l);
    for (set<Node>::iterator it = itl; it != itr; ++it)
        vec.push_back(make_pair(it->val, it->r - it->l + 1));
    sort(vec.begin(), vec.end());
    for (vector< pair<long long, int> >::iterator it = vec.begin(); it != vec.end(); ++it)
        if ((k -= it->second) <= 0) return it->first;
    return -1; //note:if there are negative numbers, return another impossible number.
}

long long seed;
long long a[100005];

inline long long rnd(){
	long long ret = seed; seed = (seed * 7 + 13) % 1000000007;
	return ret;
}

int main(){
	int n = read(), m = read(); seed = read(); long long vmax = read();
	for (int i = 1; i <= n; ++i){
		a[i] = (rnd() % vmax) + 1;
		st.insert((Node){i, i, a[i]});
	}
	long long x, y;
	for (int i = 1; i <= m; ++i){
	    int op = (rnd() % 4) + 1, l = (rnd() % n) + 1, r = (rnd() % n) + 1;
	    if (l > r){int tmp = l; l = r, r = tmp;}
	    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, x);
	    else if (op == 2) assign(l, r, x);
	    else if (op == 3) printf("%I64d\n", queryKth(l, r, x));
	    else if (op == 4) printf("%I64d\n", querySumWithPow(l, r, x, y));
	}
    return 0;
}