基础莫队


基础知识:

莫队&分块

莫队二次离线

P4887【模板】莫队二次离线

题目让我们统计每个区间中有多少对 $a_i,a_j$ 满足 $a_i\oplus a_j$ 得出的值在二进制表示下有 $k$ 个 $1$。

异或 $\oplus$ 的运算逻辑:

例如:$5\oplus 3=6$

二进制表示:

这里如果 $k=2$ 那么如果 $5,2$ 在一个区间中这一对数就符合要求,可以统计。

$5\oplus 5=0$ 任何数异或自己为 $0$,那么根据这个特性就有:

也就是:

代码思路

  • 读入,初始化基本信息。

由于每个元素的值都很小。所以可以预处理出,所有可以满足 $k$ 个 $1$ 的值。

后面通过这些值,在枚举值时,可以快速描述符合要求的“另一半”是谁。用的就是上面说异或的特性。

处理块长,将每个数分到对应块中。

  • 将询问排序(第一次离线)。

排序的方式仍然时分奇偶块,奇升序,偶降序或者反之都可以(保证伸缩的程度尽量小)。

  • 将区间的变化“量”存储(第二离线)。

每两个询问的“伸缩”的改变的范围存储,左端点,右端点的变化量分开存。

  • 统计每个数能和前面后面分别匹配几个。

从前向后,从后向前分别扫一次。方法就是上面说的异或的特性。

每次扫的时候,将所有能匹配的值都累计,但是不是立刻用,而是遍历到后面直接用。

例如(瞎举的,仅是方便理解):从前往后遍历,在遍历 $1$ 的时候,将后面的 $2,4,6$ 都标记了,那么 $2,4,6$ 的标记都增加 $1$。那么如果后面有 $4$ 这个值,就可以直接获取标记的值为 $1$,代表前面有一个可以匹配。

  • 区间变化“量”的处理。

左右段点分开处理,现在假设枚举左端点,处理的右端点的变化“量”,我们实时更新前面有几个可以和当前节点匹配,将这些能匹配的对数都减去或加上。

因为左端点“回缩”或者“扩张”,要看存的时候是怎么变化的。

枚举右端点,处理左端点变化同理,但是处理的是当前节点能匹配后面的点有几对。

  • 答案统计。

上个步骤处理完后,我们要求答案贡献的前缀和因为我们用莫队的时候是根据“伸缩”来变化的,而“伸缩”描述的是上一个状态变成新的状态做出的变化。简而言之,就是当前状态是在上一个状态的基础上实现的。

  • 输出。

代码

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
#include<vector>
#include<bitset>
#include<stack>
#include<map>
using namespace std;
long long r_r(){
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar(); 
  }
  return x*f;
}
const int o_o=1e5+10;
int n,m,k;
int a_a[o_o];//初始序列 
int x_c[o_o];//所有符合 k 个 1 要求的数(只有这些数符合要求,通过一个数,快速找另一个数) 
int s_m;//一共有多少个符合 k 个 1 要求的数 
int k_i[o_o];//每个数属于哪一块 
int t_i[o_o];//实时更新符合要求的数 
long long a_s[o_o];//统计结果 
long long q_z[o_o],h_z[o_o];//符合情况的个数的前缀后缀 
struct qq{
  int l,r;
  int i_d;//编号 
}q_i[o_o];//第一次离线(新闻离线) 
struct po{
  int l,r;
  int p;//判断加上或者减去 
  int i_d;//编号 
};
vector<po>v_1[o_o],v_2[o_o];//第二次离线,变化离线,分为右端点变化和左端点变化 
bool cmp(qq a,qq b){
  //如果是一个块,如果是第一个数是奇数,右边从小到大,偶数从大到小 
  //如果不是一个块,块小的靠前 
  return(k_i[a.l]^k_i[b.l]?k_i[a.l]<k_i[b.l]:(k_i[a.l]&1?a.r<b.r:a.r>b.r));
}
int l_b(int x){
  return x&(-x);
}
int g_h(int x){
  int a_s=0;
  while(x){
    a_s++;
    x-=l_b(x);
  }
  return a_s;
}
int main(){
  n=r_r(),m=r_r(),k=r_r();
  for(int i=1;i<=n;i++)a_a[i]=r_r();//读入原始序列 
  
  //16384 是 a_i 的数据范围 
  for(int i=0;i<=16384;i++)if(g_h(i)==k)x_c[++s_m]=i;//找出所有二进制下 1 的个数为 k 的数 
  int s_i=sqrt(n);//块长 
  for(int i=1;i<=n;i++)k_i[i]=(i-1)/s_i+1;//记录每个数属于那个块 
  for(int i=1;i<=m;i++){//所有询问记录(离线) 
    q_i[i].l=r_r();
    q_i[i].r=r_r();
    q_i[i].i_d=i;
  }
  sort(q_i+1,q_i+m+1,cmp);//将询问排序 
  int l=1,r=0;
  for(int i=1;i<=m;i++){
    
    //记录右边界 
    if(r<q_i[i].r)v_1[l].push_back((po){r+1,q_i[i].r,1,q_i[i].i_d});
    if(r>q_i[i].r)v_1[l].push_back((po){q_i[i].r+1,r,-1,q_i[i].i_d});
    r=q_i[i].r;//更新右边界 
    
    //记录左边界 
    if(l<q_i[i].l)v_2[r].push_back((po){l,q_i[i].l-1,-1,q_i[i].i_d});
    if(l>q_i[i].l)v_2[r].push_back((po){q_i[i].l,l-1,1,q_i[i].i_d});
    l=q_i[i].l;//更新左边界 
  }
  for(int i=1;i<=n;i++){ 
    q_z[i]=1ll*t_i[a_a[i]];//当前的数可以和几个前面的匹配成功 
    
    //所有可以和当前数异或后成为目标的数,累计答案 
    for(int j=1;j<=s_m;j++)t_i[a_a[i]^x_c[j]]++;
  }
  memset(t_i,0,sizeof(t_i));
  for(int i=n;i>=1;i--){
    h_z[i]=1ll*t_i[a_a[i]];//当前的数可以和几个后面的匹配成功 
    
    //所有可以和当前数异或后成为目标的数,累计答案 
    for(int j=1;j<=s_m;j++)t_i[a_a[i]^x_c[j]]++;
  }
  memset(t_i,0,sizeof(t_i));
  for(int i=1;i<=n;i++){//左端点 
    for(int j=0;j<v_1[i].size();j++)//处理所有的右端点的情况 
      for(int k=v_1[i][j].l;k<=v_1[i][j].r;k++)//枚举范围 
        //统计答案,当前范围内能和前面匹配的对数 
        a_s[v_1[i][j].i_d]+=1ll*v_1[i][j].p*(q_z[k]-1ll*t_i[a_a[k]]);
    
    //只能统计已经出现的贡献,所以要边处理,边更新贡献 
    for(int j=1;j<=s_m;j++)t_i[a_a[i]^x_c[j]]++;//累计答案 
  }
  memset(t_i,0,sizeof(t_i));
  for(int i=n;i>=1;i--){//右端点 
    for(int j=0;j<v_2[i].size();j++)//处理所有的左端点的情况 
      for(int k=v_2[i][j].l;k<=v_2[i][j].r;k++)//枚举范围 
        //统计答案,当前范围内能和后面匹配的对数 
        a_s[v_2[i][j].i_d]+=1ll*v_2[i][j].p*(h_z[k]-1ll*t_i[a_a[k]]);
    
    //实时更新 
    for(int j=1;j<=s_m;j++)t_i[a_a[i]^x_c[j]]++;
  }
  
  //前缀和统计结果(莫队每回根据上一次的状态调整到目标状态,所以要获取上一次的“状态”) 
  for(int i=2;i<=m;i++)a_s[q_i[i].i_d]+=a_s[q_i[i-1].i_d];
  for(int i=1;i<=m;i++)printf("%lld\n",a_s[i]);//输出所有询问的结果 
  return 0;
}

歴史の研究

AT1219 歴史の研究

题目要求每个区间范围的最大权值($=$ 数的值 $\times $ 这个数的数量)。

回滚莫队思路

  • 读入基本信息,分块。

  • 离散化。

  • 读取询问,离线操作。

  • 询问排序。

住在不一个块中左端点排序,否则右端点排序,尽量减少“伸缩”次数。

  • 处理每个询问。

如果在一个块中,单独处理直接解决。

如果右端点在块外,往外延伸,记录此时的最大值

如果左端点在块外,往外延伸,但是最后找到最大值后,记结果后,要将左边再缩回来。此时最大值也更新为上面记录的最大值。在排序时,我们的右端点是有序的。

  • 将询问的顺序排回来,按顺序输出。

回滚莫队代码

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
#include<vector>
#include<bitset>
#include<stack>
#include<map>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar(); 
  }
  return x*f;
}
const long long o_o=2e5+10;
struct po{
    long long x,y;//左右端点 
  long long i_d;//记录第几个询问 
  long long a_s;//记录结果 
}q_i[o_o];//询问信息 
struct pp{
    long long x_x;//原始序列 
  long long i;//编号 
}a_a[o_o];//节点信息 
long long k_i;//块长 
long long b_b[o_o];//当前节点在那个块中 
long long x_x[o_o];//离散化 
long long b_p[o_o];//统计块内每个数出现的数量 
long long c_p[o_o];//存原值 
long long m_m[o_o];//临时数组,处理在一个块内时每个点出现的次数 
long long m_a;//最大值 
long long k_k;//处理到第几个询问了 
long long n,m;
bool c_1(pp a,pp b){//将值从小到大排序 
    return a.x_x<b.x_x||(a.x_x==b.x_x&&a.i<b.i);
}
bool c_2(po a,po b){
    if(b_b[a.x]!=b_b[b.x])return a.x<b.x;//不是一个块,按左端点排序 
    return a.y<b.y;//是同一个块按右端点排序 
}
bool c_3(po a,po b){//按照询问的先后顺序排序 
    return a.i_d<b.i_d;
}
long long f_i(long long l,long long r){
  long long m_k=0;//初始化最大值 
    for(long long i=l;i<=r;i++)m_m[x_x[i]]=0;//初始化,清空 
    for(long long i=l;i<=r;i++){
        m_m[x_x[i]]++;//更新值出现的次数 
        m_k=max(m_k,m_m[x_x[i]]*c_p[i]);//更新最大值 
    }
    return m_k;//返回最大值 
}
long long g_i(long long k_k,long long x){
    m_a=0;
  long long l_t=0;
  long long i=k_k;//记录处理到第几个询问了 
    for(long long j=1;j<=n;j++)b_p[j]=0;//初始化 
    long long l_r=min(k_i*x,n);
    long long l=l_r+1,r=l_r;//当前块左端 
    while(b_b[q_i[i].x]==x){//当前询问的左端点还在当前块 
        if(b_b[q_i[i].x]==b_b[q_i[i].y]){//左右端点在同一个块中 
      q_i[i].a_s=f_i(q_i[i].x,q_i[i].y);//记录结果 
      i++;//下一个 
      continue;
    }
        while(r<q_i[i].y){//右端点未达到询问 
          b_p[x_x[++r]]++;//右端点外延 
      m_a=max(m_a,b_p[x_x[r]]*c_p[r]);//更新最大值 
    }
        l_t=m_a;//记录最大值 
        while(l>q_i[i].x){//左端点未达询问 
          b_p[x_x[--l]]++;//左端点外延 
      m_a=max(m_a,b_p[x_x[l]]*c_p[l]);//更新最大值 
    }
        q_i[i].a_s=m_a;//更新最大值 
        while(l<l_r+1)b_p[x_x[l++]]--;//左端点恢复到块左端 
        m_a=l_t;//更新最大值 
    i++;//下一个询问 
    }
    return i;//返回进行到哪个询问了 
}
int main(){
    n=r_r(),m=r_r();
  k_i=sqrt(n);//分块 
    for(long long i=1;i<=n;i++){
    a_a[i].x_x=r_r();//读入原始序列 
    c_p[i]=a_a[i].x_x;//复制 
    a_a[i].i=i;//记录位置 
    b_b[i]=(i-1)/k_i+1;//记录属于哪一块 
  }
  long long n_m=b_b[n];//记录一共有多少块 
    sort(a_a+1,a_a+n+1,c_1);//值从小到大排序 
    for(long long i=1,j=0;i<=n;i++){//离散化 
        if(i==1||a_a[i].x_x!=a_a[i-1].x_x)j++; 
        x_x[a_a[i].i]=j;
    }
    for(long long i=1;i<=m;i++){//读入询问(离线) 
        q_i[i].x=r_r();
    q_i[i].y=r_r();
        q_i[i].i_d=i;
    }
    k_k=1;
    sort(q_i+1,q_i+m+1,c_2);//询问排序 
    for(long long i=1;i<=n_m;i++)k_k=g_i(k_k,i);//处理每个块 
    sort(q_i+1,q_i+m+1,c_3);//顺序排序 
    for(long long i=1;i<=m;i++)//输出 
        printf("%lld\n",q_i[i].a_s);
    return 0;
}

分块思路

这回分块不再是将整个序列分成小块,而是将整个序列看成第一块,然后不断将左端右移但有段不动,分成不同大小的块。

例如:现在又序列长度 $64$ 的一个区间,那么第一个块:$[1,64]$,将左端右移,有段动根据块长分成第二块:$[9,64]$,第三块:$[17,64]…$

我们在每块处理最大值和每个数出现的次数(由于数的范围过大,但数量小,所以要离散化)。

每个块在处理最大值的时候,我们还要将右端点记录。就是左端点分块,但是右端点暴力枚举。

然后处理询问,我们已经与处理好块了,所以可以在线处理。首先找到左端点在的块中,然后找到下一个块到右端点之间的最大值(处理块的时候有端点是暴力的)。

最后处理左端点所在的块的情况。

将块中范围内的数全部记录到一个数组中。并统计每个数出现的次数。统计每个数在整个区间出现的次数:

左边部分:暴力统计(刚刚已经完成了)。

中间部分:左端点所在的块的下一个块和右端点所在的块之间数量差(就是到右段点块之前这个数出现的次数)。

右边部分:暴力统计右端点所在的块范围内每个数的贡献。

同时通过每个数的贡献不断更新最大权值,输出即可。

分块代码

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
#include<vector>
#include<bitset>
#include<stack>
#include<map>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar(); 
  }
  return x*f;
}
const int o_o=1e5+10;
int n=r_r(),q=r_r();
int k_i;//块长 
int a_a[o_o];//原序列 
int b_b[o_o];//当前值属于那个块 
long long c_p[o_o];//辅助数组(离散化) 
long long f_i[500][o_o];//每个块到每个数的最大值 
long long c_t[500][o_o];//每块中,每个数出现的数量 
long long s_t[o_o];//对于块内讨论的队列 
long long n_m[o_o];//数出现的次数 
int main(){
  k_i=sqrt(n);//块长 
  for(int i=1;i<=n;i++){
    a_a[i]=r_r();//原始序列 
    b_b[i]=(i-1)/k_i+1;//属于哪个块 
    c_p[i]=a_a[i];//复制 
  }
  sort(c_p+1,c_p+1+n);//排序 
  int k_b=unique(c_p+1,c_p+1+n)-c_p-1;//去重 
  for(int i=1;i<=n;i++)a_a[i]=lower_bound(c_p+1,c_p+1+k_b,a_a[i])-c_p;//离散化 
  for(int i=1;i<=b_b[n];i++){//处理每个块 
    long long n_w=0;
    for(int j=lower_bound(b_b+1,b_b+1+n,i)-b_b;j<=n;j++){//第 i 块开头和之后的部分 
      c_t[i][a_a[j]]++;//累计块中当前值相同的数的个数 
      n_w=max(n_w,c_t[i][a_a[j]]*c_p[a_a[j]]);//统计最大值 
      f_i[i][j]=n_w;//记录最大值 
    }
  }
  while(q--){
    int x=r_r(),y=r_r();//询问区间 
    long long a_s=f_i[b_b[x]+1][y];//从 x 的块开始到 y 的最大值 
    int t_p=0;//队列“清空” 
    int t_m=lower_bound(b_b+1,b_b+1+n,b_b[y])-b_b;//y 所在块的开头 
    for(int i=t_m;i<=y;i++){
      n_m[a_a[i]]++;//统计出现次数 
      s_t[++t_p]=a_a[i];//入队 
    }
    t_m=lower_bound(b_b+1,b_b+1+n,b_b[x]+1)-b_b;//x 所在块的结尾 +1 
    for(int i=x;i<t_m;i++){
      n_m[a_a[i]]++;//统计出现次数 
      s_t[++t_p]=a_a[i];//入队 
      
      //统计最大值(套公式) 
      a_s=max(a_s,(c_t[b_b[x]+1][a_a[i]]-c_t[b_b[y]][a_a[i]]+n_m[a_a[i]])*c_p[a_a[i]]);
    }
    for(int i=1;i<=t_p;i++)n_m[s_t[i]]=0;//清空 
    printf("%lld\n",a_s);//输出 
  }
  return 0;
}

回滚莫队&不删除莫队

P5906【模板】回滚莫队&不删除莫队

题目让维护的是在区间内两值的最远距离。在处理每个值时,如果第一次出现,记录位置没否则的话就尝试更新最大值。

  • 初始化,读入基本信息,分块。

  • 离散化。

  • 询问离线,排序。

  • 在一块内,所有询问左端点在块内的都进行处理,如果走右端点都在块内,单独处理。

  • 记录最大值,顺序输出。

和上面的题的思路相似。

代码

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
#include<vector>
#include<bitset>
#include<stack>
#include<map>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar(); 
  }
  return x*f;
}
const int o_o=1e6+10;
int m_b;//数列去重的数量 
int n,m;
int a_a[o_o];//原数列 
int a_s[o_o];//答案 
int m_a[o_o];//当前值的位置记录 
int b_b[o_o];//属于那个块 
int b_n;//块的数量 
int c_p[o_o];//复制 
int s_t[o_o];//存当前值第一次在区间中出现的位置 
int x_p;//已经记录几个不同的值 
int n_w[o_o];//每个不同的值是谁 
struct po{
  int l,r;//左右端点 
  int i;//编号 
  bool operator <(const po &n)const{
    //再一个块中,按右端点排序否则,按块排序 
    return (b_b[l]^b_b[n.l])?b_b[l]<b_b[n.l]:r<n.r; 
  }
}q_i[o_o];
int f_i(int l,int r){
  int l_t[o_o],r_s=0;
  for(int i=l;i<=r;i++)l_t[a_a[i]]=0;//清空 
  for(int i=l;i<=r;i++)
    if(!l_t[a_a[i]])l_t[a_a[i]]=i;//记录 
    else r_s=max(r_s,i-l_t[a_a[i]]);//更新最小值 
  return r_s;
}
int main(){
  n=r_r();
  int k_i=sqrt(n);//块长 
  for(int i=1;i<=n;i++){
    a_a[i]=r_r();
    c_p[i]=a_a[i];//复制 
    b_b[i]=(i-1)/k_i+1;//判断属于哪个块 
  }
  b_n=b_b[n];//存储块数 
  sort(c_p+1,c_p+1+n);//排序 
  m_b=unique(c_p+1,c_p+1+n)-c_p-1;//去重 
  for(int i=1;i<=n;i++)a_a[i]=lower_bound(c_p+1,c_p+1+m_b,a_a[i])-c_p;//离散化 
  m=r_r();
  for(int i=1;i<=m;i++){//读入询问离线 
    q_i[i].l=r_r();
    q_i[i].r=r_r();
    q_i[i].i=i;
  }
  sort(q_i+1,q_i+1+m);//询问排序 
  for(int i=1,j=1;j<=b_n;j++){//处理每一块 
    int k_k=min(n,j*k_i);
    int l=k_k+1,r=l-1;//当前块的左右范围 
    int r_s=0;
    x_p=0;
    while(b_b[q_i[i].l]==j){//左端点在当前块内 
      if(b_b[q_i[i].r]==j){//左右端点在同一块中 
        a_s[q_i[i].i]=f_i(q_i[i].l,q_i[i].r);//统计结果 
        i++;//下一询问 
        continue;
      }
      while(r<q_i[i].r){//右阔 
        r++;
        m_a[a_a[r]]=r;//更新 
        if(!s_t[a_a[r]]){//记录 
          s_t[a_a[r]]=r;
          n_w[++x_p]=a_a[r];
        }
        r_s=max(r_s,r-s_t[a_a[r]]);//更新最大值 
      }
      int t_p=r_s;//记录最大值 
      while(l>q_i[i].l){//左阔 
        l--;
        if(m_a[a_a[l]])r_s=max(r_s,m_a[a_a[l]]-l);//更新最大值 
        else m_a[a_a[l]]=l;//记录 
      }
      a_s[q_i[i].i]=r_s;//记录结果 
      while(l<=k_k){//将左端点缩回来 
        if(m_a[a_a[l]]==l)m_a[a_a[l]]=0;//清空 
        l++;
      }
      r_s=t_p;//更新最大值 
      i++;//下一个询问 
    }
    for(int i=1;i<=x_p;i++)m_a[n_w[i]]=s_t[n_w[i]]=0;//清空 
  }
  for(int i=1;i<=m;i++)printf("%lld\n",a_s[i]);//输出结果 
  return 0;
}

文章作者: 王大神——A001
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王大神——A001 !
  目录