莫队套值域分块

时间:2021-07-21
本文章向大家介绍莫队套值域分块,主要包括莫队套值域分块使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

莫队套值域分块

在经典的“静态区间第 \(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;
}
繁华尽处, 寻一静谧山谷, 筑一木制小屋, 砌一青石小路, 与你晨钟暮鼓, 安之若素。

原文地址:https://www.cnblogs.com/zaza-zt/p/15041167.html