浅谈分块
更优体验请移步CSDN
前言
NOIP已过,训练难度瞬间变大。很多没有学过的知识点以各种方式出现在题目里。而本蒟蒻的脑子里只有那惨兮兮一点点的算法,于是本蒟蒻就开始走上恶补知识点的道路。
突然想起来很久很久之前有道用分块做的题目,当时听的云里雾里,然后同年级的某位大佬表示:分块很简单。今天又听到同学说起分块,就上OI-WIKI查了一下,没想到很快就理解然后敲题了……
何为分块
分块其实是一种思想,本质上跟暴力差不多,常用于处理区间问题。做法是将区间分成一个个小块,然后暴力维护,时间复杂度和分出来的块的个数及块的大小有关
结合例题讲解分块的具体操作
例题1
给出一个长为n的数列,以及n个操作
操作有两种情况,每次操作输入4个数opt、l、r、c
若opt=0,表示将位于[l,r]的之间的数字都加c。
若opt=1,表示询问位于[l,r]的所有数字的和mod (c+1) 。
1≤n≤50000,保证答案在long long范围内
例题思路分析#
只要学过线段树,第一秒想到的基本都会是线段树,但是这题有一个很大的不同在于每次求值的时候会模一个非固定的数,那么如果要用线段树来做就需要在建树的时候不取模,求值的时候再取模,这样的话就有可能会导致线段树上的值超过long long甚至unsigned long long范围(__int128就不要多想了)
所以我们要抛弃线段树,去寻找另一种解决方案。而分块,就是解决这个问题的一个很好的办法
对于一个区间,我们把它分成若干个长度为s的块,最后一个块的长度可以不足s,因为没有规定s必须是n的因数
一个区间a就可以分成
a1,a2…,as⏟b1,as+1,…,a2s⏟b2,…,a(s−1)×s+1,…,an⏟bns
其中bi维护第i个块内的和,可以在读入的时候就记录好每个元素是哪个块,同时维护b数组
更改#
分类讨论一下
如果l,r在同一块内,则直接暴力更改,时间复杂度O(s)
如果不在,就可以分三部分:(1)以l开头的一个不完整块;(2)中间若干个完整块;(3)以r结尾的一个不完整块。其中(1)(3)部分可以暴力更改,(2)部分就直接更改bi,以及xi。其中xi表示整个区间加上了多少,时间复杂度O(ns+s)
查询#
跟更改很像,也是要分类讨论
l,r在同一个块内就直接暴力统计,同时注意加上x数组,时间复杂度O(s)
不在同一个块内也是分三部分,分法同更改部分,(1)(3)部分查询也是暴力,跟l,r同块一样,注意加上x数组。(2)部分直接加上中间完整块的b数组,这里就不用加上x数组。时间复杂度O(ns+s)
时间复杂度分析#
综合更改和查询,一次操作的时间复杂度就是O(ns+s),显然当s是√n的时候是最优的,那么一次操作的时间复杂度就是O(√n),总时间复杂度O(n√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≤N≤100000)个数A1…An组成的序列上进行M(1≤M≤100000)次操作,操作有两种:
(1)1 L R C:表示把AL到AR增加C(|C|≤10000);
(2)2 L R:询问AL到AR之间的最大值。
其实这是线段树的模板题,放到这里来是想要体现分块在更改的时候的一个注意事项(其实是为了加字数)
例题思路分析#
首先还是分块的基本操作,每个块长度为s,同时维护每个块内的最大值b数组
更改#
注意到C的取值范围加了绝对值,就说明C可能是负数。那么如果修改区间内有最大值,就可能会影响最大值。所以说如果我们不是修改整个块,那么就需要重新统计更改后块的最大值
l,r同块的无需多言,直接暴力更改,同时重新维护块的最大值。时间复杂度O(s)
l,r不同块的还是照样分成三部分,头和尾暴力更改,更新最大值,中间的块给整个区间加上C,同时最大值可以直接加C(可以简单推理得到)。时间复杂度O(ns+s)
查询#
l,r同块直接暴力,记得加上给挂在整个块的值
l,r不同块也是分成三部分,中间部分直接与bi进行比较,首尾暴力比较(不要漏掉挂在块上的值)
时间复杂度分析#
s还是取√n最优,时间复杂度仍然是O(n√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都是取√n最优,要根据问题选择合适的s
另外分块也可以做一些奇奇怪怪的毒瘤题,例如吉司机线段树……
祝大家新年快乐!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)