Query on a tree 系列之樹剖解法
\(\texttt{Qtree}\) 系列樹剖題解(告別 \(\texttt{LCT}\) !)
前言
眾所周知 Qtree 系列是 LCT 的題目
但由於蒟蒻平衡樹寫的fhq,不會Splay,所以蒟蒻不會 LCT,所以蒟蒻決定用樹剖寫完 Qtree 系列的所有題
QTREE - Query on a tree
直接樹剖加線段樹,每個點的權值為指向父親的邊權。
注意不要把 LCA 算進去了
Luogu 的 vjudge 只能用 C語言 ,害得我 CE 了一頁
關於本題 C 語言的 \(\texttt{Tips}\) :
- 不要用
inline
- 標頭檔案選擇
#include <ctype.h>
#include <stdio.h>
就夠了 - 不要快讀,只能用
scanf
和printf
- 沒有
max
和swap
函式,要自己手寫 - 沒有
const
- 不能
using namespace std;
- 沒有
string
型別 - 函式間不能用
,
,比如不能寫dfs(1), dfs(2);
,只能寫dfs(1); dfs(2);
希望你能少 CE 一點
QTREE2 - Query on a tree II
路徑和可以樹上字首和
求第 \(k\) 個點時先判和 LCA 的關係,如果大於 \(dep_u - dep_{lca}\) ,就找 \(v\) 的 \(len - k\)
LCA 用重剖,\(k\) 級祖先用長剖,總時間 \(O(nlogn)\)
長剖寫錯,多測不清空會 RE!!!
QTREE3 - Query on a tree III
線段樹維護區間最淺點,查詢向上跳就行了
最簡單的一題 (要不是 Qtree1 只能寫 C語言)
QTREE4 - Query on a tree IV
難度陡然上升
先樹剖,並對每條重鏈開一棵線段樹
對於線段樹上的一個節點 \(p\),其對應區間為 \([L,R]\)(重鏈上的一條路徑)
記 \(lmx_p\) 表示從 \(L\) 出發,在 \([L,R]\)
考慮線段樹合併
記當前線段樹上節點為 \(p\),區間為 \([L,R]\) ,左右兒子分別為 \(ls\) 對應\([L,mid]\),\(rs\) 對應 \([mid+1,R]\)
\(lmx_p=\max\{lmx_{ls},dis(L,mid+1)+lmx_{rs}\}\)
\(rmx_p=\max\{rmx_{rs},dis(mid,R)+rmx_{ls}\}\)
\(mx_p=\max\{mx_{ls},mx_{rs},rmx_{ls}+dis(mid,mid+1)+lmx_{rs}\}\)
接下來考慮邊界(線段樹葉子節點)
記當前線段樹上節點為 \(p\),對應 \([L,L]\)
記 \(d_i, d’_i\) 表示以 \(i\) 為根的輕子樹內,根到白點的最遠距離和次遠距離,不存在即為 \(-\infty\)
當 \(L\) 為白點時:
\(lmx_p=rmx_p=\max\{0,d_L\}\)
\(mx_p=\max\{0,d_L,d_L+d'_L\}\)
當 \(L\) 為黑點時:
\(lmx_p=rmx_p=d_L\)
\(mx_p=d_L+d'_L\)
\(d\) 和 \(d’\) 如何維護?
記當前節點為 \(u\) ,它的一個輕兒子為 \(v\)
由於 \(u\) 在重鏈上,\(v\) 是 \(u\) 的輕兒子,所以 \(v\) 一定是另一條重鏈的開端
所以 \(v\) 的 dfn 就是 \(v\) 所在的這條重鏈所對應區間的左端點
所以從 \(v\) 出發到其子樹內最遠白點的距離就是這條重鏈的線段樹的根的 \(lmx\)
記這個根為 \(p\)
若 \(u\) 為白點:\(d_u=\max\{w(u,v)+lmx_p,0\}\)
若 \(u\) 為黑點:\(d_u=\max\{w(u,v)+lmx_p\}\)
具體可以每個節點開一個 multiset,把輕兒子的 \(lmx+w\)丟進去,如果是白點再丟個 \(0\) ,那麼 \(d\) 就是堆頂
對於修改,只需不斷向上跳重鏈,同時刪除原來貢獻,加入新的貢獻
由於一次修改最多更新 \(logn\) 次,所以時間複雜度為 \(O(nlog^2 n)\)
這樣寫屬實有點搞心態,而且三個 \(log\) 有點醜
還有個比較簡便的做法
可以新增虛點將樹變為二叉樹,如圖
紅點為虛點,紅邊邊權為 \(0\)
這樣每個點只有一個重兒子,一個輕兒子,那麼 \(d\) 就唯一確定,\(d'\) 必為 \(-\infty\) ,不需要開 multiset 了
QTREE5 - Query on a tree V
樹剖解法
網上沒找到寫樹剖的題解,蒟蒻試著寫了一下樹剖,其表現良好,寫篇題解為像我一樣不會 LCT 的同學提供一種思路
雖然樹剖比 LCT 多一個 log,但憑藉優秀的常數跑得還是比大部分 lct 更快 (目前排 luogu 最優解第四)
與 Qtree4 類似,每條重鏈開一棵線段樹
對於線段樹上的一個節點 \(p\),其對應區間為 \([L,R]\)(重鏈上的一條路徑)
記 \(lmn_p\) 表示從 \(L\) 出發,在 \([L,R]\) 間的某個節點拐出重鏈能到達的最近白點(這點與 Qtree4 不同),\(rmn_p\) 同理。(由於詢問是關於某個點的資訊,不再是全域性資訊了,所以不需要維護 \(mx\))
轉移與 Qtree4 類似
\(lmn_p = \min\{lmn_{ls},dis(L,mid+1)+lmn_{rs}\}\)
\(rmn_p = \min\{rmn_{rs},dis(mid, R)+rmn_{ls}\}\)
記 \(d_i\) 表示以 \(i\) 為根的輕子樹內,根到白點的最遠距離,不存在為 \(+\infty\) (這點不同)
這裡同樣改建二叉樹以維護 \(d_i\) ,\(v\) 是 \(u\) 的輕兒子
若 \(u\) 為白點:\(d_u=\min\{w(u,v)+lmn_p,0\} = 0\)
若 \(u\) 為黑點:\(d_u=w(u,v)+lmn_p\)
線段樹邊界節點 \(p\),對應區間 \([L, L]\):
當 \(L\) 為白點時:
\(lmn_p=rmn_p=\min\{0,d_L\} = 0\)
當 \(L\) 為黑點時:
\(lmx_p=rmx_p=d_L\)
以上部分與 Qtree4 的解題思路相仿,略微改動就行,更新顏色也和 Qtree4 一樣,不斷跳鏈即可
重點是如何根據我們維護的 \(lmn\) 和 \(rmn\) 得到距 \(u\) 最近白點 \(v\) 的 \(dis(u, v)\)
由於我們維護的是以左端或右端為起點的最小距白點距離,所以只有當 \(u\) 為區間的邊界時,我們才能直接用上對應的 \(lmn\) 和 \(rmn\)
所以設當前詢問點 \(u\) 所在重鏈的 \(\text{dfn}\) 區間為 \([L,R]\)
那麼 \(u\) 在這條重鏈上的答案(指白點在以鏈頂為根的子樹內)可由以下兩種情況貢獻:
-
\(u\) 作為左端點,貢獻 \(lmn\),即 \(lmn_{[dfn_u,R]}\),相當於統計鏈底到 \(u\) 的答案
-
\(u\) 作為右端點,貢獻 \(rmn\),即 \(rmn_{[L,dfn_u]}\),相當於統計 \(u\) 到鏈頂的答案
這條重鏈的答案為上述兩種情況取 \(\min\)
而對於白點在子樹外的情況,只需不斷跳重鏈,跳到根為止,統計新的重鏈的答案,跳的時候加上鍊頂到 \(u\) 的距離,最後再對條重鏈上的答案取個最小值便是此次詢問的答案
由於跳 \(O(logn)\) 條重鏈,每條重鏈統計答案需要 \(O(logn)\) 的時間,所以總時間複雜度 \(O(nlog^2n)\)
注意的是這道題所有節點初始為黑色
QTREE6 - Query on a tree VI
這題樹剖有兩種解法,一種是延續 IV、V 的思路,維護端點最值,另一種是每個點單獨考慮,這邊先寫的第二種,第一種後面補
設 \(b_u, w_u\) 分別表示在以 \(u\) 為根的子樹內,如果 \(u\) 為 黑/白 點,\(u\) 的同色最大聯通塊的大小
初始由於全是黑點,所以 \(b_u = siz_u, w_u = 1\)
詢問時,只需向上跳到最遠的與 \(u\) 同色的點,它的 \(b\) /\(w\) 即為答案
對於修改,先考慮把 \(u\) 由黑變白
-
由於 \(u\) 不再是黑點了,所以有它是黑點所帶來的貢獻要全部刪去。
而 \(u\) 所貢獻的只能是它的祖先,並且與它在同一個黑色連通塊內,或者是在 \(u \rightarrow root\) 路徑上第一個白點(貢獻這個白點為黑點時的答案),所以找到第一個白點 \(v\),\(fa_u \rightarrow v\) 路徑上所有點的 \(b\) 減去 \(b_u\) (相當於 \(u\) 阻斷了祖先的黑色連通塊與子樹內的黑色連通塊)
-
由於 \(u\) 變為了白點,所以它會帶來一些貢獻
而 \(u\) 所貢獻的也是它的祖先,並與它在同一個白色聯通塊內,或者是在 \(u \rightarrow root\) 路徑上第一個黑點,所以找到第一個黑點 \(v\),\(fa_u \rightarrow v\) 路徑上所有點的 \(b\) 加上去 \(w_u\) (相當於 \(u\) 聯通了祖先的白色連通塊和子樹內的白色聯通塊)
這兩步都可以用線段樹區間加實現
還有個問題就是怎麼求 \(u \rightarrow root\) 路徑上的第一個 ,這個與 Qtree3 類似
而至於求 \(u \rightarrow root\) 上最遠的同色連通塊內的點,Qtree3 的操作改一下就行了,即每次查父親到父親鏈頂的第一個異色點,沒查到就跳到父親鏈頂的位置,否則特判:如果這個異色點是父親,就返回當前點;否則返回這個點的重兒子。具體看程式碼
細節比較多,但只要熟悉樹剖都能發現
碼量和常數似乎都變大了
QTREE7 - Query on a tree VII
還是利用 QTree4 的思想
線段樹合併:當前節點 \(p\) ,表示區間為 \([L,R]\)
設 \(lmx_{p}\) 表示 \(L\) 所在同色連通塊內的最大權值
設 \(rmn_{p}\) 表示 \(R\) 所在同色連通塊內的最大權值
光這樣顯然是合併不了的,考慮合併需要什麼
對於當前節點 \(p[L,R]\) ,左右兒子分別為 \(ls[L,mid]\),\(rs[mid + 1, R]\)
顯然 \(lmx_{ls}\) 可以貢獻給 \(lmx_p\) ,但 \(lmx_{rs}\) 呢?
如果左兒子全是一個顏色,並且與右兒子最左邊的顏色相同,那麼是不是 \(lmx_{rs}\) 也可以貢獻給 \(lmx_p\)?
所以設 \(lsiz_p\) 表示左端點所在同色連通塊包含區間內的點數(即 \(L\) 在重鏈上的同色連通塊的大小),\(rsiz_p\) 同理
那麼轉移就出來了:
\(lmx_p = \max(lmx_{ls} , lmx_{rs}[lsiz_{ls} = mid - l + 1][col_{mid} = col_{mid +1}])\)
\(rmx_p = \max(rmx_{rs} , rmx_{ls}[rsiz_{rs} = r - mid][col_{mid} = col_{mid+}])\)
\(lsiz_p = lsiz_{ls} + lsiz_{rs}[lsiz_{ls} = mid - l + 1][col_{mid} = col_{mid +1}]\)
\(rsiz_p = rsiz_{rs} + rsiz_{ls}[rsiz_{rs} = r - mid][col_{mid} = col_{mid +1}]\)
邊界情況:設 \(v\) 為 \(u\) 的輕兒子
\(lmx_u = rmx_u = \max\limits_v \{lmx_{rt_v}[col_u = col_v], w_u\}\)
\(lsiz_u = rsiz_u = 1\)
詢問:
只需跳到最淺的,同一同色連通塊內的祖先,再查,該祖先到其所在鏈鏈底這個區間, \(lmx\) 即為答案,但要注意特判跳到根的情況,以及跳時顏色是否相同,細節看程式碼
修改:
先刪除原來的貢獻,加入新的貢獻,這個只需每個點每個顏色用一個 multiset
維護,記錄該顏色連通塊內的最大權值即可
然後一邊跳重鏈一邊改,同時記錄上一個節點(從哪裡跳來),和之前的貢獻,以便刪除貢獻和新增貢獻,細節在於改顏色時上一個點是否就是修改的點,如果是,在刪貢獻是要在反色 multiset
裡面刪(它原來的貢獻是存在那裡面的)
也是調了好久,拍了一下午
好在樹剖依舊穩定輸出,拿下最優解第四