分块算法入门:这次菜鸟真的看得懂!

2020.2.11
#写在前面的话:

庚子鼠年,楚地大疫,皆无舟车,举城闭户。然四海同心,九州一力,同仇敌忾。有医名曰南山,带白甲千万,御雷火二神兽,力挽狂澜,战数月,灭疫千里,国泰民安。太史公曰,至此千年,天下之幸,非仗天地庇佑,乃仁治而得太平,医者无私,军民无畏,至此,华夏一族,当立万年!

**致敬医务工作者们!!**游子在外,没能帮上太大的忙,哪怕是一起面对,我很惭愧。希望那边快点好起来吧,前方的工作者们,注意安全!

正文
为什么突然开始学分块了呢?当然是因为线段树我不会啊! 这都要归功于ICPC的一道题,Travelling Merchant, 起初我觉得这道题是个线段树维护区间最大最小值嘛,题解也写了,这几天翻出来仔细研究的时候发现这题没我想得那么简单,除了最值,还有顺序,线段树就是把大问题分成一个一个的子问题,现在要找顺序,这我怎么可能会搞嘛。后来搞oi的学弟给我贡献了点思路,就是这个结论是可以向后转移的,可是我还是不会啊,区间合并别说想了,给我个代码看都看不太懂,那怎么办,学弟说只能分块,好像Mr.Skirt老哥也这样说的,我说好吧这个我也不太会,但我可以学鸭!

其实我原先对于分块是拒绝的,我从小看到根号头就大,加上有人说大部分分块的题其实线段树也能做,就没去深入了解这个算法。扒出来资料自己看了看,我发现这个真不简单,学弟的教练说得一句话我感觉特别对,如果一个方法能做出来一道题,那不叫好方法,如果一道题的方法能够推广到很多题上,这个方法才是真的好。我一看,有一说一,确实,分块比我想象中的要简单。不过我说这句话并不容易,因为OI爷们的码风各异,我看了n份教程,基本上没有两个是一样的,有些代码我在某个地方看不懂,然后换了份题解,发现又看不懂了,前几天就在看不懂的边缘试探。不过后来终于爬出来了,终于搞懂这个东西的时候我才发现,是学的方式有点问题。读惯了商业代码的我对于OI爷们用的前向星和快读诸如此类的东西真的是既爱又恨,好用是真的好用,但是难看懂也是真的难看懂,有一两个能够按我的习惯来的都被我当成宝贝供着,不过这些都不要紧,这是算法竞赛,区别于隔壁某论文排版大赛,会做题就行了,要不人家怎么叫爷呢?我花了好长时间搞懂left和right到底指什么,然后自己试图通过自己的理解把这些总结成自己的商业风代码,终于我做到了!我看懂之后不禁感叹,我了割草,简直一条捷径啊,只要数据不卡,线段树能做的这玩意儿都能做,很多线段树不能维护的信息也都可以轻松维护,那么我们来看看这个分块的具体步骤吧。

分块,顾名思义,就是把信息分成一块一块(废话!), 我觉得这个例子不够恰当。我觉得如果各种树形结构是私家车,那么分块就是自行车+地铁。在一个城市i如果我要到一个地方,那么毫无疑问,私家车是不二之选,方便,快。然而,私家车也存在一些解决不了的问题,一个是门槛高,不是什么人都买的起,树形结构往往涉及到大量的递归,逻辑上很复杂,代码量大。另一个是,有时候会堵车啊单双号禁行啊,让你查区间众数,用线段树主席树基本不可能维护,就算能维护出来,也会大大超出题目的时间范围。所以我们需要一点更好的办法。

假设城市地铁站密度够大的话,那么我们可以用地铁啊,好处是便宜,不是人人都买的起车,但一定所有人都坐得起地铁。二是地铁可以走一些车走不了的地方。但是缺点也很明显,在郑州一般我坐地铁都得提前个二十分钟走,因为家离地铁站的距离虽然可接受,但是远,所以虽然地铁不堵车,但是我到地铁站这一段距离仍然省不掉骑车。可能到这里听起来有点晕了,那么我么开始正题。

分块呢,就是把数据分成根号N组,然后每一组都有需要维护的信息,可以是区间和,可以是最大值,最小值,也可以是其它的。当我们查询一个区间的时候就好比要到哪个地方去,在大多数情况下,两者之间如果有地铁站,傻子才会骑车过去,如果中间没有地铁站,在假设地铁站到处都是的情况下,说明这两点之间的距离应该不远,骑车应该也用不了多久对吧?

这里,那根号N组就是我们的车厢!!!我查一段区间的信息,比如区间求和,那么我如果提前处理好根号n个区间和,然后打包好变成一个块,我们下次需要调用的时候直接就可以把这一块代表的区间和加到我们的答案里对不对?你可以一次跳过根号N个数据。比如你要查现在1-8区间的总和,而块的大小是3,线段树的做法是递归找到[1,8]的节点返回区间和和inc增量,而分块的做法是,我们在开始的时候先把数组分成根号N块,每块暴力计算出区间和和左右端点。求和的时候,我们依然暴力求1-3, 7- 8 的区间和,但是对于4-6,我们已经预处理过这一块的和了,我们显然不需要4-6挨个去问,我们直接加上这一块的和就可以了,就是这么简单。回到刚才的例子,1-3和7-8暴力搜查类似于例子里的自行车,慢是慢,不过没有多少路,还可以忍受,4-6就是坐地铁经过的路程了,快,但是覆盖区域比汽车要小。现在应该很清楚分块是怎么回事了吧?

别看在这里面好像并没有比logN好到哪里去,但是我们的目标是卡过测评机的时限,NOIP赛场上自不用说,暴力搜索肯定被送回去学文化课,就算数据会卡,在写不出树的情况下用分块虽然拿不到全分,拿部分分也比什么都没有好。ACM对时间要求没有那么死,而且以前没有OI基础的同学学起树学来,肯定没有分块简单容易上手。那么就来看看这种数据结构吧。

首先分块的组成部分包括三个,一个是块状链表,一个是映射数组,另一个是原数组。具体长这个样子
分块算法入门:这次菜鸟真的看得懂!_第1张图片
块状链表:主要参数为left,right,和需要维护的参数。left和right很好理解,就是代表这块链表所覆盖的起始端点,一个块的大小是根号n的,不过具体是多少看实际情况,跟数据有关,总之目标只有一个,尽量减少自行车的利用率。

映射数组:这个一般叫做belong数组或者pos数组,咱们有块状链表了,但是每个块包含根号N个点,我们怎么知道要去查哪个?这里pos数组的作用就是查询要第i个点在第几块。比如你有10个数字0-9,第1,2,3块就属于第一块,如果我查询pos[i], i = 1,2,3, 就会告诉我是1, 依次递推。

原数组:我们分块有根号N块,然而每次询问不可能刚好有根号N那么大,或者说正好错开,这时候我们想出来一个策略,别忘了我们刚才说什么,如果亮点之间有地铁我们首先坐地铁,那么地铁站两边覆盖不到的,我们就骑自行车过去。这里,如果通过pos数组我得知我的起始端点在一个块里,这样我就可以暴力在原数组上进行我们*猥琐* 的操作,如果不是同一块里,我们先计算要骑自行车那部分,具体等同于上述操作。

操作流程,重点来了,我们路程中覆盖过的块直接调取信息就行了,比如这里我们查最大值,我们只需要先比较左右区间剩下的边角料的最大值,再和块内已经储存的最大值进行比较再return就ok了。那句话,我知道这根号n个位置上的最大值是1,我还用关心这个区间每个值是多少么??对于其他的也是同理,复杂度根号,完全符合大多数题目的要求。而且想一下,这个方法可以推广到很多区间询问的题上,比如查区间求和,我们可以维护一个sum和一个inc,类似于线段树,用上述思想进行操作,可不可以?再比如区间众数,这个不带修改的话用线段树得套n个set才能搞出来,但是分块就能轻易得写出来。还有之后会学到的莫队,本质上都是分块的思想。这个方法学会有很多不可做题就可以做了,难道不是么?

分块需要注意的点,块大小为sqrtn,但是往往这个大小是不够用的,那么如果我们最后一块的右端点right没有覆盖到n怎么办,当然是再加一块啦。
代码如下,我把left和right数组具象成struct的封装内容了,理解起来应该没那么难。下面是一道POJ3264 的例题

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;
#define limit 500000 + 5//防止溢出
#define INF 0x3f3f3f3f
#define inf 0x3f3f3f3f3ff
#define lowbit(i) i&(-i)//一步两步
#define EPS 1e-6
#define Modulo 1000000
#define FASTIO  ios::sync_with_stdio(false);cin.tie(0);
#define ff(a) printf("%d\n",a );
#define MOD 1000000000 + 7
#define midd l + (r - l ) / 2
#define curr tree[kth][root]
#define mint(a,b,c) min(min(a,b), c)
#define FOPEN freopen("C:\\Users\\administrator01\\CLionProjects\\untitled24\\data.txt", "rt", stdin)
typedef long long ll;
void read(int &x){
    char ch = getchar();x = 0;
    for (; ch < '0' || ch > '9'; ch = getchar());
    for (; ch >='0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
}//快读

int read(){
    int x;
    char ch = getchar();x = 0;
    for (; ch < '0' || ch > '9'; ch = getchar());
    for (; ch >='0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
    return x;
}//快读
void write(int x){
    if(x / 10) write(x / 10);
    putchar(x % 10 + '0');
}
struct node{
    int l, r;
    int maxx, minn;
    node():l(0),r(0), maxx(-INF) , minn(INF){}
    int size(){
        return r - l + 1;
    }
}block[limit];
int pos[limit];//映射
int n , m;
int a[limit];
int blo;
void calc(){
    for(int i = 1 ; i <= blo ; ++i){
        block[i].l = (i - 1) * sqrt(n * 1.0) + 1;
        block[i].r = i * sqrt(n * 1.0);
    }
    if(block[blo].r < n){
        ++blo;
        block[blo].l = block[blo - 1].r + 1;
        block[blo].r = n;
    }
    for(int i = 1 ; i <= blo ; ++i){
        for(int j = block[i].l ; j <= block[i].r ; ++j){
            pos[j] = i;
            block[i].maxx = max(a[j] ,block[i].maxx);
            block[i].minn = min(a[j] ,block[i].minn);
        }
    }
}
int querymin(int l , int r){
    int vs = pos[l] , ve = pos[r];
    int ret = INF;
    if(vs == ve){
        
        for(int i = vs; i <= ve ; ++i){
            ret = min(ret , a[i]);
        }
        return ret;
    }else{
        for(int i = l ; i <= block[vs].r ; ++i){
            ret = min(ret , a[i]);
        }
        for(int i = vs + 1 ; i < ve ; ++i){
            ret = max(ret, block[i].minn);
        }
        for(int i = block[ve].l ; i <= r ; ++i){
            ret = min(ret, a[i]);
        }
        return ret;
    }
}
int querymax(int l , int r){
    int vs = pos[l] , ve = pos[r];
    int ret = -INF;
    if(vs == ve){
        for(int i = vs; i <= ve ; ++i){
            ret = max(ret , a[i]);
        }
        return ret;
    }else{
        for(int i = l ; i <= block[vs].r ; ++i){
            ret = max(ret , a[i]);
        }
        for(int i = vs + 1 ; i < ve ; ++i){
            ret = max(ret, block[i].maxx);
        }
        for(int i = block[ve].l ; i <= r ; ++i){
            ret = max(ret, a[i]);
        }
        return ret;
    }
}
int main(){
#ifdef LOCAL
    FOPEN;
#endif
    scanf("%d%d" , &n, &m);
    blo = sqrt(n * 1.0);
    for(int i = 1 ; i <= n ; ++i)scanf("%d", &a[i]);
    calc();
    for(;m--;){
        int l,r;
        scanf("%d%d" , &l, &r);
        printf("%d\n", querymax(l, r) - querymin(l, r ));
    }
    return 0;
}

希望大家都能学会吧

你可能感兴趣的:(分块算法入门:这次菜鸟真的看得懂!)