zoukankan      html  css  js  c++  java
  • [总集] LOJ 分块1 – 9

    [总集] LOJ「分块」数列分块入门1 – 9


    分块9题

    出题人hzw的解析


    (tips.以下代码中IO优化都已省去,想看可以点传送门)


    数列分块入门 1

    修改:区间加

    查询:单点值查询

    这是一道经典题目,线段树、树状数组等都可以搞,这里讲讲分块

    分块就是将一定长度的一段数打包成块,统一处理的算法

    每个块都有自己的信息,自己的标记,统一维护,统一查询

    我们可以将每个区间修改或查询拆分成在若干个整块,和头尾两个不完整的块中修改、查询后信息的总和

    那此题中块要分多大呢?答案是$ sqrt n $

    由于本人太弱,下面给出hzw大佬的证明:

    如果我们把每m个元素分为一块,共有 $ frac{n}{m} $ 块,每次区间加的操作会涉及 $ O( frac{n}{m} ) $ 个整块,以及区间两侧两个不完整的块中至多2m个元素。
    我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接 $ O(1) $ 标记,而不完整的块由于元素比较少,暴力修改元素的值。
    每次询问时返回元素的值加上其所在块的加法标记。
    这样每次操作的复杂度是 $ O( frac{n}{m} )+ O(m) $ ,根据均值不等式,当m取 $ sqrt n $ 时总复杂度最低

    代码

    #include <bits/stdc++.h>
    #define rint register int
    using namespace std;
    const int N=50005,B=225; //B是最大块数
    int n,len,bn; //bn是块数,len是块长
    int L[B],R[B],tag[B],a[N],block[N];
    //L[i]是块i的左边界,R[i]是块i的右边界,tag[i]是块i的统一加法标记
    //a[i]表示元素i不加标记时的值,block[i]表示元素i的所属块
    inline void add(int l,int r,int c){
    	int p=block[l],q=block[r]; //p是左边界的所属块,q是右边界的所属块
    	if(p==q){ //如果左右边界在同一块中
    		for(int i=l;i<=r;++i) //直接对[l,r]每个元素进行修改
    			a[i]+=c;
    		return ;
    	}
    	for(rint i=p+1;i<=q-1;++i)
    		tag[i]+=c; //给整块打上加法标记,就可以忽略块中的元素
    	for(rint i=l;i<=R[p];++i)
    		a[i]+=c; //给左边剩下的元素进行处理
    	for(rint i=L[q];i<=r;++i)
    		a[i]+=c; //给右边剩下的元素进行处理
    }
    signed main(){
    	read(n);
    	for(rint i=1;i<=n;++i)
    		read(a[i]);
    	bn=len=sqrt(n);
    	for(rint i=1;i<=bn;++i){
    		L[i]=(i-1)*len+1;
    		R[i]=i*len;
    		for(rint j=L[i];j<=R[i];++j)
    			block[j]=i; //给没个元素规定所属块
    	}
    	if(R[bn]<n){ //如果没分完就再分一个
    		L[++bn]=R[bn-1]+1;
    		R[bn]=n;
    		for(rint i=L[bn];i<=n;++i)
    			block[i]=bn;
    	}
    	for(rint i=1;i<=n;++i){
    		int opt,l,r,c;
    		read(opt);
    		read(l);
    		read(r);
    		read(c);
    		if(!opt) add(l,r,c); //修改
    		else Write(a[r]+tag[block[r]],'
    '); //查询:自己的值加上自己所属块的统一标记
    	}
    }
    

    数列分块入门 2

    修改:区间加

    查询:区间排名

    这题与1相似,只是需要额外维护一个st[i][j]表示块i从小到大排序后排名第j的数

    用sort暴力维护它的单调性有助于使用lower_bound()直接得到某个块中的排名,再将信息合并即可

    修改部分只需在1的基础上在每次对一个块完成修改后维护它的st[][]即可

    查询部分对于零散小块,暴力枚举判断;对于整块,二分查找确定排名即可得到该块中的答案;最后再将所有块中的答案累加即可

    代码

    #include <bits/stdc++.h>
    #define rint register int
    using namespace std;
    const int N=50050,B=235;
    int n,len,bn,L[B],R[B],tag[B],a[N],block[N],st[B][B];
    //其余含义同一,st[i][j]表示块i从小到大排序后排名第j的数
    inline void maintain(const int &x){ //将块x中元素放入st[][]并重新排序,以维护st[x][]中的有序性
    	memset(st[x],0,sizeof st[x]);
    	for(rint i=L[x];i<=R[x];i++)
    		st[x][++st[x][0]]=a[i];
    	sort(st[x]+1,st[x]+1+st[x][0]);
    }
    inline void add(int l,int r,int c){
    	/*基本同一*/
    	int p=block[l],q=block[r];
    	if(p==q){
    		for(int i=l;i<=r;++i)
    			a[i]+=c;
    		maintain(p); //维护块p
    		return ;
    	}
    	for(rint i=p+1;i<=q-1;++i)
    		tag[i]+=c;
    	for(rint i=l;i<=R[p];++i)
    		a[i]+=c;
    	for(rint i=L[q];i<=r;++i)
    		a[i]+=c;
    	maintain(p); //维护块p
    	maintain(q); //维护块q
    }
    inline int que(int l,int r,int c){
    	int p=block[l],q=block[r],res=0;
    	if(p==q){
    		for(rint i=l;i<=r;i++)
    			res+=(a[i]+tag[p]<c); //直接遍历[l,r]每个元素,判断是否满足条件
    		return res;
    	}
    	for(rint i=p+1;i<=q-1;i++){
    		int x=lower_bound(st[i]+1,st[i]+1+st[i][0],c-tag[i])-st[i]; //利用STL二分查找方便的得到要求值在该块中的排名
    		res+=x-1; //他之前的都是符合条件的,下标减一加入答案即可
    	}
    	for(rint i=l;i<=R[p];i++)
    		res+=(a[i]+tag[p]<c); //暴力处理剩余部分
    	for(rint i=L[q];i<=r;i++)
    		res+=(a[i]+tag[q]<c);
    	return res;
    }
    signed main(){
    	read(n);
    	for(rint i=1;i<=n;++i)
    		read(a[i]);
    	/*基本同一*/
    	bn=len=sqrt(n);
    	for(rint i=1;i<=bn;++i){
    		L[i]=(i-1)*len+1;
    		R[i]=i*len;
    	}
    	if(R[bn]<n){
    		L[++bn]=R[bn-1]+1;
    		R[bn]=n;
    	}
    	for(rint i=1;i<=bn;i++){
    		for(int j=L[i];j<=R[i];j++){
    			block[j]=i;
    			st[i][++st[i][0]]=a[j]; //将初值放入st[][]并排序使有序
    		}
    		sort(st[i]+1,st[i]+1+st[i][0]);
    	}
    	for(rint i=1;i<=n;++i){
    		int opt,l,r,c;
    		read(opt);
    		read(l);
    		read(r);
    		read(c);
    		if(!opt) add(l,r,c); //修改
    		else Write(que(l,r,c*c),'
    '); //查询
    	}
    }
    

    数列分块入门 6

    修改:单点插入

    查询:单点值

    这道题我们采用动态插入的方法

    借助vector的insert过程我们可以方便地完成插入操作

    而插入的位置可以用下面一段代码找到:

    (x初值为所要找的下标,终值为所在块中的下标,p为所在块的编号,vec[]为块所用的存储结构)

    while(x>vec[p].size()) x-=vec[p].size(),p++;
    

    而查询的操作也需要调用上面一段代码即可轻松找到所要位置

    ε = = (づ′▽`)づ

    有本题数据是随机的,以上的部分已经可以应对,但如果不随机呢?

    不停地插入会使某个块变得十分臃肿,使复杂度退化为(O(n^2))

    那该怎么办呢?

    我们采用分块重构,当某个块超过原块长的(sqrt 块长)后,拆掉原有块,组建新的块

    重构部分代码如下:

    	int n=0; //从0开始计数元素数
        for(rint i=1;i<=bn;i++){
            int kkk=vec[i].size();
            for(int j=0;j<kkk;j++)
                a[++n]=vec[i][j]; //将原来块中元素按顺序移入a[]
            vec[i].clear(); //清空原块
        }
        len=sqrt(n); //重新定块长
        for(rint i=1;i<=n;i++)
            vec[(i-1)/len+1].push_back(a[i]); //将a[]中元素重新分块
        bn=(n-1)/len+1; //重新确定块数
        lim=len*sqrt(len); //重新定限制
    

    (tips.LOJ上以及hzw的题解中倍数都定为20倍,其实是(sqrt[4] {n}),比较粗略,可以用我的准确倍数优化,即上方代码的最后一行)

    代码

    #include <bits/stdc++.h>
    #define rint register int
    using namespace std;
    const int N=200050,B=460; //开双倍内存
    int a[N],n,len,lim,bn;
    vector<int> vec[B];
    struct res{int block,pos;};
    inline res get(int x){ //获取位置,见上方解释~~
        int p=1;
        while(x>vec[p].size()) x-=vec[p].size(),p++;
        return (res){p,x-1};
    }
    inline void rebuild(){ //重构,见上方解释~~
        int n=0;
        for(rint i=1;i<=bn;i++){
            int kkk=vec[i].size();
            for(int j=0;j<kkk;j++)
                a[++n]=vec[i][j];
            vec[i].clear();
        }
        len=sqrt(n);
        for(rint i=1;i<=n;i++)
            vec[(i-1)/len+1].push_back(a[i]);
        bn=(n-1)/len+1;
        lim=len*sqrt(len);
    }
    inline void edi(int x,int v){
        res now=get(x);
        vec[now.block].insert(vec[now.block].begin()+now.pos,v); //找到位置后用insert插入
        if(vec[now.block].size()>lim) rebuild(); //如果块大小超过限制就重构
    }
    inline int que(int x){
        res now=get(x); //直接查询
        return vec[now.block][now.pos];
    }
    signed main(){
        read(n);
        for(rint i=1;i<=n;i++)
            read(a[i]);
        len=sqrt(n);
        for(rint i=1;i<=n;i++)
            vec[(i-1)/len+1].push_back(a[i]); //分块
        bn=(n-1)/len+1;
        lim=len*sqrt(len); //定限制
        while(n--){
            int opt,l,r,c;
            read(opt),read(l),read(r),read(c);
            if(!opt) edi(l,r);
            else Write(que(r),'
    ');
        }
    }
    
    

    数列分块入门 7

    修改:区间加,区间乘

    查询:单点查询

    这道题十分经典,洛谷P3373是它的区间求和版,用线段树做

    对于这道题,你需要有一个小学前置知识:乘法优先级比加法高

    因此,在这道题中,对于每个块我们所要维护的两个信息:加法标记和乘法标记,很明显乘法标记优先级是要高于加法标记的

    之后就是分类讨论啦:

    • 设整块x的加法标记为add,乘法标记为mul,给出的操作值为val,则:
    1. 当x要进行一次加法操作时:
    add+=val;
    
    1. 当x要进行一次乘法操作时:
    mul*=val;
    add*=val;
    
    • 对于零块,暴力枚举修改即可

    另外,由于由于零块操作的不确定性,对零块要单独进行一次push操作,用改块的标记去修改该块中的每个元素(先乘后加),并将乘标记归1,加标记归0

    以上就是修改操作

    至于查询,直接将元素值先乘上它的乘法标记,再加上它的加法标记即可

    代码

    #include <bits/stdc++.h>
    #define rint register int
    using namespace std;
    const int mod=1e4+7,N=1e5+20,B=335;
    int n,a[N],block[N],L[B],R[B],mul[B],add[B],bn,len;
    inline void push(const int &x){
        for(rint i=L[x];i<=R[x];i++)
            a[i]=(a[i]*mul[x]%mod+add[x])%mod;
        mul[x]=1; //千万记得要将标记还原!!!本人就在这里错过。。。
        add[x]=0;
    }
    inline void edi(bool flag,int l,int r,int v){ //flag表示当前操作种类,1是乘,0是加
        int p=block[l],q=block[r];
        push(p);
        if(p==q){
            for(rint i=l;i<=r;i++)
                a[i]=flag?(a[i]*v)%mod:(a[i]+v)%mod; //暴力枚举修改
            return ;
        }
        push(q);
        for(rint i=p+1;i<=q-1;i++){
            if(flag){
                (mul[i]*=v)%=mod; //乘法对两种标记都有影响
                (add[i]*=v)%=mod;
            }
            else{
                (add[i]+=v)%=mod;
            }
        }
        for(rint i=l;i<=R[p];i++)
            a[i]=flag?(a[i]*v)%mod:(a[i]+v)%mod; //同理,暴力枚举
        for(rint i=L[q];i<=r;i++)
            a[i]=flag?(a[i]*v)%mod:(a[i]+v)%mod;
    }
    signed main(){
        read(n);
        for(rint i=1;i<=n;i++)
            read(a[i]),a[i]%=mod;
        bn=len=sqrt(n);
        for(rint i=1;i<=bn;i++){
            L[i]=(i-1)*len+1;
            R[i]=i*len;
        }
        if(R[bn]<n){
            L[++bn]=R[bn-1]+1;
            R[bn]=n;
        }
        for(rint i=1;i<=bn;i++){
            mul[i]=1; //记得乘法标记初值为1
            for(rint j=L[i];j<=R[i];j++)
                block[j]=i;
        }
        while(n--){
            int opt,l,r,c;
            read(opt),read(l),read(r),read(c);
            if(opt<=1){
                edi(opt,l,r,c);
            }
            else{
                c=block[r];
                Write((a[r]*mul[c]%mod+add[c])%mod,'
    ');
            }
        }
    }
    
    

    数列分块入门 8

    修改:区间赋值

    查询:区间计数

    区间赋值的操作非常简单暴力,枚举即可

    对于查询,我们发现所有块大致可以被分为2类,即数字相同的,和数字不相同的

    所以我们可以维护一个same[]数组,记录每个块的相同值,不同的话即为-oo

    (tips.oo即无穷,可以选择为0x3f3f3f3f或0x7fffffff之类极值)

    (tips.hzw的代码是可以被hack的,因为他的-oo是-1,而我们知道一个块内的元素是有可能全-1的)

    有了same[]后怎么办呢?

    当然是奇技淫巧 有技巧地暴力啦!

    1. 赋值可以被简化,整块直接O(1)修改same值即可,左右零散块再暴力枚举
    2. 计数时,对于块x,查询值v,可以用下面一段伪代码进行快速判断:
    if(same[x]==-1){
    	from 左边界 to 右边界
    		枚举判断;
    }
    else
    if(same[x]==v){
    	ans+=右边界-左边界+1;
    }
    else{
    	ans+=0;
    }
    
    1. 为了避免冗余操作,在每次要求查询前,对要被查询且same值被修改过的块进行一次push操作,将该块的same值赋给该块中的每一个元素,进行维护

    至于复杂度,让我再引用一次hzw大佬的证明:

    这样看似最差情况每次都会耗费 (O(n)) 的时间,但其实可以这样分析:
    假设初始序列都是同一个值,那么查询是 (O(sqrt n)) ,如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是 (O(sqrt n))

    代码

    #include <bits/stdc++.h>
    #define rint register int
    using namespace std;
    const int N=1e5+30,B=333,oo=0x3f3f3f3f;
    int a[N],block[N],L[B],R[B],same[B],n,bn,len;
    inline void push(const int &x){ //用same维护块内元素,应对暴力枚举的情况
        if(same[x]!=-oo){
            for(rint i=L[x];i<=R[x];i++)
                a[i]=same[x];
            same[x]=-oo;
        }
    }
    inline int work(int l,int r,int v){
        int p=block[l],q=block[r],res=0;
        push(p); //维护左块
        if(p==q){
            if(same[p]==v) return r-l+1; //如果same值满足条件,直接计算出结果
            for(rint i=l;i<=r;i++) //暴力枚举&赋值
                if(a[i]==v) res++;
                else a[i]=v;
            return res;
        }
        push(q); //维护右块
        for(rint i=p+1;i<=q-1;i++){
            if(same[i]==v) res+=R[i]-L[i]+1;
            else{
                if(same[i]==-oo)
                    for(rint j=L[i];j<=R[i];j++)
                        if(a[j]==v) res++;
                        else a[j]=v;
            }
            same[i]=v; //记得把整块same标记更换掉
        }
        /*以下处理方法同上p==q时的情况*/
        if(same[p]==v) res+=R[p]-l+1;
        else
        for(rint i=l;i<=R[p];i++)
            if(a[i]==v) res++;
            else a[i]=v;
        if(same[q]==v) res+=r-L[q]+1;
        else
        for(rint i=L[q];i<=r;i++)
            if(a[i]==v) res++;
            else a[i]=v;
        return res;
    }
    signed main(){
        read(n);
        for(rint i=1;i<=n;i++)
            read(a[i]);
        bn=len=sqrt(n);
        for(rint i=1;i<=bn;i++){
            L[i]=(i-1)*len+1;
            R[i]=i*len;
        }
        if(R[bn]<n){
            L[++bn]=R[bn-1]+1;
            R[bn]=n;
        }
        for(rint i=1;i<=bn;i++){
            same[i]=-oo; //一开始认为块内每个元素都不相同,打个-oo标记
            for(rint j=L[i];j<=R[i];j++)
                block[j]=i;
        }
        while(n--){
            int l,r,c;
            read(l),read(r),read(c);
            Write(work(l,r,c),'
    ');
        }
    }
    
    
  • 相关阅读:
    python打印4个九九乘法表
    mysql语法归纳总结
    帆软BI报表的创建
    jmeter+influxdb+grafana可视化监控接口测试
    jmeter 自动化
    linux安装docker+jmeter分布式
    jmeter连接mysql数据库
    cmd切换盘符
    jmeter接口测试教程
    python3 进程中 获取进程号和杀死进程
  • 原文地址:https://www.cnblogs.com/think-twice/p/11295978.html
Copyright © 2011-2022 走看看