• 静态莫队分块


    静态莫队分块

    ( ext{Update:2021/7/9})

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

    ( ext{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_jin 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(Nsqrt N)) 。(实际上莫队跑得比这个快多了)

    但是这个复杂度是建立在可以 (O(1)) 处理单个元素对答案的影响之上的。如果用莫队套了某种数据结构(比如说堆、平衡树等),这样处理单个元素的复杂度提升为 (logN) ,就会出现 (O(Nsqrt 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} imes frac{x_i-1}{r-l}= frac{x_i^2-x_i}{(r-l+1) imes(r-l)} ]

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

    (p=frac{sum_cx_i^2-sum_cx_i}{(r-l+1) imes (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
    ",ans1[i],ans2[i]);
      return 0;
    }
    

    T2 洛谷P3901 数列找不同

    题目链接:Link

    题目描述:

    给定长度为 (n) 的序列 (A(A_iin [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_iin [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--;
    }
    
    繁华尽处, 寻一静谧山谷, 筑一木制小屋, 砌一青石小路, 与你晨钟暮鼓, 安之若素。
  • 相关阅读:
    Centos7搭建OpenNebula云平台
    Python中__new__和__init__的区别与联系
    16个python常用魔法函数
    微信小程序< 1 > ~ Hello 微信小程序
    扬帆起航,再踏征程(一)
    Java 社区平台
    Java 社区平台
    <Android 应用 之路> 一个类似今日头条的APP
    使用标准C读取文件遇到的结构体对齐问题及其解决办法
    编译64位cu文件的设置
  • 原文地址:https://www.cnblogs.com/zaza-zt/p/14979757.html
Copyright © 2020-2023  润新知