1. 程式人生 > 其它 >帶修莫隊分塊

帶修莫隊分塊

帶修莫隊分塊

這篇部落格中,我已經介紹了“靜態莫隊”演算法,它可以離線解決一類靜態(不帶修改)的區間問題。

經過後人的不斷完善,出現了“帶修莫隊”,讓莫隊可以支援修改操作。

什麼是帶修莫隊?什麼是帶修莫隊?如果你想了解,什麼是帶修莫隊的話,現在就帶你研究。

Part 1 帶修莫隊原理

引入時間軸

帶修莫隊和普通莫隊的基本原理大同小異,都是排序後優化訪問順序,然後暴力。

因為要支援修改操作,帶修莫隊除了要知道查詢區間的位置之外,還要知道它在什麼時候進行查詢,以便把序列更新到這次查詢時應有的狀態。於是在詢問的結構體中多開一個變數 \(t\) ,用來記錄這次詢問之前第一個修改操作的位置。也就是說,這次查詢基於第 \(t\)

次修改後的序列(第 \(t\) 個版本)。

在排序的時候,先按照左端點所在塊由小到大排序,再按照右端點所在塊由小到大排序(可以根據左端點所在塊的編號進行奇偶性優化),再按照時間從小到大排序。

在用上一次的答案 \(Q_{i-1}\) 更新這一次答案 \(Q_i\) 的時候,像普通莫隊一樣,通過移動左右指標進行區間的增減,把上一次查詢的區間位置 \([l_{i-1},r_{i-1}]\) 轉換成 \([l_i,r_i]\) 。又因為上一次的查詢基於第 \(t_{i-1}\) 個版本,還要移動時間軸,把序列更新為第 \(t\) 個版本。具體的更新方法就是直接在序列上依次執行 \(t_{i-1}\)

\(t_i\) 之間所有的修改操作,同時更新答案。

幾何法理解帶修莫隊

還記得靜態莫隊的幾何法證明時間複雜度嗎?當時我們把每個詢問 \([l,r]\) 看成了平面內一個點 \((l,r)\) 。現在加入了時間軸 \(t\) ,就可以把一個詢問 \([l,r],t\) 看成三維空間內一個點 \((l,r,t)\) 。從原點出發,沿著座標軸走(增減、更新序列),當走到點 \((l,r,t)\) 時,得到詢問 \([l,r],t\) 的答案,一直走下去直到空間內所有點都走過,即得到所有詢問的解。

複雜度證明

帶修莫隊的複雜度比較玄學,我也不太會證,這裡寫個大概,僅供參考。

設:塊長為 \(L\)

\(c\) 為修改數,\(q\) 為詢問數,塊指代詢問(左端點)所在的塊,詢問為 \([l,r]\)

  1. 對於時間指標 \(t\) :左端點所在塊相同時,右端點所在塊單調遞增,如果右端點相同,那麼 \(t\) 遞增,此時 \(t\) 最多移動 \(c\) 次。左端點相同的詢問有 \(\frac n L\) 個,則這些詢問中右端點所在塊相同的有 \(\frac {n^2} {L^2}\) 個,總次數 \(\frac {n^2c} {L^2}\)
  2. 對於左指標 \(l\) :在左端點所在的塊內移動,移動次數不超過 \(2L\) ,總次數 \(qL\)
  3. 對於右指標 \(r\) :當左端點所在塊相同時,右端點所在塊遞增,最壞移動為 \(n\) 。一共有 \(\frac n L\) 個塊,總次數 \(\frac {n^2} L\)

故所有指標的總移動複雜度是 \(O\left( \frac {n^2c}{L^2}+qL+\frac {n^2}{L} \right)\)

但是一般的題目不會告訴你具體多少次詢問修改,所以統一用運算元 \(m\) 表示,即 \(O\left( \frac {n^2m}{L^2}+mL+\frac {n^2}{L} \right)\)

這裡我們想要莫隊跑的更快,操作空間就只有塊長 \(L\)

那麼 \(L\) 具體取多少呢......藉助一些神奇的計算軟體,我得到了這個式子:

\[L=\frac {n^2}{\sqrt[3] 3\sqrt[3]{\left(9m^3n^2+\sqrt 3\sqrt{27m^6n^4-m^3n^6}\right)}}+\frac{\sqrt[3] {\left(9m^2n^2+\sqrt 3\sqrt {27m^6n^4-m^3n^6}\right)}}{\sqrt[3]{n^2}m} \]

emmm...... 還是不要糾結塊長多少的好。視作 \(n,m\) 為同數量級,有 \(L=\sqrt[3]{n^2}\) 時取得漸進時間複雜度約為 \(O(\sqrt[3]{n^5})\)

所以在設定塊長的時候可以 len=(int)pow(n,0.6666666666);

Part 2 帶修莫隊例題

帶修莫隊我目前沒找到大量練習題目,只有這一道板子。這裡挖個坑:以後如果遇到帶修莫隊的題目要在這裡整理總結。

[國家集訓隊]數顏色

題目連結:Link

題目描述:

給你長度為 \(N\) 的序列 \(A\) ,有 \(m\) 次操作。

  1. 形如 Q L R 的指令,查詢 \([L,R]\) 之間有多少個不同的元素。
  2. 形如 R P C 的指令,表示把 \(A_P\) 修改為 \(C\)

Solution:

這題和 HH 的項鍊那題非常像,就是多了一個修改操作,別的沒了。

於是用帶修莫隊時間軸維護修改即可,注意程式碼實現與常數優化(否則你過不去這個板子)。

莫隊由於本身效率算不上高,這裡有一些卡常數小技巧:

  • 每一條語句能精簡就精簡,不要使用過多的 if-else 語句巢狀,儘量使用三目運算子替代。語句中 == 符號可以用異或 x^x 替代(這個做法我不知道有沒有用)。

    比如這一段(過載小於號運算子用來排序),上面的寫法會比下面的寫法快(儘管看上去是等價的)。

    在同樣評測環境下(C++11 標準,開啟 O2 優化,luogu 評測機),第一種寫法最大資料點僅僅執行 861ms,而第二種寫法卻會超時(執行時間大於 2700 ms)。

    inline bool operator < (const Node a,const Node b){
      return (bel[a.l]^bel[b.l]) ? bel[a.l]<bel[b.l] : ((bel[a.r]^bel[b.r]) ? ((bel[a.l]&1) ? bel[a.r]<bel[b.r]:bel[a.r]>bel[b.r]) : a.t < b.t);
    }
    /*
    inline bool operator < (const Node a,const Node b){
      if(bel[a.l]!=bel[b.l]) return bel[a.l]<bel[b.l];
      else if(bel[a.r]!=bel[a.r]){
        if(bel[a.l]&1) return bel[a.r]<bel[b.r];
        else return bel[a.r]>bel[b.r];
      }else return a.t<b.t;
    }
    */
    //即使上面那種寫法比較陰間,但是你也得硬著頭皮這麼寫!
    
  • 注意奇偶性優化,這題我一開始沒加奇偶性優化,TLE 到飛起(不過好像有人沒開奇偶優化也過了)。

  • 儘量少使用 STL 模板庫中一些實現簡單的函式,因為它會慢(但是它不像某些人所宣傳的那樣慢的駭人聽聞)。比如這一段程式碼,我用到了交換也就是 std::swap() 函式。

    #define swap(x,y) x^=y,y^=x,x^=y;
    swap(x,y);
    /*
    #include<algorithm>
    std::swap(x,y);
    */
    

    上面的 swap 是我巨集定義的,而下面是演算法庫裡自帶的。

    在同樣評測環境下(C++11 標準,開啟 O2 優化,luogu 評測機),上面的寫法最大資料點執行 861 ms,下面的寫法最大資料點執行 931 ms(雖然差不了多少,但是能快一點是一點啊)。

Code:

感覺上面敘述了一大頓也沒講明白,那就看程式碼吧...

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>

//using namespace std;

const int maxn=140005;
#define swap(x, y) x ^= y, y ^= x, x^= y
// #define int long long

template <typename _T>
inline void read(_T &x){
  x=0;int fh=1;
  char ch=getchar();
  while(!isdigit(ch)){
    if(ch=='-')
      fh=-1;
    ch=getchar();
  }
  while(isdigit(ch)){
    x=(x<<3)+(x<<1)+ch-'0';
    ch=getchar();
  }
  x*=fh;
}

int n,m,len;
int A[maxn],bel[maxn];//A存序列,表示第i個元素屬於bel[i]塊

struct Node{
  int l,r,t,org;
};

int Qnum;//詢問總數
struct Node query[maxn];
inline bool operator < (const Node a,const Node b){
  return (bel[a.l]^bel[b.l]) ? bel[a.l]<bel[b.l] : ((bel[a.r]^bel[b.r]) ? ((bel[a.l]&1) ? bel[a.r]<bel[b.r]:bel[a.r]>bel[b.r]) : a.t < b.t);
}//奇偶性優化
struct QAQ{
  int pos,val;
};

int Mnum;//修改總數
struct QAQ modify[maxn];//存修改操作

int ans,cnt[1000005];//答案和用來更新它的桶

inline void add(int i){
    ans+=!cnt[i]++;//陰間卡常操作
}

inline void del(int i){
    ans-=!--cnt[i];
}

inline void change(const int now,const int i){
  if(modify[now].pos >= query[i].l && modify[now].pos <=query[i].r)
    del(A[modify[now].pos]),add(modify[now].val);//如果修改在這段詢問區間內,那麼要更新答案
  swap(modify[now].val,A[modify[now].pos]);
  //交換值,這裡不能直接賦值,因為在之後的求解中有可能要把序列改回之前的某一個版本。
}

int ans1[maxn];

signed main(){
  #ifdef WIN32
    freopen("a.in", "r", stdin);
    freopen("a.out","w",stdout); 
  #endif
  read(n),read(m);
  len=(int)pow(n,0.6666666666);//上面已證帶修莫隊最佳塊長
  for(int i=1;i<=n;++i){
    bel[i]=i/len+1;
    read(A[i]);
  }
  for(int i=1;i<=m;++i){
    char opt[3];
    scanf("%s",opt);
    if(opt[0]=='Q'){
      ++Qnum;
      read(query[Qnum].l);
      read(query[Qnum].r);
      query[Qnum].t=Mnum;
      query[Qnum].org=Qnum;
    }else{
      ++Mnum;
      read(modify[Mnum].pos);
      read(modify[Mnum].val);
    }
  }//讀入所有操作
  std::sort(query+1,query+Qnum+1);
  for(int i=1;i<=Qnum;++i){
    for(int j=query[i-1].l;j<query[i].l;++j)
      del(A[j]);
    for(int j=query[i-1].l-1;j>=query[i].l;--j)
      add(A[j]);
    for(int j=query[i-1].r+1;j<=query[i].r;++j)
      add(A[j]);
    for(int j=query[i-1].r;j>query[i].r;--j)
      del(A[j]);
    for(int j=query[i-1].t+1;j<=query[i].t;++j)
      change(j,i);//移動時間軸,上面的都和普通莫隊無二
    for(int j=query[i-1].t;j>query[i].t;--j)
      change(j,i);
    ans1[query[i].org]=ans;
  }
  for(int i=1;i<=Qnum;++i)
    printf("%d\n",ans1[i]);
  return 0;
}
繁華盡處, 尋一靜謐山谷, 築一木製小屋, 砌一青石小路, 與你晨鐘暮鼓, 安之若素。