CHANGE LOG
- 2022.2.14:重構莫隊部分。
- 2022.2.15:重構根號分治部分。
1. 根號分治
1.1 算法簡介
根號分治本質上是一種 按規模大小分類討論 的思想而非分治算法。對於規模為 \(x\) 的問題,如果我們能在 \(\mathcal{O}(x)\) 和 \(\mathcal{O}(\frac n x)\) 的時間內解決,可以考慮根號分治:\(x\leq \sqrt n\) 時使用 \(\mathcal{O}(x)\) 的算法,\(x > \sqrt n\) 時使用 \(\mathcal{O}(\frac n x)\) 的算法。這相當於尋找 \(x\) 和 \(\dfrac n x\) 的較小值的最大值:顯然,當 \(x = \sqrt n\) 時,\(\min\left(x, \dfrac n x\right)\) 取到最大值 \(\sqrt n\)。因此該算法的時間復雜度即 \(\mathcal{O}(q\sqrt n)\),其中 \(q\) 是詢問組數。
更一般的,如果有若干算法 \(f_i\),在解決規模為 \(x\) 的問題時時間復雜度為 \(f_i(x)\),那么通過分類討論,我們可以在 \(\min f_i(x)\) 的時間內解決規模為 \(x\) 的問題。總時間復雜度即 \(\mathcal{O}(q \max_x (\min_i f_i(x)))\)。
通常,問題規模較小時,我們通過預處理所有問題的答案做到均攤 \(\mathcal{O}(x)\)。因此,根號分治也可以看做在 預處理 和 詢問 的復雜度之間尋找平衡的一種思想。
- Trick:根號分治進入較大的分支調不出來時,試試將塊大小設為 \(1\)。
1.2 例題
I. CF797E Array Queries
注意到若 \(k > \sqrt n\),答案必定不大於 \(\sqrt n\),對於所有位置預處理出所有 \(k\leq \sqrt n\) 的答案,若 \(k>\sqrt n\) 直接暴力查詢即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。代碼。
*II. CF1039D You Are Given a Tree
注意到若 \(k > \sqrt n\),答案必定不大於 \(\sqrt n\)。對於 \(1\leq k\leq \sqrt n\),直接暴力樹形 DP。然后再枚舉 \(1\leq ans\leq \sqrt n\),不過枚舉的是 鏈的條數,即答案。顯然答案單調不升,因此二分出答案為 \(ans\) 的 \(k\) 的區間即可。
樹形 DP 求鏈上經過的點的個數為 \(k\) 時的答案:分兩種情況討論。記 \(mx_1,mx_2\) 為 \(i\) 的兒子所傳入的最長的兩條鏈,若 \(mx_1+mx_2+1\geq k\),將 \(i\) 與它的兩個兒子配成一條鏈更優,答案加 \(1\);否則將 \(mx+1\) 傳上去到其父節點即可。時間復雜度線性。
綜上,總時間復雜度 \(\mathcal{O}(n\sqrt n \log n)\)。代碼。
卡常技巧:預處理每個節點的父親,然后將所有節點按照 dfs 序排序。這樣樹形 DP 就不需要 dfs 了。
IV. CF1580C Train Maintenance
一個非常顯然的根號分治題目。若 \(x + y \leq B\),我們可以用桶記錄其對 \(i\bmod (x + y) = d\) 的每個天數 \(i\) 的貢獻。若 \(x + y > B\) 直接差分即可。注意取消差分貢獻時下標對 \(i\) 取 \(\max\),因為作用在 \(i\) 以前的位置 \(j\) 的差分需要在 \(i\) 處更新:對差分數組位置 \(j\ (j+1<i)\) 的更新是不會在位置 \(i\) 中體現的,\(j\) 已經過時了。
時間復雜度 \(\mathcal{O} \left(\dfrac{nm}B + mB\right)\),取 \(B=\sqrt m\) 有最優復雜度 \(n\sqrt m\)。代碼。
VI. P3591 [POI2015]ODW
比較套路的根號分治題目。由於當步長 \(>B\) 時最多走 \(\dfrac n B\) 步,所以我們設置閾值 \(B\),表示若步長 \(\leq B\) 則使用預處理的信息,若步長 \(>B\) 則暴力樹上倍增計算。
預處理的信息只需要 \(v_{k,u}\) 表示 \(u\) 每次向上跳 \(k\) 步能到達的所有節點權值之和,即 \(\sum_{\\v\in \mathrm{ancestor}(u)}a_v[k\mid dep_u-dep_v]\)。可以在 \(\mathcal{O}(nB)\) 的復雜度內求得。
綜上,時間復雜度 \(\mathcal{O}\left(nB+\dfrac{n^2}{B}\log n\right)\),當 \(B\) 取 \(\sqrt{n\log n}\) 時有理論最優復雜度 \(n\sqrt{n\log n}\)。如果用長鏈剖分求樹上 \(k\) 級祖先則可做到嚴格 \(n\sqrt n\)。
由於數據原因,實際表現中取 \(B=20\) 會很快。
2. 分塊
2.1 算法簡介
分塊的本質是 暴力重構 和 懶標記 的結合。
對於序列分塊,我們會將序列分成 \(\sqrt n\) 個大小為 \(\sqrt n\) 的塊。對於區間修改,遇到整塊打標記,其余散點暴力重構其所在的塊。因為最多重構兩個塊,打根號次標記,所以單次修改的時間復雜度一般為 \(\sqrt n\)。這是分塊的基本思想,即時間復雜度能承受就重構,不能承受就打標記。
分塊的主要作用有兩個,一是 平衡復雜度,二是維護一些 \(\log\) 數據結構無法維護的信息。
- 對於區間加法,單點查詢,一般的思路是使用樹狀數組維護。此時修改和查詢的復雜度均為 \(\log n\)。分塊的優勢在於它可以讓修改和查詢當中的任意一個變為 \(\mathcal{O}(1)\),另一個變為 \(\sqrt n\)。當詢問或修改的次數非常多時,如莫隊二次離線算法中的 \(\mathcal{O}(n)\) 次區間修改,\(\mathcal{O}(n\sqrt n)\) 次單點查詢,就可以使用分塊平衡復雜度,做到非常優秀的 \(n\sqrt n\)。
- 當遇到無法 快速合並 和 快速刪除 的信息時,對於 單點 修改,區間查詢,傳統的維護半群的線段樹就失效了。但分塊仍然可以做到優秀的復雜度:單點修改直接暴力重構,區間查詢對整塊和散點都容易直接查詢。具體見例 I.
對於第二點,筆者在和機房同學(ycx)討論后獲得了更深刻的理解。普通的線段樹也可以做到維護無法快速合並和刪除的信息。
考慮將信息的合並 限制在一定層數內,這樣我們必須將詢問 下放 至該層數以下才能獲得信息。具體地,設立閾值 \(B\),當區間長度 \(\leq 2 ^ B\) 時,合並兩個子樹的信息。否則視為空節點。對於區間查詢,我們僅在區間長度 \(\leq 2 ^ B\) 的節點查詢信息。這說明即使當前節點所表示的區間被查詢區間完全包含,若其長度 \(> 2 ^ B\),說明它沒有存儲任何信息,必須向左右兩個子節點遞歸直到區間長度 \(\leq 2 ^ B\)。
分析復雜度:視合並復雜度為區間長度,查詢某區間信息的時間復雜度為 \(\mathcal{O}(1)\),則單次單點修改的復雜度為 \(1 + 2 + \cdots + 2 ^ B = \mathcal{O}(2 ^ B)\),區間查詢的復雜度為 \(\mathcal{O}\left(\dfrac n {2 ^ B}\right)\)。不難發現令 \(B = \dfrac {\log_2 n} 2\) 時復雜度最優,為 \(n\sqrt n\)。
若將分塊看成僅有 \(2\) 層的 \(\sqrt n\) 叉線段樹,則暴力重構本質上就是將信息合並的規模限制在 \(\sqrt n\) 級別,並將區間查詢下放到每個塊和散點。它是上述做法的一種非常簡便的實現。
2.2 時間軸分塊:根號重構
對時間軸分塊的思想可運用於多次修改和詢問,需要用數據結構維護,但數據結構不支持修改的情況。若修改相對 獨立,即我們能分開考慮用數據維護好的信息以及沒有被更新的修改快速得到詢問的答案,那么可設閾值 \(B\),若 “積壓” 的修改數量 \(\geq B\) 則重構數據結構,否則暴力遍歷所有沒有在數據結構上更新的修改。
時間復雜度與重構復雜度相關。若重構復雜度為線性,則時間復雜度為線性根號。
2.3 例題
I. COCI2012/2013 Contest#2 F 市場監控
題意簡述:單點加入 / 刪除直線,每個位置最多有一條直線。查詢一段區間的直線在 \(x=T\) 時的最值。保證 \(T\) 遞增。位置數 \(n\leq 10^5\),操作數 \(m\leq 3\times 10^5\),\(T\leq 10^6\)。
帶刪除和區間查詢讓李超樹沒有了用武之地,因此像這種嚴格強於某個經典問題的題目,如果想不到 \(\mathrm{polylog}\) 的做法,可以考慮分塊。
每個位置最多一條直線保證了重構直線凸包的復雜度,而 \(T\) 遞增則保證了查詢塊內直線時不需要二分。塊內插入直線時直接用 multiset 對直線斜率排序,修改(重構)一次的時間復雜度為 \(\mathcal{O}(\sqrt n+\log n)\)。總時間復雜度 \(\mathcal{O}(m \sqrt n)\)。
*II. 2019 五校聯考鎮海 小 ω 的樹
見計算幾何初步凸包部分例題。
*3. 莫隊
莫隊是優雅的暴力。
3.1 算法介紹
莫隊算法用於 離線 處理多組詢問。它支持修改,這將在帶修莫隊部分介紹。
莫隊的核心思想十分簡單:維護兩個指針 \(l, r\) 表示當前區間,並按照一定順序處理詢問,使得時間復雜度最小。如果按詢問順序伸縮區間,每次指針移動的距離可能達到 \(\mathcal{O}(n)\),無法接受。
為了讓指針移動距離盡可能少,我們可以將詢問以某個端點為關鍵字排序。盡管該端點的移動距離均攤線性,但另一個端點的移動距離無法保證。
注意到兩個端點的移動距離分別是 \(\mathcal{O}(n)\) 和 \(\mathcal{O}(nq)\)。為了平衡復雜度,自然想到使用 根號平衡 的思想。如果將左右端點的移動距離都控制在 \(n\sqrt n\) 以內,我們就得到了一個時間復雜度為 \(\mathcal{O}(n\sqrt n\times k)\) 的優秀算法,其中 \(k\) 是指針移動的復雜度。
考慮如何分塊。設塊大小為 \(B\)。我們將所有詢問離線下來,並按照左端點 塊編號 為第一關鍵字,右端點為第二關鍵字排序,按照該順序處理所有詢問的時間復雜度為 \(\mathcal{O}\left(\dfrac{nq}B + qB\right)\)。這是因為每個塊內右端點的總移動距離不超過 \(n\),每次詢問左端點的移動距離不超過 \(B\)。假設 \(n, q\) 同級,取 \(B = \sqrt n\),時間復雜度為 \(\mathcal{O}(n\sqrt n \times k)\)。
- 奇偶排序優化:如果左端點在奇塊,右端點從小到大排序,否則從大到小排序。這保證了在左端點跨塊時,右端點不需要再從最右邊掃到最左邊。其類似波浪的左右掃動可以有效減小常數。
3.2 莫隊二次離線
對於大部分題目,伸縮區間的時間復雜度為 \(\mathcal{O}(1)\)。如果無法在線性時間內伸縮區間,我們可使用莫隊二次離線優化時間復雜度。
考慮為什么無法快速伸縮區間:新增的位置對整個區間的貢獻和區間內每個數都有關,需要用數據結構維護,如 區間逆序對數。通常這樣的信息是 可減 的。因此,恰當地差分可以將貢獻的形式寫得更加整潔,從而通過再次離線求解。
接下來,我們以 P4887 為例,深入探究莫隊二離的整個過程。
設 \(f([l, r], i)\) 表示區間 \([l, r]\) 對位置 \(i\) 的貢獻,即 \(\sum\limits_{j\in [l, r]}[\mathrm{popcount}(a_j \oplus a_i) = k]\)。右端點 右移 \(r - 1\to r\) 時,新增的貢獻為 \(f([l, r - 1], r)\)。若信息滿足 可減性,\(f([l, r - 1], r)\) 可寫作 \(f([1, r - 1], r) - f([1, l - 1], r)\)。因此,假設右端點 向右 移動 \(r\to r'\ (r < r')\),產生的貢獻即
對於前半部分,對每個位置 \(i\) 預處理 \(f([1, i - 1], i)\),右端點移動時可即時計算。該部分時間復雜度 \(\mathcal{O}(n\binom {14} k + n\sqrt n)\)。
對於后半部分,可以看做一段 前綴 對一個 區間 的貢獻。設 \(g([l_1, r_1], [l_2, r_2])\) 表示 \(\sum\limits_{i \in [l_2, r_2]} f([l_1, r_1], i)\) 即 \(\sum\limits_{j\in [l_1, r_1]} \sum\limits_{i\in [l_2, r_2]}[\mathrm{popcount}(a_j\oplus a_i) = k]\),則可寫為 \(g([1, l - 1], [r + 1, r'])\)。
因為前綴數量為 \(n\),而 \([r + 1, r']\) 區間總長級別為 \(\mathcal{O}(n\sqrt n)\),故考慮將這些詢問 二次離線(莫隊本身就是一次離線)下來。具體地,我們在 \(l - 1\) 處插入區間 \([r + 1, r']\),然后用類似 掃描線 的方法,按序添加每個位置的貢獻。添加位置 \(i\) 時我們就得到了前綴 \([1, i]\) 的信息,此時回答所有被掛在該位置上的詢問區間 \([r + 1, r']\) 即可。
- 換句話說,\(g([1, l - 1], [r + 1, r'])\) 不僅等於 \(\sum\limits_{i\in [r + 1, r’]} f([1, l - 1], i)\),也可以看做 \(\sum\limits_{j\in [1, l - 1]} f(j, [r + 1, r'])\)。注意,對於本題,\(f(i, j)\) 和 \(f(j, i)\) 相等,即若 \(\mathrm{popcount}(a_j\oplus a_i) = k \iff \mathrm{popcount}(a_i\oplus a_j) = k\),因此 \(i, j\) 之間無序。但對於部分題目,如區間 逆序對 數量,\(f([1, l - 1], i)\) 相當於求 \(a_1\sim a_{l - 1}\) 當中有多少個數 大於 \(a_i\),而 \(f(i, [r + 1, r'])\) 相當於求 \(a_{r + 1} \sim a_{r'}\) 當中有多少個數 小於 \(a_i\)。此時要分清對應的大小關系。
不難發現上述做法需要進行 \(\mathcal{O}(n)\) 次修改,\(\mathcal{O}(n\sqrt n)\) 次查詢,這是因為 \(\sum r' - r\approx n\sqrt n\)。 修改的本質是將某個數加入可重集 \(S\),而查詢的本質是對於某個 \(a_i\),求數集 \(S\) 內有多少個數與 \(a_i\) 的異或和的 \(\mathrm{popcount} = k\)。
修改和查詢的數量不在同一級別,考慮平衡復雜度:設 \(f_v\) 表示數集 \(S\) 內有多少個數與 \(v\) 的異或和在二進制下 \(1\) 的個數為 \(k\)。加入 \(a_i\) 時,對於所有 \(p\) 滿足 \(\mathrm{popcount}(p\oplus a_i) = k\),令 \(f_p \gets f_p + 1\)。查詢 \(a_i\) 的答案即 \(f_{a_i}\)。通俗地說,\(f\) 本質上就是一個桶。這一部分時間復雜度為 \(\mathcal{O}(n\binom {14} k + n\sqrt n)\)。
注意,以上僅是 \(r\to r'\ (r < r')\) 的處理方法。剩下三種情況(左 / 右端點左移和左端點右移)如法炮制即可,請讀者自行推導。綜上,我們在 \(\mathcal{O}(n\binom {14} k + n\sqrt n)\) 的時間復雜度內解決了問題。代碼見例題 I.
從上述例題當中,我們可以感受到莫隊二次離線的威力:在運用莫隊的根號平衡思想基礎上,利用 信息可減性 作差,並 轉換貢獻的相對關系, 離線 將計算轉化為一段 前綴 對總長為 \(n\sqrt n\) 的區間的貢獻,從而做到 線性 次修改。再利用修改次數為線性的性質,更進一步地通過各種數據結構(通常是分塊)平衡 修改 和 查詢 的復雜度,完美解決問題。它是一個非常高妙的算法。
-
推導貢獻的過程中,注意 \(g\) 的符號。如當左端點 \(l\) 右移至 \(l'\) 時,貢獻為 \(-\sum\limits_{i \in [l, l' - 1]} f([i + 1, r], i)\),拆成 \(\left(\sum\limits_{i \in [l, l' - 1]} f([1, i], i)\right) - g([1, r], [l, l' - 1])\)。
-
注意特殊考慮一個數對它本身的貢獻:當 \(k = 0\) 時,\(f([1, i], i)\) 可由已經預處理的 \(f([1, i - 1], i)\) 加上 \(1\) 得到。
3.3 回滾莫隊
維護不具有 可減性 的信息時(如區間最大值),盡管我們可以快速擴展區間,但無法高效地 縮短 區間。考慮如何不刪除地回答詢問。這看似是不可能的,但不要忘記,即使是不可減的信息,也可以快速 撤銷。
將所有詢問按照左端點所在的塊排序,然后依次處理所有左端點落在某個塊內的詢問 \((l_i,r_i)\),需要確保這些詢問按照 \(r_i\) 從小到大 有序。類似地,我們仍然維護兩個指針,不同的是對於每個塊,初始左指針 \(l\) 指向 下一個塊的開頭,右指針 \(r=l-1\) 表示當前區間為空。對於右端點,由於其有序,我們可以直接擴展。右端點擴展完畢后,再擴展左端點直到目標位置並記錄答案。
接下來我們撤回擴展左端點時對信息的修改,這個可以在 \(\sqrt n\) 的時間內完成,因為左端點移動長度不超過 \(\sqrt n\)。撤回后再處理下一個詢問,這就是回滾莫隊。
- 回滾莫隊無法處理左右端點在同一塊的情況。此時直接暴力即可。
- 每做完一個塊,都需要將所有信息清空,並初始化左端點。
若信息可 快速刪除,但無法高效擴展,也可以使用回滾莫隊。對於每個塊,初始左端點指向當前塊開頭,右端點指向 \(n\)。容易使用類似的算法解決問題。此時我們可以處理左右端點在同一塊的情況,不需要特判。
3.4 帶修莫隊
眾所周知,莫隊是一個靜態離線算法,所以不支持修改。但我們可以在其基礎上進行加工。注意到單次修改很容易處理,所以嘗試再加入一維 修改指針。原來只有兩個參數 \(l, r\),現在加入了一個修改參數 \(k\),只需將 \(k\) 類似 \(l,r\) 一樣移動即可。
排序首先按照 \(l\) 所在塊從小到大排,然后按照 \(r\) 所在塊從小到大排,若 \(l, r\) 所在塊相同則按 \(k\) 排序。考慮塊大小 \(B\) 應該開多少。視詢問和修改總次數與 \(n\) 同級。
- \(l\) 的移動次數:\(l\) 跨塊時總移動次數為 \(n\),每兩個詢問之間 \(l\) 的移動距離為 \(B\),故總移動次數為 \(nB\)。
- \(r\) 的移動次數:\(l\) 跨塊時總移動次數為 \(nB\),\(l\) 不跨塊時總移動次數為 \(\dfrac {n ^ 2} B\)。
- \(k\) 的移動次數:對於 \(l, r\),一共有 \(\min\left(n, \dfrac{n ^ 2}{B ^ 2}\right)\) 個有效的塊,每個塊移動 \(n\) 次,故移動 \(k\) 的總復雜度為 \(\dfrac{n^3}{B^2}\)(若 \(\min\) 取到 \(n\) 則復雜度變成 \(n ^ 2\),顯然不優,因此令 \(B > \sqrt n\))。
綜上,我們要確定一個 \(B\) 使得 \(\max\left(\dfrac{n^3}{B^2},nB,\dfrac{n^2}{B}\right)\) 最小。當 \(nB = \dfrac{n ^ 3}{B ^ 2}\) 時,\(B = \sqrt[3]{n ^ 2}\),上式取到最小值。
3.5 例題
- 莫隊:IV, V, VI, VIII, IX, X.
- 莫隊二離:I, II, III, XI.
- 回滾莫隊:VII, XIII.
- 帶修莫隊:XII.
I. P4887 【模板】莫隊二次離線(第十四分塊(前體))
莫隊二離的例題。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 5;
int n, k, q, cnt, a[N], id[N], buc[N];
ll f[N], ans[N];
struct query {
int l, r, blk, id;
bool operator < (const query &v) const {
return blk != v.blk ? blk < v.blk : blk & 1 ? r < v.r : r > v.r;
}
} c[N];
struct dat {
int l, r, id;
};
vector <dat> qu[N];
int main() {
cin >> n >> q >> k;
if(k > 14) {
for(int i = 1; i <= q; i++) puts("0");
exit(0);
}
for(int i = 0; i < 1 << 14; i++)
if(__builtin_popcount(i) == k)
id[cnt++] = i;
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]), f[i] = f[i - 1];
for(int j = 0; j < cnt; j++) f[i] += buc[a[i] ^ id[j]];
buc[a[i]]++;
}
for(int i = 1; i <= q; i++) {
scanf("%d %d", &c[i].l, &c[i].r);
c[i].id = i, c[i].blk = c[i].l / 333;
}
sort(c + 1, c + q + 1);
for(int i = 1, l = 1, r = 0; i <= q; i++) {
if(r < c[i].r) {
if(l > 1) qu[l - 1].push_back({r + 1, c[i].r, -c[i].id});
ans[c[i].id] += f[c[i].r] - f[r], r = c[i].r;
}
if(l > c[i].l) {
qu[r].push_back({c[i].l, l - 1, c[i].id});
ans[c[i].id] -= f[l - 1] - f[c[i].l - 1] + (k ? 0 : l - c[i].l), l = c[i].l;
}
if(r > c[i].r) {
if(l > 1) qu[l - 1].push_back({c[i].r + 1, r, c[i].id});
ans[c[i].id] -= f[r] - f[c[i].r], r = c[i].r;
}
if(l < c[i].l) {
qu[r].push_back({l, c[i].l - 1, -c[i].id});
ans[c[i].id] += f[c[i].l - 1] - f[l - 1] + (k ? 0 : c[i].l - l), l = c[i].l;
}
}
memset(buc, 0, sizeof(buc));
for(int i = 1; i <= n; i++) {
for(int j = 0; j < cnt; j++) buc[a[i] ^ id[j]]++;
for(dat it : qu[i]) {
int id = abs(it.id), sgn = id / it.id;
for(int p = it.l; p <= it.r; p++) ans[id] += buc[a[p]] * sgn;
}
}
for(int i = 2; i <= n; i++) ans[c[i].id] += ans[c[i - 1].id];
for(int i = 1; i <= n; i++) printf("%lld\n", ans[i]);
return 0;
}
II. P5047 Yuno loves sqrt technology II
區間逆序對數也是莫隊二離的模板題。設 \(F([l_1, r_1], [l_2, r_2]) = \sum\limits_{i \in [l_1, r_1]}\sum\limits_{j\in [l_2, r_2]} [a_i > a_j]\),\(G([l_1, r_1], [l_2, r_2]) = \sum\limits_{i \in [l_1, r_1]}\sum\limits_{j\in [l_2, r_2]} [a_i < a_j]\),\(f_i = F([1, i - 1], i)\),\(g_i = G([1, i - 1], i)\)。不難發現 \(f_i\) 也等於 \(F([1, i], i)\),\(g_i\) 也等於 \(G([1, i], i)\)。
- 右端點向右擴展:\(\sum\limits_{i\in [r + 1, r']} F([l, i - 1], i)\),差分得 \(\sum\limits_{i\in [r + 1, r']} f_i - F([1, l - 1], i)\)。第二項可寫為 \(-F([1, l - 1], [r + 1,r'])\)。
- 左端點向左擴展: \(\sum\limits_{i\in [l', l - 1]} G([i + 1, r], i)\),差分得 \(\sum\limits_{i\in [l', l - 1]} G([1, r], i) - g_i\)。 第一項寫為 \(G([1, r], [l', l - 1])\)。
- 右端點向左擴展:相對於右端點向右擴展的情況,貢獻符號相反。即 \(F([1, l - 1], [r’, r - 1]) - \sum\limits_{i\in [r', r - 1]} f_i\)。
- 左端點向右擴展:同理,相對於左端點向左擴展的情況,貢獻符號相反。
綜上,在莫隊二離的過程中,我們需要維護兩個 值域分塊 數組,一個為了查詢 \(F\),加入 \(a_i\) 時將 \(1\sim a_i - 1\) 加 \(1\)(查詢 \(S\) 內有多少個數比它大,那么在加入一個數的時候將比它小的數的值 \(+1\)),另一個為了查詢 \(G\),加入 \(a_i\) 時將 \(a_i+1\sim n\) 加 \(1\)。不要忘記離散化。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
*III. P5501 [LnOI2019]來者不拒,去者不追
考慮右端點右移時需要求出哪些信息:\(a_{r + 1}\) 的排名以及 \([l, r]\) 比 \(a_{r + 1}\) 大的數的和,使用莫隊二離 + 值域分塊即可。時間復雜度 \((n + m) (\sqrt n + \sqrt V)\)。
IV. P4462 [CQOI2018]異或序列
一道莫隊裸題。對 \(a\) 求異或前綴和,根據 \(a \oplus b = k \iff a \oplus k = b\),記錄每個數的出現次數即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
V. CF617E XOR and Favorite Number
雙倍經驗。
VI. P4396 [AHOI2013]作業
莫隊 + 值域分塊,時間復雜度線性根號。也可以三維偏序做到線性對數平方。
VII. P5906 【模板】回滾莫隊
回滾莫隊的模板題。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
const int B = 450;
struct dat {
int mx, mn, val;
} stc[B + 5];
struct query {
int l, r, blk, id;
bool operator < (const query &v) const {
return blk != v.blk ? blk < v.blk : r < v.r;
}
} c[N];
int n, m, q, top, a[N], d[N];
int pre[N], suf[N], ans[N];
int add(int x, int tp) {
if(tp) stc[++top] = {suf[a[x]], pre[a[x]], a[x]};
if(x > suf[a[x]]) suf[a[x]] = x;
if(x < pre[a[x]]) pre[a[x]] = x;
return max(x - pre[a[x]], suf[a[x]] - x);
}
void Rollback() {
while(top) {
pre[stc[top].val] = stc[top].mn;
suf[stc[top].val] = stc[top].mx, top--;
}
}
int main() {
cin >> n, memset(pre, 0x3f, sizeof(pre));
for(int i = 1; i <= n; i++) scanf("%d", &a[i]), d[i] = a[i];
cin >> m, sort(d + 1, d + n + 1);
for(int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + n + 1, a[i]) - d;
for(int i = 1; i <= m; i++) {
int l, r; scanf("%d %d", &l, &r);
if(l / B == r / B) {
for(int j = l; j <= r; j++) ans[i] = max(ans[i], add(j, 1));
Rollback();
} else c[++q] = {l, r, l / B, i};
}
sort(c + 1, c + q + 1);
for(int i = 1, l, r, cur; i <= q; i++) {
if(i == 1 || c[i].blk != c[i - 1].blk) {
l = min(n + 1, c[i].blk * B + B), r = l - 1, cur = 0;
memset(pre, 0x3f, sizeof(pre));
memset(suf, 0, sizeof(suf));
}
while(r < c[i].r) cur = max(cur, add(++r, 0));
int tmp = cur;
while(l > c[i].l) cur = max(cur, add(--l, 1));
ans[c[i].id] = cur, cur = tmp, Rollback();
l = min(n + 1, c[i].blk * B + B);
}
for(int i = 1; i <= m; i++) printf("%d\n", ans[i]);
return 0;
}
VIII. P3709 大爺的字符串題
題意翻譯過來就是求區間眾數,使用莫隊,維護每個數的出現次數以及出現次數為 \(i\) 的數有多少個即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
IX. P3730 曼哈頓交易
仍然是莫隊裸題,求出現次數第 \(k\) 大可以分塊,將根號平衡的思想貫徹到底。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
*X. P7708「Wdsr-2.7」八雲藍自動機 Ⅰ
一道莫隊好題。本題最有價值的地方在於對單點修改的轉化,以及對交換兩個數的處理:維護原來每個位置現在的位置,以及現在每個位置原來的位置。
注意到單點修改並不方便實現,將其轉化為交換兩個數。對於 \(a_x\gets k\),我們新建 \(a_c = k\),並將其看做 \(a_x\) 與 \(a_c\) 交換。這一步非常巧妙,因為它消滅了單點修改這一類麻煩的操作。
多次詢問一段區間的操作結果,一般使用莫隊實現。因此,考慮區間在伸縮時需要維護哪些信息。為了支持在操作序列最前面加入交換兩個數的操作,可以想到維護:
- 序列 \(a\) 在操作后的形態。
- \(pos_i\) 表示 原 位置 \(i\) 的 現 位置。
- \(rev_i\) 表示 現 位置 \(i\) 的 原 位置。
- \(add_i\) 表示 現 位置 \(i\) 上的數被查詢了多少次。
- 當右端點右移 \(r - 1\to r\) 時:
- 若第 \(r\) 個操作是交換 \(x, y\),則交換 \(a_x\) 和 \(a_y\),\(rev_x\) 和 \(rev_y\),\(pos_{rev_x}\) 和 \(pos_{rev_y}\)。
- 若第 \(r\) 個操作是查詢 \(x\),則令 \(ans\gets ans + a_x\),\(add_x\gets add_x + 1\)。
- 當左端點左移 \(l+1\to l\) 時:
- 若第 \(l\) 個操作是交換 \(x,y\),注意我們相當於 交換原位置 上的兩個數,因此對答案有影響。先交換 \(a_{pos_x}\) 和 \(a_{pos_y}\),\(rev_{pos_x}\) 和 \(rev_{pos_y}\),\(pos_x\) 和 \(pos_y\)。由於交換原位置上的兩個數並不影響現位置被查詢的數的次數(因為我們已經交換了 \(a_{pos_x}\) 和 \(a_{pos_y}\),或者說 \(a\) 和 \(add\) 當中只要交換一個即可描述本次操作,多交換反而會讓答案錯誤),因此答案加上 交換后 的 \((a_{pos_x} - a_{pos_y})(add_{pos_x} - add_{pos_y})\),相當於把每個數原來的貢獻減掉,加上新的貢獻。
- 若第 \(l\) 個操作是查詢 \(x\),則令 \(ans\gets ans + a_{pos_x}\),\(add_{pos_x} \gets add_{pos_x} + 1\)。
右端點左移和左端點右移的情況分別與上述兩種情況相似,僅是符號相反,此處不再贅述。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。
XI. P3604 美好的每一天
一些字符重排后能形成回文串當且僅當出現奇數次的字符不多於 \(1\) 個,所以我們只需要知道一段區間所有字符出現次數的奇偶性,不難想到狀壓 + 開桶記錄。時間復雜度 \(\mathcal{O}(|\Sigma|n\sqrt n)\),空間復雜度 \(2^{|\Sigma|}\),需要使用 unsigned short 壓縮空間。
本題可以莫隊二離去掉時間復雜度當中的的字符集因子。試着實現了一下,直接跑到了最優解(2022.2.15)。
XII. P1903 [國家集訓隊]數顏色 / 維護隊列
帶修莫隊模板題。時間復雜度 \(\sqrt[3]{n^5}\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, m, B, a[N], cnt, q, pos[N], col[N], buc[N], cur, ans[N];
void add(int x) {cur += !buc[x], buc[x]++;}
void del(int x) {buc[x]--, cur -= !buc[x];}
struct query {
int l, r, k, blkl, blkr, id;
bool operator < (const query &v) const {
if(blkl != v.blkl) return blkl < v.blkl;
if(blkr != v.blkr) return blkl & 1 ? blkr > v.blkr : blkr < v.blkr;
return blkr & 1 ? k > v.k : k < v.k;
}
} c[N];
int main() {
cin >> n >> m, B = pow(n, 0.67);
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= m; i++) {
char s; int l, r;
cin >> s >> l >> r;
if(s == 'Q') c[++q] = {l, r, cnt, l / B, r / B, q};
else pos[++cnt] = l, col[cnt] = r;
}
sort(c + 1, c + q + 1);
for(int i = 1, l = 1, r = 0, k = 0; i <= n; i++) {
while(r < c[i].r) add(a[++r]);
while(l > c[i].l) add(a[--l]);
while(r > c[i].r) del(a[r--]);
while(l < c[i].l) del(a[l++]);
while(k < c[i].k) {
k++;
if(l <= pos[k] && pos[k] <= r) del(a[pos[k]]);
swap(col[k], a[pos[k]]);
if(l <= pos[k] && pos[k] <= r) add(a[pos[k]]);
}
while(k > c[i].k) {
if(l <= pos[k] && pos[k] <= r) del(a[pos[k]]);
swap(col[k], a[pos[k]]);
if(l <= pos[k] && pos[k] <= r) add(a[pos[k]]);
k--;
}
ans[c[i].id] = cur;
}
for(int i = 1; i <= q; i++) cout << ans[i] << "\n";
return 0;
}
XIII. P8078 [WC2022] 禿子酋長
考慮用鏈表維護每個值的前驅和后繼在原序列中的位置。由於在鏈表中插入一個數時,至少也需要 \(\log\) 的時間查詢前驅后繼,所以使用不需要修改的回滾莫隊即可。時間復雜度 \(\mathcal{O}(n\sqrt n)\)。