1. 程式人生 > 實用技巧 >LeetCode 107. 二叉樹的層次遍歷 II | Python

LeetCode 107. 二叉樹的層次遍歷 II | Python

目錄


分塊思想

引用一下 oi-wiki 的話:

分塊的基本思想是:通過對原資料的適當劃分,並在劃分後的每一個塊上預處理部分資訊,從而較一般的暴力演算法取得更優的時間複雜度。

一些有意思的暴論:

分塊就是分治!

沒有什麼錯誤,考慮分治思想的抽象過程:

  1. 分解問題,對應對原資料的劃分。
  2. 解決問題,對應查詢預處理的資訊。
  3. 合併貢獻,對應將得到的資訊合併。

分塊完全與上述過程相符合,它實際上就是一種分治思想的具體應用。
特別地,對於不在完整塊中的元素,需要暴力統計它們的貢獻。

“線段樹也是分塊!” :

線段樹實質上也是通過對資料的劃分,並預處理每塊資料的資訊。
但線段樹可以多次劃分資料,直至資料規模為 \(1\),這點與分塊只劃分一次有所不同。

線段樹與分塊都是分治思想的具體應用,因此上面這句話有一定道理。
但過於絕對,於是歸到暴論裡。

“陣列也是分塊!”

可看做塊大小為 1 的分塊。
從形式上來說,完全符合分塊的定義,但分塊後並沒有降低複雜度,嚴格上說並不能叫分塊。

“int 型變數也是分塊!”

顯然,能提出這麼深刻道理的萬古神犇,實在是太厲害了!
我專程寫部落格來膜拜它!


數列分塊

引入

Loj6280. 數列分塊入門 4

給定一長度為 \(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)\)

  • 否則答案由三部分構成:

    1. \(l\) 開頭的不完整塊的和。
    2. \(r\) 結尾的不完整塊的和。
    3. 中間的完整塊的和。

    對於 1、2 兩部分,可暴力求和,複雜度上界 \(O(T)\)
    對於 3,直接查詢預處理的完整塊和即可,複雜度上界 \(O(\frac{n}{T})\)(查詢所有的完整塊)。
    單次查詢總複雜度上界 \(O(\frac{n}{T}+T)\)


修改

修改操作的過程與查詢操作相同,設修改區間為 \([l,r]\)

  • \(l,r\) 在同一塊內,直接暴力修改,並更新區間和。塊長為 \(T\),單次修改複雜度上界 \(O(T)\)

  • 否則將修改區間劃分成三部分:

    1. \(l\) 開頭的不完整塊。
    2. \(r\) 結尾的不完整塊。
    3. 中間的完整塊。

    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 個經典題目,涉及了數列分塊的大部分重要思想:

  1. 區間加,單點查詢:基礎模板。
  2. 區間加,區間查詢小於給定值 元素數:維護單調性。
  3. 區間加,區間查詢小於給定值 最大元素:維護單調性。
  4. 區間加,區間求和:基礎模板,最簡單的區間資訊合併。
  5. 區間開方,區間求和:複雜度分析。
  6. 單點插入,單點查詢:複雜度分析,塊狀陣列。
  7. 區間加,區間乘,單點查詢:較複雜的區間資訊合併。
  8. 區間賦值,查詢區間等於給定值元素數:複雜度分析。
  9. 查詢區間最小眾數:複雜區間資訊的預處理,複雜的區間資訊合併。

均值法複雜度分析

引入

在上述例題區間和中,修改操作與查詢操作的複雜度是平衡的,因此可以簡單地將塊大小設為 \(\sqrt{n}\)
但在某些數列分塊的題目中,會出現修改查詢操作複雜度並不平衡的狀況,此時需要通過計算得到最優的塊大小。

設數列長度為 \(n\),查詢次數為 \(m\),單次修改複雜度 \(O(p)\),單次查詢複雜度 \(O(q)\)\(p,q\) 的大小均與塊大小相關。
總複雜度一般可以表示成這種形式:\(O(np + mq)\)
根據高中必修 1 的均值不等式,可知 \(np + mq\ge 2\sqrt{nmpq}\),當且僅當 \(np=mq\) 時,等號成立,複雜度最低。
即可解得最優的塊大小。


確定最優塊大小

[Ynoi]舌尖上的由乃

給定一長度為 \(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)\)

這就是一種平衡結合。
根據修改與查詢次數的關係,通過調整維護的手段,使複雜度達到更低級別。

例一

bzoj3809. Gty的二逼妹子序列

給定一長度為 \(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})\) 級別,可過。

這同時也是一個平衡結合的例子,通過根號分治使時間複雜度達到平衡。


例二

bzoj3744. Gty的妹子序列

給定一長度為 \(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})\)
考慮單次查詢的過程,發現可拆分為下面幾個部分:

  1. 整塊內的逆序對數,已預處理得到,查詢複雜度 \(O(\dfrac{n}{\sqrt{n}}) = O(\sqrt{n})\)
  2. 散塊內的逆序對數,樹狀陣列暴力即可,複雜度 \(O(\sqrt{n}\log \sqrt{n})\)
  3. 整塊與整塊間的逆序對。
  4. 散塊與整塊間的逆序對。

考慮如何得到上述 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. 整塊內的逆序對數。
  2. 散塊內的逆序對數。
  3. 整塊與整塊間的逆序對。
  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 的講解

還會回來的!