分块,莫队,BSGS,块状链表

Posted lcezych

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分块,莫队,BSGS,块状链表相关的知识,希望对你有一定的参考价值。

分块

分块往往是在一个序列上进行(当然也有树上分块)。对于一个长度为(N)的序列A,我们设一个块大小为(s),然后按照每连续(s)个数一组分成若干组(最后一组可能不满s)

那么我们如果对区间([l,r])操作,就可以把区间([l,r])分成中间的整块和两边的不超过(2*s)个单点

一般来说我们取(s=sqrt n),使得块和单点的复杂度平衡

分块的优势:好想。

除了好写之外,分块的常用做法是需要预处理出一些递推的前缀和以及从第i块到第j块的信息,这样就可以合并一些线段树做不到的信息.

例题:

给出一个长度为(N)的整数序列A,有(Q)组询问,第(i)组询问给 出(l_i,r_i),询问(l_i)(r_i)之间有多少个不同的数.

(Nleq 100000)

离散化之后,先做整块,再把零散点暴力加上

(F_{i,j})表示前(i)块,数(j)出现的次数

(G_{i,j})表示第(i)块到第(j)块不同的数字的个数

(G_{i,j}=G_{i,j-1}+work(i,j-1,j))

(work(i,x,y))表示在第i块到第x块的末尾加入第y块增加的不同的个数

我们暴力枚举第(y)块中的每一个数,那么与这个数相同的出现过的数来自于两个部分,一个是(i x)块的内容,一个是第(y)块之前枚举的内容.

对于第一部分,我们直接用之前算得的前缀和,看看是不是0。对于第二部分,我们维护一个数组(B_i) ,表示第(i)个数是否在之前被枚举到过。

那么我们就在加入的时候,将这一位赋值成1,然后计算的时候判断一下这个数是不是1。同时在出这一块的时候将对应的位置去掉.

由于一共有(N/S)块起点,每次询问的复杂度都是(O(N)),则预处理的复杂度都是(O(N^2/S)).

考虑一个询问,假设询问包含了第(ll_i)到第(rr_i)块里的所有数,以及一些零散的数。首先我们的第一个答案是(F_{ll_i ,rr_i}) ,然后对于剩下的零散块,就直接用类似预处理时的方法加入就可以了。

单次询问的复杂度是(O(N/S + S)),假设(Q)(N)同阶,两部分的总复杂度就是(O(N^2/S + NS)). 一般取(S = sqrt N),则保证两边相同,复杂度是(O(N sqrt N)).

code:

int main()
{
        scanf("%d",&n);
        for (i=1;i<=n;++i) scanf("%d",&a[i]);
        int S=(int)sqrt(n);
        for (i=1;i<=n;i=ed[tot]+1)
        {
                st[++tot]=i;
                ed[tot]=min(n,i+S-1);
        }
        for (i=1;i<=tot;++i) //sqrt(n) 
        {
                for (j=1;j<=n;++j) F[i][j]=F[i-1][j]; //O(n) 
                for (j=st[i];j<=ed[i];++j) F[i][a[j]]++; //sqrt(n)
        }
        //[i..j-1] a[k] F[j-1][a[k]]-F[i-1][a[k]]
        for (i=1;i<=tot;++i) //sqrt(n)
            for (j=i;j<=tot;++j) //sqrt(n)
            {
                    G[i][j]=G[i][j-1]; 
                    for (k=st[j];k<=ed[j];++k) //sqrt(n) 
                        if (!(F[j-1][a[k]]-F[i-1][a[k]])&&!vis[a[k]]) G[i][j]++,vis[a[k]]=1;
                    for (k=st[j];k<=ed[j];++k) vis[a[k]]=0;
            }
        for (Q=1;Q<=m;++Q)
        {
                scanf("%d%d",&l,&r);
                int left=1,right;
                //[left,right] 
                if (r-l<=2*S)
                {
                        baoli(l,r);
                        continue;
                }
                while (st[left]<l) ++left;
                while (ed[right]<=r) ++right; --right;
                ans=G[left][right];
                for (i=st[left]-1;i>=l;--i)
                {
                        if (!(F[right][a[i]]-F[left-1][a[i]])&&!vis[a[i]]) ans++,vis[a[i]]=1;
                }
                for (i=ed[right]+1;i<=r;++i)
                {
                        if (!(F[right][a[i]]-F[left-1][a[i]])&&!vis[a[i]]) ans++,vis[a[i]]=1;
                } 
                printf("%d
",ans);
                for (i=st[left]-1;i>=l;--i)
                {
                        vis[a[i]]=0;
                }
                for (i=ed[right]+1;i<=r;++i)
                {
                        vis[a[i]]=0;
                } 
        }
}

莫队:

用于区间相关的离线问题,本质是暴力

引入:现在我们有一个区间([l,r])的信息,现在要改求([l',r'])的信息

只要我们暴力的把(L)一步一步地移动到(L ′) ,再把(R)一步一步移动到(R ′)就可以辣.

但是如果我们直接暴力总复杂度肯定不对,所以要求离线,用一些分析去把复杂度降低.

在之后的讨论里,我们认为序列长度(N)和询问次数(Q)是同阶的.

莫队的思想就是通过改变暴力移动左右端点的顺序从而优化时间复杂度的方法

我们先将序列按照(sqrt n)分块

把询问先按照左端点所在的块排序,再按照右端点从小到大排序

复杂度分析:左端点的次数如果是在同一块内,单次不超过(sqrt n),如果是跨块,由于次数不超过(sqrt n)次,总复杂度不超过(Nsqrt N)

对于右端点,在每一块都会至多花费(O(N))的时间从头扫到尾再从尾扫到头,这个时候总复杂度为(O(N sqrt N)).

和分块的联系:

用分块的复杂度分析实现暴力,核心是暴力

而分块的核心就是分块

复杂度非常稳定,基本上就是(O(nsqrt n))

区间mex(最小没有出现过的自然数)

给出一个序列A,Q次询问,每次询问在([Li , Ri ])的mex是多少.

(N, Q ≤ 100000).

维护每个数有没有出现和它出现的次数

分块记录每一块的出现次数的和。如果这一块的出现次数=块的大小,说明mex不在这段区间里,否则就暴力找到这段区间里第一个没有出现的数

q和n同阶时,复杂度为(O(nsqrt n))

树上莫队

把区间放到树上变成一条链,对一条链进行询问

写法1:树上分块

写法2:dfs序

(st_i)表示dfs整棵树时首次访问到这个节点的时间,(ed_i)表示dfs回到这个节点的时间,这样所有数是在[1..2 * N]之间的。

性质:

子树里面的dfn被根节点的dfn包含,并且两个不相关的节点dfn序也不相关

一条路径可以用dfs序表示(折返)

和异或类似,出现奇数次才算进答案,偶数次不算

如果不是同一条链((st_a<st_b<ed_b<ed_c)),除了路径之外,还要加上他们的lca

code:

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<ctime>
#include<set>
#include<vector>
#include<map>
#include<queue>

#define N 100005
#define M 8000005
#define K 17

#define ls (t<<1)
#define rs ((t<<1)|1)
#define mid ((l+r)>>1)

#define mk make_pair
#define pb push_back
#define fi first
#define se second

using namespace std;

int i,j,m,n,p,k,st[N],ed[N],tot,fa[N][K+1],deep[N],A[N],x,y,Que[N*2],B[N],S;

int nowans,vis[N],cnt[N],Ans[N];

vector<int>v[N];

struct Node{
        int a,b,l,r,extra,id;
}Q[N];

void dfs(int x)
{
        int i; //计算st和ed的值,并计算倍增父亲数组 
        st[x]=++tot; Que[tot]=x;
        for (i=1;i<=K;++i) fa[x][i]=fa[fa[x][i-1]][i-1];
        for (i=0;i<(int)v[x].size();++i)
        {
                int p=v[x][i];
                if (fa[x][0]==p) continue;
                fa[p][0]=x; deep[p]=deep[x]+1;
                dfs(p);
        }
        ed[x]=++tot;  Que[tot]=x;
}

int cmp(Node a,Node b)
{
        if ((a.l-1)/S!=(b.l-1)/S) return (a.l-1)/S<(b.l-1)/S;
        return a.r<b.r;
}

void add_cnt(int x)
{
        cnt[x]++;
        if (cnt[x]==1) ++nowans; 
}

void del_cnt(int x)
{
        cnt[x]--;
        if (cnt[x]==0) --nowans;
}

/*
 计算每个位置出现的奇偶性,奇数加偶数减 
*/ 

void Add(int x)
{
        vis[x]++;
        if (vis[x]==1) add_cnt(A[x]);
        if (vis[x]==2) del_cnt(A[x]); 
}

void Del(int x)
{
        vis[x]--;
        if (vis[x]==1) add_cnt(A[x]);
        if (vis[x]==0) del_cnt(A[x]); 
}

int get_lca(int x,int y)
{
        if (deep[x]<deep[y]) swap(x,y);
        int i,k;
        for (i=K,k=deep[x]-deep[y];i>=0;--i)
            if (k&(1<<i)) x=fa[x][i];
        if (x==y) return x;
        for (i=K;i>=0;--i)
            if (fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
        return fa[x][0];
}

int main()
{
        scanf("%d%d",&n,&m); //n<=100000 
        for (i=1;i<=n;++i) scanf("%d",&A[i]),B[i]=A[i]; 
        sort(B+1,B+n+1); B[0]=unique(B+1,B+n+1)-(B+1); //离散化,限制数据范围在[1,n]之间 
        for (i=1;i<=n;++i) A[i]=lower_bound(B+1,B+B[0]+1,A[i])-B;
        for (i=1;i<n;++i) scanf("%d%d",&x,&y),v[x].pb(y),v[y].pb(x);
        dfs(1);
        for (i=1;i<=m;++i)
        {
                scanf("%d%d",&Q[i].a,&Q[i].b);
                if (st[Q[i].a]>st[Q[i].b]) swap(Q[i].a,Q[i].b);
                if (st[Q[i].a]<=st[Q[i].b]&&ed[Q[i].b]<=ed[Q[i].a]) 
                    Q[i].l=st[Q[i].a],Q[i].r=st[Q[i].b],Q[i].extra=-1; //祖先关系,不需要额外加父亲
                else
                    Q[i].l=ed[Q[i].a],Q[i].r=st[Q[i].b],Q[i].extra=get_lca(Q[i].a,Q[i].b);  
                Q[i].id=i;                                       
        }
        S=(int)sqrt(2*n);
        sort(Q+1,Q+m+1,cmp);
        int ansl=1,ansr=1; Add(Que[1]);
        for (i=1;i<=m;++i)
        {
                int l=Q[i].l,r=Q[i].r;
                while (ansl>l) --ansl,Add(Que[ansl]);
                while (ansr<r) ++ansr,Add(Que[ansr]);
                while (ansl<l) Del(Que[ansl]),++ansl;
                while (ansr>r) Del(Que[ansr]),--ansr;
                if (Q[i].extra!=-1) Add(Q[i].extra);
                Ans[Q[i].id]=nowans;
                if (Q[i].extra!=-1) Del(Q[i].extra);
        }
        for (i=1;i<=m;++i) printf("%d
",Ans[i]); 
}

带修莫队

洛谷P1903

墨墨购买了一套(N)支彩色画笔(其中有些颜色可能相同) 摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

1、(Q L R)代表询问你从第(L)支画笔到第(R)支画笔中共有几种不同颜色的画笔。

2、(R P Col) 把第(P)支画笔替换为颜色(Col)。为了满足墨墨的要求,你知道你需要干什么了吗?

(N, M ≤ 50000)

考虑除了左右端点以外,还有第三维时间,所以一个询问其实是三元组((l,r,t))

还是在完整的块上打标记,不完整的块暴力修改

经过严谨的证明块是(N^{2/3})大小

对询问分别按照(t,l,r)排序

暴力查询时,如果当前修改数比询问的修改数少就把没修改的进行修改,反之回退

习题:

一个长为n的序列,m次操作:

区间加法

询问区间内大于某个值x的元素个数

两个数组,一个原数组一个升序

整块的就加tag,零散的就暴力加上然后暴力把这一块从小到大排序

虽然多一个排序,但是几乎不会产生影响

(b+frac{n}{b}logfrac{n}{b}geq2sqrt{nlogfrac{n}{b}})

分块的长度也可以得出

询问直接二分查找

BZOJ2724 蒲公英

离线时:

莫队+map+set

map:记录种类和次数的映射

set:动态维护出现次数的排序

在线:

考虑分块

众数只有可能是块的众数或者是两个不完整的块的众数(因为只有完整的块的众数才有可能是答案)

对每一个数x开一个vector表示它出现的位置,然后只需要把位置的下标相减就可以知道这个数出现了多少次

还可以优化

(f[i][j])表示第i个块到第j个块的众数,可以预处理

这样自己只需要在(n/b)这个数量级的块中选一个就行了

BSGS

(a^bequiv c (mod p))

已知(a,c)(b)

根据欧拉定理,如果a和p互质,b一定小于等于p

(B=sqrt p)

大步:(a^0,a^B,a^{2B}...)存到map中

小步:枚举(a^1...a^{B-1})

如果存在B-k满足:(a^{b-k}equivfrac{c}{a^k}(mod p))

(frac{c}{a^k})放到map中查

这样就可以在(sqrt p)的时间内找到答案

块状链表

数组的好处是查询方便,但是插入困难

链表的好处是插入方便,但是查询困难

把链表进行分块处理,就可以平均插入和查询的复杂度

大块直接跳,小块暴力找

插入:在目标位置的大块分裂成两个大块,在中间插入。判断末尾的大块是否可以合并,如果可以就把两个大块合并起来

删除也是一样

以上是关于分块,莫队,BSGS,块状链表的主要内容,如果未能解决你的问题,请参考以下文章

块状链表

块状数组

[NOI2003] 文本编辑器

BZOJ.3920.Yuuna的礼物(莫队 分块套分块)

bzoj 3809 Gty的二逼妹子序列 —— 莫队+分块

BZOJ3217ALOEXT 分块