1. 程式人生 > 其它 >(log)資料結構專題

(log)資料結構專題

目錄

log資料結構專題

前言

log的資料結構,有哪些?

這裡的定義並不嚴格,涉及到log就行,就算複雜度可能有sqrt

主流:

  • 線段樹/BIT (SGT-Beats) (LiChaoTree)
  • 平衡樹
  • 上述結構互相巢狀 (樹套樹)
  • 上述結構可持久化
  • 樹剖
  • (Link Cut Tre) (這個是NOI大綱的10級演算法)
  • 特殊:trie(維護整數)

少見:

  • 笛卡爾樹
  • 堆 / 左偏樹

如果只是板子,相信大家都會打。

本篇在板子的基礎上,討論如何去應用這些工具。

(注:課件裡的東西我還沒補完,這裡的內容並不全qaq)

線段樹/BIT

線段樹有很多形式,有些題目中只用到最板子的線段樹——這種題一般其它的步驟思維難度大;有些題目要用到線段樹的變形,如李超樹,吉司機樹,或者要用線段樹合併等演算法,這種題一般思維難度和結構本身的難度都挺大的。

線段樹相當於是把分治的過程記錄下來了,類似點分樹(其實應該是點分樹類似線段樹(小聲)

對於連續的一段區間的操作與詢問,我們把它拆成若干個區間的整體操作/詢問,併合並區間的答案。

lazytag的理解與歷史最值

線段樹中,我們會用到 lazytag 這個東西。它其實代表的是待進行的操作 序列 合併後的結果。

這篇 中,我們提到了 lazytag 更深層的理解,並用這個理解搞定了區間歷史最值問題。

思維題

前面提到,很多線段樹題並不是難線上段樹,而是難在如何使用線段樹

loj3033:離線,來回貢獻

注意到本題沒有強制線上。那就可以為所欲為的離線。

首先我們可以把 \(|a-b|\) 看成 \(\max(a-b,b-a)\),做兩遍,然後再取max。然後這個絕對值就廢了。

那我們在算一次的時候,不妨只考慮 “前-後” 的情況。

對於一個詢問,假設有兩個訊號塔 \(i,j(i<j)\)。有兩個條件要滿足,\(i\) 能到 \(j\)\(j\) 能到 \(i\),即,“互相聯絡”。此時就可以貢獻一個 \(h_i-h_j\)

像這種一看就不好線上做的題,就先離線再掃描線,是經典的考慮角度。

根據這個套路,把每個詢問掛在 \(r\) 上,然後對 \(r\) 掃描線。每次處理到一個 \(r\),維護一顆線段樹,\(i\) 位置的值表示左端點取在 \(i\) 的答案。遍歷掛在 \(r\) 上的詢問 \([l,r]\),查詢一下線段樹的 \(l\) 位置就得到答案了。

稍微想一下,對於本題,我們得這樣搞:

  • 設線段樹的 \(i\) 位置表示,左邊的那個塔 恰好\(i\) 位置,\(h_i-h_j\) 的最大值 (注意到“恰好”比較好做)。

那取在 \(l\) 位置的答案,就是 \([l,r]\) 的區間最大值。

由於我們是掃描線的處理,只需要考慮 加入一個位置 \(r\) 之後線段樹如何變化。

根據題意,肯定是和 \(r\) 位置能互相聯絡的塔,權值可能要變。問題就在於,如何處理互相聯絡。對於一個在前面的塔 \(i(i<r)\),要滿足兩條:

  1. \(i\) 能到 \(r\)
  2. \(r\) 能到 \(i\)

然後貢獻一個 \(h_i-h_r\)。我們把 \(h_i\)\(-h_r\) 分開來做,即,我們維護倆權值,然後把它倆加起來後求最大值。

②條件非常好搞,區間 \([r-b_r,r-a_r]\) 就是 \(r\) 能到的所有 \(i\)

對於①條件,我們這樣考慮:假設 \(i\) 能到 \(r\),我們稱這個 \(i\) 是“可用的”。

由於 \(i<r\),那 \(r\) 肯定是要在 \([i+a_i,i+b_i]\) 裡面才,\(i\) 才可用。我們發現這玩意類似一個區間加,單點求和的過程 (單點求有沒有被覆蓋到)。如何維護這玩意呢? (自己想一下,很容易想到)

這是一個經典問題,我們把它作一個 “差分”:對於每個 \(i\),看成有一個 “開關”。

我們把它開啟,\(i\) 就可用。如果我們不動它,開關狀態不變,那 \(i\) 還是可用的。等我們把它關閉,\(i\) 才變得不可用。設 “開啟”是 \(+1\),“關閉”是 \(-1\),我們發現開關的開啟,關閉,就相當於 \(i\) 的可用性(0/1)的一個差分。

那我們在 \(i+a_i\) 的位置加入(vector維護)一個 “開啟開關 \(i\)” 操作,在 \(i+b_i+1\) 的位置加入一個 “關閉開關 \(i\)” 的操作。每次到一個 \(r\),把 \(r\) 位置的所有操作都做一遍。那就可以動態的維護哪些 \(i\) 是“可用”的了。

對於可用的位置 \(i\),它的權值就是 \(h_i\)。而如果 \(i\) 不可用,那 \(h_i-h_r\) 的貢獻其實是不合法的。我們令它的權值為 \(-\infin\),就避免了它貢獻過來。設這個權值是 \(c_i\)。即,c[i]=(i可用)?h[i]:-INF

對於 \(r\) 能到的位置,還要貢獻過去一個 \(-h_r\) 的權值。我們設這部分的權值是 \(c'_i\)

對於一個 \(i\),可能有很多 \(r\) 能到它,我們要找 \(h_i-h_r\) 最大的。

那它的最優選擇肯定是 \(h_i+\max(-h_r)\),也就是線段樹維護的 \(i\) 位置的值。我們設它為 \(d_i\)

這部分不太懂的,回去看下線段樹維護的是啥。

注意到 \((-h_r)\) 這個值,我們只要求最大的那個。那每次 \(r\) 貢獻過去,相當於是區間的 \(c'\)\(-h_r\)\(\max\)。這樣得到的 \(c'\) 就是 \(\max (-h_r)\)。那直接令 \(d_i=c_i+c'_i\),就是線段樹上的答案了。

總結一下,我們的線段樹要支援:

  • 單點改 \(c\)
  • 區間的 \(c'\)\(x\)\(max\)
  • 詢問 \(d=c+c'\) 的區間最大值

我們似乎不太好處理改 \(c\) 之後再加上 \(c'\) 最大值如何變化。但其實我們每次改 \(c\) 的時候,它也許剛變成可用,還沒有 \(r\) 貢獻過去,\(c'\)\(-\infin\);它也許剛變得不可用,那我們顯然也不用管它對 \(d\) 的影響。

那其實不用直接考慮單點改 \(c\)\(d\) 的影響,直接 pushup 的時候順便看一下就行。對 \(d\) 的影響,我們完全可以在改 \(c'\) 的時候考慮。

對於 \(c'\) 的區間取max,我們可以維護一個lazytag,\(t_i\) 表示 \(i\) 節點待處理的操作序列。“操作”就是取max,要合併這個操作序列,顯然直接取最大的那個就行。那 \(t_i\) 就記一個數,表示操作序列裡最大的那個。

每次pushdown的時候,更新 \(c'\) 之後順便更新 \(d\) 就行。

程式碼

loj2873:轉化

首先把相同的 \(a\) 合併到一起,然後認為 \(a\) 是不同的。

我們發現題意的限制就像是一個 “山峰”,講人話就是,中間大於兩邊。

考慮如下的構造:

  • 先把 \(a\) 逆序排
  • 每次插在最左/最右

這樣很明顯滿足山峰的條件。再一想,每一個“山峰”都能用這種方法構造。

於是這樣的一種構造就和原題要求的序列等價了。

再考慮另一個問題,有倆排列 \(p,q\),每次只能交換 \(p\) 中相鄰倆位置,要換到 \(q\),最少幾步?

這是個經典題,答案是:排列 \(\dfrac{p}{q}\) 的逆序對數。

注意到排列可以和一個每行每列只有一個 \(1\) 的矩陣一一對應。

這裡排列的除法,看成是變成矩陣,除完之後,再變成排列。

或者說就是,如果把 \(q\) 重排成 \(1,2...n\)\(p\) 的逆序對數。

那我們每次就看一下插在左邊的逆序對多還是插在右邊的逆序對多就行了。

為什麼這樣的貪心是對的呢?因為每一步獨立

為什麼每一步獨立呢?因為我前面不管是啥順序插,我新來的一個數,都是讓以前的 所有 數一塊在它前面/後面,這部分的逆序對貢獻,與前面如何排列無關。

然後直接樹狀陣列就可以做。

loj2346:套路轉化+線段樹

我們發現這個地層的斜向平移非常的不好做。怎麼做呢?把座標軸轉個 \(45\degree\),它就變成了橫豎向的平移。

轉45°:\((i,j)\to (i-j,i+j)\)

依然不太好做。再發現,地層運動一波之後,最後一次運動的那一根線一定是完整的,而其它的線可能會被切開,變成斷斷續續的。

自然的,我們順著這根完整的線,“追溯”回去。

我們可以考慮最後地平線上的狀態(即,答案),它們的這些地層是哪裡來的?我們可以把操作反過來做,就可以實現這個 “追溯”,然後就可以找到它原來是哪來的。

這有啥用麼?那當然很有用,它的深度是多少,答案就是多少!

於是我們把所有的操作反向,把上移改成下移,做一遍之後,原來地表上那些位置最後的深度,就是答案。

轉座標軸之後,設地表上每個點在 \((x,y)\)。每次操作相當於,對於 \(x<k\) 的點,\(y\) 增加 \(a\),或者把 \(x,y\) 換一換。

我們發現每次直線切過來,它“上面”的點都是一段字首/字尾,所以這個 \((x,y)\) 一定都有單調性,可以利用線段樹的結構二分得到長度。

實現細節:我們不需要得到長度之後再做區間加,我們可以直接一邊二分一邊打tag。順便,需要比較精細的推式子。

程式碼

Segment Tree Beats!

這個名字來源於 吉如一選手(吉老師)的課件。

這種結構可以維護區間對某個數取min的操作。沒有區間加操作時,複雜度是嚴格 \(O(n\log n)\)

當存在區間加操作時,吉老師的單log證明假了,現在只能證明出 \(O(n\log^2 n)\),但實際速度接近 \(O(n\log n)\)

模板題見 bzoj4695

CF1290E:經典套路+吉老師樹維護

根據笛卡爾樹的基礎知識,\(i\) 位置的size就是 \(nex_i-pre_i+1\),其中 \(pre_i,nex_i\) 分別表示向前/向後第一個比 \(i\) 大的位置。我們把這個東西求和。

如果後面沒有大的,\(nex=n+1\);如果前面沒有大的,\(pre=0\)

注意到它完全可以拆開求。然後再注意到 \(nex\)\(pre\) 就是反一反(之後推一下式子即可)。於是問題變成了求 \(nex\) 的和。

我們能夠預先知道每個數最終在哪裡,我們直接把它們放在最終位置,在空的位置上補 \(0\)。則插入就變成了修改一個為 \(0\) 的位置。考慮每次插入之後 \(nex\) 如何變化。

設插入 \(i\) 的最終位置為 \(p\),在當前子序列裡的位置是 \(q\)\(q\) 可以用樹狀陣列求得。

很明顯的是,\(p\) 後面都被擠了一位,那它們的 \(nex\)\(+1\)

另外,\(i\) 之前插的數是 \([1,i-1]\),它們都比 \(i\) 小。那 \(p\) 之前的數, \(nex\) 應該都不會越過 \(i\),所以對 \(q\)\(min\)

\(p\) 位置本身的 \(nex\)\(i+1\),顯然。

因此我們要支援,區間加,區間取min,區間求和。寫一個吉老師樹就行了。

uoj515:換維度考慮(掃描線)+吉老師樹(帶擴充套件)

我們要維護不同的字尾最小值個數。還要支援單點修改。

我們把圖畫在草稿紙上。對於每次修改,我們用一個類似“可持久化”的東西,把它改掉的位置新建一個副本,寫在原來的序列下面,而不是直接塗改掉。

然後我們就發現,我們其實並不需要一行一行考慮(即,順序維護每次操作),而是可以按列考慮,似乎也很有規律。

即,我們對下標做掃描線,設當前列舉到 \(p\),從 \(n\)\(1\)。線段樹的第 \(i\) 個位置,維護第 \(i\) 個時刻 \(p\) 位置的字尾最小值(記為 mn)是多少,以及它有多少段不同的字尾min(記為cnt)。每個位置還有個初始值,我們認為這是在 \(0\) 時刻做了一個修改操作。

我們把一個點有哪些操作記一下。修改是直接覆蓋的,所以一個操作只能影響到下一個操作之前,設當前操作在 \(t\) 時刻發生,下一個操作在 \(t'\) 時刻發生,那它能影響的區間就是 \([t,t'-1]\),設這個是它的“影響區間”。

對於一個修改為 \(x\) 的操作,影響區間為 \([l,r]\),對於 \([l,r]\) 位置,mn顯然要和 \(x\)\(\min\)。如果它原來的mn嚴格大於 \(x\),就給它cnt的 \(+1\)

第一個操作就是吉老師樹。對於第二個操作,我們也可以藉助吉老師樹順便維護:

對於一個區間,我們記它的最大值是多少(mx),以及它出現了多少次(mxcnt),這是最基本的。

對於一個取min操作,如果它正好在次大和最大之間,就直接令 mx=x,然後 cnt+=mxcnt 即可。

這很明顯,因為最大值有 mxcnt 個,它們一定都被取min了,所以被取min的次數就新增了 mxcnt 個。

然後就可以打tag,遞迴處理了。

線段樹合併

這是一個經典技巧。通常可以解決集合的“並”問題。它與啟發式合併的區別在於,它只有一個log,並且可以順帶的維護很多東西,這是啟發式合併所做不到的。

做題的時候,如果發現要維護一個集合,值域比較小(1e5),甚至元素會帶個權值,並且要把兩個集合合併,並在中途支援一些別的東西,那就可以想到用線段樹合併來維護。

下面是一些例題

結合SAM維護right集合

SAM的fail樹,可以搞出來一個串的所有後綴。

如果一個串出現了,它的字尾也應該會出現。但如果直接插入的時候放一個 \(i\) 在集合裡,只會在大串裡被算一遍,應該把它所有的字尾都算一遍。

轉化問題,一個串的right集合等於它fail樹上所有兒子right集合的並。

那就可以用線段樹合併來搞這個 “並”。

複雜度是 \(O(n\log n)\)

維護這個之後,就可以做很多較複雜的SAM題。

loj2537

觀察題目要求的式子發現,把分佈列求出來啥都好做。

觀察分佈列這個東西的形式:等於值 \(x\) 的概率為 \(P(x)\)

我們把葉子權值離散化,那 \(x\) 的取值就在 \([1,n]\) 中,\(P(x)\)看成一個權值:非常像一個線段樹的形式。考慮線段樹維護分佈列。

對於一個點 \(u\) ,只有一個兒子直接繼承即可,考慮有倆兒子 \(a,b\) 的情況。

\(a\) 中列舉一個 \(x\),表示 \(a\) 權值為 \(x\),概率為 \(P_a(x)\)。考慮啥時候 \(u\) 取到 \(x\)

  1. \(p_u\) 的概率,二者取 \(\max\),此時的概率為 \(P_b(1...x-1)\)
  2. \(1-p_u\) 的概率,二者取 \(\min\),此時的概率為 \(P_b(x+1...n)\)

我們發現,要求的本質上是一個 “外部貢獻”:\(x\) 左邊的乘一個 \(p_u\)\(x\) 右邊的乘一個 \(1-p_u\),二者相加,作為一個係數乘給 \(x\)

假設線段樹合併到了區間 \([l,r]\),發現我們可以一邊遞迴區間一邊維護區間的“外部貢獻” (\(a,b\) 都要搞)。

比如我們遞迴到左半邊,那就加上右半邊乘以 \(1-p_u\) 的“外部貢獻”。

\(a\) 的“外部貢獻”,即,\(p_u\times P_a(1...l-1)+(1-p_u)\times P_a(r+1...n)\),為 \(p_a\);同理 \(b\) 的“外部貢獻”為 \(p_b\)

如果當前的區間裡只有 \(a\),那我們就把它整個都乘以 \(p_b\)。同理,如果只有 \(b\),那我們就把它整個都乘以 \(p_a\)

然後就可以解決每個位置的 “外部貢獻” 了。

像這樣就可以一邊合併,一邊維護額外的權值。最後每個位置的值就是根節點取值的分佈列。

程式碼

平衡樹

平衡樹,就是在二叉搜尋樹的基礎上加入某種機制使樹平衡。那相當於我們有了一個二叉搜尋樹,並且樹高不高。

我們拿它做什麼呢?可以用來維護值,就是 pre/nex/kth/rank 之類的(見,普通平衡樹)。

對於部分平衡樹,如 splay/treap,可以方便的提取區間,就可以用來維護序列 (見,NOI2005維護序列)

bzoj3786:維護括號序

括號序:進一個點,放進來(這次稱為“左括號);出一個點,再放進來(這次稱為“右括號”)。

很明顯左右括號是匹配的。

樹的括號序有很多性質,因為它的括號自動匹配,只需要少量修改就可以做很多奧祕操作。

順便,如果我們把一個點的左括號那邊放一個權值,右括號那邊放一個負的權值,那一個位置的字首和就是它到根的路徑和。

證:不在棧中的肯定一左一右抵消了,在棧裡的只有左括號出現,就正好是它到根的路徑

根據這個,我們考慮用平衡樹做這題:

  • 對於詢問,直接查詢字首和
  • 對於修改連結,我們把子樹的區間提取出來,斷開原來連結,並插入到新父親的左括號後面就行
  • 對於子樹的修改,我們直接打tag做加法

注意,子樹修改的時候,由於某些位置是正權,有些位置是負權,我們需要記哪些位置是正,哪些位置是負,才能夠處理。

CF573E:維護決策

這個題看起來很dp,確實也容易寫出 \(O(n^2)\) 的dp。而正解需要藉助貪心的思想,並結合平衡樹做。

有一個結論是,選 \(k+1\) 個數的最優方案,一定存在一個,包含選 \(k\) 個數的最優方案。

證明:奧妙重重,我不會證

因此,我們直接一個一個決策就行,按這個說法,一定能夠找到其中一個最優解。

這玩意的一個等價表達是,如果某個 \(k\) 的最優解選了 \(i\) 位置,那麼選 \(>k\) 個數一定也會選 \(i\) 位置。

我們列舉 \(i\),表示做好了前面 \(i-1\) 個選啥的決策,平衡樹上 \(k\) 位置表示選 \(k\) 個的最優解。現在要來搞 \(i\)。對於當前的 \(i\),由上面的等價表達,我們直接在平衡樹上二分從哪裡開始加,然後需要維護區間平移,區間加等差數列(推式子得)。

區間平移可以藉助平衡樹的插入的優秀性質,來間接的維護。區間加等差數列,可以維護差分之後變成區間加。

然後就splay搞就行,複雜度是 \(O(n\log n)\)。 由於splay的常數很大,我們需要忍一下。

它大概只能跑到 \(1e5\),noi.ac上有一個類似的題但那個題資料 \(1e6\),我到現在也只過了 \(60\)

怎麼只有這麼點東西啊

會加的會加的(資料結構題是真做不動,可能會更的較慢qaq)