静态莫队分块

静态莫队分块

\(\text{Update:2021/7/9}\)

  • 优化了叙述顺序
  • 增加了莫队算法的几何证明

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

  • 更新了码疯,用 while 替代了莫队中的 for 语句,并压缩了代码。

前言

分块的另一种重要形式是对询问分块。这是一种离线做法,又被称为“莫队算法”(前国集队长莫涛在“小 Z 的袜子”一题中创造性的提出了这种做法,因此得名)。

在了解静态莫队算法之前,可以先思考这样一个问题:

  • 为什么在线的暴力算法很慢?

显然,对于每次询问,暴力算法都重新处理一遍答案,输出结果。由于没有数据结构的优化,导致效率很低。

莫队算法的本质也是暴力,但它的优点在于调整了询问出现的顺序,使得重复利用之前处理过的答案成为可能。

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

Part1 静态莫队原理

既然叫“静态莫队”,肯定是处理只有区间询问,没有修改的问题啦!

预处理

首先读入需要操作的数组 \(A\) 和操作序列 \(Q,(Q_i=[l_i,r_i])\),离线下来。

把数组 \(A\) 分成 \(\sqrt N\) 块,第 \(i\) 块记作 \(A_i\) ,再把所有询问 \(Q_j\) 按照左端点 \(l_j\) 从小到大排序,使每一个 \(Q_j\)\(l_j\) 落在某个块 \(A_i\) 内(如果 \(Q_j\) 的左端点 \(l_j\) 落在 \(A_i\) 块内,那么记这个 \(Q_j\in A_i\))。然后把每个 \(A_i\) 块内包含的所有 \(Q_j\) 按照 \(r_j\) 从小到大排序。

回答问题?

莫队快速回答问题的基础是“上一次查询的答案”,于是先把每一个 \(A_i\) 的第一个询问 \(Q_j\) 暴力处理出来,得到一组答案(当然如果懒的话,只处理第一块的第一个询问也是 OK 的。下面的代码中由于我懒,所以只处理第一块的第一个询问)。然后把这组答案经过左右端点暴力调整更新后,用来回答所属块内的其他询问。

几何法理解莫队

如果把查询区间的 \([l,r]\) 看作平面内一个点 \((l,r)\) 的话,就相当于平面上存在 \(m\) 个点。从原点出发(初始不知道任何点的答案),沿着坐标轴移动(就是增加、删除操作)修改 \(l,r\) ,更新答案,直到走到点 \(l,r\) ,此时我们就可以回答询问 \([l,r]\) 。如果我们走到了所有的点,那么就相当于完成了问题的求解。

复杂度分析

整体分析复杂度:

排序之后,在每块内部,两个相邻询问的左端点变化 \(<\sqrt N\) ,右端点变化单调递增。如果我们以上一次询问的回答为基础,那么每次最多花 \(\sqrt N\) 的时间处理两次询问左端不同的部分。而 \(A_i\) 整块内,右端点增长的范围之和 \(N\)

一共有 \(N\) 次询问 ,\(\sqrt N\) 个块需要处理,总共处理左端点 \(N\) 次乘 \(\sqrt N\) 处理一次,总共处理 \(\sqrt N\) 个块,处理一个块内的右端点总共花 \(N\) ,总复杂度 \(O(N\sqrt N)\) 。(实际上莫队跑得比这个快多了)

但是这个复杂度是建立在可以 \(O(1)\) 处理单个元素对答案的影响之上的。如果用莫队套了某种数据结构(比如说堆、平衡树等),这样处理单个元素的复杂度提升为 \(logN\) ,就会出现 \(O(N\sqrt NlogN)\) 这种玄学复杂度。

Part 2 静态莫队例题

T1 [国家集训队]小Z的袜子

最经典的莫队例题,也是莫队算法的出处。

题目链接:Link

题目描述:

给定一个长度为 \(n\) 的序列 \(A\) ,有 \(m\) 次询问,每次在 \([l,r]\) 内随机选两个数 \(A_i,A_j\) ,询问这两个数相同的概率。

Solution:

要求概率,先得把式子一推。

考虑在区间 \([l,r]\) 之间, 数 \(i\) 出现了 \(x_i\) 次,那么两抓都抓到 \(i\) 的概率很好算:

\[p=\frac{x_i}{r-l+1}\times \frac{x_i-1}{r-l}= \frac{x_i^2-x_i}{(r-l+1)\times(r-l)} \]

如果这段区间内一共有 \(c\) 种数,根据加法原理,总概率为:

\(p=\frac{\sum_cx_i^2-\sum_cx_i}{(r-l+1)\times (r-l)}\)

其中 \(\sum_cx_i\) 显然就是区间中所有的数之和,也就是 \(r-l+1\) ,于是式子中只有 \(\sum_c x_i^2\) 是变量,也就是我们需要维护的量。建立一个变量 \(ans\) 来维护这个值,那么整段的答案就可以由 \(ans\) 计算得到。

要想使用莫队,还得搞定单个元素对答案 \(ans\) 的影响:

  1. 添加一个元素对 \(ans\) 的影响:

    设这个元素在添加前在 \([l,r]\) 内共出现过 \(x\) 次,那么有:

    \[\Delta x=(x+1)^2-x^2=2x+1 \]

  2. 删除一个元素对 \(ans\) 的影响:

    设这个元素在删除前在 \([l,r]\) 内共出现过 \(x\) 次,那么有:

    \[\Delta x=(x-1)^2-x^2=-2x+1 \]

处理好单个元素影响后,就可以调整询问的左右端点,处理答案了。

Code:

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

//using namespace std;

const int maxn=50005;
#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 gcd(const int x,const int y){
  if(!y) return x;
  return gcd(y,x%y);
}//题目要求输出最简分数

int n,m,len;
int A[maxn],bel[maxn];

struct Node{
  int l,r,org;//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;
}//先按块分,再按右端点分

int num[maxn],ans;//num是桶,即i在当前区间出现了num[i]次

void add(const int i){
  ans+=num[i]*2;
  ans++;
  num[i]++;
}

void del(const int i){
  ans-=num[i]*2;
  ans++;
  num[i]--;
}//添加,删除单个元素影响
//根据上面推出的式子更新ans的值

int ans1[maxn],ans2[maxn];//这两个用来记录分子分母

signed main(){
  read(n),read(m);
  len=(int)std::sqrt(n);
  for(int i=1;i<=n;++i){
    bel[i]=(i-1)/len+1;//记第i个元素属于bel[i]块
    read(A[i]);
  }
  for(int i=1;i<=m;++i)
    read(query[i].l),read(query[i].r),query[i].org=i;
  std::sort(query+1,query+1+m);//读入所有询问后排序
  int l=1,r=0;
  for(int i=1;i<=m;++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--]);//调整左右端点,使之与查询区间吻合
    ans1[query[i].org]=ans-(query[i].r-query[i].l+1);
    ans2[query[i].org]=(query[i].r-query[i].l+1)*(query[i].r-query[i].l);
    if(ans1[query[i].org]==0)
      ans2[query[i].org]=1;//特判一下,统计答案
    else{
      int g=gcd(ans1[query[i].org],ans2[query[i].org]);
      ans1[query[i].org]/=g;
      ans2[query[i].org]/=g;
    }
  }
  for(int i=1;i<=m;++i)
    printf("%lld/%lld\n",ans1[i],ans2[i]);
  return 0;
}

T2 洛谷P3901 数列找不同

题目链接:Link

题目描述:

给定长度为 \(n\) 的序列 \(A(A_i\in [1,n])\),有 \(m\) 次询问,每次询问 \([l,r]\) 内元素是否各不相同。

Solution:

有点像上个例题,还是考虑开桶记录每个 \(A_i\) 的出现次数。同时还要维护一个 \(cnt\) 表示当前区间有多少个重复元素,每次更新桶内元素时顺便更新 \(cnt\) 。如果 \(cnt=0\) ,说明这段内元素各不相同,否则则出现重复。

Code:

主体框架和 T1 一样,故只给出 add 和 del 部分的代码。

void add(const int i){
  num[i]++;
  if(num[i]>1) cnt++;
  if(cnt>0) ans=0;
  else ans=1;
}

void del(const int i){
  num[i]--;
  if(num[i]>=1) cnt--;
  if(cnt==0) ans=1;
  else ans=0;
}

T3 [SDOI2009]HH的项链

这题在我之前的博客里讨论过主席树做法,现在看看莫队做法。

题目链接:Link

题目描述:

给定一个长度为 \(n\) 的序列 \(A(A_i\in [1,1e6])\),有 \(m\) 次询问,每次询问 \([l,r]\) 之间有多少个互不相同的元素。

Solution:

这题用主席树还要想想,起码要想到预处理 \(A_i\) 下一次什么时候出现,然后才能用主席树维护。但是如果用莫队...

这不是 shabi 题吗?

还是开桶,直接用 \(ans\) 维护不同元素的数量,回答每个询问。

Code:

和 T1 基本一样,故仍只给出 add 和 del 的代码。

因为这题出题人有意在卡莫队,所以莫队只能拿 72 分(当年用主席树也没放我过啊)

另外,静态莫队还有一个小优化:奇偶性优化。即编号是奇数的块按 \(r_j\) 从小到大,否则从大到小排序。

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;
}//奇偶性优化

inline void add(const int i){
  num[i]++;
  if(num[i]==1) ans++;
}

inline void del(const int i){
  num[i]--;
  if(num[i]==0) ans--;
}
posted @ 2021-07-07 04:39  ZTer  阅读(97)  评论(1编辑  收藏  举报