動態dp
原理
對於樹上一類支持動態修改,動態查詢dp結果的問題,可以用動態dp解決。這種dp狀態不能太多,不然復雜度會有問題。
其核心思想就是,我們把轉移看做一個線性變化,對於重鏈維護線段樹保存矩陣乘的答案,然后對於每個點的輕兒子的貢獻當做一個常數矩陣,我們要查某個點的信息就可以在當前這個點向下延伸到底的那條重鏈上詢問。
然后考慮修改的時候,跳到 top
然后改父親的輕兒子的常數矩陣,大部分題都可以直接進行加減,但是對於一些特殊問題(比如切樹游戲),不支持逆操作(減法或者除法)你需要多開一棵線段樹維護對於每個點維護所有輕兒子的矩陣積。
然后復雜度雖然是 \(\mathcal O(L^3 n \log^2 n)\) (其中 \(L\) 為矩陣長度),但在不是特殊構造的圖下效率還行。
一些小技巧
對於一些把某些狀態給禁止掉的 \(dp\) ,我們可以這么做:
- 對於計數類 \(dp\) 考慮把所有能轉移上來的位置設成 \(0\) 。
- 對於最優化 \(dp\) 考慮把所有能轉移上來的位置設成 \(\pm \infty\) 。
然后需要撤回的話,考慮記一下原來的矩陣,然后按順序改回去就行了。
一些注意事項
- 一開始預處理靜態 \(dp\) 那里,對於輕兒子的轉移一定不要寫錯!
- 禁止狀態的時候一定不要漏或者多轉移。
- 對於葉子常常需要特殊考慮矩陣。(因為沒有輕兒子)
例題
NOIP2018 保衛王國
題意
一顆 \(n\) 個點的樹,每次強制兩個點選還是不選,求一個最小權覆蓋集。
題解
設 \(f_{i, 0 / 1}\) 為 \(i\) 選還是不選的最少代價,轉移顯然。
記一條重鏈上相鄰兩個點為 \(j, i\) ( \(j\) 是 \(i\) 的重兒子),記 \(g_{i, 0}\) 為 \(i\) 所有輕兒子的 \(\min(f_{v, 0}, f_{v, 1})\) 的和,\(g_{i, 1}\) 為他們的 \(f_{v, 1}\) 的和,那么有:
然后直接維護即可。\(\mathcal O(8n \log^2 n)\) 。
代碼
「SDOI2017」切樹游戲
動態維護集合冪級數 \(dp\) 。
點分樹
原理
可以把點分樹看做一個比較強大的數據結構使用,通常是為了動態處理有關距離或者點對貢獻的問題。
其主要思想就是維護點分治的結構,也就是對於點分樹兩個父子節點就是,靜態點分治時候相鄰的兩個分治重心。
我們解決一些問題,可以先考慮靜態點分治的時候如何解決,就可以考慮在點分樹上動態維護這個過程了。
大概就是修改一個節點的時候,我們考慮它在點分樹上到根的那條長為 \(\mathcal O(\log n)\) 的鏈,維護一下它對於這個分治重心的貢獻;詢問的時候,由於分治子樹重重包含,大部分時候用容斥去掉重復貢獻就行了。
一些小技巧
- 大部分信息(例如到分治重心的距離)可以直接在靜態點分治的時候預處理掉。
- 通常為了容斥,我們要維護這個節點在點分樹上到祖父的貢獻。
一些注意事項
- 如果找重心找錯,會直接層數爆炸,然后獲得 \(\text{MLE/RE/TLE}\) 。
- 如果寫法有問題的話,那個 \(sz[u]\) 不是真正的大小,我們要 \(\text{dfs}\) 一遍得到正確的大小,雖然對於找重心來說復雜度沒問題,但有時候用了這個變量就會出問題。
例題
BZOJ 3730: 震波
題意
應該是較為經典的一道題了。
一棵 \(n\) 個點的樹,需要支持查詢到一個點 \(x\) 距離 \(\le k\) 的點權之和,以及修改一個點的權值,強制在線。
題解
詢問的時候考慮在點分樹上跳從 \(v\) 跳到父親 \(u\) ,然后用 \(k\) 和 \(dist(u, x)\) 算出對於 \(u\) 分治樹內距離 \(u\) 多遠的點合法。
然后直接加會有子樹算多次,我們考慮每次把 \(v\) 分治樹內的貢獻減掉就行了。
這個直接用兩個樹狀數組(一開始 resize
)一個維護本來的貢獻,一個維護對於祖父的貢獻即可。
\(\mathcal O(n \log^2 n)\)
代碼
「ZJOI2015」幻想鄉戰略游戲
動態維護帶權重心,以及計算帶權距離。
邊分治
原理
顧名思義,就是對於邊進行分治。這樣有什么好處呢?每次我們只需要合並兩邊的信息,這樣很多時候問題就可以大大簡化。
但是直接分治找一條邊使得較大子樹盡量小的話,復雜度會在菊花圖時退化成 \(\mathcal O(n^2)\) 的。
考慮優化,我們顯然只需要保證度數,那么復雜度就是正確的了,這個只需要二叉化。給一種比較方便的實現:
void Build(int u, int fa = 0) {
int lst = u;
Travel(i, u, T) if (v != fa)
Build(v, u), DT.Add(lst, node, 0), DT.Add(node, v, w), lst = node;
}
什么意思呢?就是每次在上一次后面接一個新點就行了,好像點數還比較優秀。
然后這樣處理合並問題就十分的方便。
但是二叉化后有一點不好,就是不能保證原圖上的兩個點之間的路徑一定會在新圖中出現,似乎有兩種解決方案?
- 我們發現只會有 \(lca\) 不在這條路徑上,特判掉。
- 把點的信息拆到新建的虛點上,但是好像只能把 \(\min, \max\) 拆上來,對於求和的似乎不好拆?
如果有大佬會比較通用的解決方法,歡迎 \(D\) 我。
一些小技巧
- 由於邊分的那條邊不太好處理,我們可以一邊給 \(w\) 的權值,另外一邊為 \(0\) 。
- 然后我們可以把兩邊定向 \(0/1\) ,可以唯一確定一個分治塊所在的位置,也就是邊分樹的結構(邊分樹是一個深度為 \(\mathcal O(\log n)\) 的二叉樹)。
一些注意事項
- 注意傳下去的 \(sz\) 這時候傳錯的話復雜度就真錯了,一邊是 \(sz_x\) 另外一遍是 \(tot - sz_x\) 。
- 二叉化后的點數會乘個常數,不要少開空間。
例題
BZOJ2870 最長道路
題意
一棵 \(n\) 個點的樹,求一條路徑使得路徑上點權最小值 \(\times\) 路徑長度最大。
題解
有很多高妙的做法,為了練板子寫個邊分治啦。。。
考慮二叉化的時候新點的權值,其實就是 \(val_u\) 。
然后邊分治后把兩邊的邊分別按路徑上點權最小值從大到小排序,用 \(\text{two-pointers}\) 掃一下算算答案就行了。
復雜度是 \(\mathcal O(n \log^2 n)\) 的。
「CTSC2018」暴力寫掛
邊分治合並好題。
長鏈剖分
原理
類似於重鏈剖分,長鏈剖分是把向下延伸最長的那個兒子當做重兒子。
有什么好處呢?它在合並與深度有關的信息可以做到 \(\mathcal O(n)\) 啦。
每次就暴力把輕兒子的信息暴力合並到重兒子上,這樣是 \(\sum\) 重鏈長的。
一些小技巧
- 雖然是 \(\mathcal O(n)\) 的,但開空間是一個比較重要的問題,怎么做呢。我們同樣可以類似於重鏈剖分那樣按 \(dfs\) 序給每個點標號即可,或者開個
deque
也行。 - 如果開
deque
記得及時clear
。。deque
維護空間,常數不小。。
BZOJ 3653: 談笑風生
模板題,統計子樹內距 \(u\) 距離不超過 \(k\) 的權值和。
UOJ#284. 快樂游戲雞
題意
一棵 \(n\) 個點的有根樹,帶點權 \(w_i\) 。
從 \(s\) 出發,希望達到 \(t\) ,每秒可以從當前點移動到某一個兒子。
有一個死亡次數,初始為 \(0\) 。若在某個點 \(i(i \not = s, t)\) 時,死亡次數 \(\le w_i\) ,那么死亡次數自增 ,\(1\)並且立刻跳回到 \(s\) 。
給出 \(q\) 組 \(s, t\) ,求最短時間。
\(n, q \le 3 \times 10^5\)
題解
顯然每次死亡都相互獨立。
記 \(f_{i,j}\) 為當前在 \(i\) ,已死亡 \(j − 1\) 次,再死一次所需時間。若 \(s, t\) 路徑上最大點權為 \(W\) ,那么答案為 \(\sum_{i=1}^W f_{s,i} + dis(s, t)\) 。
這樣設計狀態的復雜度太大,發現 \(f\) 單調並且有大量狀態相同。
優化:\(g_{i,j}\) ,從 \(i\) 出發,前多少次死亡的消耗時間不超過 \(j\) ( \(i\) 子樹內與其距離不超過 \(j\) 的點中,最大權值)。
然后我們想一下這個如何維護,可以優先考慮序列上如何解決,我們從后往前維護對於 \(w_i\) 的一個單調遞增的單調棧,然后每次詢問的時候在棧上二分第一個 \(i\) 滿足 \(w_i \ge W\) ,動態維護一下后綴和算一下貢獻即可。
放到樹上,我們可以考慮把這個單調棧啟發式合並即可。具體來說就是把大的前小的 \(size\) 個全部拿出來,然后這些按順序逐個插到單調棧前面即可。
復雜度是 \(\mathcal O((n + q) \log n)\) 的,跑的還挺快qwq
代碼
其他的一些樹上數據結構以及算法
不想重新寫了。。看我 之前的總結 算了。。。
各種套路以及解法寫的還挺詳細的。。
決策單調性優化dp
原理
我們可以跑一個暴力 \(dp\) 觀察,或者 感性理解 證明一波決策單調性。
我們通常有有兩種解決辦法,各有利弊。
- 維護決策時間單調的一個單調棧,二分找出后者比前者優的時刻;
- 分治區間中點決策最優點,遞歸左右處理。
第一種在處理 \(f_i = \min_{j < i} (f_j + s(j, i))\) 類似的問題時候是必要的。
對於第二種如果先遞歸左區間,那么復雜度是錯誤的,但是有兩個好處:好寫;以及處理 \(s(j, i)\) 不是那么好算的函數時,可以類似與莫隊一樣計算。(例如區間相同數字對數)
一些小技巧
- 分層 \(dp\) or 求一個最優區間,通常使用第二種做法。
- 二分找時刻,相當於二分對於這兩個函數相交點。
一些注意事項
-
對於決策單調性有兩種表述形式:
- \(j < k\) 在某個時刻 \(j\) 比 \(k\) 優,那么 \(j\) 永遠比 \(k\) 優;
- \(j < k\) 在某個時刻 \(k\) 比 \(j\) 優,那么 \(k\) 永遠比 \(j\) 優。
這兩種是需要不同的做法!!后面會有兩道例題介紹為什么是不同的。
例題
[NOI2009]詩人小G
題意
把整個序列划分成很多段,每段的權值是 \((S_i - S_j - L)^P\) 最小化總權值。
題解
這是介紹的最為經典與常見的前面說的第二種情況,我們依次考慮加入,由於每次決策區間要優的話肯定是插在后面,所以直接和最后一個比一下就行了。
掛個之前寫的鏈接 qwq
[JSOI2011] 檸檬
題意
把整個序列划分成很多段,每段選擇其中一個元素 \(a\) 設其出現次數為 \(t\) ,它的權值是 \(at^2\) ,最大化總權值。
題解
我們發現這個有些不同的地方,也就是求最大化權值,顯然這是對應的第一種情況,做法是顯然不同的。
每次選擇的區間兩個端點的元素是一樣的時候不劣。然后可以我們可以對於每個元素維護一個單調棧維護決策點。
但是我們需要維護單調棧中的元素滿足,每一個元素超過上一個元素的時間也是單調的。
這個維護沒有那么直觀,我們需要一些小東西。
對於任意的 \(j1<j2<i1<i2\) ,可以發現如果 \(j1\) 超過 \(i1\) 的時間小於 \(j2\) 超過 \(i1\) 的時間,那么 \(j1\) 超過 \(i2\) 的時間也一定比 \(j2\) 超過 \(i2\) 的時間早。
設 \(beyond(x, y)\) 為 \(x\) 超過 \(y\) 的時間,這個可以二分零點求。
那么我們要保證棧中元素的時間單調,考慮棧頂的兩個元素,如果 \(beyond(stk[top - 1], stk[top]) \le beyond(stk[top], i)\) 那么意味着 \(stk[top]\) 在這之后都不如別的優秀。
然后如果 \(beyond(stk[top - 1], stk[top]) \le i\) 那么以后 \(stk[top]\) 都不如 \(stk[top - 1]\) 優,都彈掉就行了。
其實可以把之前那個單調棧倒着做,因為每次更新的區間都在最前面了,似乎是差不多的。
代碼
JOISC Day4T1 cake3
題意
有 \(n\) 塊蛋糕,每塊蛋糕有兩個權值 \(V_i\) 和 \(C_i\) 。你需要選出其中的 \(m\) 個排成一個圓排列,設排列\(p_1,p_2...p_m\)(定義 \(p_{m+1}=p1\) ),則收益為 \(\sum_{i=1}^m V_{p_i} −\sum_{i=1}^m|C_{p_i}−C_{p_i} + 1|\) 。求最大收益。
\(n,m \le 2 \times 10^5\)
題解
顯然對於選出的 \(m\) 塊蛋糕肯定從小到大排序,每次選擇一段連續的區間。
對於每個區間(區間長度 \(\ge m\) )的權值其實就是從中選出前 \(m\) 大的數然后減去 \(2(C_r - C_l)\) 。
考慮 \(l\) 從小到大,每次選擇右端點是單調不降的,用決策單調性就好啦。
似乎這個只能主席樹做到 \(\mathcal O(n \log^2 n)\) 。
注意一些坑點:答案可能為負數,主席樹到最下面節點的時候可能不選完。
CodeForces 868F Yet Another Minimization Problem
這個就是那個權值不能快速算的題啦,戳 這里 qwq
群論計數
原理
求解在置換下本質不同的方案數的時候,我們通常使用 \(\text{Burnside}\) 引理/ \(\text{Polya}\) 定理輔助計數。
\(\mathrm{Burnside}\) 引理:
\[L = \frac{1}{|G|} \sum_{i = 1}^{|G|} c_1(g_i) \]其中 \(G=\{g_1, ..., g_s\}\) ,其中 \(c_1(g_i)\) 為所有染色方案在 \(g_i\) 置換下不發生改變的數量(也就是不動點個數),\(L\) 為本質不同的方案數。
這個在特殊情況下才要用,更多時候它的過於繁瑣,我們有更方便的 \(\text{Polya}\) 定理。
\(\mathrm{Polya}\) 定理:
\[L=\frac{1}{|G|}\sum \limits _{i=1} ^{|G|} m^{c(g_i)} \]其中 \(G=\{g_1, ..., g_s\}\) ,其中 \(c(g_i)\) 為置換 \(g_i\) 的循環節個數,\(m\) 是可選擇的顏色種數,\(L\) 為本質不同的方案數。
就是上面這兩個定理啦qwq
一些小技巧
- 如果我們用 \(\text{Burnside}\) 引理,可以類似於 \(\text{Polya}\) 定理來進行計數,也就是對於每個置換,我們需要知道它的每個循環的長度,顯然每個循環內只能放一樣的元素。就可以跑 \(dp\) 或者各種奇怪計數了。
- 如果我們用 \(\text{Polya}\) 定理,那么我們只需要知道循環節的個數。對於特殊類型的置換,每個元素所在循環節的長度是一樣的,我們只需要求出循環節的長度,就可以知道循環節的個數啦。
一些注意事項
- 用這些定理的時候,首先是要滿足群的封閉性,也就是對於任意兩個群上的置換結合后的結果,仍然是群上的一個置換。
例題
[HNOI2008]Cards
題意
有 \(Sr\) 張紅牌,\(Sb\) 張藍色,\(Sg\) 張綠色,給 \(m\) 個置換構成的群(保證封閉性),求洗牌的方式。
\(\max\{Sr, Sb, Sg\} \le 20, m \le 60\)
題解
套用 \(\text{Burnside}\) 引理,我們就是要求每個置換的不動點個數,我們找出每個循環節的長度,直接用個背包計數即可。
注意,不要漏了還有 不動 這個置換。
\(\mathcal O(mS^3)\)
UVA10294 Arif in Dhaka
經典的求翻轉和旋轉的染色方案,戳俺的題解qwq
[Shoi2006]color
經典的求 \(n\) 點完全圖 \(m\) 染色非同構數問題,戳這里QAQ
[HNOI2009]圖的同構記數
題意
求有多少個本質不同的 \(n\) 個點的簡單圖。\(n \le 60\)
題解
顯然把每條邊當做可以染黑白兩種顏色,代表出現/不出現。
那么就是 \(m = 2\) 的上一題啦2333
點雙聯通分量(圓方樹)
原理
對於一些任意圖(或仙人掌)問題,我們通常可以考慮它的圓方樹。
簡單來說就是給每一個點雙新建一個方點,然后連向這個點雙中的每一個圓點(原圖上的點)。
點雙咋求? \(Tarjan\) 記錄一下 \(lowlink\) 就好啦。
特別的,把兩個點互相連通的也視作一個點雙。
這有什么好處呢?我們可以把原圖上兩個點的路徑視作圓方樹上的路徑,方點的時候特殊處理一下,圓點直接走,並且路徑上的所有圓點必然是要經過的。
這樣對於大部分的題目,我們可以方點特殊處理,圓點直接當做樹上問題解決了。
一些小技巧
- 新建的方點編號從 \(n + 1\) 開始就很好區分圓點和方點了。
一些注意事項
- 對於路徑 \(u, v\) 的 \(lca\) 是方點的時候需要特殊考慮。
- 圓方樹點數可能是 \(2\) 倍,數組記得要開大,並且后面預處理樹上信息的時候,點數不要寫成 \(n\) 。
- 圖可能不聯通,記得遍歷每個聯通分量處理。
例題
[APIO2018] Duathlon 鐵人兩項
題意
一個 \(n\) 點 \(m\) 條邊的無向圖,求有多少個三元組 \((s, c, f)\) 滿足從 \(s \to f\) 的簡單路徑經過了 \(c\) 。
題解
對於一對點 \((s, f)\) 的貢獻就是路徑可以經過上的節點個數。
將方點的權值標為點雙大小,圓點的權值標為 \(-1\) ,則某條路徑上可能經過的點數就是圓方樹上路徑點權和。
然后只需要統計每個點被多少條路徑經過,也就是 \(sz(tot - sz)\) 。
\(\mathcal O(n)\)
「SDOI2018」戰略游戲
題意
一個 \(n\) 點 \(m\) 條邊的無向圖,\(q\) 次給一個點集 \(S\) ,求有多少個點刪掉后(不能為 \(S\) 集合里的點),可以使得 \(S\) 不聯通。
題解
考慮圓方樹上哪些點可以當做點集的割點。
顯然不能是方點,一定是圓點,然后它子樹內外都至少有一個點集中的點即可。
那么每次虛樹算一下答案即可。
\(\mathcal O(n \log n)\)
代碼
【CF Round #278】Tourists
題意
一個 \(n\) 點 \(m\) 條邊的無向聯通圖,每個點有一個點權,支持動態修改點權。
\(q\) 次詢問,求從 \(u\) 到 \(v\) 的所有簡單路徑中經過的最小點權是多少。
題解
可以建出圓方樹,在每個方點上維護這個點雙中的最小點權,那么每次詢問就是查詢一個路徑最小值了。
有修改,如果直接改完圓點后改和它相鄰的方點,顯然復雜度有問題。可以卡成 \(\mathcal O(n^2)\) 。
每個方點維護的信息中不包括它的父親圓點,這樣修改圓點的時候就只需要修改它的父親方點。查詢的時候如果路徑的 \(lca\) 是方點,才算上面那個父親圓點。
我們利用 鏈剖線段樹+可刪除堆 維護一下就好了。
\(\mathcal O(n \log^2 n)\)
代碼
割點/橋/邊雙聯通分量
原理
好像這些沒啥用。。
割點咋求? \(Tarjan\) 的時候,判斷一下子樹內是否有點能伸出來即可,特判根只有一個兒子的情況。
橋咋求? \(Tarjan\) 的時候,判斷一下子樹內是否有邊能伸出即可。
邊雙咋求?把橋邊斷了,剩下的每個聯通分量就是邊雙了。
好像很簡單的樣子。。
一些小技巧
- 求邊雙似乎可以先求 \(dfs\) 樹,然后每次用並查集把路徑上的點縮起來。
一些注意事項
- 注意有重邊的時候,求橋邊需要標記邊是否經過,而不是只判父親。
例題
[HNOI2012]礦場搭建
題意
\(m\) 條邊的無向圖,計算至少需要設幾個特殊點,滿足每個點刪去后后,剩下的所有點都至少能到一個特殊點。還是求方案數。
題解
用 \(Tarjan\) 跑出割點,然后割開后對於每個聯通塊單獨考慮。(也就是每個點雙聯通分量)
如果當前沒有割點,因為放一個特殊點可能被刪掉,那么放兩個特殊點。
如果只有一個割點,那么割點刪了就怎么也出不去了,所以要設一個特殊點。
兩個以上的話,刪哪個都能跑到有出口的地方,就不用設置了。
\(\mathcal O(n + m)\)
POJ3177 Redundant Paths
題意
一個 \(n\) 點 \(m\) 條邊的無向聯通圖,問至少要加多少條邊才能使得整個圖邊雙聯通。
題解
首先把邊雙聯通分量縮一起,就成了一棵樹。
對於樹來說,為了使得每個葉子都在雙聯通分量里,那么葉子必定連邊,不難發現葉子兩兩連邊是最優的,此時答案達到了下界。
\(\mathcal O(n + m)\)
強連通分量/Two-Sat
原理
對於有向圖,我們對於強連通分量特殊處理,然后利用縮點后的 \(DAG\) 跑一些 \(dp\) 。
然后對於一類真假判定性問題,我們如果每次都是二元關系,可以利用 \(\text{Two-Sat}\) 解決問題。
怎么做?看我之前的筆記 qwq
大概就是真假狀態根據推導關系進行連邊,然后跑個縮點就好啦。
一些小技巧
- 由於 \(\text{Two-Sat}\) 我們常常要連逆否命題的邊,所以可以單獨寫個函數來把兩條邊都連上。
- 暴力建圖一般邊數較多,可以嘗試一波很多點連到一起減少邊數。
- 有時候可能一個數可能有很多個取值,但是他們存在性如果是依賴的,也可以上 \(\text{Two-Sat}\) 。
一些注意事項
- 還是和前面很多算法一樣,數組不要開小。
- 輸出方案的時候,是拓撲序較大,也就是 \(sccno\) 較小的在前。
例題
[SDOI2010] 所駝門王的寶藏
題意
有一個 \(r \times c\) 的網格,網格的 \(n\) 個格子放着寶藏,每個有寶藏的格子都有一個傳送門,傳送門是以下三種中的一種:
- “橫天門“:由該門可以傳送到同行的任一格子;
- “縱寰門”:由該門可以傳送到同列的任一格子;
- ”自閉門“:由該門可以傳送到以該門所在格子為中心周圍8格
中任一格子。
找一條路徑(起點終點任意,可以經過重復的點)經過盡量多的寶藏(重復經過只算一次)。
\(n \le 10^5, r, c \le 10^6\)
題解
從傳送門直接向可以傳送到的有寶藏的格子連邊,然后縮強連通分量,得到一個 \(DAG\) 。
答案就是 \(DAG\) 上 \(size\) 之和最大的鏈,可以簡單地拓撲 \(dp\) 得到答案。但這樣邊數是 \(\mathcal O(n^2)\) 的。
對於所有行和列額外建一個點,向這行/列的每一個有寶藏的格子連邊,然后有傳送整行/列的傳送門就可以直接向新建的點連邊了。
\(\mathcal O(r + c + n)\)
51nod 1318 最大公約數與最小公倍數方程組
一道看起來沒那么 \(\text{Two-Sat}\) 的 \(\text{Two-Sat}\) 。
原來寫的題解 QAQ。。
這啟示我們,對於多維最大最小值限制的題,也可以使用 \(\text{Two-Sat}\) 。
LOJ #6036.「雅禮集訓 2017 Day4」編碼
題意
給出 \(n\) 個 \(0/1\) 串,每個 \(0/1\) 串中至多有一個 \(?\) 。問是否存在一種在 \(?\) 處填 \(0/1\) 的方案,使得 \(n\) 個串中,不存在一個串是另一個串的前綴。
\(n, \sum |S| \le 5 \times 10^5\)
題解
不難發現是一個 \(\text{Two-Sat}\) 模型,可以直接枚舉每兩個串判斷是否沖突進行連邊,但這樣是平方的。
由於限制和前綴有關,可以想到用Trie來優化建邊。
一種做法是,\(Trie\) 上每個節點 \(u\) 記一個輔助變量 \(f_u\) ,表示這個子樹中是否有串被選擇。
把串按長度從大到小加入 \(Trie\) ,並將 \(Trie\) 可持久化,連邊比較簡單,只需保證加入的這個串與之前的串沒有沖突即可。
大概就是枚舉每個 \(?\) 的 \(0 / 1\) 取值。當到串尾的時候克隆一個新節點出來,然后之前子樹內取值都不能為 \(1\) ,然后當前的狀態向新節點的取值 \(1\) 的狀態連邊。然后每個點的狀態其實就是一個前綴狀態。
\(\mathcal O(n \log n + |S|)\)