分块小结

分块概念

就是把一个长序列分成 \(\sqrt{n}\) 个区间,分别维护每个区间内的信息和,然后查询时可以优化时间复杂度。

还可以完成一些线段树完成不了的神秘操作,比如这道题

但是总体时间复杂度不如线段树,但它的扩展性比线段树还要强,因为分块中每个区间的信息和不需要具有传递性

怎么理解?

就比如说,需要对一个序列维护区间取模,我们可以开一个数组专门存储当前区间的所有数是否都小于要取模的数,以此实现修改的加速。

线段树的做法就会难想很多,不做赘述。

代码结构

预处理

预处理出每个区块的起始点和重点,以及每个数属于哪个区块。

必要时要处理处每个区块的长度(如要区间加)。

int a[100011];
int bel[100010];
int st[5000],ed[5000],siz[5000],sum[5000];
int cnt[5001],f[5001];
void init()
{
	int sq=sqrt(n);
	for(int i=1;i<=sq;i++)
	{
		st[i]=n/sq*(i-1)+1;
		ed[i]=n/sq*i;
	}
	ed[sq]=n;
	for(int i=1;i<=sq;i++)
	{
		for(int j=st[i];j<=ed[i];j++)
		{
			bel[j]=i;sum[i]+=a[j];
			if(a[j]==1) cnt[i]++;
		}
		siz[i]=ed[i]-st[i]+1;
	}
}

修改

首先判断当前要修改的区间 \([x,y]\) 是否在同一区块内:

if(bel[x]==bel[y])
{
	for(int i=x;i<=y;i++)
	{
		//process
	}
}

否则,分成三个区域修改:

  1. \([x,end[bel[x]]]\)

  2. \((bel[x],bel[y])\)

  3. \([st[bel[y]],y]\)

for(int i=x;i<=ed[bel[x]];i++)
{
	//process
}
for(int i=st[bel[y]];i<=y;i++)
{
	//process
}
for(int i=bel[x]+1;i<bel[y];i++)
{
	//process(区块整块)
}

而且,分块能加速的重要一环就是处理 \((bel[x],bel[y])\)

查询

查询代码与修改代码大同小异,就像是树剖求树链和与树链修改的关系一样。

例题

例题 1:P4145 上帝造题的七分钟 2 / 花神游历各国

link

这个题是维护区间开方和区间和,区间开方用线段树很难搞了,使用分快的思想:对于序列中最大的数 \(10^{12}\),开方 \(6\) 次就会变成 \(1\)

因此,在修改操作中,最浪费时间的不是对于非 \(1\) 的数开方,而是对非常多\(1\) 进行开方。

所以,我们可以在每个区间中维护一个标记 \(flag\),表示当前区间内的所有数是否都为 \(1\)

如果都是 \(1\),直接跳过,否则 \(O(\sqrt{n})\) 修改当前区块的值(\(sqrt\) 视为 \(O(1)\))。

对于区间和,我们可以维护区块和,每次修改区间的时候先减去当前 \(a[i]\) 的值,再给 \(a[i]\) 开方,最后把区间和加上 \(a[i]\) 的值。

这样就搞定了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int a[100011];
int bel[100010];
int st[5000],ed[5000],siz[5000],sum[5000];
int cnt[5001],f[5001];
void init()
{
	int sq=sqrt(n);
	for(int i=1;i<=sq;i++)
	{
		st[i]=n/sq*(i-1)+1;
		ed[i]=n/sq*i;
	}
	ed[sq]=n;
	for(int i=1;i<=sq;i++)
	{
		for(int j=st[i];j<=ed[i];j++)
		{
			bel[j]=i;sum[i]+=a[j];
			if(a[j]==1) cnt[i]++;
		}
		siz[i]=ed[i]-st[i]+1;
	}
}
void change(int x,int y)
{
	if(y<x) swap(x,y);//很恶心,卡了我半个小时
	if(bel[x]==bel[y])
	{
		for(int i=x;i<=y;i++)
		{
			if(a[i]==1) continue;//防止 cnt 数组重复计算
			sum[bel[i]]-=a[i];//sum 先减去 a[i]
			a[i]=sqrt(a[i]);//开方
			sum[bel[i]]+=a[i];//加回来
			if(a[i]==1) cnt[bel[i]]++;
			if(cnt[bel[i]]>=siz[bel[i]]) f[bel[i]]=1;//记录区块全为 1
		}
	}
	else
	{
		for(int i=x;i<=ed[bel[x]];i++)
		{
			if(a[i]==1) continue;
			sum[bel[i]]-=a[i];
			a[i]=sqrt(a[i]);
			sum[bel[i]]+=a[i];
			if(a[i]==1) cnt[bel[i]]++;
			if(cnt[bel[i]]>=siz[bel[i]]) f[bel[i]]=1;
		}
		for(int i=st[bel[y]];i<=y;i++)
		{
			if(a[i]==1) continue;
			sum[bel[i]]-=a[i];
			a[i]=sqrt(a[i]);
			sum[bel[i]]+=a[i];
			if(a[i]==1) cnt[bel[i]]++;
			if(cnt[bel[i]]>=siz[bel[i]]) f[bel[i]]=1;
		}
		for(int i=bel[x]+1;i<bel[y];i++)
		{
			if(f[i]) continue;//精髓!!
			else
			{
				for(int j=st[i];j<=ed[i];j++)
				{
					if(a[j]==1) continue;
					sum[bel[j]]-=a[j];
					a[j]=sqrt(a[j]);
					sum[bel[j]]+=a[j];
					if(a[j]==1) cnt[bel[j]]++;
					if(cnt[bel[j]]>=siz[bel[j]]) f[bel[j]]=1;
				}
			}
		}
	}
}
int query(int x,int y)
{
	if(y<x) swap(x,y);
	int res=0;
	if(bel[x]==bel[y])
	{
		for(int i=x;i<=y;i++)	res+=a[i];
	}
	else
	{
		for(int i=x;i<=ed[bel[x]];i++)	res+=a[i];
		for(int i=st[bel[y]];i<=y;i++)	res+=a[i];
		for(int i=bel[x]+1;i<bel[y];i++)	res+=sum[i];
	}
	return res;
}
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	init();cin>>m;
	for(int i=1;i<=m;i++)
	{
		int k,x,y;
		scanf("%lld%lld%lld",&k,&x,&y);
		if(!k)	change(x,y);
		else	cout<<query(x,y)<<endl;
		
	}
}

例题 2:P2801 教主的魔法

link

维护区间和与区间最小值。

判断当前整区块需要遍历查询的条件是区块最小值加标记是否大于等于 \(c\)

如果最小值都比 \(c\) 大了那么整个区块内所有数都比它大了。

这样就能加速了(但是第二个点 hack 数据过不去啊啊啊)。

因为这个点构造的数据需要我程序每次遍历全部数组。。。

加上卡常和面向数据编程,我们就会得到此题的最优解:

image

再次优化:

在哪里浪费了时间?

我们看到如果上面的判断不能满足答案,只能块内暴力。

那我们可以再引入一个数组 \(d\),存储 \(a\) 的值,但是块内是排过序的。

对于一个整块,直接块内二分查找答案。

就好了。

for(int i=bel[l]+1;i<bel[r];i++)
{
	if(c>mx[i]+sum[i])
	{
		continue;
	}
	if(c<=mi[i]+sum[i])
	{
		res+=siz[i];continue;
	}
	int ll=st[i],rr=ed[i],result=0,mid;
	while(ll<=rr)
	{
		mid=(ll+rr)>>1;
		if(d[mid]+sum[i]>=c)
			rr=mid-1,result=ed[i]-mid+1;
		else
			ll=mid+1;
	}
	res+=result;
}

例题 3:P3203 [HNOI2010] 弹飞绵羊

搞人。我服了。

就是分块,维护每个点弹出本块的步数弹出本块到哪个点

然后就是各种坑点:

  1. 预处理和修改要倒序,就像 \(dp\) 一样,由上一个点转移过来。
for(int i=sq;i>=1;i--)
{
	for(int j=ed[i];j>=st[i];j--)
	{
		if(j+a[j]>ed[i])
		{
			step[j]=1,to[j]=j+a[j];
		}
		else
		{
			step[j]=step[j+a[j]]+1;
			to[j]=to[j+a[j]];
		}
	}
}
  1. 计算答案的时候,要先记录当前 \(step\),再更新位置。
int solve(int x)
{
	int res=0,k=x;
	while(k<=n)
	{
		res+=step[k];//就是这个
		k=to[k];
	}
	if(k==n)
	res++;
	return res;
}

然后就很水了。

例题 4:P4168 [Violet] 蒲公英

link

有点码量,花了两三天时间才调出来。

在本题中,我们需要维护一个东西:\(num[i][j]\) 对于每个数字 \(j\),在第 \(i\) 块和之前出现的总次数,即前缀和。

因为 \(a_i\) 的范围很大,需要提前离散化,我先自己写了一个,感觉复杂度有点高,又网上找了一个:

for(int i=1;i<=n;i++) 
{
	cin>>a[i],b[i]=a[i];
}
sort(b+1,b+1+n);
int sum=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++)
	a[i]=lower_bound(b+1,b+1+sum,a[i])-b,bg=max(bg,a[i]);

我们得到的 \(a\) 数组就是离散化后的值,举个例子:一串数列:\(9999,12,1,3\),离散化后 \(a[1]=4,a[2]=3,a[3]=1,a[4]=2\)\(b[a[1]]=9999,b[a[2]]=12\)

就是这样。

预处理就是分块的预处理方法,预处理 \(num\) 数组的时候暴力统计就行了,时间复杂度约为 \(O(n\sqrt{n})\),实际比它略大。

对于每个询问,我们分这几种情况:

  • \(l,r\) 在一个块中,直接暴力统计。
  • \(l,r\) 在两个不同的块中,统计 \([l,end[bel[l]]],[bel[l]+1,bel[r]-1],[start[bel[r]],r]\) 这三个区间。

然后就好了。

不知道为啥我第一遍写的常数极大,大数据跑了 4.3 秒。

inline int query(int l,int r)
{
	int mxnum=0,nu=0;
	for(int i=1;i<=bg;i++) t[i]=0;
	if(bel[l]==bel[r])
	{
		for(int i=l;i<=r;i++)
		{
			t[a[i]]++;
			if(t[a[i]]>mxnum||(t[a[i]]==mxnum&&b[a[i]]<nu))
			{
				nu=b[a[i]],mxnum=t[a[i]];
			}
		}
	}
	if(bel[l]!=bel[r])
	{
	for(int i=l;i<=ed[bel[l]];i++)
	{
		t[a[i]]++;
		if(t[a[i]]>mxnum||(t[a[i]]==mxnum&&b[a[i]]<nu))
		{
			nu=b[a[i]],mxnum=t[a[i]];
		}
	}
	for(int i=st[bel[r]];i<=r;i++)
	{
		t[a[i]]++;
		if(t[a[i]]>mxnum||(t[a[i]]==mxnum&&b[a[i]]<nu))
		{
			nu=b[a[i]],mxnum=t[a[i]];
		}
	}
	}
	if(bel[r]-bel[l]>1)
	{
		for(int i=1;i<=bg;i++)
		{
			t[i]+=(num[bel[r]-1][i]-num[bel[l]][i]);
			if(t[i]>mxnum||(t[i]==mxnum&&b[i]<nu))
			{
				nu=b[i],mxnum=t[i];
			}
		}
	}
	return nu;
}

这是里面最难的部分了,剩下的就很简单。

例题 6:#P352. 颜色

link

RE 自动机。。

分别维护一次的和二次的前缀和就好。

细节超级超级多。。

然后就是因为要涉及一个三维数组,所以块长不能定义成 \(\sqrt{n}\),我简单地设成 1000 了。

还有就是如果 #define int long long 会炸,只能把个别需要的设成 \(long long\)

#include<bits/stdc++.h>
using namespace std;
int n,m,q;
int A[50005];
int st[52],ed[52],bel[50005];
int num[52][20005];
int t[50005];
const int len=1000;
long long ans[52][52][20005];
void init()
{
	for(int i=1;i<=n;i++)
	{
		bel[i]=(i+len-1)/len;
		ed[bel[i]]=i;
		if(!st[bel[i]])st[bel[i]]=i;
	}
	for(int i=1;i<=bel[n];i++)
	{
		for(int j=1;j<=m;j++) num[i][j]=num[i-1][j];
		for(int j=st[i];j<=ed[i];j++)
		{
			num[i][A[j]]++;
		}
	}
	for(int i=1;i<=bel[n];i++)
	{
		for(int j=i;j<=bel[n];j++)
		{
			for(int k=1;k<=m;k++) ans[i][j][k]=ans[i][j-1][k];
			for(int k=st[j];k<=ed[j];k++) ans[i][j][A[k]]+=t[A[k]]*2+1,t[A[k]]++;
		}
		for(int j=st[i];j<=n;j++) t[A[j]]=0;
	}
	for(int i=1;i<=bel[n];i++)
	{
		for(int j=i;j<=bel[n];j++)
		{
			for(int k=1;k<=m;k++) ans[i][j][k]+=ans[i][j][k-1];
		}
	}
}
long long lans;
long long query(int l,int r,int a,int b)
{
	long long res=ans[bel[l]+1][bel[r]-1][b]-ans[bel[l]+1][bel[r]-1][a-1];
	if(bel[l]==bel[r])
	{
		for(int i=l;i<=r;i++)
		{
			if(A[i]>=a&&A[i]<=b)res+=t[A[i]]*2+1,t[A[i]]++;
		}
		for(int i=l;i<=r;i++)t[A[i]]=0;
	}
	else
	{
		for(int i=l;i<=ed[bel[l]];i++)
		{
			if(A[i]<a||A[i]>b) continue;
			if(!t[A[i]]) t[A[i]]=num[bel[r]-1][A[i]]-num[bel[l]][A[i]];
			res+=t[A[i]]*2+1,t[A[i]]++;
		}
		for(int i=st[bel[r]];i<=r;i++)
		{
			if(A[i]<a||A[i]>b) continue;
			if(!t[A[i]]) t[A[i]]=num[bel[r]-1][A[i]]-num[bel[l]][A[i]];
			res+=t[A[i]]*2+1,t[A[i]]++;
		}
		for(int i=st[bel[r]];i<=r;i++)t[A[i]]=0;
		for(int i=l;i<=ed[bel[l]];i++)t[A[i]]=0;
	}
	return res;
}
signed main()
{
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++) cin>>A[i];
	init();
	while(q--)
	{
		int l,r,a,b;
		cin>>l>>r>>a>>b;
		l^=lans,r^=lans,a^=lans,b^=lans;
		lans=query(l,r,a,b);
		cout<<lans<<endl;
	}
	return 0;
}

莫队

概念

就是用一种离线的方法,把区间查询排序,每次移动不超过 \(\sqrt{n}\),然后就好了。。

bool cmp(node x,node y)
{
	if((x.x-1)/sq==(y.x-1)/sq)return x.y<y.y;
	return x.x/sq<y.x/sq;
}
void add(int x)
{
	c+=2*b[x]+1;
	b[x]++;
}
void del(int x)
{
	c-=2*b[x]-1;
	b[x]--;
}

然后是查询:

sort(op+1,op+1+m,cmp);
for(int i=1;i<=m;i++)
{
	left=op[i].x,right=op[i].y;
	while(ans1>left) ans1--,add(a[ans1]);
	while(ans2<right) ans2++,add(a[ans2]);
	while(ans1<left) del(a[ans1]),ans1++;
	while(ans2>right) del(a[ans2]),ans2--;
	ans[op[i].id]=c;
}

\(ans1\) 代表上一次的左端点,\(ans2\) 代表上一次的右端点;\(left\) 表示当前左端点,\(right\) 表示当前右端点。

例题

例题 1:2709 小B的询问

link

2024-04-17 20:21:25 星期三

就是模板,每次进来 \(b[i]\times 2+1\) 表示平方。

#include<bits/stdc++.h>
using namespace std;
int n,m,k;
int a[50001],b[50001];
struct node{
	int x,y,id;
}op[50001];
int sq;
long long ans[50010],c;
bool cmp(node x,node y)
{
	if((x.x-1)/sq==(y.x-1)/sq)return x.y<y.y;
	return x.x/sq<y.x/sq;
}
void add(int x)
{
	c+=2*b[x]+1;
	b[x]++;
}
void del(int x)
{
	c-=2*b[x]-1;
	b[x]--;
}
int main()
{
	int left,right,ans1=1,ans2=0;
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)cin>>a[i];
	sq=sqrt(n);
	for(int i=1;i<=m;i++)cin>>op[i].x>>op[i].y,op[i].id=i;
	sort(op+1,op+1+m,cmp);
	for(int i=1;i<=m;i++)
	{
		left=op[i].x,right=op[i].y;
		while(ans1>left) ans1--,add(a[ans1]);
		while(ans2<right) ans2++,add(a[ans2]);
		while(ans1<left) del(a[ans1]),ans1++;
		while(ans2>right) del(a[ans2]),ans2--;
		ans[op[i].id]=c;
	}
	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
}

例题 2:P4396 [AHOI2013] 作业

调了一万年,交了一亿次、、

用莫队维护操作的 \(l\)\(r\),用值域分块维护操作的 \(a\)\(b\)

值域分块就和桶是一个原理的,把块从分下标改为分数字。

有一万个细节。。

  1. 在查询的时候要先把 \(r\) 划定范围,即令 \(r=\min(r,mx)\),其中 \(mx\) 表示数组中最大的数;

  2. 维护莫队和分块需要使用两套 \(bel\) 数组,分别计算。

总之就是很滞胀。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1000011;
int n,m;
int a[N];
struct node{
	int id,a,b,l,r;
}op[N];
int bel[N],be[N];
bool cmp(node x,node y)
{
	return (be[x.l]^be[y.l])?be[x.l]<be[y.l]:((be[x.l]&1)?x.r<y.r:x.r>y.r);
}
int a1[N],a2[N]; 
int st[1011],ed[1011];
void init(int k)
{
	int sq=1000;
	for(int i=1;i<=k;i++)
	{
		bel[i]=i/sq;
		if(!st[bel[i]]) st[bel[i]]=i;
		ed[bel[i]]=i;
	}
}
int t[N];
int sum[1011];
int ans1,ans2,mx;
int cnt[1011];
void add(int k)
{
	t[a[k]]++;
	sum[bel[a[k]]]++;
	if(t[a[k]]<=1) cnt[bel[a[k]]]++;
}
void del(int k)
{
	t[a[k]]--;
	sum[bel[a[k]]]--;
	if(t[a[k]]<=0) cnt[bel[a[k]]]--;
}
int getans1(int l,int r)
{
	r=min(r,mx);
	int res=0;
	if(bel[l]==bel[r])
	{
		for(int i=l;i<=r;i++) res+=t[i];
		return res;
	}
	else
	{
		for(int i=l;i<=ed[bel[l]];i++)	res+=t[i];
		for(int i=bel[l]+1;i<bel[r];i++)res+=sum[i];
		for(int i=st[bel[r]];i<=r;i++)	res+=t[i];
	}
	return res;
}
int getans2(int l,int r)
{
	r=min(r,mx);
	int res=0;
	if(bel[l]==bel[r])
	{
		for(int i=l;i<=r;i++) res+=(bool)t[i];
		return res;
	}
	else
	{
		for(int i=l;i<=ed[bel[l]];i++)	res+=(bool)t[i];
		for(int i=bel[l]+1;i<bel[r];i++)res+=cnt[i];
		for(int i=st[bel[r]];i<=r;i++)	res+=(bool)t[i];
	}
	return res;
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],mx=max(mx,a[i]);
	for(int i=1;i<=m;i++) cin>>op[i].l>>op[i].r>>op[i].a>>op[i].b,op[i].id=i;
	int kk=1000;
	for(int i=1;i<=n;i++)be[i]=i/kk;
	sort(op+1,op+1+m,cmp);
	init(mx);
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(l<op[i].l) del(l++);
		while(l>op[i].l) add(--l);
		while(r<op[i].r) add(++r);
		while(r>op[i].r) del(r--);
		a1[op[i].id]=getans1(op[i].a,op[i].b),a2[op[i].id]=getans2(op[i].a,op[i].b);
	}
	for(int i=1;i<=m;i++) cout<<a1[i]<<" "<<a2[i]<<endl;
	return 0;
}
posted @ 2024-03-08 21:59  ccjjxx  阅读(61)  评论(0)    收藏  举报