分块学习笔记

在一次的luogu比赛后,问学长一道题的做法,学长说了句分块,那是第一次听说分块,那道题我的第一直觉是线段树可是我线段树不好写不出来,学长的一句分块让我有了一种用分块代替线段树的想法。我在网上搜博客的时候看到了hzwer大神的分块练习1~9某位博主说从学习开始四天做完这九道题,我就暗下决心怎么样四天之内也要写完吧,最好更快。然而现在我心态爆炸,已经没有心情继续写下去,所以原定写完九道题再写的总结,还是在我忘掉之前写完吧。。。。。。

希望退役之前我可以把1~9写完吧。

分块,说白了就是大暴力,把原来的区间分成一块一块的,区间修改时把包含在这个区间中的各个块进行标记修改,不在一整块中的部分即最左端和最右端进行暴力修改,(例如:在9个数的序列中修改第2位到第7位,

image

9个数可以分为3块(一般每个块的长度均为根号下n,我也不知道为什么,只知道这样操作确实快,唉,我太弱了) ,每块长度为3,

image

那么2在第一块,7在第3块,

image

所以从2开始修改,

image

修改到其所在块的最右端,接着一整块都包含在修改区间内,

image

所以标记此块,而到下一块我们发现我们需要修改的区间此块只包含7,所以我们只修改7)

image

对于这样处理起来可以节省很多时间,这就是分块,时间复杂度o(√n)。
在loj上有hzw的九道数列分块入门题

image.png

每道题的难度显而易见了

数列分块入门1:

操作:区间加法,单点查值

线段树和树状数组都可以进行的经典操作,而分块怎么进行呢,其实就和上面的讲解一样,定义一个标记数组存储下区间内每个数需要修改的值,对于直接暴力的地方(如:上面的2,3和7)直接在原数组上修改,输出时需要记得加上标记数组中的值。
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
inline long long read(){//读入优化
    long long x = 0; long long f = 1; char c = getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();}
    while(c<='9'&&c>='0'){x=x*10+c-'0';c=getchar();}
    return x*f;
}
int n;
int m;
int opt,l,r,c;
int z[500521];//原数组
int pos[500521];//存储每个点所在的块
int tag[500521];//标记数组
void modify(int l,int r,int c){
    if(pos[l] == pos[r])//如果在同一个块内
//直接暴力修改
        for(int i = l; i<=r; i++)
        z[i] += c;
    else {
//修改右边不在一整块中的部分
        for(int i = l; i<=pos[l]*m; i++)
        z[i] += c;
//标记每个块需要加上的值
        for(int i = pos[l]+1; i<=pos[r]-1; i++)
        tag[i] += c;
//修改左边不在一整块中的部分
        for(int i = (pos[r]-1)*m+1; i<=r; i++)
        z[i] += c;
    }
    
}
int main(){
    n = read();
    m = sqrt(n);//开方操作,确定每块的长度,sqrt在math库中需要加上
    for(int i = 1; i<=n; i++) z[i] = read();
    for(int i = 1; i<=n; i++) pos[i] = (i-1)/m+1;//计算每个点所在的块可以算一下进行证明
    for(int i = 1; i<=n; i++){
         opt = read();
         if(opt == 0){
            l = read();
            r = read();
            c = read();
            modify(l,r,c);
         }
         if(opt == 1){
            l = read();
            r = read();
            c = read();
//最后输出的值就是该点的值加上该点所在块的标记值(即需要加上的值)
            printf("%d\n",z[r]+tag[pos[r]]);
         }
    }
    return 0;
}

其中修改操作为了简便也可以这样写

void modify(int l,int r,int c)
{
//修改左边不在区间中的部分
    for(int i=l;i<=min(pos[l]*m,r);i++)
        z[i]+=c;
//修改右边不在区间中的部分
    if(pos[l]!=pos[r])
        for(int i=(pos[r]-1)*m+1;i<=r;i++)
            v[i]+=c;
//修改中间的块
    for(int i=pos[l]+1;i<=pos[r]-1;i++)
        tag[i]+=c;
}

数列分块入门2:

操作:区间加法,询问区间内小于某个值 xxx 的元素个数

如果说我们每个数都去比较一遍,肯定是会t的,我们学分块主要还是学分块思想,我们对于每个块进行操作查询每个块中小于某个值 xxx 的元素个数,不就可以大大节省时间了吗?那由此可知为了更快获得小于某个数的个数,我们必须要求块内数据有序。
黄大神采用的是vector,开个二维vector<int>v[xxxx],利用vector的lower_bound操作。同时我们需要注意一点,在原数据有序的基础上进行加减,原数据次序并不会比打乱,所以修改包含的块直接修改,但是对于那些并不是在一整块中的数据进行修改时,原次序可能会被打乱,所以对于这一部分数据要进行重新排序

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#include<vector> 
using namespace std;
inline long long read(){
    long long x = 0; long long f = 1; char c = getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();}
    while(c<='9'&&c>='0'){x=x*10+c-'0';c=getchar();}
    return x*f;
}
long long  n;
long long  m;
long long  opt,l,r,c;
long long  z[5211314];
long long  pos[5211314];
long long  tag[5211314];
vector<long long>v[1314];
void reset(long long  x){
    v[x].clear();//清空该块内的元素
     //重新插入
    for(long long  i = (x-1)*m+1; i<=x*m; i++) v[x].push_back(z[i]); 
    // 重新排序
    sort(v[x].begin(),v[x].end());
}
void modify(long long  l,long long  r,long long  c){//修改函数
    if(pos[l] == pos[r]){//在同一块内
        for(long long  i = l; i<=r; i++)
        z[i] += c;
         // 排序只是在vector中有序,因为是原数组修改,所以需要清空此块重新插入进行排序
        reset(pos[l]);
    }
    else {
        for(long long  i = l; i<=pos[l]*m; i++)
        z[i] += c;
 // 排序只是在vector中有序,因为是原数组修改,所以需要清空此块重新插入进行排序
        reset(pos[l]);
//对块进行标记时,区间加法并不会影响序列次序,所以只需要标记块
        for(long long  i = pos[l]+1; i<=pos[r]-1; i++)
        tag[i] += c;
        for(long long  i = (pos[r]-1)*m+1; i<=r; i++)
        z[i] += c;
 // 排序只是在vector中有序,因为是原数组修改,所以需要清空此块重新插入进行排序
        reset(pos[r]);
    }
}

long long query(long long  l,long long  r,long long  f){
    long long  ans = 0;
    if( pos[l] == pos[r]){
        for(long long  i = l; i<=r; i++)
        if(z[i] + tag[pos[i]] < f)
        ans++;
        return ans;
    }
    else {
        for(long long  i = l; i<=pos[l]*m; i++)
            if(z[i] + tag[pos[i]]< f)
            ans++;
        for(long long  i = pos[l]+1; i<=pos[r]-1;i++){
            long long  t = f - tag[i];
    //lowe_bound返回第一个大于或等于t的位置,减去begin得到区间内元素个数
            ans += lower_bound(v[i].begin(),v[i].end(),t) - v[i].begin();
        }
        for(long long  i = (pos[r]-1)*m+1; i<=r; i++)
            if(z[i] +tag[pos[i]] < f)
            ans++;
    }
    return ans;
}
int main(){
    n = read();
    m = sqrt(n);
   //预处理每个点所在的快
    for(long long  i = 1; i<=n; i++) pos[i] = (i-1)/m+1;
    for(long long  i = 1; i<=n; i++) {
        z[i] = read();
      //将数据z[i]存入所在的块pos[i]
        v[pos[i]].push_back(z[i]); 
    }
     //利用sort把每个块内的数据排好序 begin和end 代表所要排序的范围即整个v[i][~]‘’
    for(long long  i = 1; i<=pos[n]; i++)sort(v[i].begin(),v[i].end());
    for(long long  i = 1; i<=n; i++){
         opt = read();
         if(opt == 0){
            l = read();
            r = read();
            c = read();
            modify(l,r,c);
         }
         if(opt == 1){
            l = read();
            r = read();
            c = read();
            printf("%lld\n",query(l,r,c*c));
         }
    }
    return 0;
}

数列分块入门3:

操作:涉及区间加法,询问区间内小于某个值 xxx 的前驱(比其小的最大元素)
这个题让我相当难受,,,hzw写的用set来实现,而我感觉用vector也能实现,又鉴于我set并不了解,所以努力实现vector版本的却无奈于实力有限,在打算放弃的时候恰巧翻到一篇vector实现的博客,所以就按照那份代码改,最后还是没改出来,wa了一页多,唯一的ac是我为了验证这份代码的正确性,还是等什么时候改出来再发吧,,,其实就是和二差不多一样的,不知道哪里错了,就是改不出来,头疼

数列分块入门4:

操作:区间加法,区间求和
操作还是不完整的块暴力,完整的标记区间,不过需要预处理出每个块的和。
修改时,修改每个点或块的同时,需要修改每个块的和

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
using namespace std;
inline int read(){
    int x = 0; int f = 1; char c = getchar();
    while(c<'0'||c>'9'){
        if(c == '-')f=-f;
        c=getchar();
    }
    while(c<='9'&&c>='0'){
        x=x*10+c-'0';
        c=getchar(); 
    }
    return x*f;
}
long long n;
long long m; 
const int maxn = 5211314;
long long  z[maxn];//数组不能随便拿long long定义我还不知道为什么
long long  pos[maxn];//刚刚做题遇到两次long long 定义MLE的情况
long long tag[maxn];//只是两天前做的就没改
long long  sum[maxn];
long long opt,l,r,c;
long long ans;
void modify(long long l,long long r,long long v){
    for(int i = l; i<=min(pos[l]*m,r); i++) {
        z[i] += v;
        sum[pos[i]] += v;
    }
    if(pos[l] != pos[r]){
         int t = pos[r];
        for(int i = (pos[r]-1)*m+1; i<=r;i++){
            z[i] += v;
            sum[t] += v;  
        }
    }
    for(int i = pos[l]+1; i<=pos[r]-1; i++){
        tag[i] += v;
        sum[i] += v*m;
    }
    
}
long long query(long long l,long long r){
    int h = pos[l];
    for(int i = l; i<=min(pos[l]*m,r); i++){
            ans += z[i] + tag[h];
    }
    if(pos[l] != pos[r]){
        int t = pos[r];
        for(int i = (pos[r]-1)*m+1; i<=r; i++){
            ans += z[i]+tag[t];
        }
    }
    for(int i = pos[l]+1; i<=pos[r]-1; i++){
        ans += sum[i];
    }
    return ans;
}
int main(){
    n = read();
    m = sqrt(n);
    for(int i = 1; i<=n; i++){
        z[i] = read(); 
        pos[i] = (i-1)/m+1;
        sum[pos[i]] += z[i];
    }
    for(int i = 1; i<=n; i++){
        opt = read();
        l = read();
        r = read();
        c = read();
        if(opt == 0)modify(l,r,c);
        else printf("%lld\n",query(l,r)%(c+1));
        ans = 0;//每次计算完需要重新赋初值
    }
    return 0;
}

数列分块入门5:

操作区间开方,区间求和
九个题当中除了第九题留给了莫队去解决,这是唯一一道没有写的题,区间开方?搞不明白,,,我太弱了

数列分块入门6:

操作:单点插入,单点询问
在loj上,这九道题中这道题的通过率仅次于第一道题,但这道题挺重要,我感觉难度上要比前几道题大一点,这道题由于插入的存在原分好的块内数据就会发生改变,而且块越来越大十分影响效率,所以需要重新分块。
//我代码还没理解透,以后再放吧。。。

数列分块入门7:

**操作:区间乘法,区间加法,单点询问****
非常经典的操作,在luogu上 线段树2和本题完全相同,还有和[AHOI2009]维护序列也是一样的,可是我一直调不出来,直接导致心态爆炸,还是太弱了。

数列分块入门8:

操作:操作涉及区间询问等于一个数 ccc 的元素,并将这个区间的所有元素改为 ccc
我的想法是和2差不很多,在每个块内使用lower_bound进行查找ccc元素,修改时使用懒标记,对于左右两个端点,暴力查找,然后实现所在块内的懒标记,后直接修改值。
可是和7一样,他们一起让我心态爆炸,,,

数列分块入门8:

操作:查询区间最小众数
这其实是莫队的题吧,用分块的话,基于桶排,每出现一个数下标为此数的数组++。同时记录数组最大值和下标即众数,每次进行比较,就能得出答案。不过,我还是感觉使用莫队,可以更快的得出答案,毕竟莫队不需要像分块重新去查找

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354