LeetCode 107. 二叉樹的層次遍歷 II | Python
目錄
分塊思想
引用一下 oi-wiki 的話:
分塊的基本思想是:通過對原資料的適當劃分,並在劃分後的每一個塊上預處理部分資訊,從而較一般的暴力演算法取得更優的時間複雜度。
一些有意思的暴論:
分塊就是分治!
沒有什麼錯誤,考慮分治思想的抽象過程:
- 分解問題,對應對原資料的劃分。
- 解決問題,對應查詢預處理的資訊。
- 合併貢獻,對應將得到的資訊合併。
分塊完全與上述過程相符合,它實際上就是一種分治思想的具體應用。
特別地,對於不在完整塊中的元素,需要暴力統計它們的貢獻。
“線段樹也是分塊!” :
線段樹實質上也是通過對資料的劃分,並預處理每塊資料的資訊。
但線段樹可以多次劃分資料,直至資料規模為 \(1\),這點與分塊只劃分一次有所不同。
線段樹與分塊都是分治思想的具體應用,因此上面這句話有一定道理。
但過於絕對,於是歸到暴論裡。
“陣列也是分塊!”
可看做塊大小為 1 的分塊。
從形式上來說,完全符合分塊的定義,但分塊後並沒有降低複雜度,嚴格上說並不能叫分塊。
“int 型變數也是分塊!”
顯然,能提出這麼深刻道理的萬古神犇,實在是太厲害了!
我專程寫部落格來膜拜它!
數列分塊
引入
給定一長度為 \(n\)
的數列,有 \(n\) 次操作。
操作分為兩種:區間加,查詢區間和。
\(1\le n\le 5\times 10^4\)。
劃分
首先將序列按每 \(T\) 個元素一塊進行分塊,並記錄每塊的區間和。
\(T\) 的大小需要根據實際要求選擇,下文複雜度分析部分會詳細解析。
劃分過程的常用模板:
void PrepareBlock() { block_size = T; //根據實際要求選擇一個合適的大小。 block_num = n / block_size; for (int i = 1; i <= block_num; ++ i) { //分配塊左右邊界。 L[i] = (i - 1) * block_size + 1; R[i] = i * block_size; } if (R[block_num] < n) { //最後的一個較小的塊。 ++ block_num; L[block_num] = R[block_num - 1] + 1; R[block_num] = n; } //分配元素所屬的塊編號。 for (int i = 1; i <= block_num; ++ i) { for (int j = L[i]; j <= R[i]; ++ j) { bel[j] = i; } } }
查詢
設查詢區間 \([l,r]\) 的區間和。
-
若 \(l,r\) 在同一塊內,直接暴力求和即可。塊長為 \(T\),單次查詢複雜度上界 \(O(T)\)。
-
否則答案由三部分構成:
- 以 \(l\) 開頭的不完整塊的和。
- 以 \(r\) 結尾的不完整塊的和。
- 中間的完整塊的和。
對於 1、2 兩部分,可暴力求和,複雜度上界 \(O(T)\)。
對於 3,直接查詢預處理的完整塊和即可,複雜度上界 \(O(\frac{n}{T})\)(查詢所有的完整塊)。
單次查詢總複雜度上界 \(O(\frac{n}{T}+T)\)。
修改
修改操作的過程與查詢操作相同,設修改區間為 \([l,r]\)。
-
若 \(l,r\) 在同一塊內,直接暴力修改,並更新區間和。塊長為 \(T\),單次修改複雜度上界 \(O(T)\)。
-
否則將修改區間劃分成三部分:
- 以 \(l\) 開頭的不完整塊。
- 以 \(r\) 結尾的不完整塊。
- 中間的完整塊。
1、2 直接暴力修改序列,3 給完整的塊打上區間加的標記。
單次修改複雜度上界 \(O(\frac{n}{T} +T)\)
複雜度分析
總複雜度為 \(O(n\times (\frac{n}{T} + T))\)。
由均值不等式,\(\frac{n}{T} + T\ge 2\sqrt{\frac{n}{T}\times T} = \sqrt{n}\),當且僅當 \(\frac{n}{T} = T\) 時等號成立,複雜度最低。
此時塊大小為 \(\sqrt{n}\),總複雜度 \(O(n\sqrt{n})\)。
程式碼
上古時期出土程式碼,經現代手段修復後勉強能看。
//知識點:分塊
/*
By:Luckyblock
*/
#include <cctype>
#include <cmath>
#include <cstdio>
#define ll long long
const int MARX = 5e4 + 10;
//===========================================================
int N, BlockNum, Belong[MARX], Fir[MARX], Las[MARX];
ll Number[MARX], sum[MARX], lazy[MARX]; // sum:元素和
//===========================================================
inline int read() {
int w = 0, f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Prepare() //預處理 分塊
{
N = read();
for (int i = 1; i <= N; i++) Number[i] = (ll)read();
BlockNum = sqrt(N);
for (int i = 1; i <= BlockNum; i++)
Fir[i] = (i - 1) * BlockNum + 1, Las[i] = i * BlockNum;
if (Las[BlockNum] < N)
BlockNum++, Fir[BlockNum] = Las[BlockNum - 1] + 1, Las[BlockNum] = N;
for (int i = 1; i <= BlockNum; i++)
for (int j = Fir[i]; j <= Las[i]; j++) Belong[j] = i, sum[i] += Number[j];
}
void Change(int L, int R, ll Val) //區間加
{
int Bell = Belong[L], Belr = Belong[R];
if (Bell == Belr) //不完整塊
{
for (int i = L; i <= R; i++)
Number[i] += Val, sum[Bell] += Val; //修改數列, 並修改元素和
return;
}
for (int i = Bell + 1; i <= Belr - 1; i++)
lazy[i] += Val; //完整塊 修改懶標記
for (int i = L; i <= Las[Bell]; i++) Number[i] += Val, sum[Bell] += Val;
for (int i = Fir[Belr]; i <= R; i++) Number[i] += Val, sum[Belr] += Val;
}
ll Query(int L, int R, ll mod) //區間查詢
{
int Bell = Belong[L], Belr = Belong[R], ret = 0;
if (Bell == Belr) //不完整塊
{
for (int i = L; i <= R; i++)
ret = ((ret + Number[i]) % mod + lazy[Bell]) % mod; //暴力查詢
return ret;
}
for (int i = Bell + 1; i <= Belr - 1; i++)
ret = ((ret + (Las[i] - Fir[i] + 1) * lazy[i] % mod) + sum[i]) %
mod; //完整塊
for (int i = L; i <= Las[Bell]; i++)
ret = ((ret + Number[i]) % mod + lazy[Bell]) % mod;
for (int i = Fir[Belr]; i <= R; i++)
ret = ((ret + Number[i]) % mod + lazy[Belr]) % mod;
return ret;
}
//===========================================================
int main() {
Prepare();
for (int i = 1; i <= N; i++) {
int opt = read(), L = read(), R = read(), Val = read();
if (!opt)
Change(L, R, (ll)Val);
else
printf("%lld\n", Query(L, R, (ll)Val + 1));
}
return 0;
}
練習
建議閱讀:LibreOJ 數列分塊入門 1 ~ ⑨。
包含下列 9 個經典題目,涉及了數列分塊的大部分重要思想:
- 區間加,單點查詢:基礎模板。
- 區間加,區間查詢小於給定值 元素數:維護單調性。
- 區間加,區間查詢小於給定值 最大元素:維護單調性。
- 區間加,區間求和:基礎模板,最簡單的區間資訊合併。
- 區間開方,區間求和:複雜度分析。
- 單點插入,單點查詢:複雜度分析,塊狀陣列。
- 區間加,區間乘,單點查詢:較複雜的區間資訊合併。
- 區間賦值,查詢區間等於給定值元素數:複雜度分析。
- 查詢區間最小眾數:複雜區間資訊的預處理,複雜的區間資訊合併。
均值法複雜度分析
引入
在上述例題區間和中,修改操作與查詢操作的複雜度是平衡的,因此可以簡單地將塊大小設為 \(\sqrt{n}\)。
但在某些數列分塊的題目中,會出現修改查詢操作複雜度並不平衡的狀況,此時需要通過計算得到最優的塊大小。
設數列長度為 \(n\),查詢次數為 \(m\),單次修改複雜度 \(O(p)\),單次查詢複雜度 \(O(q)\),\(p,q\) 的大小均與塊大小相關。
總複雜度一般可以表示成這種形式:\(O(np + mq)\)。
根據高中必修 1 的均值不等式,可知 \(np + mq\ge 2\sqrt{nmpq}\),當且僅當 \(np=mq\) 時,等號成立,複雜度最低。
即可解得最優的塊大小。
確定最優塊大小
給定一長度為 \(n\) 的數列,有 \(n\) 次操作。
操作分為兩種:區間加,查詢區間第 \(k\) 大。
\(1\le n,m\le 10^5\)。
先吐槽一句,這題原題是樹上的:bzoj4867. [Ynoi2017]舌尖上的由乃,為方便講解換成了序列,但樹上版本核心做法與序列上相同。
首先有個超級暴力的做法:
區間加不影響區間內大小關係,考慮維護每塊排序後的序列。
修改時整塊打標記,散塊修改後重構排序後序列。
查詢時二分答案判定,散塊暴力,整塊內二分。
設塊大小為 \(T\),單次修改複雜度為 \(O(\frac{n}{T} + T\log n)\),單次查詢複雜度為 \(O((\frac{n}{T}\log n+T)\log n)\)。
單次詢問複雜度嚴格大於修改複雜度,則總複雜度為 \(O(n(\frac{n}{T}\log^2 n +T\log n))\)。
由均值不等式,\(\frac{n}{T}\log^2 n +T\log n\ge 2\sqrt{n\log n}\log n\),當且僅當 \(\frac{n}{T}\log^2 n =T\log n\) 時等號成立,複雜度最低。
此時塊大小 \(T = \sqrt{n\log n}\),總複雜度 \(O(n\sqrt{n\log n}\log n)\)。
當然這個做法是過不去的,考慮更優秀的做法。
發現修改操作時,重構散塊不需要重新排序。
將端點所在塊分為兩部分:需要被修改的,不需要被修改的。
修改後,各部分內部分別有序,元素大小關係不變。
考慮直接歸併,單次修改複雜度變為 \(O(\frac{n}{T} + T)\)。
詢問時,考慮將兩個散塊先歸併成一個有序的數列,再進行二分。
這樣對散塊部分的查詢也可以二分,單次查詢複雜度變為 \(O(\frac{n}{T}\log^2 n + \log^2 n + T)\)。
單次詢問複雜度仍嚴格大於修改複雜度,則總複雜度為 \(O(n(\frac{n}{T}\log^2 n +T))\)。
由均值不等式,\(\frac{n}{T}\log^2 n +T\ge 2\sqrt{n}\log n\),當且僅當 \(\frac{n}{T}\log^2 n =T\) 時等號成立,複雜度最低。
此時塊大小 \(T = \sqrt{n}\log n\),總複雜度 \(O(n\sqrt{n}\log n)\)。
然後卡億點常就可以過啦!
莫隊的複雜度
設序列長度為 \(n\),詢問次數為 \(m\),假設可以 \(O(1)\) 地處理端點的移動。
考慮莫隊演算法中對詢問的排序過程:
先按照左端點的塊編號升序排序,左端點在同一塊中的按右端點升序排序。
設塊大小為 \(T\),排序後將詢問分為了 \(\frac{n}{T}\) 塊,每塊內右端點單調遞增。
考慮每一塊的右端點單調遞增,移動的複雜度為 \(O(n)\)。
右端點的垮塊移動量上界為 \(O(n)\),則右端點移動的總複雜度為 \(O(\frac{n^2}{T})\)。
對於塊內的每一次詢問,左端點移動量 \(\le T\)。
垮塊移動左端點的改變數為 \(T\),則左端點移動的總複雜度為 \(O(mT)\)。
演算法的總複雜度為 \(O(\frac{n^2}{T}+mT)\),由均值不等式,\(\frac{n^2}{T}+mT\ge 2\sqrt{n^2m}\),當且僅當 \(\frac{n^2}{T}=mT\) 時,複雜度最低。
解得最優塊大小為 \(\frac{n}{\sqrt{m}}\),演算法總複雜度為 \(O(n\sqrt{m})\)。
平衡結合
引入
考慮這樣一個問題:維護數列,支援單點修改,區間求和。
顯然可以線段樹,樹狀陣列求解,它們的複雜度是平衡的,修改查詢均為 \(O(\log n)\) 級別。
可以在修改,查詢次數同級時獲得較優的時間複雜度。
但修改與查詢次數可能是不平衡的,這時可以通過別的手段來處理該問題:
考慮分塊解法,修改 \(O(1)\),查詢 \(O(\sqrt{n})\) ,在修改數大於查詢數時,可以獲得更優的時間複雜度。
若查詢次數遠大於修改次數,甚至可以考慮直接維護字首和,修改 \(O(n)\),但查詢的複雜度僅為 \(O(1)\)。
這就是一種平衡結合。
根據修改與查詢次數的關係,通過調整維護的手段,使複雜度達到更低級別。
例一
給定一長度為 \(n\) 的數列 \(a\),\(m\) 次詢問。
每次詢問給定引數 \(l,r,a,b\),求區間 \([l,r]\) 內權值 \(\in [a,b]\) 的數的種類數。
\(1\le a_i\le n\le 10^5\),\(1\le m\le 10^6\)。
發現莫隊比較便於維護種類數,套一個莫隊消去區間的限制。
考慮值域的限制,權值線段樹進行維護。
單次 修改/查詢 複雜度均為 \(O(\log n)\)。
設塊大小為 \(\dfrac{n}{\sqrt{m}}\),總複雜度為 \(O(n\sqrt{m}\log n + m\log n)\)。
發現修改和查詢的次數並不平均,而線段樹查詢修改的複雜度是平均的。
考慮能否替換成另一種 修改複雜度較小,查詢複雜度較大的資料結構。
想到直接使用陣列進行O1修改On查詢
只有單點修改,考慮對值域分塊,維護塊內不同的數的個數,可在莫隊左右端點移動順便維護。
查詢時,在查詢值域內的完整塊直接統計答案,不完整塊暴力查詢。
設塊大小為 \(\dfrac{n}{\sqrt{m}}\),單次修改複雜度 \(O(1)\),查詢複雜度 \(\sqrt{n}\),總複雜度為 \(O(n\sqrt{m} +m\sqrt{n})\)。
程式碼
//知識點:莫隊,分塊
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e5 + 10;
const int kMaxm = 1e6 + 10;
const int kMaxSqrtn = 320 + 10;
//=============================================================
struct Que {
int l, r, a, b, id;
} q[kMaxm];
int n, m, a[kMaxn];
int block_size, block_num, L[kMaxn], R[kMaxn], bel[kMaxn];
int nowl = 1, nowr, cnt[kMaxn], sum[kMaxSqrtn];
int ans[kMaxm];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void GetMin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
bool CompareQuery(Que fir, Que sec) {
if (bel[fir.l] != bel[sec.l]) return bel[fir.l] < bel[sec.l];
return fir.r < sec.r;
}
void Prepare() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
for (int i = 1; i <= m; ++ i) {
q[i] = (Que) {read(), read(), read(), read(), i};
}
block_size = (int) sqrt(n), block_num = n / block_size;
for (int i = 1; i <= block_num; ++ i) {
L[i] = (i - 1) * block_size + 1;
R[i] = i * block_size;
}
if (R[block_num] < n) {
L[++ block_num] = R[block_num - 1] + 1;
R[block_num] = n;
}
for (int j = 1; j <= block_num; ++ j) {
for (int i = L[j]; i <= R[j]; ++ i) {
bel[i] = j;
}
}
std :: sort(q + 1, q + m + 1, CompareQuery);
}
int Query(int l_, int r_) {
int ret = 0;
if (bel[l_] == bel[r_]) {
for (int i = l_; i <= r_; ++ i) ret += (cnt[i] > 0);
return ret;
}
for (int i = bel[l_] + 1; i <= bel[r_] - 1; ++ i) {
ret += sum[i];
}
for (int i = l_; i <= R[bel[l_]]; ++ i) ret += (cnt[i] > 0);
for (int i = L[bel[r_]]; i <= r_; ++ i) ret += (cnt[i] > 0);
return ret;
}
void Delete(int now_) {
cnt[a[now_]] --;
if (! cnt[a[now_]]) sum[bel[a[now_]]] --;
}
void Add(int now_) {
if (! cnt[a[now_]]) sum[bel[a[now_]]] ++;
cnt[a[now_]] ++;
}
//=============================================================
int main() {
Prepare();
for (int i = 1; i <= m; ++ i) {
int l = q[i].l, r = q[i].r, a = q[i].a, b = q[i].b, id = q[i].id;
while (nowl < l) Delete(nowl), nowl ++;
while (nowl > l) nowl --, Add(nowl);
while (nowr > r) Delete(nowr), nowr --;
while (nowr < r) nowr ++, Add(nowr);
ans[q[i].id] = Query(a, b);
}
for (int i = 1; i <= m; ++ i) printf("%d\n", ans[i]);
return 0;
}
例二
給定一長度為 \(n\) 的數列 \(a\),\(q\) 次詢問。
每次詢問給定引數 \(l,r,x,y\),求:\[\sum_{i=l}^{r} [a_i \bmod x = y] \]
\(1\le n,q,a_i\le 5\times 10^4\)。
先咕著:
我們不妨考慮如何離線。
可以使用莫隊+線段樹做到 O(n√n log n),非常菜雞。
可以發現我們只有n次詢問卻有n√n次插入刪除,如果我們使用線段樹是很虧的。
考慮平衡複雜度:
kth是有辦法O(1)-O(√n)的,具體做法是維護值域分塊,以及一個桶。
值域分塊就是把值域做分塊,維護每一塊有多少個數。
那麼可以用√n的時間確定答案在哪一塊,然後暴力掃這個塊,通過桶定位答案。
程式碼見這個金牌爺的部落格:WerKeyTom_FTD。
預處理之王
引入
字面意思,通過預處理,合併完整塊的資訊,從而避免在查詢時對資訊的合併,以降低時間複雜度。
因為有些預處理手段十分神仙,處理物件複雜,處理方式神奇,而且為了總的時間複雜度不擇手段。
所以把這種處理手段稱為 預處理之王。
例一
簡單的預處理
「hackerrank」Range Modular Queries
給定一長度為 \(n\) 的數列 \(a\),\(q\) 次詢問。
每次詢問給定引數 \(l,r,x,y\),求:\[\sum_{i=l}^{r} [a_i \bmod x = y] \]
\(1\le n,q,a_i\le 5\times 10^4\)。
資料範圍比較喜人,\(n,q,a_i\) 同級,先考慮暴力。
考慮莫隊搞掉區間限制,維護當前區間內各權值出現的次數。
設 \(cnt_{i}\) 表示當前區間內權值 \(i\) 出現的次數,顯然答案為:
\[\sum_{k = 1}cnt_{y+kx} \]
複雜度 \(O(\text{Unknowen})\),過不了。
發現上述的演算法查詢複雜度與 \(x\) 有關。
當 \(x\) 較小時,查詢複雜度較高,可達到 \(O(n)\) 級別。\(x\) 較大時複雜度較優秀。
考慮根號分治,對模數 \(x\le 200\) 和 \(x > 200\) 的詢問分開考慮。
對於 \(x\le 200\) 的詢問,考慮分塊。
設塊大小為 \(\sqrt{n}\),預處理 \(f_{i,j,k}\) 表示前 i 個塊中,% j = k 的數的個數。
預處理複雜度上界 \(O(200^3) \approx O(n\sqrt{n})\)。
詢問時整塊直接 \(O(1)\) 查詢預處理的字首和,散塊暴力。
單次查詢複雜度 \(O(\sqrt{n})\)。
對於 \(x> 200\) 的詢問,套用上述莫隊演算法即可。
單次查詢複雜度上界為 \(O(200) \approx O(\sqrt{n})\) 級別。
\(n,q\) 同級,總複雜度約為 \(O(n\sqrt{n})\) 級別,可過。
這同時也是一個平衡結合的例子,通過根號分治使時間複雜度達到平衡。
例二
給定一長度為 \(n\) 的數列 \(a\),\(m\) 次查詢操作。
每次查詢給定引數 \(l,r\),求區間 \([l,r]\) 內的逆序對數。
強制線上。
\(1\le n,m\le 5\times 10^4\),\(1\le a_i\le 2^{31}-1\)。
不強制線上是 CDQ 傻逼題。
強制線上,沒有什麼方便的資料結構維護,考慮分塊。
這是一開始的想法:
按塊大小為 \(\sqrt{n}\) 分塊,樹狀陣列暴力預處理每塊的逆序對數,複雜度 \(O(\sqrt {n}\sqrt {n}\log \sqrt{n}) = O(n\log \sqrt{n})\)。
考慮單次查詢的過程,發現可拆分為下面幾個部分:
- 整塊內的逆序對數,已預處理得到,查詢複雜度 \(O(\dfrac{n}{\sqrt{n}}) = O(\sqrt{n})\)。
- 散塊內的逆序對數,樹狀陣列暴力即可,複雜度 \(O(\sqrt{n}\log \sqrt{n})\)。
- 整塊與整塊間的逆序對。
- 散塊與整塊間的逆序對。
考慮如何得到上述 3,4 兩項的貢獻。
發現預處理整塊之間的逆序對數時,需要遍歷整個序列。
太浪費了!考慮順便預處理第 3 項。
設 \(f_{l,r}\) 表示整塊 \(l\sim r\) 的逆序對數。
對每個塊都維護一個權值樹狀陣列,在列舉到塊 \(j\) 的某元素時,列舉之前塊的樹狀陣列。
設列舉到塊 \(i\),查詢比該元素大的數的個數,即為當前元素 與 塊 \(i\) 的逆序對數,累加到 \(f_{j,i}\) 中。
這樣做完後,\(f_{i,j}\) 存的只是塊 \(i\) 和塊 \(j\) 的逆序對數,DP 處理 \(f\),有轉移:
\[f_{i,j} = f_{i,j} + f_{i,j-1} + \sum_{k=i+1}^{j}{f_{k,j}} \]
需要列舉每塊的每一個數再列舉所有之前的塊再在樹狀陣列中查詢。
總複雜度為 \(O(\sqrt{n}\sqrt{n}\sqrt{n}\log\sqrt{n}) = O(n\sqrt{n}\log\sqrt{n})\)。
是不是很扯淡
考慮第 4 項,由於散塊較小,考慮直接列舉散塊元素。
對於右側散塊,即查詢整塊中比它大的數的個數。
考慮主席樹維護,左側散塊同理,查詢比它小的數的個數即可。
複雜度 \(O(\sqrt{n}\log n)\)。
累計一下,總複雜度為 \(O(n\sqrt{n}\log\sqrt{n} +m\sqrt{n}\log n)\),\(n,m\) 同級,為 \(O(n\sqrt{n}\log n)\) 級別,瓶頸似乎在主席樹上。
細節比較多,調調調過了幾組手玩的資料,交上去 T 了。
常數過大被卡了。
發現預處理並不是複雜度瓶頸。
且得到的資訊並不豐富,僅求出了連續的塊的逆序對,dp 陣列的大小僅為 \(O(\sqrt{n}\sqrt{n})=
O(n)\) 級別。
而預處理過程中列舉到了所有元素,且求出了它與之前所有塊的逆序對數,卻把資訊壓縮成這樣,這顯然不大合適。
考慮修改預處理物件,設 \(f_{l,r}\) 表示,從塊 \(l\) 的開頭,到 位置 \(r\) 的逆序對數。
預處理時列舉所有塊,從它的開頭,一直掃到整個數列的尾部,暴力用樹狀陣列求逆序對。
複雜度 \(O(n\sqrt{n}\log n)\)。
再考慮單次查詢時的 4 部分:
- 整塊內的逆序對數。
- 散塊內的逆序對數。
- 整塊與整塊間的逆序對。
- 散塊與整塊間的逆序對。
查詢時,可直接得到第一個整塊到查詢區間右端點的逆序對數,第 1,3 項複雜度變為 \(O(1)\),且同時得到了右側散塊與它自身,與整塊的逆序對數。
發現僅剩左側散塊與它自身,與整塊的逆序對了。
考慮合併 2,4 兩項,直接用主席樹求。
列舉左側散塊中所有數,查詢從 該數到右端點 內,比它大的數的個數。
複雜度 \(O(\sqrt{n}\log n)\)。
總複雜度不變,仍為 \(O(n\sqrt{n}\log n)\),但通過增大預處理的複雜度,減小了查詢的巨大常數。
然後就可過了。
還有 \(O(n\sqrt n)\) 的預處理之王做法,通過歸併來進行實現。
感覺比較神,建議閱讀題解學習,之後再補。
程式碼
//知識點:分塊
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxm = 5e5;
const int kMaxn = 5e5 + 10;
const int kMaxSqrtn = 230 + 10;
//=============================================================
int n, m, block_size, block_num;
int root[kMaxn];
int maxa, map[kMaxn], a[kMaxn], data[kMaxn];
int bel[kMaxn], L[kMaxSqrtn], R[kMaxSqrtn], f[kMaxSqrtn][kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void GetMin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
//ScientificConceptOfDevelopmentTree
namespace HjtTree{
#define ls (lson[now_])
#define rs (rson[now_])
#define mid (L_+R_>>1)
int node_num, lson[kMaxn << 4], rson[kMaxn << 4], size[kMaxn << 4];
void Insert(int &now_, int pre_, int L_, int R_, int val_) {
now_ = ++ node_num;
size[now_] = size[pre_] + 1;
ls = lson[pre_], rs = rson[pre_];
if (L_ == R_) return ;
if (val_ <= mid) Insert(ls, lson[pre_], L_, mid, val_);
else Insert(rs, rson[pre_], mid + 1, R_, val_);
}
int Query(int r_, int l_, int L_, int R_, int ql_, int qr_) {
if (ql_ > qr_) return 0;
if (ql_ <= L_ && R_ <= qr_) return size[r_] - size[l_];
int ret = 0;
if (ql_ <= mid) ret += Query(lson[r_], lson[l_], L_, mid, ql_, qr_);
if (qr_ > mid) ret += Query(rson[r_], rson[l_], mid + 1, R_, ql_, qr_);
return ret;
}
}
struct TreeArray {
#define lowbit(x) (x&-x)
int time, ti[kMaxm + 10], t[kMaxm + 10];
void Clear () {
time ++;
}
void Add(int pos_, int val_) {
for (; pos_ <= maxa; pos_ += lowbit(pos_)) {
if (ti[pos_] != time) {
t[pos_] = 0;
ti[pos_] = time;
}
t[pos_] += val_;
}
}
int Sum(int pos_) {
int ret = 0;
for (; pos_; pos_ -= lowbit(pos_)) {
if (ti[pos_] != time) {
t[pos_] = 0;
ti[pos_] = time;
}
ret += t[pos_];
}
return ret;
}
} tmp;
void Prepare() {
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = data[i] = read();
}
data[0] = - 0x3f3f3f3f;
std :: sort(data + 1, data + n + 1);
for (int i = 1; i <= n; ++ i) {
if (data[i] != data[i - 1]) maxa ++;
data[maxa] = data[i];
}
for (int i = 1; i <= n; ++ i) {
int ori = a[i];
a[i] = std :: lower_bound(data + 1, data + maxa + 1, ori) - data;
map[a[i]] = ori;
}
}
void PrepareBlock() {
int i = 1;
block_size = (int) sqrt(n);
block_num = n / block_size;
for (i = 1; i <= block_num; ++ i) {
L[i] = (i - 1) * block_size + 1;
R[i] = i * block_size;
}
if (R[block_num] < n) {
L[++ block_num] = R[block_num - 1] + 1;
R[block_num] = n;
}
for (int j = 1; j <= block_num; ++ j) {
for (int i = L[j]; i <= R[j]; ++ i) {
bel[i] = j;
}
}
for (int l = 1; l <= block_num; ++ l) {
tmp.Clear();
for (int r = L[l]; r <= n; ++ r) {
f[l][r] = f[l][r - 1] + tmp.Sum(maxa) - tmp.Sum(a[r]);
tmp.Add(a[r], 1);
}
}
}
//=============================================================
int main() {
Prepare();
PrepareBlock();
for (int i = 1; i <= n; ++ i) {
HjtTree :: Insert(root[i], root[i - 1], 1, maxa, a[i]);
}
m = read();
int lastans = 0;
while (m --) {
int l = read() ^ lastans, r = read() ^ lastans;
lastans = 0;
if (l > r) {
printf("0\n");
continue ;
}
if (bel[l] == bel[r]) {
tmp.Clear();
for (int i = l; i <= r; ++ i) {
lastans += tmp.Sum(maxa) - tmp.Sum(a[i]);
tmp.Add(a[i], 1);
}
printf("%d\n", lastans);
continue ;
}
lastans = f[bel[l] + 1][r];
for (int i = l; i <= R[bel[l]]; ++ i) {
lastans += HjtTree :: Query(root[r], root[i], 1, maxa, 1, a[i] - 1);
}
printf("%d\n", lastans);
}
return 0;
}
寫在最後
參考資料:
分塊思想 - OI Wiki
學長 zbq 的講解
還會回來的!