骗分导论--ODT珂朵莉树

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/niiick/article/details/83062256

珂朵莉树の由来

珂朵莉树(或称ODT(Old Driver Tree老司机树))
这毒瘤算法由CodeForces - 896C Willem, Chtholly and Seniorious 的正解衍化而来

由于其骗分暴力的非正统算法思想
虽然很多时候在随机数据下跑时不错
但切记这只是骗分暴力时间复杂度上并不正确


什么时候用珂朵莉树

珂朵莉树一般用来解决本来应当由线段树解决的区间类问题

而使得珂朵莉树暴力艹标程的关键是assign区间推平操作
也就是题目中必须有区间赋值操作,而且数据纯随机的情况下才有复杂度保证


珂朵莉树–初始化

一般珂朵莉树选择使用 set 维护数列
set中每个结点维护三个值 ( l l , r r , v a l ) (ll,rr,val) ,意为 [ l l , r r ] [ll,rr] 内的值都为 v a l val
也就是每个节点保存一段连续的值相同的区间
set中以每个区间左端点排序

struct node
{
    int ll,rr;
    mutable int val;
    node(int L,int R=-1,int V=0): ll(L), rr(R), val(V) {}
    bool operator < (const node& tt)const { return ll<tt.ll;}//以区间左端点排序
};
set<node> st;

需要注意的是 m u t a b l e mutable
没有它对 v a l val 的修饰,在接下来add函数里导致CE。

接下来为方便有关迭代器的操作,我们做一个宏定义

#define IT set<node>::iterator

珂朵莉树–分裂Split

s p l i t ( p o s ) split(pos) 操作是指将原来含有pos位置的节点分成两部分 [ l , p o s 1 ] [l,pos-1] [ p o s , r ] [pos,r]
并返回分裂后以 p o s pos 为左端点的结点的迭代器

IT split(int pos)
{
    IT it=st.lower_bound(node(pos));//二分找到第一个左端点不小于pos的区间
    if(it!=st.end()&&it->ll==pos) return it;//pos本身就是某个区间的左端点,不用分裂
    --it;//否则上一个区间才是包含pos的区间
    int ll=it->ll,rr=it->rr,val=it->val;
    st.erase(it);//删除原结点
    st.insert(node(ll,pos-1,val));
    return st.insert(node(pos,rr,val)).first;//这里.first返回的是迭代器
}

珂朵莉树–区间赋值Assign

珂朵莉树就是靠这个东西维持其不正确的复杂度的

void assign(int ll,int rr,int val)
{
    IT itr=split(rr+1),itl=split(ll);
    st.erase(itl,itr);
    st.insert(node(ll,rr,val));
}

分裂出需要的区间,删除后重新插入一个新的
注意分裂出 [ l l , r r ] [ll,rr] 区间时要先分裂右端点,在分裂左端点

e r a s e erase 方法可以删除迭代器描述的区间 [ f i r s t , l a s t ) [first,last) ,注意左闭右开

void erase (iterator first, iterator last)

数据纯随机的情况下,可以证明每次 a s s i g n assign 区间长度期望 N / 3 N/3
于是 s e t set 规模迅速下降,随后达到接近 O ( m l o g n ) O(mlogn) 玄学非正确复杂度


珂朵莉树–其他暴力操作

其他操作真的就是规规矩矩的纯暴力
直接取出对应区间,暴力对每一个进行操作

一般就是长这样

void fun(int ll,int rr)
{
    IT itr=split(rr+1),itl=split(ll);
    for(;itl!=itr;++itl) 
    {
    	//...
    }
}
煮个栗子–区间求和
int qsum(int ll,int rr)
{
    int res=0;
    IT itr=split(rr+1),itl=split(ll);
    for(;itl!=itr;++itl) res+=(itl->rr-itl->ll+1)*itl->val;//注意乘(itl->rr-itl->ll+1)
    return res;
}
再煮个复杂点的栗子–区间Kth

也是一样的暴力

#define pir pair<int,int>//前一个记录值,后一个记录出现次数
int kth(int ll,int rr,int k)
{
    vector<pir> vec;
    IT itr=split(rr+1),itl=split(ll);
    
    for(;itl!=itr;++itl)
    vec.push_back( pir(itl->val,itl->rr-itl->ll+1) );
    sort(vec.begin(),vec.end());//全部存下来排序就好
    
    for(vector<pir>::iterator it=vec.begin();it!=vec.end();++it)
    {
        k-=it->second;
        if(k<=0) return it->first;
    }
    return -1;
}

珂朵莉树–暴力AC应用

CodeForces - 896C Willem, Chtholly and Seniorious 毒瘤ODT的源头
洛谷P2787 语文1(chin1)- 理理思维 时空消耗量都艹过正解不止一个数量级


再次提醒

不管珂朵莉树才实际中再怎么优秀,那都只是数据随机的结果
说到底只是暴力骗分手段
尽管可以用ODT AC很多题,但改变不了时间复杂度是非正确的的事实

建议平时的练习中少用,因为这样会忽略了题目本身正解的精髓
考场上对题目没有思路倒是一个很好的骗分手段

猜你喜欢

转载自blog.csdn.net/niiick/article/details/83062256