替罪羊樹學習筆記

DycIsMyName發表於2024-06-08

替罪羊樹學習筆記

史!

思想

眾所周知,替罪羊樹是一種平衡二叉搜尋樹,其擁有雖然我不理解為什麼,但是很牛的複雜度。其思想在於透過一個係數進行定期重構,使得維護所有資訊的樹是一棵接近平衡樹的偽平衡樹,那麼他依然擁有 \(O(\log n)\) 級別的層高,因此對於跳轉查詢依舊具有優異的複雜度。

但是,顯然對於重構來說,同時需要一定的複雜度,那麼我們應該按照什麼樣的標準評判當前子樹是否需要重構呢?這個時候就要用到我們自主決定的係數 \(\alpha\)。對於一棵平衡樹來說,我們要求其每一個節點的左右兩個子樹中的節點數量都儘可能一樣,那麼此時,我們認為這棵樹不失衡。

更多的,如果這棵樹上有刪除操作,我們採用惰性刪除,即只對於當且節點的大小進行維護,即使這個節點已經被刪除,我們也認為它仍然存在,只在查詢時特判,不對答案產生貢獻。那麼顯然,如果這樣名存實亡的節點過多,會使得查詢複雜度下降,因此當已經被刪除的節點過多時,我們也認為當前子樹是失衡的。

重構

根據剛才的思想,我們為重構提出了兩個條件:子樹大小相當和刪除節點較少。那麼我們可以維護下面兩個資訊:子樹中的節點大小和子樹中有效的節點大小。這樣就可以判斷了。另外,因為重構本質上只是將節點換了位置,因此編號等資訊無需更改,因此,最多插入多少個節點,替罪羊樹的空間複雜度就是多少。

當然在沒有刪除的時候,後面的條件是無用的。

程式碼

void Update(int id){
    Sz(id)=Sz(L(id))+Sz(R(id))+1;
    Sm(id)=Sm(L(id))+Sm(R(id))+C(id);
    Rs(id)=Rs(L(id))+Rs(R(id))+(C(id)!=0);
    return ;
}//更新資訊
void Clear(int id){
    if(id==0)return ;
    Clear(L(id));
    if(C(id)!=0)b[++top]=id;
    Clear(R(id));
    return ;
}//統計需要重構的編號
int Build(int l,int r){
    if(l>r)return 0;
    int mid=(l+r)>>1;
    int id=b[mid];
    for(int i=l;i<=r;i++)t.Insert(id,V(b[i]));
    L(id)=Build(l,mid-1);
    R(id)=Build(mid+1,r);
    Update(id);
    return id;
}//這樣子重構出來的一定是平衡到不能再平衡的平衡樹
bool IsReb(int id){
    return Sz(id)*A<=(double)max(Sz(L(id)),Sz(R(id)))||Sz(id)*A>=(double)Rs(id);
}//兩個條件滿足其一即可重構
void Rebuild(int &id){
    top=0;
    Clear(id);
    id=Build(1,top);
    return ;
}//當滿足重構條件時才呼叫這個函式

修改

對於修改,一般分為插入和刪除,同時還分為按照權值排序和按照位置排序兩種。

權值相關

為了降低相關複雜度,我們選擇將所有權值相同的點歸為一個點(當然也可以不這麼做),讓所有節點關於權值是一棵二叉搜尋樹即可。沒有對應的當前節點時,我們直接維護新節點;當前節點的權值等於 \(x\) 時,只用增加個數;當前節點的權值大於 \(x\) 時,朝左子樹遞迴;當前節點的權值小於 \(x\) 時,朝右子樹遞迴。刪除操作也基本同理。

程式碼

void Insert(int &x,int p){
    if(x==0){//新建節點
        x=++tot;
        if(rt==0)rt=1;
        V(x)=p;//維護的對應點的權值
        L(x)=R(x)=0;//維護的左右子樹編號
        C(x)=Sz(x)=Sm(x)=Rs(x)=1;//分別是當前節點有多少個數、子樹內有多少個節點、子樹內有多少個數,子樹內有多少個有效節點
        return ;
    }
    if(p==V(x))C(x)++;//只增加節點數的個數
    else if(p<V(x))Insert(L(x),p);//遞迴左子樹
    else Insert(R(x),p);//遞迴右子樹
    Update(x);//更新貢獻
    if(IsReb(x))Rebuild(x);//判斷是否重構
    return ;
}
void Del(int &x,int p){
    if(x==0)return ;//沒有能刪除的
    if(p==V(x)){
        if(C(x)!=0)C(x)--;//只減少節點中數的個數
    }else if(p<V(x))Del(L(x),p);//遞迴左子樹
    else Del(R(x),p);//遞迴右子樹
    Update(x);//更新貢獻
    if(IsReb(x))Rebuild(x);//判斷是否重構
    return ;
}

位置相關

考慮如果每個數在不同的位置上的貢獻互不一致,那麼就只能按照位置建立二叉搜尋樹。一般以前面有多少個數為媒介。如果當前節點的前面的數的個數不少於 \(k\) 個,那麼新建的節點只能在左子樹中;不然新建的節點只能在右子樹中。當確定了位置後新建節點即可。對於刪除也一樣,找到對應位置後只改變節點中數的個數即可。

程式碼

void Insert(int &id,int p,int k){
    if(id==0){//新建節點
        id=++tot;
        if(rt==0)rt=id;
        V(id)=p;
        C(id)=Sz(id)=Rs(id)=1;//因為一個節點要麼有,要麼沒有,不會有多個數同時在裡面,因此不用維護子樹內數的個數
        L(id)=R(id)=0;
        return ;
    }
    if(Rs(L(id))>=k)Insert(L(id),p,k);//注意空節點沒有貢獻
    else Insert(R(id),p,k-Rs(L(id))-C(id));
    Update(id);
    if(IsReb(id))Rebuild(id);
    return ;
}
void Delete(int &id,int k){
    if(Rs(L(id))==k&&C(id)!=0)C(id)--;//注意空節點不會作為被刪除的節點
    else if(Rs(L(id))>k)Delete(L(id),k);
    else Delete(R(id),k-Rs(L(id))-C(id));
    Update(id);
    if(IsReb(id))Rebuild(id);
    return ;
}

查詢

查詢主要有查詢第 \(k\) 小/第 \(k\) 位、查詢排名(只針對權值)等查詢,當然,因為替罪羊樹某種意義上來說也是線段樹,所以可以按照線段樹的思路查詢。同樣的,查詢可以分為關於權值的查詢和關於位置的查詢。

但是查詢沒有辦法講解,按照線段樹的思路:特判能否直接透過子樹維護的資訊概括 \(\to\) 分成左子樹、右子樹遞迴求解,一般都能求解,可以多多做題,多多積累一些常見的模板。

提醒

首先對於係數的選擇,需要先進行粗略的評估,即重構的複雜度和不重構的複雜度那一個佔比更大從而選擇更合適的係數。一般來說,我們都不希望重構進行太多次,因此我們都會選擇能夠讓重構更少的係數。根據我個人的經驗,一般情況下,有刪除時 \(\alpha=0.75\) 最優,無刪除時 \(\alpha=0.85\) 最優。

接著對於重構,有多種寫法進行最佳化,但是一般針對於無刪除。因為在有刪除時重構下面的子樹可能導致上面的子樹從不平衡變得平衡,因此只針對無刪除進行最佳化。更一般的,可以理解為,既然下面的節點不論如何重構都不影響上面的重構結果,那麼不妨只重構最上面的位置,這樣一定可以最佳化。但因為重構需要更新左右子樹資訊,因此同時需要知道重構的節點是隸屬上一層的左子樹還是右子樹。 下面給出程式碼:

void Insert(int &id,int p,int k){
    if(id==0){
        id=++tot;
        if(rt==0)rt=id;
        V(id)=p;
        C(id)=Sz(id)=Rs(id)=1;
        L(id)=R(id)=0;
        return ;
    }
    if(Rs(L(id))>=k)Insert(L(id),p,k);
    else Insert(R(id),p,k-Rs(L(id))-C(id));
    Update(id);
    if(IsReb(id)){
        S=id;
        F=0;
    }else if(F!=0)F=id;
    return ;
}
void Change(int p,int k){
    Insert(rt,p,k);
    if(s!=0){
        if(f==0)Rebuild(rt);
        else{
            if(L(f)==s)Rebuild(L(f));
            else Rebuild(R(f));
            f=0;
        }
        s=0;
    }
    return ;
}

還有,替罪羊不僅用於平衡樹,在一些需要重構樹形資料結構的情況下,我們也可以利用這樣的思想,也就是當其失衡導致求解層數過大時進行重構。

相關文章