<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Splay學習筆記

      Splay 學習筆記

      前面的瞎BB

      我很喜歡 Splay,雖然其中有一部分因素來源于對 Tarjan 老爺爺的崇拜(bushi

      不過一想到 LCT 和 Splay 都是 Tarjan 和另外一位大佬發明的,就有種 Splay 為了 LCT 而生的錯覺(bushi

      在學習之前建議先了解 BST 和平衡樹的概念當初我沒看這兩玩意直接沖Splay看得頭大

      而且,我覺得如果你真的理解了 splay 這個操作,其實 Splay 寫起來是真的不要腦子,但是很多板子都有著相當多的亂七八糟的分類討論(讓我頭大)……

      關于 Splay

      其實 Splay 只有一個核心函數:旋轉到根。

      rotate

      先講一下旋轉。旋轉函數實質上就是將當前節點 \(p\) 的父親變為其兒子,同時此過程依舊要維護 BST 的性質。如下圖:

      這種情況,我認為 旋轉 這個詞其實并不貼切,因為如果你用 旋轉 去形容它的話不那么直觀,我更愿意將其稱作 上移 ——將當前節點移到它的父親上面,同時滿足 BST 的性質。(但是旋轉念順口了啊喂qwq)

      接下來我們做個分類討論:

      • 如果當前節點(p)是其父親節點(fa)的左兒子:
        為了保證 BST 的性質,那么在旋轉過后 fa 將變成 p 的右兒子,而 p 原本的右兒子將會變成 fa 的左兒子。

      • 如果當前節點(p)是其父親節點(fa)的右兒子:
        那么旋轉過后 fa 將會變成 p 的左兒子,而 p 原本的左兒子將會變成 fa 的右兒子。

      此處建議大家手玩一下這個旋轉操作,就會發現,所謂的 旋轉,其實本質上就是將當前節點 p 上移以后重新處理和其兒子、父親的連邊。

      指針實現:

      struct node {
        node *son[2], *fa;
      };
      
      // 對于一個節點 *p, p->son[0] 為它的左兒子,p->son[1] 為它的右兒子
      
      int get(node *p) {
        if (p->fa->son[0] == p) return 0;
        return 1;
      }
      // 可簡寫成: return p->fa->son[1] == son;
      
      // 將 fa 和 son 重新連邊,并且 son 是 fa 的左/右子樹
      void connect(node *fa, node *son, int d) {
        if (fa != nullptr) fa->son[d] = son;
        if (son != nullptr) son->fa = fa;
      }
      
      // 重新維護 p,這個重新維護其實和線段樹里的 pushup 本質上是一樣的
      void maintain(node *p) {
        if (p == nullptr) return;
        // ...
      }
      
      void rotate(node *p) {
        node *fa = p->fa, *grand = fa->fa;
        int d = get(p), fd = get(fa);
        connect(fa, p->son[d ^ 1], d); // p 的 d ^ 1 兒子將會變成 fa,所以這個兒子必須被 fa 所管理
        connect(p, fa, d ^ 1); // fa會變成 p 的與 d 剛好相反的兒子
        connect(grand, p, fd); // 無論 p 和 fa 怎么玩,它們相對 grand 的子樹將始終不變
        // 在一次旋轉過程中,將會影響三個節點:fa,p,grand
        // 同時在旋轉之后從下往上的順序也是fa, p, grand,所以維護先后順序也要注意。
        maintain(fa), maintain(p), maintain(grand);
      }
      

      數組實現:

      const int kMaxN = 1e6;
      
      struct node {
        int son[2], fa;
      }a[kMaxN];
      
      int get(int p) {
        return a[a[p].fa].son[1] == p;
      }
      
      void connect(int fa, int son, int d) {
        if (fa) a[fa].son[d] = son;
        if (son) a[son].fa = fa;
      }
      
      void maintain(int p) {
        // ...
      }
      
      void rotate(int p) {
        itn fa = a[p].fa, grand = a[fa].fa;
        int d = get(p), fd = get(fa);
        connect(fa, a[p].son[d ^ 1], d);
        connect(p, fa, d ^ 1);
        connect(gradn, p, fd);
        maintain(fa), maintain(p), maintain(grand);
      }
      

      這份代碼中 rotate 用 connect 避免了對空指針的特殊考慮,將空指針的特殊情況由 connect 統一管理。這么寫會讓代碼更加直觀一些。

      等會啊,rotate 有個啥用呢?它也不能保證旋轉某個節點后樹一定是平衡的啊。如下圖(節點旁數字代表高度):

      這本來是一個很平衡的狀態,然后我們輕輕地將 2 旋轉一下:

      完犢子嘞!以 2 為根的兩個子樹并不能保證平衡。也就是說,你可以通過各種稀奇古怪的操作使兩個子樹的高度差越來越大、越來越大……最后甚至可能退化成一個鏈(當然也許沒這么恐怖但是大差不差了)

      如果我們不進行場景而直接使用旋轉亂搞的話,很容易導致 BST 不再平衡然后被各種毒瘤出題人卡飛

      splay

      Splay 獨有的伸展操作/splay操作就是為了解決這個問題而誕生的。

      首先 splay 強制要求你每次訪問一個節點之后要將其旋轉至根節點。沒事也就只是多個常數而已(所以Splay常數大XD)。同時在 Splay 操作中維護 p 到根節點上的點使其能夠保持平衡。

      首先分三種情況討論:

      • 父親節點就是根節點。沒什么好說的直接移就完事。
      • 當前節點、父親節點、祖先節點共線:

        這種情況下,先旋轉父親節點再旋轉當前節點。
      • 當前節點、父親節點、祖先節點不共線:

        這種情況下,直接旋轉兩遍當前節點。

      啊,你問我為啥這樣子整體會更平衡?

      巧了我也不會證,但是oi-wiki上有

      但是不可否認的是它確實將時間復雜度壓了下來,并且均攤成了 \(\log n\)

      指針實現:

      void splay(node *p) {
        while (1) {
          if (p->fa == root) {
            rotate(p);
            break;
          }
          if (get(p->fa) == get(p)) {
            rotate(p->fa);
            rotate(p);
          } else {
            rotate(p);
            rotate(p);
          }
        }
        root = p;
      }
      

      數組實現:

      void splay(int p) {
        while (1) {
          if (a[p].fa == root) {
            rotate(p);
            break;
          }
          if (get(a[p].fa) == get(p)) {
            rotate(a[p].fa);
            rotate(p);
          } else {
            rotate(p);
            rotate(p);
          }
        }
      }
      

      看上去很難看,有一個更簡潔的寫法:

      指針實現:

      void splay(node *p) {
        for (node *fa; (fa = p->fa) != nullptr; rotate(p)) {
          if (fa->fa != nullptr) {
            rotate(get(p) == get(fa) ? fa : p);
          }
        }
        root = p;
      }
      

      數組實現:

      void splay(int p) {
        for (int fa; (fa = a[p].fa) != 0; rotate(p)) {
          if (a[fa].fa != 0) {
            rotate(get(p) == get(fa) ? fa : p);
          }
        }
        root = p;
      }
      

      很顯然,根節點是沒有父親的。如果一個節點沒有父親,那么我便可以認為它已經成為了根節點。

      基于這個思路,我們還可以將 splay 操作拓展理解為:將某個節點不斷旋轉,直到它的父親成為了另外一個節點。如果它的父親指針為空,那么等價于它會移到根節點。

      指針實現:

      void splay(node *p, node *top) {
        for (node *fa; (fa = p->fa) != top; rotate(p)) {
          if (fa->fa != top) {
            rotate(get(p) == get(fa) ? fa : p);
          }
        }
        if (top == nullptr) root = p;
      }
      

      數組實現:

      void splay(int p, int top) {
        for (int fa; (fa = p->fa) != top; rotate(p)) {
          if (a[fa].fa != top) {
            rotate(get(p) == get(fa) ? fa : p);
          }
        }
        if (top == 0) root = p;
      }
      

      一些代碼細節

      哨兵節點

      燒餅節點

      哨兵節點僅僅只是一種占位節點。它通常表示為無窮大或者無窮小。

      在上圖,我們設定了兩個哨兵節點代表無窮大和無窮小。我們并不關系它們是否有具體取值,但是在 BST 中它的鍵值就是無窮大和無窮小。

      可以根據情況來選擇寫一個或兩個哨兵節點。但是無論如何,哨兵節點是一定要寫的,這樣可以減少許多的分類討論(如插入節點是不需要再判斷樹是否為空)。

      用一個節點代替空指針

      如果你是用數組實現的 Splay,那么這里可以跳過。

      如果你用指針寫,那么多少會遇到在 maintain 等環節需要考慮子樹是否為空的苦惱,當訪問到空指針也確實是一件比較難調的事情。

      我們可以自己定義一個指針 nil,在默認情況下它將是所有節點的兒子節點、父親節點:

      nil 自己的左右兒子和父親將永遠是自己,它不會指向其它節點只有其它節點會指向它。

      nil 不是必須的,它的唯一作用就是代替空指針,并且可以為它賦上一定的值,這樣講減少很多情況下針對空指針的分類討論。

      具體操作

      kth、查詢前驅后繼

      BST 的基礎操作,自己去搞。

      不過記得找到之后把它 splay 到根

      插入

      假如我要往節點 p 后插入一個節點。

      先把 p 旋轉到根

      然后直接插入就完事了

      p 原本的右子樹就接到 new 上面去了。

      插入序列/Splay合并

      如果我們要往 p 后插入一個序列,和上面插入節點操作一樣,先將 p 旋轉到根,然后把需要插入的序列構造成一個平衡樹。

      有兩個插入的方法:

      • 直接插入:

        操作完之后最好再把 8 號節點 splay 一遍,這樣可以維護好它到根節點所有節點的信息。

      • 查詢后繼后插入:
        這么做有一個問題:如果我沒有后繼怎么?所以上文中提到了無窮大的哨兵節點就是這么用的,它保證了一定會有后繼。
        首先將 p 的后繼直接旋轉到 p 下面來。

        suffix 是一定沒有左子樹的,基于 BST 的性質,接下來可以直接把要插入的序列插入進去

        維護好 suffix 和 p 的信息就完了。

      基于這個思路,若Splay維護的序列上的信息你也可以用這種方式快速合并(不過要把哨兵節點去掉)

      當然,如果你是將兩個按照鍵值排序的Splay合并……老老實實寫啟發式合并吧。

      操作連續子序列/Splay分裂

      其實Splay也是可以處理分裂的,只是不用分裂合并維護平衡罷了。

      如果我們想要操作一個區間,一個合理的方式是將這個區間內的節點全部獨立到一顆子樹里,如下圖:

      雖然看上去并不好看,但是確實達成了我們的要求。將我們需要操作的節點都獨立到了一個子樹里。這樣我們做區間修改還是區間查詢時,只需要對這個子樹的根節點進行操作就可以了。

      假設我們要獨立一個區間 l, r,那么只需要把 l - 1r + 1 (下面稱呼為前驅和后繼)的節點通過 splay 操作向上旋轉即可。我常做的方法是將前驅旋轉至根、將后繼旋轉至前驅下,如下圖:

      由于我們并不會破壞 BST 的性質,旋轉后的后繼的左子樹,一定是 l, r 對應的子樹。

      也許你會有疑問:第一個節點沒有前驅、最后一個點沒有后繼。如果你使用了哨兵節點來代表無窮小和無窮大,這個問題就可以避免,減少分類討論的必要。

      利用將子樹獨立出來的方式,可以將一個 Splay 分裂成兩個 Splay,具體實現應該很顯然。

      刪除

      依舊用上面的樹為例子:

      直接將要刪除的區間獨立出來,然后可以方便刪除。

      板子

      這里只給出了最基礎最基礎的板子,沒有任何實際功能。

      class Splay {
        struct node {
          node *son[2] = {nullptr, nullptr}, *fa = nullptr;
        } *root;
      
        int get(node *p) {
          return p->fa->son[1] == p;
        }
      
        void connect(node *fa, node *son, int d) {
          if (fa != nullptr) fa->son[d] = son;
          if (son != nullptr) son->fa = fa;
        }
      
        void maintain(node *p) {}
      
        void rotate(node *p) {
          int d = get(p), fd = get(p->fa);
          node *fa = p->fa, *grand = fa->fa;
          connect(p->fa, p->son[d ^ 1], d);
          connect(p, fa, d ^ 1);
          connect(grand, p, fd);
          maintain(fa), maintain(p), maintain(grand);
        }
      
        void splay(node *p, node *top = nullptr) {
          for (node *fa; (fa = p->fa) != top; rotate(p)) {
            if (fa->fa != top) rotate(get(p) == get(fa) ? fa : p);
          }
          if (top == nullptr) root = p;
        }
      
      public:
      };
      

      數組實現:

      你問這個啊?懶得寫了自己寫去吧qwq

      我覺得我寫的還是挺詳細的,然后所有的內容自己去補充吧……

      posted @ 2023-03-17 20:45  sudoyc  閱讀(22)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 欧美黑人乱大交| 国产成人a在线观看视频免费| 久久精品国产一区二区蜜芽| AV人摸人人人澡人人超碰| 我要看亚洲黄色太黄一级黄| 亚洲精品久久一区二区三区四区| 国内精品久久人妻无码不卡| 区一区二区三区中文字幕| 国产精品午夜福利视频| 亚洲国产成人精品无码一区二区| 亚洲无av在线中文字幕| 欧美国产日产一区二区| 国产成人精品一区二区不卡| 日本A级视频在线播放| 国产一区在线播放av| 黑巨人与欧美精品一区| 国产精品天干天干综合网| 亚洲伊人久久大香线蕉| 福利视频在线一区二区| 国精品无码一区二区三区在线蜜臀| 亚洲中文字幕无码爆乳| 人妻中文字幕精品系列| 亚洲av不卡电影在线网址最新| 欧美不卡无线在线一二三区观| 亚洲精品一区二区制服| 日韩午夜福利片段在线观看 | 深夜视频国产在线观看| 粉嫩一区二区三区粉嫩视频 | 国产精品高清一区二区三区| 99久久国产精品无码| 毛片久久网站小视频| 无码va在线观看| 国产成人片无码视频在线观看| 国产在线亚州精品内射| 国内自拍av在线免费| 国产精品一区在线蜜臀| 欧美黑人又粗又大久久久| 日韩精品二区三区四区| 国产亚洲精品AA片在线爽| 视频一区视频二区制服丝袜| 日本免费观看mv免费版视频网站|