1. 程式人生 > 其它 >莫隊套值域分塊

莫隊套值域分塊

莫隊套值域分塊

在經典的“靜態區間第 \(k\) 小”問題中,我們已經知道有可持久化線段樹(主席樹)做法和線段樹套平衡樹等做法。假設現在要求支援單點修改,變成“動態區間第 \(k\) 小”問題,線段樹套平衡樹仍然可做,主席樹則需要在外側套一個樹狀陣列。這樣總複雜度為 \(O(Nlog^2N)\)

但是眾所周知,樹套樹死難寫,一不小心就會造成“想題 5 分鐘,寫掛 2 小時”的慘案。今天介紹一種更簡單的實現“動態區間 \(k\) 小”的做法——莫隊套值域分塊,也就是“塊套塊”。

Part 1 值域分塊

值域分塊,顧名思義是對序列 \(A\) 的值域 \((A_i\in [1,M])\) 進行分塊。主要用途是維護一堆數,支援 \(O(1)\)

插入、刪除,以及 \(O(\sqrt N)\) 複雜度實現查詢前驅、後繼、\(k\) 小、\(x\) 的排名(類似於平衡樹)。

問題引入

在介紹值域分塊之前,先思考這樣一個問題:

  • 如果讓你用一個桶來維護一列正整數數中的第 \(k\) 小,你會怎麼做?

一個顯然的做法是把所有的數離散化後扔進桶裡(這相當於桶排序),每次從 1(值域下界)開始列舉到值域上界 \(M\),找到出現的第 \(k\) 個數是幾。假設有 \(N\) 次詢問,那麼這個演算法的時間複雜度是 \(O(NM)\) 的,並不優秀。如果我們使用分塊來維護這個桶,就可以把時間複雜度降低到 \(O(NT)\) ,其中 \(T\) 為塊長。而這,就是所謂“值域分塊”。

操作介紹

假設一個無序數列 \(A\) ,其中 \(A_i\in [1,M]\) (需離散化)。

  • 預處理

    對區間 \([1,M]\) 分塊處理,設塊長 \(s=T\) 。設桶 \(cnt[x]\) 表示數 \(x\) 出現的次數,另外塊內再維護一個數組 \(num[x]\) ,表示第 \(x\) 塊內一共有幾個數(也就是 \(\sum_{l_x}^{r_x} cnt[i]\))。 然後暴力地把 \(A\) 中所有的數插入到桶 \(cnt\) 中,利用上面的式子計算出 \(num[i]\) 的值。

  • 查詢 \(k\)

    因為把所有操作都講一遍實在太複雜了,這裡用查詢 \(k\) 小舉例子。

    “問題引入”部分中提到過,查詢“第 \(k\) 小”核心的演算法是:從 1(值域下界)開始列舉到值域上界,桶中出現的第 \(k\) 個數就是答案。

    現在從第一塊開始一塊一塊地列舉,\(num[1]\) 表示值域區間 \([1,T]\) 中有幾個數。如果 \(k\geq num[1]\) ,說明值在 \([1,T]\) 中的數不足 \(k\) 個,那麼 \(k\) 小也就肯定不在第一塊裡。現在已經出現了 \(num[1]\) 個數了,我們一共要找 \(k\) 個,那麼還要找 \(k-num[1]\) 個數,更新 \(k\) 的值,去下一塊內找。下一塊處理過程以此類推。

    重複這個尋找過程,直到在第 \(i\) 塊內,\(k\leq num[i]\) ,這說明 \(k\) 小一定出現在這個塊內。

    \(i\) 塊代表的值域區間是 \([l_i,r_i]\) ,答案一定在區間 \([l_i,r_i]\) 裡。從 \(l_i\) 列舉到 \(r_i\) 。利用剛才預處理出來的桶 \(cnt[x]\) 檢查當前列舉到的數 \(x\) 是不是在序列中出現過,如果出現過,\(k\) 自減 \(cnt[x]\)

    重複這個尋找過程,更新 \(k\) 的值,直到 \(k\leq 0\) 此時列舉到的數就是 \(k\) 小,即答案。

  • 插入、刪除

    這個操作很簡單,直接在桶中和這個數對應的塊中增減出現次數即可。

  • 時間複雜度

    查詢操作先 \(O(T)\) 枚舉了塊,然後在某個塊內暴力列舉 \(k\) 小的大小(列舉不超過 \(T\) 次),這樣查詢總複雜度 \(O(T)\)

    插入、刪除操作顯然是 \(O(1)\) 的,直接在對應陣列(\(cnt[\ ],num[\ ]\))中增減即可。

    但是這樣寫也有可能不對,值域分塊的正確寫法應該是塊狀連結串列

    如果一個塊內出現的數字過多,時間複雜度很有可能退化到 \(O(N)\) ,這時需要塊狀連結串列的分裂操作。

    然而塊狀連結串列太難寫,況且離散化之後同一塊內數字過多這種情況很難出現,所以直接寫分塊就好。

Part 2 莫隊套值域分塊

書接上回說到:值域分塊可以解決序列上的一些問題,其具體功能類似於平衡樹。注意到值域分塊插入刪除都是 \(O(1)\) 的,這和需要大量插入刪除操作以維護區間資訊的莫隊簡直是天作之合。

例1 經典區間第 \(k\) 小問題

  • 您需要寫一種資料結構,支援查詢一段序列中給定區間 \([l,r]\) 中所有元素中第 \(k\) 小的數。

讀者可能已經有思路了:用普通的莫隊維護區間,問題間轉移的時候 \(O(1)\) 增減值域分塊中的元素。當莫隊把已知區間移動到詢問區間的時候,利用值域分塊查詢答案即可。

例2 二逼平衡樹

例題 1 很簡單對吧?現在看一個加強版的例題:

  • 您需要寫一種資料結構,來維護一個有序數列,其中需要提供以下操作:
    1. 查詢 \(v\) 在區間 \([l,r]\) 中的排名。
    2. 查詢區間 \([l,r]\) 中排第 \(k\) 名元素的值。
    3. 修改某個位置上的數值。
    4. 在區間 \([l,r]\) 內查詢 \(k\) 的前驅(嚴格小於 \(k\) 且最大的數,若不存在輸出 -2147483647)。
    5. 在區間 \([l,r]\) 內查詢 \(k\) 的後繼(嚴格大於 \(k\) 且最小的數,若不存在輸出 2147483647)。

查詢區間第 \(k\) 小、查排名、查前驅、查後繼,如果讀者已經理解了值域分塊的原理,那麼這些操作都不難實現。

等等...這個題還要求支援修改操作,怎麼辦?你怕不是忘了莫隊可以帶修啊?

好了恭喜您又雙叒叕秒了一個題,用帶修莫隊套值域分塊的辦法,可以在 \(O(n^\frac 5 3 +n\sqrt n)\) 的時間內求解本題。

\(\text{Talk is cheap,shou you the code.}\)

需要注意的是,這個題的值域很大,需要離散化處理,特別是操作中的某些數也需要一起離散化掉。

ps:變數名在初始化函式中定義,先閱讀 void Init() 函式有助於理解程式碼。

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

namespace Fast_IO{
  template <typename _T>
  inline _T const& 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*10+ch-'0';
      ch=getchar();
    }
    return x*=fh;
  }
  void write(int x){
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+'0');
  }
} 
// namespace Fast_IO

using namespace Fast_IO;
//using namespace std;

const int maxn=100005;
const int maxm=10005;
#define ll long long
#define swap(x, y) x ^= y, y ^= x, x^= y

int n,m,tot,len,it;
int A[maxn],B[maxn*2];

int num[maxm],cnt[maxn];//num[i]表示第i塊中數字的數量,cnt[i]表示i出現的數量
int bel[maxn],L[maxm],R[maxm];//bel[i]表示i屬於第幾塊.L[i],R[i]表示第i塊的左右端點

struct Node{
  int opt,l,r,t,k,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 ? a.r<b.r : a.r>b.r) : a.t<b.t);
}//過載運算子,用以排序

struct QAQ{
  int pos,val;
};

int Mnum;
struct QAQ modify[maxn];//記錄詢問

inline void add(const int i){
  cnt[i]++;
  num[bel[i]]++;
}

inline void del(const int i){
  cnt[i]--;
  num[bel[i]]--;
}//在桶中新增,刪除元素,很簡單的

inline void change(const int now,const int i){
  if(query[i].l<=modify[now].pos && modify[now].pos<=query[i].r)
    add(modify[now].val),del(A[modify[now].pos]);
  swap(modify[now].val,A[modify[now].pos]);
}//帶修莫隊要更新維護時間軸

int get_rank(int k){//get rank of k in range[l,r]
	int rank=1;
	for(int i=1;i<=tot;++i){//找到值域上界
		if(k<=R[i])//k不如這個塊右端點大,那麼k一定在這個塊中
			for(int j=L[i];j<k;++j)//暴力列舉到k,找到比k小的有幾個書
				rank+=cnt[j];
		else rank+=num[i];//如果k比這個塊中元素都大,那麼答案加上這個塊中的元素數量。
	}
	return rank;
}

int get_kth(int k){//get kth number in range[l,r]
  for(int i=1;i<=tot;++i){//同上,列舉到值域上界
    if(k<=num[i])//如果k出現次數不足這個塊中的數的數量了,說明第k名在這個塊內
      for(int j=L[i];j<=R[i];++j){//列舉這個塊中所有元素
        k-=cnt[j];//每找到一個元素,k減去這個元素數量
        if(k<=0) return B[j];//如果k小於0,說明當前元素就是第k名
      }  
    else k-=num[i];//k不在這個塊中,減掉這個塊中數的數量,去下個塊查
  }
  return -1;//gg,返回-1
}

/*
查k的前驅可以先查k的排名rank,然後查排名為rank-1的數的值
查k的後繼同上
*/

int get_pre(const int k){
  int rank=get_rank(k);
  if(rank==1) return -2147483647;//k已經是第一名了,不存在前驅
  int pre=get_kth(rank-1);//查排名前一位元素
  return pre;
}

int get_back(const int k){
  int rank=get_rank(k);
  if(rank==-1) return 2147483647; //k最大,不存在答案
  int back=cnt[k]>0?get_kth(rank+1):get_kth(rank);
    //這裡細節一波,因為查的數本身可能出現在序列裡,故特判
  if(back==-1) return 2147483647; //沒查到後繼,不存在答案
  return back;
}

void Init(){
  read(n),read(m);//n個元素,m個操作

  //對原陣列分塊
  len=pow(n,0.6666666666);
  for(int i=1;i<=n;++i){
    bel[i]=(i-1)/len+1;//預處理,第i個元素屬於bel[i]塊
    B[++it]=read(A[i]);//準備離散化
  }

  //讀入,對詢問排序
  for(int i=1,op;i<=m;++i){
    read(op);
    if(op==3){//replace A[pos] with val
      Mnum++;
      read(modify[Mnum].pos);
      B[++it]=read(modify[Mnum].val);
    }else{
      Qnum++;
      query[Qnum].opt=op,query[Qnum].t=Mnum;
        //其實這裡查排名為k的元素不能離散化,但是為了方便,一起加入離散化陣列到時候再特判掉
      read(query[Qnum].l),read(query[Qnum].r);
      B[++it]=read(query[Qnum].k);
      query[Qnum].org=Qnum;
    }
  }
  std::sort(query+1,query+1+Qnum);//排序
  
  //離散化
  std::sort(B+1,B+1+it);
  it=std::unique(B+1,B+1+it)-B-1;
  for(int i=1;i<=n+Mnum+Qnum;++i){
    if(i<=n) A[i]=std::lower_bound(B+1,B+it+1,A[i])-B;
    else if(i<=n+Mnum) modify[i-n].val=std::lower_bound(B+1,B+it+1,modify[i-n].val)-B;
    else if(query[i-n-Mnum].opt!=2) query[i-n-Mnum].k=std::lower_bound(B+1,B+it+1,query[i-n-Mnum].k)-B;//2操作是查排名為k的元素,這個k不能離散化,特判掉
  }
  //初始化值域分塊
  len=sqrt(it);//塊長為sqrt(it)
  tot=it/len;//總共tot個塊
  
  for(int i=1;i<=it;++i)
    bel[i]=(i-1)/len+1;//重定義bel[i]表示值為i的元素屬於值域分塊的bel[i]塊
  for(int i=1;i<=tot;++i){
    if(i*len>it) break;
    L[i]=(i-1)*len+1;
    R[i]=i*len;//預處理每一塊的左右端點
  }
  if(R[tot]<it)
    tot++,L[tot]=R[tot-1]+1,R[tot]=it; 
}

int ans1[maxn];

signed main(){
  Init();
  int l=1,r=0,now=0;
  for(int i=1;i<=Qnum;++i){
    while(l<query[i].l) del(A[l++]);
    while(l>query[i].l) add(A[--l]);
    while(r<query[i].r) add(A[++r]);
    while(r>query[i].r) del(A[r--]);
    while(now<query[i].t) change(++now,i);
    while(now>query[i].t) change(now--,i);//正常帶修莫隊操作
    if(query[i].opt==1)
      ans1[query[i].org]=get_rank(query[i].k);
    else if(query[i].opt==2) 
      ans1[query[i].org]=get_kth(query[i].k);
    else if(query[i].opt==4)
      ans1[query[i].org]=get_pre(query[i].k);
    else if(query[i].opt==5)
      ans1[query[i].org]=get_back(query[i].k);//按 要 求 回 答 問 題
  }
  for(int i=1;i<=Qnum;++i)
    printf("%d\n",ans1[i]);//輸出答案
  return 0;
}

很好,做完了,交一發。哦!天哪!我跑到了洛谷最優解第二名!

例3 [AHOI]2013 作業

其實本來例 3 不是這道題,但是由於原來準備當例 3 的那個題理解起來簡直讓人逝世(各種陣列連環巢狀),所以臨時決定換這道題。正好也是省選題目,質量應該也比某谷月賽題目好罷。

題目連結:Link

題目描述:

給一個長度為 \(n(n\leq 10^5)\) 的正整數數列。

\(m(m\leq 10^5)\) 次操作,每次給定 \(l,r,a,b\) ,要求輸出大小在 \([a,b]\) 之間的數的個數,以及符合條件的數有幾種。

Solution:

經典莫隊+值域分塊的題目。查詢 \([a,b]\) 中的數時,先暴力 \(a,b\) 所在段元素,然後整段處理。如果 \(a,b\) 在同一段直接暴力。另外對於第二問再開一個桶分別統計答案即可,具體看程式碼。

Code:

由於上面的註釋比較詳細了,這裡程式碼不再加註。望理解。

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

namespace IO{
  template <typename _T>
  inline _T const& 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*10+ch-'0';
      ch=getchar();
    }
    return x*=fh;
  }
  inline void write(long long a){
    if(a>=10) write(a/10);
    putchar(a%10+'0');
  }
} // namespace IO

using namespace IO;
//using namespace std;

const int maxn=100005;
const int maxm=2005;

int n,m,len,tot;
int A[maxn],bel[maxn];//*對A[i]值域分塊

int cnt[maxn];
int L[maxm],R[maxm],num[maxm],val[maxm];//num[i]表示第i塊內的數字個數,val[i]表示第i塊內的數字種類

struct Node{
  int l,r,a,b,org;
};

struct Node query[maxn];
bool operator < (const Node a,const Node b){
  return bel[a.l]^bel[b.l]?bel[a.l]<bel[b.l]:(bel[a.l]&1?a.r<b.r:a.r>b.r);
}

inline void add(const int i){
  ++num[bel[A[i]]];
  ++cnt[A[i]];
  if(cnt[A[i]]==1) val[bel[A[i]]]++;
}

inline void del(const int i){
  --num[bel[A[i]]];
  --cnt[A[i]];
  if(cnt[A[i]]==0) val[bel[A[i]]]--;
}

int ans,kind;

inline void get_num(int a,int b){
  ans=0;kind=0;
  if(bel[a]==bel[b]){
    for(int i=a;i<=b;++i)
      ans+=cnt[i],kind+=cnt[i]>0;
    return;
  }
  for(int i=a;i<=R[bel[a]];++i)
    ans+=cnt[i],kind+=cnt[i]>0;
  for(int i=bel[a]+1;i<=bel[b]-1;++i)
    ans+=num[i],kind+=val[i];
  for(int i=L[bel[b]];i<=b;++i)
    ans+=cnt[i],kind+=cnt[i]>0;
}

void Init(){
  read(n),read(m);
  len=sqrt(n);
  tot=n/len;
  for(int i=1;i<=tot;++i){
    if(i*len>n) break;
    L[i]=(i-1)*len+1;
    R[i]=i*len;
  }
  if(R[tot]<n)
    tot++,L[tot]=R[tot-1]+1,R[tot]=n;
  for(int i=1;i<=n;++i){
    bel[i]=(i-1)/len+1;
    read(A[i]);
  }
  for(int i=1;i<=m;++i)
    read(query[i].l),read(query[i].r),read(query[i].a),read(query[i].b),query[i].org=i;
  std::sort(query+1,query+1+m);
}

std::pair<int,int> ans1[maxn];

signed main(){
  Init();
  int l=1,r=0;
  for(int i=1;i<=m;++i){
    while(l<query[i].l) del(l++);
    while(l>query[i].l) add(--l);
    while(r<query[i].r) add(++r);
    while(r>query[i].r) del(r--);
    get_num(query[i].a,query[i].b);
    ans1[query[i].org].first=ans;
    ans1[query[i].org].second=kind;
  }
  for(int i=1;i<=m;++i)
    printf("%d %d\n",ans1[i].first,ans1[i].second);
  return 0;
}
繁華盡處, 尋一靜謐山谷, 築一木製小屋, 砌一青石小路, 與你晨鐘暮鼓, 安之若素。