淺談分塊

H發表於2021-01-14

更優體驗請移步CSDN

前言

\(NOIP\)已過,訓練難度瞬間變大。很多沒有學過的知識點以各種方式出現在題目裡。而本蒟蒻的腦子裡只有那慘兮兮一點點的演算法,於是本蒟蒻就開始走上惡補知識點的道路。

突然想起來很久很久之前有道用分塊做的題目,當時聽的雲裡霧裡,然後同年級的某位大佬表示:分塊很簡單。今天又聽到同學說起分塊,就上OI-WIKI查了一下,沒想到很快就理解然後敲題了……

何為分塊

分塊其實是一種思想,本質上跟暴力差不多,常用於處理區間問題。做法是將區間分成一個個小塊,然後暴力維護,時間複雜度和分出來的塊的個數及塊的大小有關


結合例題講解分塊的具體操作

例題1

LOJ#6280.數列分塊入門4

給出一個長為\(n\)的數列,以及\(n\)個操作

操作有兩種情況,每次操作輸入4個數\(opt、l、r、c\)

\(opt=0\),表示將位於\([l,r]\)的之間的數字都加\(c\)

\(opt=1\),表示詢問位於\([l,r]\)的所有數字的和\(mod\ (c+1)\)

\(1\le n \le50000\),保證答案在\(long\ long\)範圍內

例題思路分析及程式碼

只要學過線段樹,第一秒想到的基本都會是線段樹,但是這題有一個很大的不同在於每次求值的時候會模一個非固定的數,那麼如果要用線段樹來做就需要在建樹的時候不取模,求值的時候再取模,這樣的話就有可能會導致線段樹上的值超過\(long\ long\)甚至\(unsigned\ long\ long\)範圍(\(\_\_int128\)就不要多想了)

所以我們要拋棄線段樹,去尋找另一種解決方案。而分塊,就是解決這個問題的一個很好的辦法

對於一個區間,我們把它分成若干個長度為\(s\)的塊,最後一個塊的長度可以不足\(s\),因為沒有規定\(s\)必須是\(n\)的因數

一個區間\(a\)就可以分成

\(\underbrace{a_1,a_2\ldots,a_s}_{b_1},\underbrace{a_{s+1},\ldots,a_{2s}}_{b_2},\dots,\underbrace{a_{(s-1)\times s+1},\dots,a_n}_{b_{\frac{n}{s}}}\)

其中\(b_i\)維護第\(i\)個塊內的和,可以在讀入的時候就記錄好每個元素是哪個塊,同時維護\(b\)陣列

更改

分類討論一下

如果\(l,r\)在同一塊內,則直接暴力更改,時間複雜度\(O(s)\)

如果不在,就可以分三部分:(1)以\(l\)開頭的一個不完整塊;(2)中間若干個完整塊;(3)以\(r\)結尾的一個不完整塊。其中(1)(3)部分可以暴力更改,(2)部分就直接更改\(b_i\),以及\(x_i\)。其中\(x_i\)表示整個區間加上了多少,時間複雜度\(O(\dfrac{n}{s}+s )\)

查詢

跟更改很像,也是要分類討論

\(l,r\)在同一個塊內就直接暴力統計,同時注意加上\(x\)陣列,時間複雜度\(O(s)\)

不在同一個塊內也是分三部分,分法同更改部分,(1)(3)部分查詢也是暴力,跟\(l,r\)同塊一樣,注意加上\(x\)陣列。(2)部分直接加上中間完整塊的\(b\)陣列,這裡就不用加上\(x\)陣列。時間複雜度\(O(\dfrac{n}{s}+s)\)

時間複雜度分析

綜合更改和查詢,一次操作的時間複雜度就是\(O(\dfrac{n}{s}+s)\),顯然當\(s\)\(\sqrt{n}\)的時候是最優的,那麼一次操作的時間複雜度就是\(O(\sqrt{n})\),總時間複雜度\(O(n\sqrt{n})\)

Code

#include<cstdio>
#include<cmath> 
#define ll long long
using namespace std;
int n,s,opt,l,r,x;
ll ans,a[50005],id[50005],b[50005],c[50005];
int read()
{
	int res=0,fh=1;char ch=getchar();
	while (ch<'0'||ch>'9') {if (ch=='-') fh=-1;ch=getchar();}
	while (ch>='0'&&ch<='9') res=res*10+ch-'0',ch=getchar();
	return res*fh;
}
int main()
{
	n=read();
	s=sqrt(n);
	for (int i=1;i<=n;++i)
	{
		a[i]=read();
		id[i]=(i-1)/s+1;
		b[id[i]]+=a[i];
	}
	for (int i=1;i<=n;++i)
	{
		opt=read();l=read();r=read();x=read();
		if (!opt)
		{
			if (id[l]==id[r])
			{
				for (int j=l;j<=r;++j)
					a[j]+=x,b[id[l]]+=x;
			}
			else
			{
				for (int j=l;id[j]==id[l];++j)
					a[j]+=x,b[id[l]]+=x;
				for (int j=id[l]+1;j<id[r];++j)
					c[j]+=x,b[j]+=s*x;
				for (int j=r;id[j]==id[r];--j)
					a[j]+=x,b[id[r]]+=x;
			}
		}
		else
		{
			ans=0;
			if (id[l]==id[r])
			{
				for (int j=l;j<=r;++j)
					ans=(ans+a[j]+c[id[l]])%(x+1);	
			}
			else
			{
				for (int j=l;id[j]==id[l];++j)
					ans=(ans+a[j]+c[id[l]])%(x+1);
				for (int j=id[l]+1;j<id[r];++j)
					ans=(ans+b[j])%(x+1);
				for (int j=r;id[r]==id[j];--j)
					ans=(ans+a[j]+c[id[r]])%(x+1);
			}
			printf("%lld\n",ans);
		}
	}
	return 0;
} 

例題2

\(N(1\le N\le100000)\)個數\(A_1\dots A_n\)組成的序列上進行\(M(1\le M\le100000)\)次操作,操作有兩種:

(1)\(1\ L\ R\ C\):表示把\(A_L\)\(A_R\)增加\(C\)\((|C|\le10000)\)

(2)\(2\ L\ R\):詢問\(A_L\)\(A_R\)之間的最大值。

其實這是線段樹的模板題,放到這裡來是想要體現分塊在更改的時候的一個注意事項(其實是為了加字數)

例題思路分析及程式碼

首先還是分塊的基本操作,每個塊長度為\(s\),同時維護每個塊內的最大值\(b\)陣列

更改

注意到\(C\)的取值範圍加了絕對值,就說明\(C\)可能是負數。那麼如果修改區間內有最大值,就可能會影響最大值。所以說如果我們不是修改整個塊,那麼就需要重新統計更改後塊的最大值

\(l,r\)同塊的無需多言,直接暴力更改,同時重新維護塊的最大值。時間複雜度\(O(s)\)

\(l,r\)不同塊的還是照樣分成三部分,頭和尾暴力更改,更新最大值,中間的塊給整個區間加上\(C\),同時最大值可以直接加\(C\)(可以簡單推理得到)。時間複雜度\(O(\dfrac{n}{s}+s)\)

查詢

\(l,r\)同塊直接暴力,記得加上給掛在整個塊的值

\(l,r\)不同塊也是分成三部分,中間部分直接與\(b_i\)進行比較,首尾暴力比較(不要漏掉掛在塊上的值)

時間複雜度分析

\(s\)還是取\(\sqrt{n}\)最優,時間複雜度仍然是\(O(n\sqrt{n})\)

Code

#include<cmath>
#include<cstdio>
#include<iostream>
#define ll long long
using namespace std;
int n,m,len,l,r,x,opt;
ll mx,a[100005],id[100005],b[100005],c[100005];
int read()
{
	int res=0,fh=1;char ch=getchar();
	while (ch<'0'||ch>'9') {if (ch=='-') fh=-1;ch=getchar();}
	while (ch>='0'&&ch<='9') res=res*10+ch-'0',ch=getchar();
	return res*fh;
}
int main()
{
	freopen("max10.in","r",stdin);
	freopen("max10.txt","w",stdout);
	n=read();
	len=sqrt(n);
	for (int i=1;i<=n;++i)
	{
		a[i]=read();
		id[i]=(i-1)/len+1;
		b[id[i]]=max(b[id[i]],a[i]);
	}
	m=read();
	while (m--)
	{
		opt=read();
		if (opt==1)
		{
			l=read();r=read();x=read();
			if (id[l]==id[r])
			{
				for (int i=l;i<=r;++i)
					a[i]+=x;
				b[id[l]]=-2147483647;
				for (int i=(id[l]-1)*len+1;id[i]==id[l];++i)
					b[id[l]]=max(b[id[i]],a[i]+c[id[i]]);
			}
			else
			{
				for (int i=l;id[i]==id[l];++i)
					a[i]+=x;
				for (int i=id[l]+1;i<id[r];++i)
					c[i]+=x,b[i]+=x;
				for (int i=r;id[i]==id[r];--i)
					a[i]+=x;
				b[id[l]]=b[id[r]]=-2147483647;
				for (int i=(id[l]-1)*len+1;id[i]==id[l];++i)
					b[id[l]]=max(b[id[i]],a[i]+c[id[i]]);
				for (int i=(id[r]-1)*len+1;id[i]==id[r];++i)
					b[id[r]]=max(b[id[i]],a[i]+c[id[i]]);
			}
		}
		else
		{
			mx=-2147483647;
			l=read();r=read();
			if (id[l]==id[r])
			{
				for (int i=l;i<=r;++i)
					mx=max(mx,a[i]+c[id[l]]);
			}
			else
			{
				for (int i=l;id[i]==id[l];++i)
					mx=max(mx,a[i]+c[id[l]]);
				for (int i=id[l]+1;i<id[r];++i)
					mx=max(mx,b[i]);
				for (int i=r;id[i]==id[r];--i)
					mx=max(mx,a[i]+c[id[r]]);
			}
			printf("%lld\n",mx);
		}
	}
	return 0;
}

小結

分塊其實是一種十分暴力的思想,旨在把區間分割開來,所以說在分塊的時候,一定要合理控制塊的大小及個數,並不是所有題目\(s\)都是取\(\sqrt{n}\)最優,要根據問題選擇合適的\(s\)

另外分塊也可以做一些奇奇怪怪的毒瘤題,例如吉司機線段樹……

祝大家新年快樂!!!

相關文章