數據結構總結

棧。。。好吧,不知道的自己去查。

單調棧

功能簡單暴力:找到從左或右遍歷第一個比它大或者小的元素位置。
維護單調性:每次插入元素時判斷棧頂元素與該元素是否滿足單調性,滿足則直接插入該元素,否則彈出棧頂元素,繼續判斷。注意判斷棧是否爲空。
習題:
HDU 1506
HDU 5033
PKU 2796
PKU 3250

隊列

同上,不解釋基礎內容。

單調隊列

不同於單調棧,它求解的是前面一段區間的最值,而最值是隊首元素。
注:單調隊列或是單調棧都可以優化dp。
習題:
Luogu 1725 琪露諾
Luogu 3088 擠奶牛
Luogu 1714 切蛋糕
Luogu 2629 好消息,壞消息
Luogu 2422 良好的感覺
SCOI2009 生日禮物
Luogu 2698 花盆
POI2011 Temperature

堆(優先隊列)

直接用c++中的queue包就可以了。。。就是一個數據結構,堆頂元素是其中所有元素的最值,複雜度O(log2n)

應用:

最短路地界斯特拉的堆優化。。。
有的題要開一個大根堆和小根堆,在貪心算法中有用,每次維護當前元素大小在兩堆堆頂元素之間即可,可以保證每次選擇都是最優的。

二叉排序樹

定義:這棵樹滿足對於所有節點均滿足它的左子樹的所有元素小於該節點的值且右子樹的所有元素大於該節點的值。但是它有一個非常不好的地方:它很容易變成一條鏈,二叉排序樹的複雜度就變成了O(n) 就需要想辦法把它左右子樹變得差不多重,將複雜度變成O(log2n) ,即維護二叉排序樹的均衡。

方法1:splay

它是將一個節點通過左右旋將其變爲該樹的根的操作,可以實現子樹的刪除,區間翻轉,插入,刪除元素等操作。由於不斷進行splay,它的平攤複雜度爲O(log2n)
所以它很容易被卡數據。splay()複雜度爲O(log2n) ,但是常數大的不行。

#define geng(x) a[x].sum=a[a[x].l].sum+a[a[x].r].sum+a[x].w;a[x].size=a[a[x].l].size+a[a[x].r].size+1;a[x].maxx=max(a[x].w,max(a[a[x].l].maxx,a[a[x].r].maxx));
#define mark(x) if(a[x].lazy)a[x].lazy=false;else a[x].lazy=true;
void put(LL x){
    if(x==0)return;a[x].lazy=false;
    swap(a[x].l,a[x].r);
    mark(a[x].l)mark(a[x].r)
}//區間翻轉打懶標記 
void you(LL x){
    LL y=a[x].fa,z=a[y].fa;
    if(a[z].l==y)a[z].l=x;else a[z].r=x;
    a[x].fa=z;
    a[y].l=a[x].r;
    a[a[x].r].fa=y;
    a[x].r=y;
    a[y].fa=x;
    geng(y);geng(x);
}//右旋 
void zuo(LL x){
    LL y=a[x].fa,z=a[y].fa;
    if(a[z].l==y)a[z].l=x;else a[z].r=x;
    a[x].fa=z;
    a[y].r=a[x].l;
    a[a[x].l].fa=y;
    a[x].l=y;
    a[y].fa=x;
    geng(y);geng(x);
    }//左旋 
void splay(LL x){
    LL y,z;
    while(a[x].fa){
        y=a[x].fa;z=a[y].fa;
        if(a[z].lazy)put(z);
        if(a[y].lazy)put(y);
        if(a[x].lazy)put(x);
        if(z){
            if(a[z].l==y){
                if(a[y].l==x){you(y);you(x);}else {zuo(x);you(x);}
            }
            else {
                if(a[y].r==x){zuo(y);zuo(x);}else {you(x);zuo(x);};
            }
        }
        else {
            if(a[y].l==x)you(x);else zuo(x);
        }
    }
    geng(root);
    root=x;
}//splay操作 
void insert(LL x){
    if(cnt==0){
        a[++cnt].w=x;
        a[cnt].fa=0;
        a[cnt].maxx=x;
        a[cnt].sum=a[cnt].w;
        a[cnt].size=1;
        root=cnt;
        return;
    }
    cnt++;LL p;
    a[cnt-1].r=cnt;a[cnt].w=x;a[cnt].fa=cnt-1;a[cnt].sum=x;a[cnt].maxx=x;a[cnt].size=1;
    splay(cnt);
}//插入操作 
LL cha(LL k){
    LL p=root,sum=0;
    while(p){
        if(a[p].lazy)put(p);
        if(a[a[p].l].size+1==k)break;
        else if(a[p].l&&a[a[p].l].size+1>k)p=a[p].l;
        else {
            k-=a[a[p].l].size+1;
            p=a[p].r;
        }
    }
    return p;
}//查詢第k大的元素 
void s(LL x,LL y){
    x=cha(x);y=cha(y);
    splay(x);
    root=a[x].r;
    a[a[x].r].fa=0;
    a[x].r=0;
    splay(y);
    a[root].fa=x;
    a[x].r=root;
    root=x;
}//將 [x,y]區間翻轉在一起。 

嗯,編程複雜度太大了。。老闆還要求10分鐘內必須打出來

法2:替罪羊樹

個人認爲是三種方法中最爲暴力的方法,但是很保險。根本沒有所謂的左右旋操作,暴力維護原樹的平衡。只支持查詢,插入單點,刪除單點操作。
我們定義一個平衡常數k ,我們要維護對於所有點,均滿足:

max(sizel,sizer)<=sizex×k

x 爲當前點。如此保證複雜度,自然有暴力的維護方法:
插入:插入後,從該點向上深搜,找到深度最小不滿足平衡的點,將該點的子樹暴力變成平衡二叉樹。
其他操作都差不多了。

法3:treap(tree+heap)

想優化也是想瘋了。。玄學拼歐氣
但是確實比splay簡單多了,而且常數也小,只需要左右旋操作即可。
對於每個點,和二叉排序樹不同的是,它每個節點有一個隨機數,記爲rad .
heap的性質來了:對於每棵子樹來說,它的根節點的rad 值是該子樹的最值,滿足堆的性質。每次維護堆的時候不能破壞原二叉排序樹的順序,即只能使用左右旋操作維護。
這樣我也不知道爲啥就維護了二叉樹的平衡。。。。他們說很管用。。。。

順便介紹一個和可持續化的樹狀結構

傳送門

分塊:

優美的暴力,運用均值不等式來使時間複雜度下降。但是有的題必須使用它。
直白點來說,將所有元素分爲很多塊,對於修改操作,維護每塊之間的關係,然後暴力修改剩下在塊外的元素;對於查詢操作,直接運用塊與塊之間維護的關係,然後暴力查詢塊外元素即可。

倍增RMQ與分治

分治如此簡單就。。。。
倍增有點像dp,實際上是O(nlogn) 預處理,O(1) 查詢。樹上倍增找公共祖先有用。

線段樹與樹狀數組

線段樹->戳傳送門
樹狀數組是單點修改,區間查詢的數據結構,查詢和修改都是O(log2n) ,比線段樹的常數小。可以差分變爲區間修改,單點查詢。常常用來求逆序對。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章