分块

原洛谷博客链接

这是神奇的分块。

分块是一种基于暴力演变而来的数据结构思想(并非完全是数据结构),其原理非常原始和简单,也比较好写。

怎么说呢,分块其实是线段树的低配版,毕竟它们两个都是块状数据结构,并且都支持单/区的修/询,能维护各种值(譬如max,sum等),甚至在做区修时都可以使用懒标记来提升效率。

分块的基本套路是将一个区间分成几个长度相等的块(最后一个块的块长不一定是之前的块长)

例如:数组a 为

1 1 4 5 1 4 1 9 1 9 8 1 0 8 8 4 8

一共有17个数。

我们按照4个一块将其分为4块。

1 1 4 5 \(\qquad\) 1 4 1 9 \(\qquad\) 1 9 8 1 \(\qquad\) 0 8 8 4 $\qquad$8


在讲述后述内容前:

我们先声明一些定义:

  • 整块 长度为程序原本设定的块长,包括最后那个长度不够的块。

给出公式:

\(i\) 个元素所在的块为第 \(\dfrac{(i-1)}{S}+1\) 个块

\(i\) 个块的起始位置为第 \((i-1) \times S + 1\) 个元素,结束位置为第 \(n\)(数列长度)个元素(也就是数列末尾)或第 \(i\times S\) 个元素

约定:

数列(区间)长度为 \(n\)

块长(相等)长度为 \(S\)

\(\dfrac{(i-1)}{S}+1 = calc(i)\)

\((i-1) \times S + 1 = start(i)\)

\(i\times S = end(i)\)

块长为 \(\sqrt n\) 时分块的复杂度最优,关于证明,在后面会讲到。

啊不是,8848胎核精野兽先辈手坤


问题1

在处理一个区间时,我们将其分为三个区间:

  • 左散块:左边不足一个整块的块

  • 右散块:右边不足一个整块的块

  • 中间整块:在中间的一些整块

比如处理区间3~15,就将其分为:

4 5 \(\quad \qquad\) 1 4 1 9 \(\quad\!\) 1 9 8 1 \(\qquad \quad\) 0 8 8

中间两个块为中间整块。左右散块分别为"4 5" 和 "0 8 8".

假设我们需要如下操作:

  • \(a[i]\)增加 \(x\)(修改)

  • 查询 \(x,y\) 的最大值(询问)

首先对于修改操作,我们直接修改原数组 \(a[i]\) 即可。

然后由于我们的分块是个块状数据结构,我们对于每个整块都用一个数组记录每个块的最大值 \(max[calc(i)]\) ,并且通过修改最大值来快速查询。

所以,当我们修改完了该块时,我们需要修改这个块所属的\(max[calc(i)]\).

然后查询的时候,分别从左右两个散块所在的数列中暴力查找最大值,然后从中间的整块中暴力查找 \(max[calc(i)]\) ,最后比较三个区域的答案,返回最终的答案

代码:

void Modify(int x,int y) //将第x个数加y
{
  int k=(x-1)/s+1; //k为x所在块块号
  int L=(k-1)*s+1; //第k块的左界
  int R=k*s; //第k块的右界
  Max[k]=-inf;
  a[x]+=y;
  for(int i=L;i<=R;i++)
  	if(a[i]>Max[k])Max[k]=a[i];
}

int Query(int x,int y) //询问a数列区间[x,y]的最大值
{
  int i=(x-1)/s+1; //i为x所在块块号,s为块长
  int j=(y-1)/s+1; //j为y所在块块号
  int ans=-inf;
  if(i==j) //[x,y]在同一块中,直接暴力枚举
  {
    for(int k=x;k<=y;k++)
    if(ans<a[k])ans=a[k];
  }
  else 
  {
    for(int k=x;k<=i*s;k++)//左侧部分
    	if(ans<a[k])ans=a[k];
    for(int k=(j-1)*s+1;k<=y;k++)//右侧
    	if(ans<a[k])ans=a[k];
    for(int k=i+1;k<j;k++)//被覆盖的整块
    	if(ans<Max[k])ans=Max[k];
  }
  return ans;
}

int main()
{
  int i,k,x,y,t,ans;
  scanf("%d%d",&n,&m);
  s=int(sqrt(n)); //块的大小为根号n
  for(i=1;i<=s+1;i++)Max[i]=inf;
  for(i=1;i<=n;i++)
  {
    scanf("%d",&a[i]);
    k=(i-1)/s+1; //计算第i个数所在块的块号k
    if(a[i]>Max[k])Max[k]=a[i]; //Max[k]记录第k块的最大值
  }
  for(i=1;i<=m;i++)
  {
    scanf("%d%d%d",&t,&x,&y);
    if(t==1)Modify(x,y);
    else printf("%d\n",Query(x,y));
  }
}


修改 \(O(S)\) 询问 \(O(2\times s + n\times s)\)


问题2

那么对于区间修改呢?既然可以用懒标记,那么怎么用?

对于左右两个散块,无论是修改还是查询,我们都直接修改数组本身,然后修改各自散块所属的 \(max[calc(i)]\)

例如修改3~11,每个数增加1,此时数组变成

1 1 5 6 \(\qquad\) 1 4 1 9 \(\qquad\) 1 9 8 1 \(\qquad\) 1 9 9 4 $\qquad$8

然后,我们对于每个块,都像 \(max\) 数组一样给定一个懒标记 \(lazy\) ,像线段树一样记录累计未加的值。

在查询时,我们可以同上个问题的询问的思路写代码,但是在判定时需要算上 \(lazy\) 数组,并且有时不能改变原数组(比如加乘并用的修改),不然小概率会出锅。

当然,如果修改时发现左右端点在同一个块内,按照这个思路,程序会把整个块(只有左右散块所以没有中间整块)修改,故大概率会出锅(怎么又是锅啊),所以需要特判。

代码:


void Modify(int x,int y,int z) //将a数列的区间[x,y]整体增加z
{
  int i=(x-1)/s+1; int j=(y-1)/s+1; //计算x和y所在块块号,s为块长
  if(i==j) //[x,y]在同一块中
  {
    for(int k=x; k<=y; k++)
    {
      a[k]+=z;
      if(a[k]>Max[i])Max[i]=a[k];
    }
  }
  else 
  {
    for(int k=x; k<=i*s; k++) //左侧散块
    { 
      a[k]+=z; 
      if(a[k]>Max[i])Max[i]=a[k];
    }
    for(int k=(j-1)*s+1; k<=y; k++) //右侧散块
    {
      a[k]+=z; 
      if(a[k]>Max[j])Max[j]=a[k];
    }
    for(int k=i+1;k<j;k++) //被覆盖的整块
    {
    	Lazy[k]+=z; 
    }
  }
}

int Query(int x,int y) //询问a数列的区间[x,y]的最大值
{
  int i=(x-1)/s+1; int j=(y-1)/s+1; //计算x和y所在块块号,s为块长
  int ans=inf;
  if(i==j) //[x,y]在同一块中
  {
    for(int k=x; k<=y; k++)
    {
    	if(a[k]+Lazy[i]>ans)ans=a[k]+Lazy[i];
    }
  }
  else 
  {
  for(int k=x; k<=i*s; k++) //左侧散块
  	if(a[k]+Lazy[i]>ans)ans=a[k]+Lazy[i]; 
  for(int k=(j-1)*s+1; k<=y; k++) //右侧散块
  	if(a[k]+Lazy[j]>ans)ans=a[k]+Lazy[j];
  for(int k=i+1;k<j;k++) //中间的整块
  	if(Max[k]+Lazy[k]>ans)ans=Max[k]+Lazy[k];
  }
  return ans;
}

时间复杂度 \(O(n+m\times \sqrt n)\)

求和操作同理。故暂不多谈。


那么有些关于分块的问题。

  • 既然是线段树的低配版,为何不写线段树呢?

其实这个问题很古德。

虽然线段树的复杂度是\(O(log n)\) 的,但是线段树的代码十分复杂,不好写,而且比分块更容易出锅。分块的出锅点基本上是固定的,而线段树虽然“板子化明显”,其出锅的点却因题而异,反正就是不好找,写的话要写很久,相对分块而言比较浪费编程时间。

“一个优秀的选手,在大部分OI赛制的、排名不计运行时间的考试里,只要不TLE,往往需要使用代码运行时间换取编程时间。”

\(\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\qquad\quad\;\)——HL

而且由于线段树自带大肠术大常数,在极限数据下可能速度不及分块。

  • 为啥块长为 \(\sqrt n\)分块效率最高?

均衡不等式,\(a + b \ge \sqrt {2ab}\)(注意看,是大于等于,不要眼花)

为什么这个成立的证明,这里略(两边同时平方一下然后展开你就懂了)

所以当

联系问题1中的询问操作,时间复杂度\(O(2\times s + n\times s)\)

提取 \(2\times s + n \times s\),故 \(\dfrac{n}{s}+ s \ge 2 \sqrt {\dfrac{n}{s}\times s}\)

所以我们要让 \(\ge\) 左右两边相等,这样时间复杂度才最低。

故当 $s = \sqrt n $ 时,\(\dfrac{n}{s}+ s \ge 2 \sqrt {\dfrac{n}{s}\times s}\) 可转换为 \(\dfrac{n}{\sqrt n}+ \sqrt n \ge 2 \sqrt {\dfrac{n}{\sqrt n}\times \sqrt n}\),即 \(\sqrt n+ \sqrt n \ge 2 \sqrt {\sqrt n\times \sqrt n}\)\(2\sqrt n \ge 2\sqrt n\),可得 \(\ge\) 左右两边相等,证毕

posted @ 2023-06-29 20:16  几何微粒子  阅读(50)  评论(0)    收藏  举报