• 分块入门学习笔记


    首先说明这篇博客写得奇差无比

      让我们理清一下为什么要打分块,在大部分情况下,线段树啊,splay,treap,主席树什么的都要比分块的效率高得多,但是在出问题的时候如果你和这些数据结构只是混的脸熟的话,一旦错误可能就会导致心态崩溃,而且调试困难(大佬:很轻松啊....)所以,分块是一个时间效率不是很高的,代码量也不是很高的数据结构,水分是可以的,在全场都是30分的情况下,你能用分块水到个60,70就是胜利,所以分块很多时候也是和STL一起用的,达到(nlogn√n)的效果吧。


    原理

    把一段数列1...n分成√n块,如果√n*√n<n,√n++。这样能保证每一块的大小都<=√n,我不会证明,但是此时时间复杂度一般为(n√n),就可以开始水分了。

        比如,在我们要解决一段100000左右的序列时,最简单的询问 (l,r)求和 给(l,r)加上一个值。

        大佬A:线段树@#@@¥%@!#(啪)

        大佬B:平衡树%¥……¥#%@@(啪)

        大佬C:树套树@¥%@#%#……(啪)

        那么身为蒟蒻的我:分块!(瑟瑟发抖)

        首先让我们来讲一讲分块是什么

        分块,我这里只是简单的把一段数,放到几个块里面,怎么放呢,按照某个大佬的证明,把每连续的√n个数放在一个块里的时间复杂度一般是最优的,当然有时候√n并不是最优解。

        那么要怎么分呢?

        首先,我们让int tmp=sqrt(当前有多少个数),这样就可以保证有√n块。
        然后如果有任何一块处于(l,r)之间,就给这个块打上标记进行操作。
        这样肯定是有可能会有 l和r 处在两个块中的,而且我们也不能对那两的块直接修改。
        就暴力修改块里面的内容。即每一次的时间复杂度大概为(3√n)。

        所以我们就是要预先处理好每一块的内容,预处理大概是(n√n)的,查询大概是(1)的。

    让我们来看这样一道问题


    题目背景

    此题约为NOIP提高组Day2T2难度。

    题目描述

    众所周知,模数的hash会产生冲突。例如,如果模的数p=7,那么411便冲突了。

    B君对hash冲突很感兴趣。他会给出一个正整数序列value[]

    自然,B君会把这些数据存进hash池。第value[k]会被存进(k%p)这个池。这样就能造成很多冲突。

    B君会给定许多个px,询问在模p时,x这个池内数的总和

    另外,B君会随时更改value[k]。每次更改立即生效。

    保证1<=p<n1<=p<n1<=p<n .

    输入输出格式

    输入格式:

    第一行,两个正整数n,m,其中n代表序列长度,m代表B君的操作次数。

    第一行,n个正整数,代表初始序列。

    接下来m行,首先是一个字符cmd,然后是两个整数x,y

    • cmd='A',则询问在模x时,y池内数的总和

    • cmd='C',则将value[x]修改为y

    输出格式:

    对于每个询问输出一个正整数,进行回答。

    输入输出样例

    输入样例#1:

    10 5
    1 2 3 4 5 6 7 8 9 10
    A 2 1
    C 1 20
    A 3 1
    C 5 1
    A 5 0

    输出样例#1:

    25
    41
    11

    说明

    样例解释

    A 2 1的答案是1+3+5+7+9=25.

    A 3 1的答案是20+4+7+10=41.

    A 5 0的答案是1+10=11.

    数据规模

    对于10%的数据,有n<=1000,m<=1000.

    对于60%的数据,有n<=100000.m<=100000.

    对于100%的数据,有n<=150000,m<=150000.

    保证所有数据合法,且1<=value[i]<=1000.

    注:以下引至大佬阮行止的讲解

    这是一道论文题。集训队论文《根号算法——不只是分块》。

    首先,题目要我们求的东西,就是下面的代码:

    for(i=k;i<=n;i+=p)
        ans+=value[i];
    

    即:从 k开始,每隔p个数取一个数,求它们的和。

    这个算法的复杂度是 (O(n^2)) 的。

    令答案为 (ans[p][k]) ,表示模数是p,余数是k.

    那么,对于第(i)个数,如何处理它对(ans)的贡献呢?

    for(p=1;p<=n;p++) //枚举模数
        ans[p][i%p]+=value[i]; //处理对应的贡献
    

    这样看上去很妙的样子,然而 (O(n^2)) 的预处理, (O(1)) 询问,空间复杂度还是

    (O(n^2))

    所以我们很自然地想到:只处理 ([1,sqrt{n}]) 以内的p

    这样的话,令 (size=sqrt{n})​ ,则可以这样预处理:

    for(p=1;p<=size;p++) //只枚举[1,size]中的
        ans[p][i%p]+=value[] //处理对应的贡献
    

    于是预处理的复杂度降到了 (O(nsqrt{n})) .

    接着考虑询问。如果询问的(p<size) ,那显然可以 (O(1)) 给出回答。

    如果p超过size,我们就暴力统计并回答。因为 (p>sqrt{n})​ ,所以少于 (sqrt{n})​ 个数对答案有贡献。所以对于 (p>sqrt{n})​ ,暴力统计的复杂度是 (O(sqrt{n})) ..

    接着考虑修改。显然我们把p<size的值全都更新一遍就行。复杂度也是 (O(sqrt{n})) .

    void change(int i,int v) //将value[i]改为v
        {
        for(p=1;p<=size;p++)
        ans[p][i%p]=ans[p][i%p]-value[i]+v; //更新答案
        value[i]=v; //更新value数组 
    }
    

    这样,我们就在 (O((m+n)sqrt{n})) .的时间内完成了任务

    这便是分块的思想。


    也许我们要结合图来说明一下

    然后当我遍历一段序列时,比如我要给第4至18加上一个数,那么分块的实现就是第一块(因为只涉及到一部分)的4,5暴力加上那个数,第四块的16,17,18暴力加上那个数,然后给第(2),(3)块打上一个lazy标记,就和线段树一样


    大概的建块过程如下

    void build()
    {
        num=tmp;if(tmp*tmp<n)num++; //因为int向下取整,所以有可能tmp*tmp<n,存不了那么多的数 for(int i=1;i<=num;i++)
        {
            l[i]=num*(i-1)+1;r[i]=num*i; //每一个块的左右区间
        }
        r[num]=n; int s=1 ; for (int i=1;i<=n;i++) {if (i>r[s]) s++; belong[i]=s;} //处理出每一个数所属于的块 //... //你要预处理的内容 
    }
    

     然后查询跟上面也差不多,因题目而异。
     下面贴一个静态查询最大值的代码

    int query(int x,int y)
    { int ans=-1000000; if(belong[x]==belong[y])//在一个块内就直接暴力统计
        { for(int i=x;i<=y;i++)
            ans=max(ans,ch[i]); return ans;
        } for(int i=x;i<=r[belong[x]];i++)//统计最左边的块的情况
            ans=max(ans,ch[i]); for(int i=belong[x]+1;i<=belong[y]-1;i++)//中间的先于预处理好,然后每一块的情况O(1)查询
            ans=max(ans,maxx[i]); for(int i=l[belong[y]];i<=y;i++)//统计最右边的块的情况
            ans=max(ans,ch[i]); return ans;
    }
    

    所以原理大概就讲到这里吧。


    关于做题的分析

    分块的题目有很多,并且尽是暴力 毒瘤 题。那么我们要怎么暴力呢?

    • 第一种情况,关于分块时,我修改值以后,并不需要修改块内的顺序。

    这句话是什么意思呢,就是说,假设,我们当前只需要统计一段区间的和或者xor值等等,那么我们并不需要改变块内数值的顺序,因为块内每个数值的分布,并不会直接影响到我的求和的时间复杂度等等。这简直就是**模版题**。

    这一类型的题目一般就是考验你对lazy数组的理解能力,往往就是标记要打对,因为关于块内的遍历询问,就是直接暴力√n的遍历

    例题

    给出一个长为 nnn 的数列,以及 nnn 个操作,操作涉及区间加法,单点查值。

    输入格式

    第一行输入一个数字 n。

    第二行输入 n 个数字,第 i 个数字为 a_i,以空格隔开。
    接下来输入 n 行询问,每行输入四个数字 opt、l、r、c,以空格隔开。

    若 opt=0,表示将位于 [l,r] 的之间的数字都加 c。

    若 opt=1,表示询问 a_r​​ 的值(l 和 c 忽略)。

    输出格式

    对于每次询问,输出一行一个数字表示答案。

    样例输入

    4
    1 2 2 3
    0 1 3 1
    1 0 1 0
    0 1 2 2
    1 0 2 0
    

    样例输出

    2
    5
    

    • 第二种情况,关于分块时,我修改值以后,数值的位置会影响结果

    这里的结果并不是指会对查询出来的结果有什么影响(目前我做的是这样...),而是对时间复杂度有影响

    比如,我要查询一段序列小于等于一个值的数字有多少个,那怎么写呢?巨老:权值线段树)
    当我们分块完毕时,我们会发现,每个块里面的小于等于一个值的数,我们每次遍历时又不能暴力一个一个去匹配,用一个vis[]数组来统计小于等于的有多少?数值大于1e8你就GG了,何况我们还要一段序列一段序列的修改。

    对于此类题目我们就会感觉的到STL的妙处了,先一个sort,把数列排好序,然后vector直接二分啊,log直接找到当前那个值的位置,那个数的前面数(指包含在当前块内的)就是小于等于查询值的嘛,如果块有标记就让查询值先减去lazy值再查询嘛。
    虽然每一个块的复杂度不再是O(1)了,当然很舒服的是查询好像只要加上一个log的复杂度,修改是√nlog√n的。

    这类题目的实质就是,数值在块内排序后可以二分查找避免暴力,如果是无序的,很明显的就二分查找不了,直接GG,一般都会涉及到STL.
    

    • 第三种问题,关于分块时,我们以大块套小块(前缀和)!

    例题

    题目描述

    神犇SJY虐完HEOI之后给傻×LYD出了一题:

    SHY是T国的公主,平时的一大爱好是作诗。

    由于时间紧迫,SHY作完诗之后还要虐OI,于是SHY找来一篇长度为N的文章,阅读M次,每次只阅读其中连续的一段[l,r],从这一段中选出一些汉字构成诗。因为SHY喜欢对偶,所以SHY规定最后选出的每个汉字都必须在[l,r]里出现了正偶数次。而且SHY认为选出的汉字的种类数(两个一样的汉字称为同一种)越多越好(为了拿到更多的素材!)。于是SHY请LYD安排选法。

    LYD这种傻×当然不会了,于是向你请教……

    问题简述:N个数,M组询问,每次问[l,r]中有多少个数出现正偶数次。

    输入输出格式

    输入第一行三个整数n、c以及m。表示文章字数、汉字的种类数、要选择M次。

    第二行有n个整数,每个数Ai在[1, c]间,代表一个编码为Ai的汉字。

    接下来m行每行两个整数l和r,设上一个询问的答案为ans(第一个询问时ans=0),令L=(l+ans)mod n+1, R=(r+ans)mod n+1,若L>R,交换L和R,则本次询问为[L,R]。

    输出格式:

    输出共m行,每行一个整数,第i个数表示SHY第i次能选出的汉字的最多种类数。

    输入输出样例

    输入样例#1:

    5 3 5
    1 2 2 3 1
    0 4
    1 2
    2 2
    2 3
    3 5

    输出样例#1:

    2
    0
    0
    0
    1

    说明

    对于100%的数据,1<=n,c,m<=10^5

    不带修改,直接查询,是不是很舒服?但是正偶数次是不是有点难处理,是的,这道题不是很难但是很难调。
    这个思想只是分块里面的一个小小的优化,用一个ans[i][j]保存第i块到第j块内符合条件的数字的个数,o(1)的查询,这种卡常的题目及其恶心。


    最后认为自己学好的julao们,把hzwer大佬的分块入门九题写一写,就o**k了


    现在我们来讲一些骚操作


    块状链表

    (我没有打过,很慌)
    就是把分块连起来

    这是插入操作
    先暴力加入这个块,当这个块大到影响时间复杂度的时候,我们有两种方法处理

    1. 从加入处重新断开构成多个块
    2. 把这两个块加上要插入的一段数暴力重构

    至于怎么连接,我们肯定是把每个块用链表结构连起来,

    for(int i=1;i<=n;i++)
    {
    	if(bl[i]+1==tmp)continue;
    	next[bl[i]]=bl[i]+1;
    	if(bl[i]!=bl[i+1]){
    	head[bl[i]]=i;
    	}
    }
    

    ps:我也没有写过,不知道对错

    删除同理嘛,设置一个上限参数,当前块内含有的数过少,这样的块如果超过一定数量,就暴力合并。

    这个等我没那么菜了以后再完善


    树上分块

    这是我在树剖里面讲解过的。
    例题有两道
    [SCOI2005]王室联邦

    这一道是告诉我们是怎么去分块的

    Description

      “余”人国的国王想重新编制他的国家。他想把他的国家划分成若干个省,每个省都由他们王室联邦的一个成
    员来管理。他的国家有n个城市,编号为1..n。一些城市之间有道路相连,任意两个不同的城市之间有且仅有一条
    直接或间接的道路。为了防止管理太过分散,每个省至少要有B个城市,为了能有效的管理,每个省最多只有3B个
    城市。每个省必须有一个省会,这个省会可以位于省内,也可以在该省外。但是该省的任意一个城市到达省会所经
    过的道路上的城市(除了最后一个城市,即该省省会)都必须属于该省。一个城市可以作为多个省的省会。聪明的
    你快帮帮这个国王吧!

    Input

      第一行包含两个数N,B(1<=N<=1000, 1 <= B <= N)。接下来N-1行,每行描述一条边,包含两个数,即这
    条边连接的两个城市的编号。

    Output

      如果无法满足国王的要求,输出0。否则输出数K,表示你给出的划分方案中省的个数,编号为1..K。第二行输
    出N个数,第I个数表示编号为I的城市属于的省的编号,第三行输出K个数,表示这K个省的省会的城市编号,如果
    有多种方案,你可以输出任意一种。

    Sample Input

    8 2
    1 2
    2 3
    1 8
    8 7
    8 6
    4 6
    6 5

    Sample Output

    3
    2 1 1 3 3 3 3 2
    2 1 8

    直径指两个点之间的距离,指它们之间算上它们有多少个点

    每一种题型都有相应的分法,怎么分看julao你。

    这一道是真正的练习题[WC2013]糖果公园


    等博主什么时候没这么菜了就更新了

  • 相关阅读:
    《Node.js 包教不包会》
    (转)Java并发编程:线程池的使用方法
    (原创)定时线程池中scheduleWithFixedDelay和scheduleAtFixedRate的区别
    JAVA线程本地变量ThreadLocal和私有变量的区别
    (原创)确保JAVA线程安全的4种常用方法
    (转)ReentrantLock可重入锁的使用场景
    (原创)JAVA阻塞队列LinkedBlockingQueue 以及非阻塞队列ConcurrentLinkedQueue 的区别
    读/写锁的实现和应用(高并发状态下的map实现)
    Jira API传字符串的换行问题 (文本编辑器使用)
    使用泛型SwingWorker与EDT事件分发线程保持通讯
  • 原文地址:https://www.cnblogs.com/hhh1109/p/9184635.html
Copyright © 2020-2023  润新知