<style> .reveal .slides { text-align: left; font-size:30px } </style> # treap Competitive programming 2 2022/1/3 --- ## 樹堆(Treap) Treap是一顆平衡二元樹 Treap = tree(二元樹) + heap(堆積) 每個節點有兩個重要的值:key, priority key用於存值,priority用來維護treap的平衡 ```cpp= struct Treap{ int key,pri,sz; //key,priority,size Treap *l, *r; //左右子樹 Treap(){} Treap(_key){ key = _key; pri = rand(); //隨機的數維持樹的平衡 sz = 1; l = r = nullptr; } } ``` ---- Treap滿足以下性質 1. 左子樹的 key 小於等於自己,右子樹的 key 大於等於自己(tree) 2. 父節點的 priority 大於其子節點的 priority (heap) 3. 左右子樹也分別是一顆treap 4. 給定 n 個節點的 key,priority 的大小關係,那麼這棵 treap 的形狀唯一。 ![](https://i.imgur.com/uoo0w5Y.png =500x) ---- 做法 Treap分為旋轉式跟非旋轉式 這邊會介紹非旋轉式Treap,也就是merge-split treap 它易實作、期望複雜度好、靈活性高 幾乎每個操作都是 $O(lgN)$ 而主要有兩個函式 - split跟merge split 將一棵treap分成兩顆 merge 將兩棵treap合併成一棵 ---- ## merge-split treap ### merge 將兩棵Treap合併成一棵 (須保證左邊那棵所有的key小於等於右邊那棵) <div style="margin-left:-140px"> ```cpp= int Size(Treap* x){ return x ? x->sz : 0 ; } void pull(Treap *x){ x->sz = Size(x->l) + Size(x->r) + 1;} Treap* merge(Treap *a,Treap *b){ if(!a || !b) return a ? a : b; //其中一個子樹為空則回傳另一個 if(a->pri > b->pri){ //如果a的pri比較大則a比較上面 a->r = merge(a->r,b); //將a的右子樹跟b合併 pull(a); return a; } else{ //如果b的pri比較大則b比較上面 b->l = merge(a,b->l); //將b的左子樹根a合併 pull(b); return b; } } ``` </div> ---- ## merge-split treap ### split by key 將一棵Treap分成兩棵, key小於等於k的分到左邊那棵a,其他分到右邊那棵b <div style="margin-left:-140px"> ```cpp= void splitByKey(Treap *x,int k,Treap*& a,Treap*& b){ if(!x){ a = b = nullptr; } else if(x->val<=k){ a = x; splitByKey(x->r, k, a->r, b); pull(a); } else{ b = x; splitByKey(x->l, k, a, b->l); pull(b); } } ``` </div> ---- ## merge-split treap ### split by k-th 將一棵Treap分成兩棵, 左邊那棵a的節點數有k個,右邊那棵b節點數為n-k個 <div style="margin-left:-140px"> ```cpp= void splitByKth(Treap *x,int k,Treap*& a,Treap*& b){ if(!x){ a = b = nullptr; } else if(Size(x->l) + 1 <= k){ a = x; //如果左子樹+自己的size小於等於k則左子樹跟自己為k個以內 splitByKth(x->r, k - Size(x->l) - 1, a->r, b); pull(a); } else{ b = x; //如果左子樹+自己的size大於k個則右子樹跟自己會分到右邊 splitByKth(x->l, k, a, b->l); pull(b); } } ``` </div> ---- 有以上split跟merge操作之後就可以完成大部分了! 像是插入元素或刪除都是在split和merge的基礎下操作 (記得左右順序要處理好!) <div style="margin-left:-165px"> ```cpp= void insert(int val){ //新增一個值為val的元素 Treap *x = new Treap(val); //設一個treap節點 Treap *l,*r; splitByKey(root, val, l, r); //找到新節點要放的位置 root = merge(merge(l,x),r); //合併到原本的treap裡 } void erase(int val){ //移除所有值為val的元素 Treap *l,*mid,*r; splitByKey(root, val, l, r); //把小於等於val的丟到l splitByKey(l, val-1, l, mid); //小於val的丟到l,等於val的就會在mid裡 root = merge(l,r); //將除了val以外的值合併 } ``` </div> ---- ## 複雜度分析 | build | split | merge | | --------- | -------- | -------- | | $O(NlgN)$ | $O(lgN)$ | $O(lgN)$ | <!-- [sample code](https://github.com/jakao0907/contest/blob/master/dataStructure/treap_split_key%26kth.cpp) --> ---- <div style="text-align:left;font-size:26px"> ### Treap and 懶標記 treap在處理序列問題非常強大像是以下題目 兩種操作: 1. 翻轉區間[l,r] 2. 詢問第i格的值 我們可以用treap儲存序列 如果要翻轉一段區間則是將一棵treap交換他的左右子樹,並且他的所有子樹每個節點都交換左右子樹,就能達到翻轉區間 顯然,如果每次翻轉操作直接硬做,複雜度會爛掉 所以我們可以用一個懶標記,把要翻轉的區間記起來,下次走到再反轉,則複雜度就會降到好好的 $O(lgN)$ ```cpp= //記得在Treap結構裡加入變數tag void push(Treap* x){ if(x->tag){ //如果區間要翻轉 swap(x->l,x->r); //交換左右子樹 if(x->l) x->l->tag ^= 1; //加tag到左子樹 if(x->r) x->r->tag ^= 1; //加tag到右子樹 x->tag = NO_TAG; } } ``` </div> --- ## Question Time and Practice [Homework Link](https://vjudge.net/contest/475226) 提交期限到下星期一 1/10 23:59 截止
{"metaMigratedAt":"2023-06-16T17:22:46.795Z","metaMigratedFrom":"YAML","title":"treap","breaks":true,"contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":3950,\"del\":0}]"}
    573 views