分块——一个神奇的暴力思想

分块的思想

对于一个区间问题,即给定一定的范围(不一定是序列,也可以是树等),对于范围中的某一些连续区间进行操作时,我们可以将给定的范围分成k块,那么我们就可以将某一个区间[l , r]分为3段:左端点所在的一块,右端点所在的一块(无所谓是否完整)和中间的整块。对于左右端点所在的两块,我们暴力处理(一般来说是一个一个点处理),对于中间的整块,我们将一块一块处理,或直接多块一起处理。
那么,我们可以发现,最终的复杂度取决于我们究竟怎样对序列进行分块,若每一块分得很少,左右端点的暴力固然可以很轻松,但对于中间的块数就会变多;分得多,左右端点的暴力就会很复杂。所以,我们需要分析它的复杂度:假设序列长度为n,分为m块,对于暴力单点和一块的操作都是O(1),那么不难发现,整个的时间复杂度为O(n / m + m),由于均值不等式,m取√n时复杂度达到最低,总复杂度为O(q√n),q为操作次数。
你或许会问,如果操作不是O(1),还取√n吗?我只能说,√n或许不是最优,但一般来说满足大部分的题目要求。
(ps:1:数学大佬直接手推,请无视我说的话
2:说几个常用的,对于操作logn级的每一块大概:sqrt(n / logn))(用cmath的log2()函数就行了),对于莫队呢,每一块n2/3比较优秀。(不对的话别来找我))

分块的应用

你或许会问,我们为什么要用分块,直接用线段树维护不是更优秀吗?事实上,很多时候,题目需要维护的值不具有可加和性或者单调性(即不是和,不是最值之类的东西),这个时候,我们就需要用到分块,以达到直接维护序列中元素的信息。

分块的实现

理解了分块的思想,你就会发现,这其实就是个暴力,不过是个比较优秀的暴力罢了。说说它一般的实现,对于一些简单题目,直接无脑上分块,暴力就完事了,但对于有些题目,你还需要预处理一些信息(如某个元素在某块内出现次数、两个块之间的答案等)。那么我们就要注意了,一般来说,预处理的复杂度不会大于分块暴力的复杂度即n√n,所以在题目需要预处理的时候,一定要考虑清楚复杂度再写。比如我们看看这道题:
https://www.luogu.org/problemnew/show/P4135
这道题就需要预处理出现次数和答案,附代码:

#include 
using namespace std;
const int maxn = 100010 , maxk = 1501;
inline char get_char()
{
	static char buf[100000] , *p1 = buf , *p2 = buf;
	if (p1 == p2)
	{
		p2 = (p1 = buf) + fread(buf , 1 , 100000 , stdin);
		if (p1 == p2)
		{
			return EOF;
		}
	}
	return *p1++;
}
inline int read()
{
	int res;
	char ch;
	while (!isdigit(ch = get_char()));
	res = ch - '0';
	while (isdigit(ch = get_char()))
	{
		res = res * 10 + ch - '0';
	}
	return res;
}
int n , m , c , bc , top;
int id[maxn] , L[maxk] , R[maxk];
int ans[maxk][maxk] , cnt[maxk][maxn];
int mark[maxn] , num[maxn] , s[maxn];
void pre()
{
	for (int i = 1; i <= id[n]; i++)
	{
		int tot = 0;
		for (int j = L[i]; j <= n; j++)
		{
			cnt[i][num[j]]++;
			if ((cnt[i][num[j]] & 1) && cnt[i][num[j]] > 1)
			{
				tot--;
			}
			else if (!(cnt[i][num[j]] & 1))
			{
				tot++;
			}
			if (id[j] != id[j + 1])
			{
				ans[i][id[j]] = tot;
			}
		}
	}
}
int main()
{
	//freopen("data.in" , "r" , stdin);
	n = read() , c = read() , m = read();
	int bc = sqrt(n / log2(n));
	for (int i = 1; i <= n; i++)
	{
		id[i] = (i - 1) / bc + 1;
		num[i] = read();
	}
	for (int i = 1; i <= id[n]; i++)
	{
		L[i] = (i - 1) * bc + 1 , R[i] = i * bc;
	}
	R[id[n]] = n;
	pre();
	int lastans = 0;
	for (int i = 1; i <= m; i++)
	{
		int l = read() , r = read();
		l = (l + lastans) % n + 1;
		r = (r + lastans) % n + 1;
		if (l > r)
		{
			swap(l , r);
		}
		//cout << l << " " << r << endl;
		int res = ans[id[l] + 1][id[r] - 1];
		top = 0;
		if (id[l] == id[r])
		{
			for (int j = l; j <= r; j++)
			{
				mark[num[j]]++;
			}
			for (int j = l; j <= r; j++)
			{
				if (mark[num[j]])
				{
					if (!(mark[num[j]] & 1))
					{
						res++;
					}
					mark[num[j]] = 0;
				}
			}
		}
		else
		{
			for (int j = l; j <= R[id[l]]; j++)
			{
				mark[num[j]]++;
				s[++top] = num[j];
			}
			for (int j = L[id[r]]; j <= r; j++)
			{
				mark[num[j]]++;
				s[++top] = num[j];
			}
			for (int j = 1; j <= top; j++)
			{
				if (mark[s[j]])
				{
					int now = max(0 , cnt[id[l] + 1][s[j]] - cnt[id[r]][s[j]]);
					mark[s[j]] += now;
					if (mark[s[j]] & 1)
					{
						if (!(now & 1) && now > 0)
						{
							res--;
						}
					}
					else
					{
						if ((now & 1) || now == 0)
						{
							res++;
						}
					}
					mark[s[j]] = 0;
				}
			}
		}
		printf("%d\n" , lastans = res);
	}
	return 0;
}

emmm,如果觉得难以理解先别急。你可以先刷刷这几道题:
入门9题:http://hzwer.com/8053.html (没必要全刷)
觉得入门了就看看上面那道题吧。
进阶 : http://hzwer.com/category/algorithm/data-structure/basic-data-structure/piecemeal
顺便可以再bzoj上看看,题目不少。

你可能感兴趣的:(分块——一个神奇的暴力思想)