数列分块入门9

数列分块入门 \(9\)

  • update on 21-10-20: 主要优化了题解的格式

题意:

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,查询 \([l,r]\) 之间的众数是什么

分析:

这道题遇到了之前没有遇到过的众数问题,那么我们怎么解决呢?

暴力显然是不行的,因此我们要考虑其他方法。

根据 陈立杰的区间众数解题报告 可知 ,\(a\)\(b\) 集合的众数,一定属于 \(a\) 集合的众数与 \(b\) 集合的交集,即一定在这两个集合的某一个集合中出现(似乎是废话)

思路:

如果边界是 \([l,r]\)\(l\) 在第 \(a\) 块,\(r\) 在第 \(b\) 块,可以分成三个部分:

  1. \(l\)\(a\) 最后一块

  2. \([a+1 \rightarrow b-1]\)

  3. \(b\) 块到 \(r\)

根据上面的性质,如果我们预先处理 \([a+1 \rightarrow b-1]\) 块的众数,再去遍历判断第一部分和第三部分是否有更合适的众数,这道题就能做出来了。

具体细节在下面代码中说:

初始化:

#include<bits/stdc++.h>
using namespace std;
/*
1.将数组v[i]离散化,不记录其值,只记录其位数
2.分块查询
*/
const int N=100005;
int n,blo,id,bl[N],v[N],val[N],cnt[N];
int f[1005][1005];
vector<int> ve[N];//储存编号相同,就是数相同时原来的第几个值,用来二分求众数
map<int,int> mp;

其中,\(bl\) 存储每个块的边界,\(val\) 存储原来的 \(v\) 数组,\(cnt\) 用来计算每个数出现的个数。

主函数:

在主函数中,由于输入进去的 \(v\) 数组太大,我们并不需要那么大的数

因此可以进行离散化,相同的数具有相同的标号,再用 \(val\) 来存储原来的 \(v\) 数组

当需要查询其值大小的时候,只需要用 \(val[v[i]]\) 就行了

\(ve\) 函数其实是查编号为 \(v[i]\) 的数在第几位出现过,为后来的二分找下标做处理

int main(){
    cin>>n;
    blo=200;//每一块分成200,在后期寻找中好计算
    for(int i=1;i<=n;i++){
        cin>>v[i];
        if(!mp[v[i]]){//之前没有出现过
            mp[v[i]]=++id;//第几个出现的数
            val[id]=v[i];//记录编号为i的原值
        }
        v[i]=mp[v[i]];//记录其是第几个出现的数(离散化)
        ve[v[i]].push_back(i);//第v[i]个出现的数在哪里出现,此句话为记录个数所用,
    }
    for(int i=1;i<=n;i++) bl[i]=(i-1)/blo+1;
    for(int i=1;i<=bl[n];i++) pre(i);//将每一段进行众数排序
    for(int i=1,a,b;i<=n;i++){
        cin>>a>>b;
        if(a>b) swap(a,b);
        cout<<val[query(a,b)]<<endl;//输出众数
    }
    return 0;
}
for(int i=1;i<=bl[n];i++) pre(i);

这个语句是为了预处理在 \([i,n]\) 中每个区间的众数,接下来上代码:

预处理众数

我们将整个处理众数的过程看成是一个表

那么我们需要算出来每一块到每一块区间的众数,包括其本身

假如说有四个区间 \([a,b,c,d]\)

那么我们就需要算 \([a,b],[a-c],[a-d],[b-c],[b-d],[c-d],[a],[b],[c],[d]\) 这一共 \(10\) 个区间的众数。

我们可以将这个看成是一个表格

那么就可以开一个二维数组 \(f[1000][1000]\) 用来记录每一块的众数是什么

void pre(int x)//预处理每一段的众数
{
    memset(cnt,0,sizeof(cnt));//一开始进行初始化
    int mx=0,ans=0;
    for(int i=(x-1)*blo+1;i<=n;i++){//第x个块左边界到n中的众数
        cnt[v[i]]++;//记录每一段每个数出现的个数
        int t=bl[i];//记录这是第几段
        if(cnt[v[i]]>mx||(cnt[v[i]]==mx&&val[v[i]]<val[ans]))//找最小众数
        ans=v[i],mx=cnt[v[i]];
        //x是初始段,t是末尾段
        f[x][t]=ans;//这一段众数是ans
    }
}

至此,我们所需要的就已经准备好了。

解题

因为我们已经预先处理好每一块的众数,因此只需要判断左右两边多出来的那一小部分中,有没有数字可以当成是新的众数。

其中第一个 \(query\) 函数是照应了在建函数中 \(ve[v[i]].push\_back(i)\) 这一步,搜索从 \([l,r]\) 中标号为 \(v[i]\) 的数出现了多少次

int query(int l,int r,int x){//二分查找这一块内编号为x的数量(ve[v[i]].push_back(i);)
    int t=upper_bound(ve[x].begin(),ve[x].end(),r)-lower_bound(ve[x].begin(),ve[x].end(),l);//大下标减去小下标
    //注意前面是upper_bound,后面是lower_bound
    return t;
}
int query(int a,int b){
    int ans,mx;
    ans=f[bl[a]+1][bl[b]-1];//意思是在(bl[a]+1~bl[b]-1)这一序列中的众数
    mx=query(a,b,ans);//查找这个众数在此区间里出现的个数
    for(int i=a;i<=min(bl[a]*blo,b);i++){
        int t=query(a,b,v[i]);//暴力查找左边多余部分每个数的数量
        if(t>mx||(t==mx&&val[v[i]]<val[ans]))
        ans=v[i],mx=t;
    }
    if(bl[a]!=bl[b])
    for(int i=(bl[b]-1)*blo+1;i<=b;i++){//暴力查找右边多余部分每个数的数量
        int t=query(a,b,v[i]);
        if(t>mx||(t==mx&&val[v[i]]<val[ans]))
        ans=v[i],mx=t;
    }
    return ans;
}

快乐的结束掉这一题!

posted @ 2020-12-17 21:23  Evitagen  阅读(685)  评论(0)    收藏  举报