带修莫队分块

带修莫队分块

\(\text{Update 2021/7/12}\)

  • 填坑,找到一道带修莫队的题目。

\(\text{Update 2021/7/10}\)

  • 更新了码疯,用 while 语句替代了莫队中的 for 语句。
  • 修改了错别字和病句。

这篇博客中,我已经介绍了“静态莫队”算法,它可以离线解决一类静态(不带修改)的区间问题。

经过后人的不断完善,出现了“带修莫队”,让莫队可以支持修改操作。

什么是带修莫队?什么是带修莫队?如果你想了解,什么是带修莫队的话,现在就带你研究。

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 带修莫队例题

带修莫队我目前没找到大量练习题目,只有这一道板子。这里挖个坑:以后如果遇到带修莫队的题目要在这里整理总结。

\(\text{Update 2021/7/12}\) :我来填坑,下去看 T2 !

T1 [国家集训队]数颜色

题目链接: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);
  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);
    ans1[query[i].org]=ans;
  }
  for(int i=1;i<=Qnum;++i)
    printf("%d\n",ans1[i]);
  return 0;
}

T2 CF940F

一道伪装成回滚莫队的带修莫队。

题目链接:Link

题目描述:

给定一个长度为 \(N\) 的序列 \(A\) ,要求支持两种操作:

  • 查询区间 \([l,r]\) 中每种数字出现次数的 \(mex\) 值。
  • 修改位置为 \(p\) 的元素。

\(mex\) 值指的是一个数集中最小的没有出现过的正整数。

Solution:

看过我的这篇博客或者做过有关求 \(mex\) 的题目的同学可能知道,一种很方便的求区间 \(mex\) 的方法是:回滚莫队。

但是这个题带修改,而带修莫队和回滚莫队又是水火不相容(如果要带修,因为 change 函数的关系,必须同时支持添加、删除两种操作,而这是回滚莫队无法做到的)。不用回滚莫队的话,删除操作之后就要疑似 \(O(N)\) 重新求 \(mex\) ,这样复杂度就不对...这个问题似乎无解?

没什么思路,我闲的没事做,先打了一发带修莫队暴力(就是直接修改、添加、删除,所有操作完之后直接重新统计答案),然后就... AC 了?

难道是数据水了?CF 的叉人机制不可能让暴力过去的,肯定是有什么性质我没发现。

注意到这个题和普通求 \(mex\) 的题不同,它是求元素出现次数\(mex\) 。可以设暴力从 1 开始枚举,最终答案是 \(k\) ,那么就枚举了 \(k-1\) 个数。而要用各个元素出现次数构成这 \(k-1\) 个数,至少用 \(\sum_{i=0}^{k-1} i\approx k^2\) 个数!我们一共才有 \(N\) 个数,那么 \(k\) 最大也就不过 \(\sqrt N\) 而已,也就是暴力的复杂度不超过 \(\sqrt N\) 。而带修莫队转移一次为 \(\sqrt[3] {N^2}\) 左右,这个复杂度完全可以接受。

Code:

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

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

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<<3)+(x<<1)+ch-'0';
    ch=getchar();
  }
  return x*=fh;
}

int n,q,len,T;
int A[maxn],B[maxn];
int bel[maxn];

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 ? a.r<b.r : a.r>b.r) : a.t<b.t);//加上奇偶性优化
}

struct QAQ{
  int pos,val;
};

int Mnum;
struct QAQ Modify[maxn];

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

inline void add(const int i){//添加值i
  ++cnt[i];
  --tot[cnt[i]-1];//比如一个数出现2次->3次,不仅要把3次的桶+1,也要把2次的桶-1
  ++tot[cnt[i]];//这相当于数从一个桶跳到另一个桶去了
//反正最后都要暴力求解,这里就不必再更新答案了
}

inline void del(const int i){//删除值i
  --cnt[i];
  --tot[cnt[i]+1];
  ++tot[cnt[i]];//原因同上调整
}

void Getans(){//暴力求解答案
  ans=1;
  while(tot[ans]>0)
    ans++;
}

inline void change(const int now,const int i){
  if(query[i].l<=Modify[now].pos && Modify[now].pos<=query[i].r)
    del(A[Modify[now].pos]),add(Modify[now].val);
  swap(Modify[now].val,A[Modify[now].pos]);
}

void Init(){
  read(n),read(q);
  len=(int)pow(n,0.6666666666);
  for(int i=1;i<=n;++i){
    bel[i]=(i-1)/len+1;
    B[++T]=read(A[i]);
  }
  for(int i=1,op;i<=q;++i){
    read(op);
    if(op==1){
      ++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);
      B[++T]=Modify[Mnum].val;//这里要一起离散化掉
    }
  } 
  std::sort(B+1,B+1+T);
  int m=std::unique(B+1,B+1+T)-B-1;
  for(int i=1;i<=n+Mnum;++i){//值域较大,要离散化(好押韵呀
    if(i<=n) A[i]=std::lower_bound(B+1,B+m+1,A[i])-B;
    else Modify[i-n].val=std::lower_bound(B+1,B+m+1,Modify[i-n].val)-B;
  }
}

int ans1[maxn];

signed main(){
  Init();
  std::sort(query+1,query+1+Qnum);
  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);//普通带修莫队
    Getans();//暴力一发
    ans1[query[i].org]=ans; 
  }
  for(int i=1;i<=Qnum;++i)
    printf("%lld\n",ans1[i]);
  return 0;
}
posted @ 2021-07-09 15:15  ZTer  阅读(149)  评论(1编辑  收藏  举报