根号类算法讲解——分块,莫队

一、分块

分块算法是比较常见的根号类算法,就是把数分成块换种形式暴力

先放题:

给你一个长度为n(n< 105 10 5 )的序列,有m个操作,操作涉及区间加和区间求和

这个东西可以做各种数据结构的裸题,肯定被各种切掉了吧…

考虑最暴力的思想,就是暴力更改统计每个区间
我们也可以将 m m 个元素分成一块,一共有k=n/m块,对于区间的修改,枚举所有区间内包含的整块,将其打上加标记,而不完整的块最多只会有两个(一个在左面,一个在右面),每个块内元素绝对不会超过 m m 个,这2m个元素就一个一个改就好了
统计时方法类似,这样就可以过啦!

这运用的就是分块的思想,可以发现,单次修改/查询复杂度最坏是 Θ(k+m) Θ ( k + m ) 级别的,因为 km k ∗ m 为定值 n n ,想让k+m最小,即 (k+m)2 ( k + m ) 2 最小,那么 k2+m2+2km k 2 + m 2 + 2 k m 就会最小,根据均值不等式,想让 k2+m2 k 2 + m 2 最小,当且仅当 k=m k = m 时, k2+m2=2km k 2 + m 2 = 2 k m ,所以 mk m 、 k n n 时,算法复杂度最优,每次操作均为 Θ(n) Θ ( n ) ,大多数分块算法块的大小基本上都是 n n

其实换句话说,分块就是高级的暴力

现在我们已经知道了分块到底是什么样个东西,现在我们深入考虑一下这个算法

给你一个长度为n(n<= 106 10 6 )的序列,有m(m<= 3103 3 ∗ 10 3 )个操作,操作涉及区间加和询问区间有多少个数大于等于k(BZOJ[3343])

考虑分块,那么我们要思考如何构造,构造后如何修改、查询
考虑好这些,问题也就迎刃而解了

因为分块是对暴力的一种改进,所以首先考虑暴力怎么做
区间加就不用说了吧…
而对于区间有多少个数大于k,除了最暴力的想法,还可以考虑对区间排序,二分查找出k的位置,就可以求出来了

类似的,在分出的每个块中,对所有元素进行排序,查找时对每个完整的块二分查找,不完整的块暴力搞一搞
完整块修改只需要加个标记即可,不完整的块在修改后要重新排序

核心代码:
将块排序(a数组是排序之前的数列,b数组是排序后的数列)

inline void reset(int x){
    if(x==block[n]) return;
    for(int i=Block_size*(x-1)+1;i<=Block_size*x;i++)
        b[i]=a[i];///直接排a的话下标会有问题
    sort(b+Block_size*(x-1)+1,b+Block_size*x+1);///玄学排序
}

区间加值 blocki b l o c k i 代表 i i 位置属于哪个块,vi是打的加标记

inline void Add(int l,int r,int k){
    if(block[r]-block[l]<2){///如果没有完整的快,就暴力更新
        for(int i=l;i<=r;i++)
            a[i]+=k;
        reset(block[l]);reset(block[r]);///更新之后不忘将块重新排序
        return;
    }
    else{
        for(int i=block[l]+1;i<block[r];i++)///完整的块打标记
            v[i]+=k;
        for(int i=l;i<=block[l]*Block_size;i++)///暴力修改左面不完整的块
            a[i]+=k;
        reset(block[l]);
        for(int i=r;i>=(block[r]-1)*Block_size+1;i--)///暴力修改右面不完整的块
            a[i]+=k;
        reset(block[r]);
    }
}

区间查询

inline int calc(int l,int r,int k){///二分查找
    int tmp=0,x=r;
    while(l<=r){
        int mid=l+r>>1;
        if(b[mid]>=k) tmp=mid,r=mid-1;
        else l=mid+1;
    }
    if(!tmp) return 0;///找不到
    return x-tmp+1;
}
inline void Query(int l,int r,int k){
    int cnt=0;
    if(block[r]-block[l]<2){///没有完整的块,暴力统计
        for(int i=l;i<=r;i++)
            if(a[i]+v[block[i]]>=k) cnt++;
        printf("%d\n",cnt);
        return;
    }
    else{
        for(int i=block[l]+1;i<block[r];i++){///完整的块二分查找
            cnt+=calc(Block_size*(i-1)+1,Block_size*i,k-v[i]);
        }
        for(int i=l;i<=block[l]*Block_size;i++)///左面不完整的块
            if(a[i]+v[block[i]]>=k) cnt++;
        for(int i=r;i>=(block[r]-1)*Block_size+1;i--)///右面不完整的块
            if(a[i]+v[block[i]]>=k) cnt++;
        printf("%d\n",cnt);
    }
}

总结一下,
构造:每个块排遍序,块大小为 n n
修改:给完整的块打标记,不完整的块暴力重构(重新排序)
查询:在每个完整的块中二分查找,不完整的快中暴力判断

几道特别的分块题

给你一个长度为n(n<= 4104 4 ∗ 10 4 )的序列,m次询问区间[l,r]的最小众数(m<= 5104 5 ∗ 10 4 ),询问强制在线(BZOJ[2724])

求区间众数,最暴力的方法就是枚举区间所有的数,枚举时顺便更新
妥妥TLE

可以用分块来优化,先将初始数列离散化,发现区间众数只可能是所有完整的块组成的区间的众数不完整的块中的数
预处理出数组 sumi,j s u m i , j 表示在前 j j 个块中数字i出现了多少次, fi,j f i , j 表示第 i i 块到第j块的众数是多少

统计时枚举不完整的块中的数,初始的出现次数( num n u m )赋为完整块中出现的次数,以后每遇到一次就+1,顺便更新一下答案即可

核心代码:
预处理 sum s u m 数组和 f f 数组

    for(int i=1;i<=n;i++) sum[a[i]][block[i]]++;
    for(int i=1;i<=tot;i++)
        for(int j=1;j<=Block_num;j++)
            sum[i][j]+=sum[i][j-1];///前缀和
    for(int i=1;i<=Block_num;i++){///Block_num是一共多少个块(block[n])
        for(int j=(i-1)*Block_size+1;j<=n;j++){
            num[a[j]]++;
            if(num[tmp]<num[a[j]] || (num[a[j]]==num[tmp] && a[j]<tmp)) tmp=a[j];
            if(block[j]!=block[j+1]) f[i][block[j]]=tmp;///是一个块的末尾,更新i到这个块的众数
        }
        memset(num,0,sizeof num);tmp=0;///tmp存当前众数
    }

查询

        if(block[r]-block[l]<2){///没有完整的块
            for(int i=l;i<=r;i++){
                num[a[i]]++;
                if(num[a[i]]>num[tmp] || (num[a[i]]==num[tmp] && a[i]<tmp)) tmp=a[i];
            }
        }
        else{///num存数出现的次数
            for(int i=l;i<=block[l]*Block_size;i++){
                if(!num[a[i]]) num[a[i]]=sum[a[i]][block[r]-1]-sum[a[i]][block[l]];
                num[a[i]]++;
                if(num[a[i]]>num[tmp] || (num[a[i]]==num[tmp] && a[i]<tmp)) tmp=a[i];
            }
            for(int i=r;i>=(block[r]-1)*Block_size+1;i--){
                if(!num[a[i]]) num[a[i]]=sum[a[i]][block[r]-1]-sum[a[i]][block[l]];
                num[a[i]]++;
                if(num[a[i]]>num[tmp] || (num[a[i]]==num[tmp] && a[i]<tmp)) tmp=a[i];
            }
            t=f[block[l]+1][block[r]-1];///判断一下所有完整块的众数
            if(!num[t]) num[t]+=sum[t][block[r]-1]-sum[t][block[l]];
            if(num[t]>num[tmp] || (num[t]==num[tmp] && t<tmp))
                tmp=t;
        }



给一些点(n<=2105),每个点i有个权值k(k>0),表示点i可以走一步到i+k上,有m(m<= 105 10 5 )个操作,支持更改一个点的k和询问某个点多少次走出去(BZOJ[2002])

这题正解其实是LCT的

记录每个点多少次能跳出自己所在的块( si s i ),和跳到哪个点( pi p i ),这样保证可以 Θ(1) Θ ( 1 ) 时间跳出自己的块,在计算时最多 n n 次就可以跳出去,修改时更改 blocki b l o c k i 的开头到 i i 这一段的s p p 就可以了(要倒着修改)

核心代码:
查询多少次跳出去

inline void Solve(int x){
    int tmp=0;
    while(""){
        tmp+=s[x];
        if(!p[x]) break;///跳出去啦!
        x=p[x];///跳出这个块
    }
    printf("%d\n",tmp);
}

构造/修改

inline void Modify(int x,int k){///修改
    a[x]=k;
    for(int i=x;i>=(block[x]-1)*Block_size+1;i--){
        if(i+a[i]>n) s[i]=1,p[i]=0;
        else if(block[i]==block[i+a[i]])
            s[i]=s[i+a[i]]+1,p[i]=p[i+a[i]];
        else s[i]=1,p[i]=i+a[i];
    }
}
    for(int i=n;i>=1;i--){///构建,要倒着来
        if(i+a[i]>n) s[i]=1;///跳出去啦,s赋为1
        else if(block[i]==block[i+a[i]])///跳一次还是在块内
            s[i]=s[i+a[i]]+1,p[i]=p[i+a[i]];
        else s[i]=1,p[i]=i+a[i];///跳出自己的块
    }



n(n<=104)个东西,每个东西有个颜色,还有m(m<= 104 10 4 )个操作,要求询问l到r区间有多少不同的颜色和修改某个东西的颜色,保证修改操作不超过 103 10 3 次(BZOJ[2120])

这个题啊,换个思维想一下,就完全水爆了

记录 prei p r e i 和前一个第 i i 位颜色相同的是哪一个,在查询区间l~ r r 时只需要在该区间找出pre小于 l l 的记录就可以(pre>l的之前一定被算过了),这样和刚才说的第二题完全一样的啊!
修改时暴力改,有发生变化的点就重建它所在的块就完全OK啦!

随便总结一下

哎呀反正这种玄学东西看到什么序列想一想总是没有什么不好的,分块大法好啊!

二、莫队

莫队是提莫队长的简称一种基于分块思想上的离线算法

莫队是我今天才学会的

然后剩下内容的我明天过几天再写吧(逃

UPD 2018/6/16 半年后我终于填了这个坑….走这里

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值