初学分块与莫队

分块

高级的暴力,把序列分成一块一块的,利用加懒标记的方式进行区间操作。而没有被分成块的就直接暴力修改,所以时间复杂度方面感觉很玄学。

例一

给一个长度为n的序列,每次给l,r和c,表示把区间l~r都加c,或者输出a[r]的值

一开始只知道原理而没见过板子的时候

void fenkuai()
{
	for(int i=1;i<=n;i+=m)kuaiz[++tot]=i;
	kuaiz[++tot]=kuaiz[tot-1]+m;
}//n为序列长度,m为块的大小

写下这种幼稚的板子,用 k u a i z [ t o t ] kuaiz[tot] kuaiz[tot] 记录每一块的左端点在序列中的下标。
然后

void xg(int x,int y,int z)
{
	int p=lower_bound(kuaiz,kuaiz+tot,x)-kuaiz;
	int q=upper_bound(kuaiz,kuaiz+tot,y)-kuaiz;
	for(int i=p+1;i<q;i++)lan[i]+=z;
	for(int i=x;i<kuaiz[p];i++)a[i]+=z;
	for(int i=kuaiz[q-1];i<=y;i++)a[i]+=z;
	return;
}

以这种错误的方式进行整块整块的懒标记,和非整块部分的直接修改,然后听取 W A WA WA 声一片。(当然不排除思路没错但写法有误。)然后在高强度自闭下看了别人的写法。
在这里插入图片描述
后来也确实有人的实现跟我一开始所想的差不多

void change(int ll,int rr,long long d)
{
	int p=pos[ll],q=pos[rr];
	if(p==q)
	{
		for(int i=ll;i<=rr;i++) a[i]+=d;
		sum[p]+=d*(rr-ll+1);
	}
	else
	{
		for(int i=p+1;i<q;i++) add[i]+=d;
		for(int i=ll;i<=r[p];i++) a[i]+=d;
		sum[p]+=d*(r[p]-ll+1);
		for(int i=l[q];i<=rr;i++) a[i]+=d;
		sum[q]+=d*(rr-l[q]+1);
	}
}

以这种方式进行修改,初始分块时:

	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int t=sqrt(n);
	for(int i=1;i<=t;i++)
	{
		l[i]=(i-1)*sqrt(n)+1;
		r[i]=i*sqrt(n);
	}
	if(r[t]<n)
	{
		t++;
		l[t]=r[t-1]+1;
		r[t]=n;
	}
	for(int i=1;i<=t;i++)
		for(int j=l[i];j<=r[i];j++)
			pos[j]=i,sum[i]+=a[j];

真是妙啊
以下为别人正确的基本分块板子
初始化分块:

for(int i=1;i<=n;i++){scanf("%d",&a[i]);wz[i]=(i-1)/m+1;}//wz[i]记录序列中下标为i的数在第几块

而修改(在此以直接加的为例):

void gai(int l,int r,int c)
{
    if(wz[l]==wz[r]){for(int i=l;i<=r;i++)a[i]+=c;return;}
    //不排除区间的左右端点在同一个块里,如果是这样就直接暴力修改,然后return;
    for(int i=l;i<=wz[l]*m;i++)a[i]+=c;//左边不完整的一个块,就算是完整也直接暴力改
    for(int i=wz[l]+1;i<=wz[r]-1;i++)lan[i]+=c;//中间完整的块直接加懒标记
    for(int i=(wz[r]-1)*m+1;i<=r;i++)a[i]+=c;//右边
}

但是我后来写这个函数的时候经常搞错边界,于是就又学到一种写法:

void change(int l,int r,int x)
{
	for(int i=l;i<=min(pos[l]*m,r);i++)//直接修改左边多出的一块,如果是一块就暴力把整个区间都修改了
		a[i]+=x;
	if(pos[l]!=pos[r])//如果不是一块上的
		for(int i=(pos[r]-1)*m+1;i<=r;i++)//给右边多出的部分暴力修改
			a[i]+=x;
	for(int i=pos[l]+1;i<pos[r];i++)//如果同在一块,就跟不执行一样
		del[i]+=x;//懒标记
}

输出:

printf("%d\n",a[r]+lan[wz[r]]);

然后正常写个输入输出啥的,该例题就直接 A A A

至于 m m m 要有多大,即每一块儿该有多大,一般是 n \sqrt{n} n 比较保险。
∙    \bullet\; 如果块太大了,被修改的区间经常不能包含整块,而还是要靠暴力,比方取极值长度为 n n n ,结果显而易见。
∙    \bullet\; 如果块太小,需要就算是懒标记也要标记很多次,同样取极值 1 1 1,每次加懒标记还不如直接取修改。
∙    \bullet\; 所以取 n \sqrt{n} n 最为合适,可以达到效果:有 n \sqrt{n} n 块,每块长度为 n \sqrt{n} n
当然不排除特殊情况,不同情况差别对待。

例二

给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。

S T L STL STL很容易去维护,然而这个题中难点在于:即使数据是随机生成的,也不排除往某个区间中猛插数,所以就需要加个特判,长度大于特定值时把分块重构一遍。这道题用线段树显然是不好搞的。
注:别人写法的板子是直接站得,所以变量和码风不一样


莫队

学完莫队,我才理解到为什么老师说信息学是目前最接近魔法的一门学科
莫队首先一般是离线操作,而我感觉最为玄学的应该是它的时间复杂度,我自己写的在 校 o j 校oj oj 跑了 1800 m s 1800ms 1800ms A A A 掉了,但是某谷上只有 72 72 72 分,于是校oj找了个跑 900 m s 900ms 900ms 的代码,某谷中只有 65 65 65 分。
在这里插入图片描述
还有一点比较疑惑的是在 O I e r d b OIerdb OIerdb 中搜索莫队的发明人——莫涛
在这里插入图片描述

不再废话了,莫队算是有点分块思想,而且必须是离线操作。在对一个序列进行多次区间操作的时候,我们可以首先把序列按每块 n \sqrt{n} n 分成 n \sqrt{n} n 块,然后对每次操作所选的区间进行排序,以左端点所在的块的下标为第一关键字,以右端点的下标为第二关键字,即排完顺序后左端点所在块靠前的靠前,若在同一个块中,则按右端点从小到大排序,分块的方法求出第一个区间的答案,然后由于第二个区间左端点在同一块内,所以先用 n \sqrt{n} n 不到的复杂度对左边进行修改,然后再看右边比前一个区间多出来的部分对答案造成的影响。
如果分块的板子跟上面一样,排序的代码大概就是这样

int get(int x){return (x-1)/t+1;}
bool mycmp(hhhc x,hhhc y)
{
	int a=get(x.l),b=get(y.l);
	return((a<b)||(a==b&&x.r<y.r));
}

通常的算区间变化后对答案有加或减的影响时:

	
	for(int k=1,i=0,j=1;k<=m;k++)
	{
		int zz=hh[k].id,l=hh[k].l,r=hh[k].r;
		while(i<r)cha(a[++i]);
		while(i>r)shan(a[i--]);
		while(j<l)shan(a[j++]);
		while(j>l)cha(a[--j]);
		ans[zz]=sum;
	}

下面这两个函数适用于最后两个例题

void cha(int x)
{
	if(!cnt[x])sum++;
	cnt[x]++;
}
void shan(int x)
{
	cnt[x]--;
	if(!cnt[x])sum--;
}

例一
                   \;\;\;\;\;\;\;\;\; [SDOI2009]HH的项链
例二
                   \;\;\;\;\;\;\;\;\; [国家集训队]数颜色 / 维护队列

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值