淺談區間眾數


區間眾數問題

區間眾數問題一般是指給定一個序列,每次詢問 \([l,r]\) 區間的眾數是幾的問題。

當然了,帶修改的區間眾數問題比較難搞,這里不展開討論,只研究靜態的區間眾數問題。

眾數並不滿足區間“可加性”,這導致它讓全部基於二分的數據結構直接 gg (比如線段樹、樹狀數組等),所以大部分研究區間眾數的算法都是基於分塊。

目前我知道的最優秀的求解區間眾數的算法是數據結構帶師 lxl 在 Ynoi 毒瘤模擬賽給出的 \(O(n^{1.485})\) 的在線算法。不過我是不會,今天只介紹一個 \(O(n^{1.5})\) 的離線做法和以及一個 \(O(n^{\frac 5 3})\) 的在線做法。

直接結合例題分析吧。

T1 faebdc 的煩惱

題目鏈接:Link

題目描述:

給定一個長度為 \(N\) 的序列,有 \(q\) 次詢問,每次詢問一個區間 \([l,r]\) 的眾數出現的次數。

Solution:

這題比區間眾數問題簡化了一點,我們只需要求出眾數出現的次數就行了,減少了一些麻煩。

看到“眾數”直接考慮分塊就行了。這題不強制在線,我選擇了離線的莫隊算法。發現向答案區間添加一個數實現比較簡單,可以順便更新眾數出現次數。而刪除操作比較操蛋,如果我們正好刪除了區間的眾數之一,可能導致眾數改變,而我們在不掃描值域的情況下,不能得知新的眾數出現次數是多少。

emmmm... 這不就是裸的回滾莫隊嗎?

回滾莫隊我之前講過,這是模板題所以不再詳細注釋代碼了,想看詳細注釋的朋友移步這里

Code:

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

//using namespace std;
//Rool Back CaptianMo's Algorithm

#define int long long
const int maxn=200005;

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

int n,q,len,tot;
int A[maxn],B[maxn];
int bel[maxn],L[maxn],R[maxn];

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

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]:a.r<b.r;
}

void Init(){
  read(n),read(q);
  len=(int)std::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;
    B[i]=read(A[i]);
  }
  std::sort(B+1,B+1+n);
  int m=std::unique(B+1,B+1+n)-B-1;
  for(int i=1;i<=n;++i)
    A[i]=std::lower_bound(B+1,B+m+1,A[i])-B;
  for(int i=1;i<=q;++i)
    read(query[i].l),read(query[i].r),query[i].org=i;
}

int cnt[maxn],cnt1[maxn];
int ans;

inline void add(const int i){
  cnt[A[i]]++;
  ans=ans>cnt[A[i]]?ans:cnt[A[i]];
}//核心,添加的同時更新眾數出現次數

inline void del(const int i){
  cnt[A[i]]--;//直接刪除,不考慮影響
}

int ans1[maxn];

signed main(){
  Init();
  std::sort(query+1,query+q+1);
  int l=R[bel[query[1].l]]+1,r=R[bel[query[1].l]],last=bel[query[1].l];
  for(int i=1;i<=q;++i){
    if(bel[query[i].l]==bel[query[i].r]){
      int tmp=0;
      for(int j=query[i].l;j<=query[i].r;++j)
        cnt1[A[j]]++;
      for(int j=query[i].l;j<=query[i].r;++j)
        tmp=tmp>cnt1[A[j]]?tmp:cnt1[A[j]];
      for(int j=query[i].l;j<=query[i].r;++j)
        cnt1[A[j]]--;
      ans1[query[i].org]=tmp;
      continue;
    }
    if(bel[query[i].l]!=last){
      while(r>R[bel[query[i].l]])
        del(r--);
      while(l<R[bel[query[i].l]]+1)
        del(l++);
      ans=0,last=bel[query[i].l];
    }
    while(r<query[i].r)
      add(++r);
    int tmp=ans,l1=l;
    while(l1>query[i].l)
      add(--l1);
    ans1[query[i].org]=ans;
    while(l1<l)//回滾還原
      del(l1++);
    ans=tmp;
  }
  for(int i=1;i<=q;++i)
    printf("%d\n",ans1[i]);
  return 0;
}

T2 [Violet]蒲公英

題目鏈接:Link

題目描述:

給定一個長度為 \(N\) 的序列,有 \(M\) 次詢問,每次詢問一個區間 \([l,r]\) 的眾數是多少。如果有多個數可以作為區間眾數,那么輸出最小的那一個。輸入數據經過加密,強制在線。

Solution:

看到“眾數”直接考慮分塊就行了。這題強制在線,把莫隊也廢了,只能考慮普通的分塊。

考慮每個詢問 \([l,r]\) ,設 \(l\) 屬於第 \(p\) 塊,\(r\) 屬於第 \(q\) 塊。分塊一般把一段區間 \([l,r]\) 分成 3 部分:

  1. 開頭的零散段 \([l,L)\)
  2. 中間的由整塊構成的段 \([L,R]\)
  3. 結尾的零散段 \((R,r]\)

根據分塊“大段維護,局部暴力”的思想,應該重點考慮如何維護 \([L,R]\) ,剩下的交給暴力。

在區間求和問題中,分塊預處理了每塊的區間和。查詢時零散段暴力求和,再加上預處理好的區間和,就得到了答案。受此啟發,感覺上也可以預處理每塊的眾數是幾。但是區間和可以相加得到更長區間的區間和,也就是區間和滿足“可加性”。眾數不滿足可加性,所以預處理不能只處理每塊的眾數,要把所有以塊的端點為端點的區間 \([L,R]\) 的眾數預處理出來。

現在考慮零散段暴力。零散段中的元素可能導致最終答案改變,所以還要記下來中間整段中每個元素出現了多少次(開桶,記為 \(cnt_{L,R}\) )。直接把兩段零散段的元素加入桶中,同時更新眾數的值。加入完零散段元素之后得到這次詢問的答案。記錄答案之后,要把加入的元素再刪掉,恢復原來的 \(cnt_{L,R}\) 的環境,以便下次使用(這不是有點像回滾莫隊嗎?)。

思路口胡完了,來分析一下時空復雜度吧(設塊長為 \(T\) )。

  • 預處理了 \(T\) 個塊,每個塊長度約為 \(N\) ,預處理時間復雜度 \(NT^2\)
  • 零散段長度 \(\frac N T\) ,每次暴力處理,回答詢問時間復雜度 \(\frac {MN} T\)
  • 每一個塊內開了長度為 \(N\) 的桶,空間復雜度 \(NT^2\)

總復雜度 \(O(NT^2+\frac {NM} T)\) ,空間為 \(O(NT^2)\) 。不妨設 \(M,N\) 同數量級,得方程:

\[NT^2=\frac {NM} T \]

解得當 \(T=\sqrt[3] N\) 時,算法最快,時間復雜度、空間復雜度都約為 \(O(N^{\frac 5 3})\)

Code:

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

//using namespace std;
//Online Solve Range Mode Problem

#define ll long long
const int maxn=40005;

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

int n,m,len,tot;
int A[maxn],B[maxn];
int bel[maxn],L[maxn],R[maxn];

int cnt[40][40][maxn];//cnt[L][R][0] refers to Range(L,R)'s mode appers times.
int mode[40][40];
int cnt1[maxn];

inline void add(const int _L,const int _R,const int i){//添加一個數,更新當前區間眾數和眾數出現的次數
  cnt[_L][_R][A[i]]++;
  mode[_L][_R] = cnt[_L][_R][A[i]] > cnt[_L][_R][0] ? B[A[i]] : mode[_L][_R];
  mode[_L][_R] = cnt[_L][_R][A[i]] == cnt[_L][_R][0] && B[A[i]] < mode[_L][_R] ? B[A[i]] : mode[_L][_R];
  cnt[_L][_R][0] = cnt[_L][_R][A[i]] > cnt[_L][_R][0] ? cnt[_L][_R][A[i]] : cnt[_L][_R][0];
}

inline void del(const int _L,const int _R,const int i){
  cnt[_L][_R][A[i]]--;
}

void Init(){
  read(n),read(m);
  len=n/pow(n,0.3333333333);
  tot=n/len;
  // printf("len=%d tot=%d",len,tot);
  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;
    B[i]=read(A[i]);
  }//原題值域較大,需要離散化
  std::sort(B+1,B+1+n);
  int l=std::unique(B+1,B+n+1)-B-1;
  for(int i=1;i<=n;++i)
    A[i]=std::lower_bound(B+1,B+1+l,A[i])-B;
//枚舉區間L,R,進行預處理
  for(int i=1;i<=tot;++i)
    for(int j=i;j<=tot;++j){
      for(int k=L[i];k<=R[j];++k)
        add(i,j,k); 
}
    
int ans;//記錄答案眾數是多少

signed main(){
  Init();  
  for(int i=1,l0,r0;i<=m;++i){
    int l=(read(l0)+ans-1)%n+1,r=(read(r0)+ans-1)%n+1;//加密方式
    if(r<l) std::swap(l,r);
    int belongL=bel[l]+1,belongR=bel[r]-1;
    if(bel[l]==bel[r] || bel[l]+1==bel[r]){//兩段相鄰或在同一段,直接暴力.
      ans=0;
      for(int j=l;j<=r;++j){
        cnt1[A[j]]++;
        ans = cnt1[A[j]] > cnt1[0] ? B[A[j]] : ans;
        ans = cnt1[A[j]] == cnt1[0] && B[A[j]] < ans ? B[A[j]] :ans;
        cnt1[0] = cnt1[A[j]] > cnt1[0] ? cnt1[A[j]] : cnt1[0];
      }//暴力統計,更新區間眾數
      printf("%d\n",ans);
      for(int j=l;j<=r;++j)
        cnt1[A[j]]--;
      cnt1[0]=0;//還原
      continue;
    }    
    int tmp1=mode[belongL][belongR];
    int tmp2=cnt[belongL][belongR][0];//類似回滾莫隊,記錄原值
    for(int j=l;j<=R[bel[l]];++j)
      add(belongL,belongR,j);
    for(int j=L[bel[r]];j<=r;++j)
      add(belongL,belongR,j);//暴力添加
    ans=mode[belongL][belongR];//統計答案
      
    mode[belongL][belongR]=tmp1;//回滾還原
    cnt[belongL][belongR][0]=tmp2;
    for(int j=l;j<=R[bel[l]];++j)
      del(belongL,belongR,j);
    for(int j=L[bel[r]];j<=r;++j)
      del(belongL,belongR,j);     
    printf("%d\n",ans);
  }
  return 0;
}

T3 大爺的字符串題

題目鏈接:Link

題目描述:

給一個長度為 \(N\) 的序列 \(A\) ,每次詢問一段區間的最大 \(rp\)

\(rp\) 定義:

每次從區間中任意選擇一個數 \(x\) ,把 \(x\) 從序列中刪除,直到區間為空。要求維護一個集合 \(S\)

  • 如果 \(S\) 為空,則你 \(rp\) 減 1 。
  • 如果 \(S\) 中有一個數嚴格大於 \(x\) ,你 \(rp\) 減 1 ,清空 \(S\)
  • \(x\) 加入集合 \(S\)

詢問之間互不影響,每次詢問初始 \(rp=0\)

Solution:

發現第一次選擇時,\(rp\) 一定減 1 ,之后第一條就沒用了(之后的 \(S\) 不可能為空)。

考慮怎么選才能不掉 \(rp\) 。顯然,只要每次選的數嚴格大於之前的數就行了,也就是說,選出的數應該構成一個嚴格上升的序列。

如果對要選擇的區間從小到大排序,然后從最小的數開始選,相同的數只選一個(保證嚴格大於,不然會直接掉 \(rp\) ),一直選到最大的數。這樣花費 1 的 \(rp\) 就把區間中每種數刪除了一次。同上方法,從剩下的數種再選一次,又花了 1 的 \(rp\) 把區間中剩下的每種數刪除了一次。這樣每次每種數只能刪掉一個,那我找找誰最抗刪不就行了?

區間眾數最抗刪,所以要花費區間眾數出現次數的 \(rp\) 刪掉區間中所有數。

答案即為 T1 答案的相反數,代碼不給了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM