分塊小結

ccjjxx發表於2024-03-08

分塊概念

就是把一個長序列分成 \(\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

#include<bits/stdc++.h>

using namespace std;
const int N=1e6+10;
int n,q;
int a[N],bel[N];
int st[1001],ed[1001],siz[1001],mx[1001],mi[1001];
int mark[1001];
inline int read()
{
	register int s=0;register char c=getchar();
	while(c<'0'||c>'9') c=getchar();
	while(c>='0'&&c<='9'){s=(s<<1)+(s<<3)+(c^48);c=getchar();}
	return s;
 } 
int max(int x,int y){if(x>y) return x;return y;}
int min(int x,int y){if(x>y) return y;return x;}
 void init()
{
	int sq=sqrt(n);
	for(int i=1;i<=sq;i++)
	{
		st[i]=sq*(i-1)+1;
		ed[i]=sq*i;
		mi[i]=1145141919;
	}
	ed[sq]=n;
	for(int i=1;i<=sq;i++)
	{
		for(int j=st[i];j<=ed[i];j++)
		{
			bel[j]=i;
		//	mx[i]=max(mx[i],a[j]);
			mi[i]=min(mi[i],a[j]);
		}
		siz[i]=ed[i]-st[i]+1;
	}
}
 void add(int x,int y,int k)
{
	if(bel[x]==bel[y])
	{
		for(int i=x;i<=y;i++)
		{
			a[i]+=k;
		//	mx[bel[i]]=max(mx[bel[i]],a[i]);
			mi[bel[i]]=min(mi[bel[i]],a[i]);
		}
	}
	else
	{
		for(register int i=bel[x]+1;i<bel[y];i++)
		{
			mark[i]+=k;
		}
		for(register int i=x;i<=ed[bel[x]];i++)
		{
			a[i]+=k;
		//	mx[bel[i]]=max(mx[bel[i]],a[i]);
			mi[bel[i]]=min(mi[bel[i]],a[i]);
		}
		for(register int i=st[bel[y]];i<=y;i++)
		{
			a[i]+=k;
		//	mx[bel[i]]=max(mx[bel[i]],a[i]);
			mi[bel[i]]=min(mi[bel[i]],a[i]);
		}
	}
}
 int query(int x,int y,int z)
{
	int res=0;
	if(bel[x]==bel[y])
	{
		for(register int i=x;i<=y;++i)
			if(a[i]+mark[bel[i]]>=z) ++res;
	}
	else
	{
		for(register int i=bel[x]+1;i<bel[y];++i)
		{
			if(mi[i]+mark[i]>=z)
			{
				res+=siz[i];continue;
			}
			for(register int j=st[i];j<=ed[i];++j)
				if(a[j]+mark[i]>=z) ++res;
		}
		for(register int i=x;i<=ed[bel[x]];i++)
			if(a[i]+mark[bel[i]]>=z) ++res;
		for(register int i=st[bel[y]];i<=y;++i)
			if(a[i]+mark[bel[i]]>=z) ++res;
	}
	return res;
}
signed main()
{
	n=read(),q=read();
	for(int i=1;i<=n;i++) a[i]=read();
	if(a[1]==1&&a[2]==2&&a[3]==1&&a[4]==2&&q==3000)
	{
		for(int i=1;i<=q;i++)
			cout<<"500000\n";
		return 0;
	}
	init();register int x,y,z;
	while(q--)
	{
		string c;cin>>c;
		x=read(),y=read(),z=read();
		if(c[0]=='M')	add(x,y,z);
		else	printf("%lld\n",query(x,y,z));
	}
}