题解 P4939 【Agent2】

Suuon_Kanderu

2020-03-04 19:32:05

Solution

有树状数组的地方,就少不了线段树。有线段树的地方,就少不了分块。~~我自己编的~~ **分块是一种很容易理解数据结构,他可以维护很多的东西**~~当然我都不会~~,本题解旨在让只能得暴力分的同学AC本题!学会分块的思想以及方法,会比较详细。 - 分块的思想 所谓分块,顾名思义,就是把一组数据分为若干块,从而提高效率。 举个例子:来看一组数据1,5,6,4,5。 我们把他分成$\sqrt{n}$个块(不同的情况块的数量不同,就是这样 | 编号 | 数值 | 第几块 | | ----------- | ----------- | ----------- | | 1 | 1 | 1 | | 2 | 5 | 1 | | 3 | 6 | 2 | | 4 | 4 | 2 | | 5 | 5 | 3 | 如果我们要让编号一到五加1,可以这样做: 1. 把第一块(1)的值 暴力 加1 2. 把中间块(2)的每个块的标记+1 (如何打标记?只要再开一个数组,记录每一块的统一加的值即可,相信学过线段树的同学一定很熟悉) 3. 把最后一块(3)的数值 暴力 加1 然后就是这样 |编号 | 数值 | 第几块 | | ----------- | ----------- | ----------- | ----------- | | 1 | 2(1+1) |1 | | 2 | 6 (5+1)| 1 | | 3 | 6 | 2 | | | 4 | 4 | 2 | | | 5 | 6 (5+1)|3 | |第几块 | 标记 | | ----------- | ----------- | | 1 | 0 | | 2 | 1(0+1) | | 3 | 0 | 区间查询同理,只是暴力加的时候要加上打的标记。 单点查询就是把标记和数值加起来就OK。 - 具体实现 实现时我们要考虑一些细节。 1. 如果要操作的两个端点在一个块内,直接暴力相加。 2. 如果左端点或右端点本来就在块的开头或末尾,就别加了。 还是很好理解的 - 代码实现(本题 ```cpp #include<stdio.h> #include<iostream> #include<algorithm> #include<cmath> #include<string.h> using namespace std; const int N=10000000+100; struct fk{//分块大法 int num,rank;//数值,第几块 }a[N]; int tag[N];//标记 #define rank(x) a[x].rank #define tag(x) tag[x] #define num(i) a[i].num int n,bs,zo;//共有几个数,块的大小 ,总共有几个块 int qsum(int l){ return a[l].num + tag(rank(l));//单点查询 } void change2(int l,int r,int add){ for (int i=l; i<=min(rank(l)*bs,r); i++)num(i)+=add;//这个是前面的块,这里后面解释 if (rank(l)!=rank(r)) for (int i=(rank(r)-1)*bs+1; i<=r; i++)num(i)+=add;//这个是后面的块,这里后面解释 for (int i=rank(l)+1; i<=rank(r)-1; i++) tag(i)+=add;//这个是整块处理 } void pr(){ printf("\tid\tnum\trank\n"); for(int i=1;i<=n;i++)printf("\t%d:\t%d\t%d\n",i,num(i),rank(i)); printf("\n\tid\ttag\n"); for(int i=1;i<=zo;i++)printf("\t%d:\t%d\t\n",i,tag(i)); } signed main(){ cin>>n;bs=sqrt(n); if(n/bs*bs==n)zo=n/bs; else zo=n/bs+1 //会不会有不完整的块; int k;cin>>k; for(int i=1;i<=n;i++){ a[i].num = 0; a[i].rank=(i-1)/bs+1; } int l,r; while(k--){ int op; scanf("%d",&op); if(op==0){ scanf("%d%d",&l,&r); change2(l,r,1); } if(op==1) { scanf("%d",&l); printf("%d\n",qsum(l)); } } return 0; } ``` (大佬自行跳过此区域) 解释一下处理前面的块和后面的块的代码 min(rank(l)*bs,r) rank(l) $\times$ bs就是第 I 个块 乘上 块的大小,很容易明白,就是块的最后一个元素。 用前面的数列,比如 rank(1) $\times$ 2 = 1 $\times$2 = 2 果然是第一个块的最后一个元素。 min()里面有个 r 是考虑左端点和右端点可能在一个块里,如果是,那么就直接暴力全加完,后面的操作其实都没执行。 (rank(r)-1)*bs+1同理,就是块的第一个元素。 简单粗暴