淺談珂朵莉樹


0.前言

如有錯誤,歡迎指出。(什么錯誤都可以。)

同步於 \(luogublog\) 發布。

前置芝士

1.還是 oi-wiki 上面的那句話, 會用 STL_SET 就行。(不會的話,也可以去學習一下。)

1.什么是珂朵莉樹

當你在 oi-wiki 上面看到一個叫做珂朵莉樹的數據結構時,你可能會很好奇,也可能會覺得 oi-wiki 非常高大尚。但是,今天我要在這里告訴你:珂朵莉樹賊簡單,在比賽中也很實用!

好,不多說廢話,我們進入正題。

珂朵莉樹起源題:CF896C,也是我們后面講述的例題。可以發現,在這個題目中,前面三個操作我們都還可以運用主席樹實現,但是當你看到第4個操作嘛,呵呵,我可以猜到你的心情。

珂朵莉樹,又名 old driver tree 老司機樹,(ODT樹),在比賽中,一般 是用來騙分的。對於這一種數據結構,它只適用於隨機數據,對於精心策划的數據將會被卡掉,但是在比賽中,卡珂朵莉樹的數據畢竟少,所以可以在不知道正解的情況下騙到很多分。

為什么我們說他實用呢?因為在比賽中,通常出現這樣的情況:正解是一棵長度需要想和寫很久的權值線段樹或者平衡樹,但是,如果你采用珂朵莉樹,你可以非常快速的的打完,並且還可以至少得到正解差不了太多分的分數(一般也就差10~20分,並且如果出題人忘記卡了的話,甚至可以得到滿分)。更為重要的是,如果你正解代碼出現了億些錯誤,那么你將會死的更慘,而珂朵莉樹易於調試不易寫錯的特點,則更能保證你在比賽中的成績。

雖然珂朵莉樹在比賽中比較實用,但是在學習 OI 的時候,最好還是不要使用它來騙分了,乖乖打正解吧!

2.珂朵莉樹具體做法

珂朵莉樹基於 STL_SET 實現,他之所以是一棵樹也跟 SET 是有關系的,他的具體思想是:將一個權值相同的子段看作一個節點來看待。

我們先定義一個結構體來儲存節點,即每個權值相同的區間:

#define ll long long//因為珂朵莉樹可以存下長度很大的節點以及權值,所以開 long long
struct odt
{	
	ll l,r;//將該字段化為一個節點后,他的左端點和右端點
	mutable ll val;//該字段的權值
	bool operator <(const odt &n)const//重載小於運算符,因為要存入set就必須有小於符號的定義
	{
		return l<n.l;
	}
	//下面的在后面代碼中有用。
	odt(ll a,ll b,ll c)
	{
		l=a,r=b,val=c;
	}
	odt(ll a)
	{
		l=a;
	}
};

剛剛在代碼中,或許你看到了一個你並不認識的關鍵字 mutablemutable譯為可變的。因為我們在 CF896C 當中有區間加操作,需要給改變 \(val\) 的值,而 set 中是默認不能改的,所以我們需要加上 mutable 關鍵字。

然后,我們在申請一個 SET 來儲存這棵珂朵莉樹。

set<odt> tree;

接着,我們再宏定義一下 set 的迭代器,方便我們在以后訪問時使用。

#define It set<odt>::iterator

注意,接下來是重點!

在珂朵莉樹中,最重要的操作無疑是 split 操作,即分裂一個節點。這時,或許你會十分疑惑,珂朵莉樹都存好了,為什么還有分裂節點呢?

我們可以先想這樣一個問題,假設現在只有一個節點,他表示的是區間 \([1,8]\)。在 CF896C 中,我們有區間加操作。如果我們要將區間 \([5,7]\) 全部加上 \(3\) ,那該怎么辦呢。由於我們在珂朵莉樹中,不存在以 \(5\)\(7\) 為開頭的節點。所以我們需要將節點 \([1,8]\) 分裂成 \([1,4][5,7][8,8]\),我們才能完成操作。所以你現在明白 split 的重要性了吧。

假設我們現在需要一個以 \(x\) 為左端點的節點。我們可以現在 \(set\) 中直接二分查找,找到第一個左端點大於等於 \(x\) 的節點。如果這個左端點於 \(x\) 相等,則就說明 set 中存在以 \(x\) 為左端點的節點,我們就不需要分裂。如果不相等,我們將這個左端點所在的節點的上一個節點進行分裂。(因為上一個節點才包含我們要找的 \(x\)。)最后,我們再返回這個節點所在的迭代器。

代碼:

It split(ll x)//分裂,返回以 x 為左端點的節點的迭代器
{
	It it=tree.lower_bound(odt(x));//直接進行二分查找
	if(it!=tree.end()&&it->l==x)//如果相等,且不是 set 中的尾指針
	return it;//直接返回迭代器。
	it--;//我們將當前迭代器減1,因為要找上一個。
	ll l=it->l,r=it->r,val=it->val;//我們找到上一個節點的所有信息,儲存下來,以免在刪除時指針失效無法訪問。
	tree.erase(it);//將該節點刪除
	tree.insert(odt(l,x-1,val));//先將該節點分裂為區間 [l,x-1]
	return tree.insert(odt(x,r,val)).first;//然后以 x 為左端點再分裂一個區間,由於 insert 后返回的是一個 pair 類型,所以我們需要返回 first 來返回迭代器。
}

時間復雜度的保證-Assign

顯然,如果我們一直分裂下去,節點就會越來越多,漸漸退化成暴力,這樣是對我們不利的。所以我們需要去改變。

現在有一個操作,assign,推平一個區間,將一個區間賦成同一個值。顯然,我們可以直接取出在這個區間里面的所有節點,然后直接刪除,最后在新建一個節點來表示這個區間。

這樣,我們就能有效控制節點個數,保證時間復雜度了。由於本人能力有限,不會證明復雜度,所以我就在這里貼一個珂朵莉時間復雜度的證明

Assign 代碼:

void assign(ll l,ll r,ll val)//將 [l,r] 賦值為 val
{
	It it2=split(r+1),it1=split(l);//我們無論是什么操作,我們都先取出右端點,因為先取左端點的話,左端點的指針可能會失效。
	tree.erase(it1,it2);//刪除區間 [l,r+1)
	tree.insert(odt(l,r,val));//新建以l為左端點,r為右端點的節點
}

其他操作

一個比一個更暴力更玄學,就是把這個區間所有的節點取出來就可以了。

操作三,求區間第 \(k\) 小整數示例代碼:

ll qsort(ll l,ll r,ll x)
{
	It it2=split(r+1),it1=split(l);//還是像往常一樣,取出以左端點開始,和 右端點加一的 迭代器。
	vector<pair<ll,ll> > p;//為了方便排序,直接用pair
	for(It it=it1;it!=it2;it++)
	{
		p.push_back(make_pair(it->val,it->r-it->l+1));//將區間中所有節點放入p數組。
	}
	stable_sort(p.begin(),p.end());//將p數組排序
	for(ll i=0;i<p.size();++i)
	{
		x-=p[i].second;//每次減去這個結點的長度
		if(x<=0)//如果小於等於0,那顯然第 x 小的整數就在這個節點上。
		return p[i].first;
	}
}

操作4,求區間每個數字的 \(x\) 次方模 \(y\) 的值的和 實例代碼:

ll qpow(ll a,ll b,ll p)//快速冪 a^b%p
{
    ll ans=1;
    a%=p;
    while(b)
    {
        if(b&1)
        (ans*=a)%=p;
        (a*=a)%=p;
        b>>=1;
    }
    return ans%p;
}
ll pow_mod(ll l,ll r,ll x,ll y)
{
	It it2=split(r+1),it1=split(l);//照常取出
	ll ans=0;//答案
	for(It it=it1;it!=it2;it++)
	{
   	       //將每個節點暴力取出算快速冪就可以了。
		ll res=qpow(it->val,x,y)*(it->r-it->l+1);
		res%=y;
		ans+=res;
		ans%=y;
	}
	return ans;//返回
}

我們很早就說到,珂朵莉樹只適合解決隨機生成的數據,而這個題目不就是給你隨機數種子自己生成數據嗎,那珂朵莉樹的復雜度是肯定可以承受住這個題目的了。

最后,貼上完整代碼

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mod int(1e9+7)
#define It set<odt>::iterator
ll n,m;
ll seed,vmax;
ll a[1000005];
ll rnd()
{
	ll res=seed;
	seed=(seed*7+13)%mod;
	return res;
}
ll qpow(ll a,ll b,ll p)
{
    ll ans=1;
    a%=p;
    while(b)
    {
        if(b&1)
        (ans*=a)%=p;
        (a*=a)%=p;
        b>>=1;
    }
    return ans%p;
}
struct odt
{
	ll l,r;
	mutable ll val;
	bool operator <(const odt &n)const
	{
		return l<n.l;
	}
	odt(ll a,ll b,ll c)
	{
		l=a,r=b,val=c;
	}
	odt(ll a)
	{
		l=a;
	}
};
set<odt> tree;
It split(ll x)
{
	It it=tree.lower_bound(odt(x));
	if(it!=tree.end()&&it->l==x)
	return it;
	it--;
	ll l=it->l,r=it->r,val=it->val;
	tree.erase(it);
	tree.insert(odt(l,x-1,val));
	return tree.insert(odt(x,r,val)).first;
}
void assign(ll l,ll r,ll val)
{
	It it2=split(r+1),it1=split(l);
	tree.erase(it1,it2);
	tree.insert(odt(l,r,val));
}
void add(ll l,ll r,ll val)
{
	It it2=split(r+1),it1=split(l);
	for(It it=it1;it!=it2;it++)
	{
		it->val+=val;
	}
}
ll qsort(ll l,ll r,ll x)
{
	It it2=split(r+1),it1=split(l);
	vector<pair<ll,ll> > p;
	for(It it=it1;it!=it2;it++)
	{
		p.push_back(make_pair(it->val,it->r-it->l+1));
	}
	stable_sort(p.begin(),p.end());
	for(ll i=0;i<p.size();++i)
	{
		x-=p[i].second;
		if(x<=0)
		return p[i].first;
	}
}
ll pow_mod(ll l,ll r,ll x,ll y)
{
	It it2=split(r+1),it1=split(l);
	ll ans=0;
	for(It it=it1;it!=it2;it++)
	{
		ll res=qpow(it->val,x,y)*(it->r-it->l+1);
		res%=y;
		ans+=res;
		ans%=y;
	}
	return ans;
}
int main()
{
	scanf("%lld%lld%lld%lld",&n,&m,&seed,&vmax);
	for(int i=1;i<=n;++i)
	{
		a[i]=rnd()%vmax+1;
		tree.insert(odt(i,i,a[i]));
	}
	tree.insert(odt(n+1,n+1,0));
	while(m--)
	{
		int op=rnd()%4+1,l=rnd()%n+1,r=rnd()%n+1,x,y;
		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,x);
		else if(op==2)
		assign(l,r,x);
		else if(op==3)
		printf("%lld\n",qsort(l,r,x));
		else
		printf("%lld\n",pow_mod(l,r,x,y));
	}
	return 0;
}

3.后記

珂朵莉樹這東西好是好,簡單是簡單。但是我還是覺得平常練習的時候就不要用了,要是教練看到你用了,一氣之下把你卡掉那可就不好玩咯。

至於有關珂朵莉樹的例題嘛,還是很多的,可以參見 oi-wiki 和其他有關珂朵莉樹的文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM