[总集] LOJ「分块」数列分块入门1 – 9


分块9题

出题人hzw的解析


(tips.以下代码中IO优化都已省去,想看可以点传送门)


数列分块入门 1

修改:区间加

查询:单点值查询

这是一道经典题目,线段树、树状数组等都可以搞,这里讲讲分块

分块就是将一定长度的一段数打包成块,统一处理的算法

每个块都有自己的信息,自己的标记,统一维护,统一查询

我们可以将每个区间修改或查询拆分成在若干个整块,和头尾两个不完整的块中修改、查询后信息的总和

那此题中块要分多大呢?答案是$ \sqrt n $

由于本人太弱,下面给出hzw大佬的证明:

如果我们把每m个元素分为一块,共有 $ \frac{n}{m} $ 块,每次区间加的操作会涉及 $ O( \frac{n}{m} ) $ 个整块,以及区间两侧两个不完整的块中至多2m个元素。

我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接 $ O(1) $ 标记,而不完整的块由于元素比较少,暴力修改元素的值。

每次询问时返回元素的值加上其所在块的加法标记。

这样每次操作的复杂度是 $ O( \frac{n}{m} )+ O(m) $ ,根据均值不等式,当m取 $ \sqrt n $ 时总复杂度最低

代码

#include <bits/stdc++.h>
#define rint register int
using namespace std;
const int N=50005,B=225; //B是最大块数
int n,len,bn; //bn是块数,len是块长
int L[B],R[B],tag[B],a[N],block[N];
//L[i]是块i的左边界,R[i]是块i的右边界,tag[i]是块i的统一加法标记
//a[i]表示元素i不加标记时的值,block[i]表示元素i的所属块
inline void add(int l,int r,int c){
int p=block[l],q=block[r]; //p是左边界的所属块,q是右边界的所属块
if(p==q){ //如果左右边界在同一块中
for(int i=l;i<=r;++i) //直接对[l,r]每个元素进行修改
a[i]+=c;
return ;
}
for(rint i=p+1;i<=q-1;++i)
tag[i]+=c; //给整块打上加法标记,就可以忽略块中的元素
for(rint i=l;i<=R[p];++i)
a[i]+=c; //给左边剩下的元素进行处理
for(rint i=L[q];i<=r;++i)
a[i]+=c; //给右边剩下的元素进行处理
}
signed main(){
read(n);
for(rint i=1;i<=n;++i)
read(a[i]);
bn=len=sqrt(n);
for(rint i=1;i<=bn;++i){
L[i]=(i-1)*len+1;
R[i]=i*len;
for(rint j=L[i];j<=R[i];++j)
block[j]=i; //给没个元素规定所属块
}
if(R[bn]<n){ //如果没分完就再分一个
L[++bn]=R[bn-1]+1;
R[bn]=n;
for(rint i=L[bn];i<=n;++i)
block[i]=bn;
}
for(rint i=1;i<=n;++i){
int opt,l,r,c;
read(opt);
read(l);
read(r);
read(c);
if(!opt) add(l,r,c); //修改
else Write(a[r]+tag[block[r]],'\n'); //查询:自己的值加上自己所属块的统一标记
}
}

数列分块入门 2

修改:区间加

查询:区间排名

这题与1相似,只是需要额外维护一个st[i][j]表示块i从小到大排序后排名第j的数

用sort暴力维护它的单调性有助于使用lower_bound()直接得到某个块中的排名,再将信息合并即可

修改部分只需在1的基础上在每次对一个块完成修改后维护它的st[][]即可

查询部分对于零散小块,暴力枚举判断;对于整块,二分查找确定排名即可得到该块中的答案;最后再将所有块中的答案累加即可

代码

#include <bits/stdc++.h>
#define rint register int
using namespace std;
const int N=50050,B=235;
int n,len,bn,L[B],R[B],tag[B],a[N],block[N],st[B][B];
//其余含义同一,st[i][j]表示块i从小到大排序后排名第j的数
inline void maintain(const int &x){ //将块x中元素放入st[][]并重新排序,以维护st[x][]中的有序性
memset(st[x],0,sizeof st[x]);
for(rint i=L[x];i<=R[x];i++)
st[x][++st[x][0]]=a[i];
sort(st[x]+1,st[x]+1+st[x][0]);
}
inline void add(int l,int r,int c){
/*基本同一*/
int p=block[l],q=block[r];
if(p==q){
for(int i=l;i<=r;++i)
a[i]+=c;
maintain(p); //维护块p
return ;
}
for(rint i=p+1;i<=q-1;++i)
tag[i]+=c;
for(rint i=l;i<=R[p];++i)
a[i]+=c;
for(rint i=L[q];i<=r;++i)
a[i]+=c;
maintain(p); //维护块p
maintain(q); //维护块q
}
inline int que(int l,int r,int c){
int p=block[l],q=block[r],res=0;
if(p==q){
for(rint i=l;i<=r;i++)
res+=(a[i]+tag[p]<c); //直接遍历[l,r]每个元素,判断是否满足条件
return res;
}
for(rint i=p+1;i<=q-1;i++){
int x=lower_bound(st[i]+1,st[i]+1+st[i][0],c-tag[i])-st[i]; //利用STL二分查找方便的得到要求值在该块中的排名
res+=x-1; //他之前的都是符合条件的,下标减一加入答案即可
}
for(rint i=l;i<=R[p];i++)
res+=(a[i]+tag[p]<c); //暴力处理剩余部分
for(rint i=L[q];i<=r;i++)
res+=(a[i]+tag[q]<c);
return res;
}
signed main(){
read(n);
for(rint i=1;i<=n;++i)
read(a[i]);
/*基本同一*/
bn=len=sqrt(n);
for(rint i=1;i<=bn;++i){
L[i]=(i-1)*len+1;
R[i]=i*len;
}
if(R[bn]<n){
L[++bn]=R[bn-1]+1;
R[bn]=n;
}
for(rint i=1;i<=bn;i++){
for(int j=L[i];j<=R[i];j++){
block[j]=i;
st[i][++st[i][0]]=a[j]; //将初值放入st[][]并排序使有序
}
sort(st[i]+1,st[i]+1+st[i][0]);
}
for(rint i=1;i<=n;++i){
int opt,l,r,c;
read(opt);
read(l);
read(r);
read(c);
if(!opt) add(l,r,c); //修改
else Write(que(l,r,c*c),'\n'); //查询
}
}

数列分块入门 6

修改:单点插入

查询:单点值

这道题我们采用动态插入的方法

借助vector的insert过程我们可以方便地完成插入操作

而插入的位置可以用下面一段代码找到:

(x初值为所要找的下标,终值为所在块中的下标,p为所在块的编号,vec[]为块所用的存储结构)

while(x>vec[p].size()) x-=vec[p].size(),p++;

而查询的操作也需要调用上面一段代码即可轻松找到所要位置

ε = = (づ′▽`)づ

有本题数据是随机的,以上的部分已经可以应对,但如果不随机呢?

不停地插入会使某个块变得十分臃肿,使复杂度退化为\(O(n^2)\)

那该怎么办呢?

我们采用分块重构,当某个块超过原块长的\(\sqrt 块长\)倍后,拆掉原有块,组建新的块

重构部分代码如下:

	int n=0; //从0开始计数元素数
for(rint i=1;i<=bn;i++){
int kkk=vec[i].size();
for(int j=0;j<kkk;j++)
a[++n]=vec[i][j]; //将原来块中元素按顺序移入a[]
vec[i].clear(); //清空原块
}
len=sqrt(n); //重新定块长
for(rint i=1;i<=n;i++)
vec[(i-1)/len+1].push_back(a[i]); //将a[]中元素重新分块
bn=(n-1)/len+1; //重新确定块数
lim=len*sqrt(len); //重新定限制

(tips.LOJ上以及hzw的题解中倍数都定为20倍,其实是\(\sqrt[4] {n}\),比较粗略,可以用我的准确倍数优化,即上方代码的最后一行)

代码

#include <bits/stdc++.h>
#define rint register int
using namespace std;
const int N=200050,B=460; //开双倍内存
int a[N],n,len,lim,bn;
vector<int> vec[B];
struct res{int block,pos;};
inline res get(int x){ //获取位置,见上方解释~~
int p=1;
while(x>vec[p].size()) x-=vec[p].size(),p++;
return (res){p,x-1};
}
inline void rebuild(){ //重构,见上方解释~~
int n=0;
for(rint i=1;i<=bn;i++){
int kkk=vec[i].size();
for(int j=0;j<kkk;j++)
a[++n]=vec[i][j];
vec[i].clear();
}
len=sqrt(n);
for(rint i=1;i<=n;i++)
vec[(i-1)/len+1].push_back(a[i]);
bn=(n-1)/len+1;
lim=len*sqrt(len);
}
inline void edi(int x,int v){
res now=get(x);
vec[now.block].insert(vec[now.block].begin()+now.pos,v); //找到位置后用insert插入
if(vec[now.block].size()>lim) rebuild(); //如果块大小超过限制就重构
}
inline int que(int x){
res now=get(x); //直接查询
return vec[now.block][now.pos];
}
signed main(){
read(n);
for(rint i=1;i<=n;i++)
read(a[i]);
len=sqrt(n);
for(rint i=1;i<=n;i++)
vec[(i-1)/len+1].push_back(a[i]); //分块
bn=(n-1)/len+1;
lim=len*sqrt(len); //定限制
while(n--){
int opt,l,r,c;
read(opt),read(l),read(r),read(c);
if(!opt) edi(l,r);
else Write(que(r),'\n');
}
}

数列分块入门 7

修改:区间加,区间乘

查询:单点查询

这道题十分经典,洛谷P3373是它的区间求和版,用线段树做

对于这道题,你需要有一个小学前置知识:乘法优先级比加法高

因此,在这道题中,对于每个块我们所要维护的两个信息:加法标记和乘法标记,很明显乘法标记优先级是要高于加法标记的

之后就是分类讨论啦:

  • 设整块x的加法标记为add,乘法标记为mul,给出的操作值为val,则:
  1. 当x要进行一次加法操作时:
add+=val;
  1. 当x要进行一次乘法操作时:
mul*=val;
add*=val;
  • 对于零块,暴力枚举修改即可

另外,由于由于零块操作的不确定性,对零块要单独进行一次push操作,用改块的标记去修改该块中的每个元素(先乘后加),并将乘标记归1,加标记归0

以上就是修改操作

至于查询,直接将元素值先乘上它的乘法标记,再加上它的加法标记即可

代码

#include <bits/stdc++.h>
#define rint register int
using namespace std;
const int mod=1e4+7,N=1e5+20,B=335;
int n,a[N],block[N],L[B],R[B],mul[B],add[B],bn,len;
inline void push(const int &x){
for(rint i=L[x];i<=R[x];i++)
a[i]=(a[i]*mul[x]%mod+add[x])%mod;
mul[x]=1; //千万记得要将标记还原!!!本人就在这里错过。。。
add[x]=0;
}
inline void edi(bool flag,int l,int r,int v){ //flag表示当前操作种类,1是乘,0是加
int p=block[l],q=block[r];
push(p);
if(p==q){
for(rint i=l;i<=r;i++)
a[i]=flag?(a[i]*v)%mod:(a[i]+v)%mod; //暴力枚举修改
return ;
}
push(q);
for(rint i=p+1;i<=q-1;i++){
if(flag){
(mul[i]*=v)%=mod; //乘法对两种标记都有影响
(add[i]*=v)%=mod;
}
else{
(add[i]+=v)%=mod;
}
}
for(rint i=l;i<=R[p];i++)
a[i]=flag?(a[i]*v)%mod:(a[i]+v)%mod; //同理,暴力枚举
for(rint i=L[q];i<=r;i++)
a[i]=flag?(a[i]*v)%mod:(a[i]+v)%mod;
}
signed main(){
read(n);
for(rint i=1;i<=n;i++)
read(a[i]),a[i]%=mod;
bn=len=sqrt(n);
for(rint i=1;i<=bn;i++){
L[i]=(i-1)*len+1;
R[i]=i*len;
}
if(R[bn]<n){
L[++bn]=R[bn-1]+1;
R[bn]=n;
}
for(rint i=1;i<=bn;i++){
mul[i]=1; //记得乘法标记初值为1
for(rint j=L[i];j<=R[i];j++)
block[j]=i;
}
while(n--){
int opt,l,r,c;
read(opt),read(l),read(r),read(c);
if(opt<=1){
edi(opt,l,r,c);
}
else{
c=block[r];
Write((a[r]*mul[c]%mod+add[c])%mod,'\n');
}
}
}

数列分块入门 8

修改:区间赋值

查询:区间计数

区间赋值的操作非常简单暴力,枚举即可

对于查询,我们发现所有块大致可以被分为2类,即数字相同的,和数字不相同的

所以我们可以维护一个same[]数组,记录每个块的相同值,不同的话即为-oo

(tips.oo即无穷,可以选择为0x3f3f3f3f或0x7fffffff之类极值)

(tips.hzw的代码是可以被hack的,因为他的-oo是-1,而我们知道一个块内的元素是有可能全-1的)

有了same[]后怎么办呢?

当然是奇技淫巧 有技巧地暴力啦!

  1. 赋值可以被简化,整块直接O(1)修改same值即可,左右零散块再暴力枚举
  2. 计数时,对于块x,查询值v,可以用下面一段伪代码进行快速判断:
if(same[x]==-1){
from 左边界 to 右边界
枚举判断;
}
else
if(same[x]==v){
ans+=右边界-左边界+1;
}
else{
ans+=0;
}
  1. 为了避免冗余操作,在每次要求查询前,对要被查询且same值被修改过的块进行一次push操作,将该块的same值赋给该块中的每一个元素,进行维护

至于复杂度,让我再引用一次hzw大佬的证明:

这样看似最差情况每次都会耗费 \(O(n)\) 的时间,但其实可以这样分析:

假设初始序列都是同一个值,那么查询是 \(O(\sqrt n)\) ,如果这时进行一个区间操作,它最多破坏首尾2个块的标记,所以只能使后面的询问至多多2个块的暴力时间,所以均摊每次操作复杂度还是 \(O(\sqrt n)\) 。

代码

#include <bits/stdc++.h>
#define rint register int
using namespace std;
const int N=1e5+30,B=333,oo=0x3f3f3f3f;
int a[N],block[N],L[B],R[B],same[B],n,bn,len;
inline void push(const int &x){ //用same维护块内元素,应对暴力枚举的情况
if(same[x]!=-oo){
for(rint i=L[x];i<=R[x];i++)
a[i]=same[x];
same[x]=-oo;
}
}
inline int work(int l,int r,int v){
int p=block[l],q=block[r],res=0;
push(p); //维护左块
if(p==q){
if(same[p]==v) return r-l+1; //如果same值满足条件,直接计算出结果
for(rint i=l;i<=r;i++) //暴力枚举&赋值
if(a[i]==v) res++;
else a[i]=v;
return res;
}
push(q); //维护右块
for(rint i=p+1;i<=q-1;i++){
if(same[i]==v) res+=R[i]-L[i]+1;
else{
if(same[i]==-oo)
for(rint j=L[i];j<=R[i];j++)
if(a[j]==v) res++;
else a[j]=v;
}
same[i]=v; //记得把整块same标记更换掉
}
/*以下处理方法同上p==q时的情况*/
if(same[p]==v) res+=R[p]-l+1;
else
for(rint i=l;i<=R[p];i++)
if(a[i]==v) res++;
else a[i]=v;
if(same[q]==v) res+=r-L[q]+1;
else
for(rint i=L[q];i<=r;i++)
if(a[i]==v) res++;
else a[i]=v;
return res;
}
signed main(){
read(n);
for(rint i=1;i<=n;i++)
read(a[i]);
bn=len=sqrt(n);
for(rint i=1;i<=bn;i++){
L[i]=(i-1)*len+1;
R[i]=i*len;
}
if(R[bn]<n){
L[++bn]=R[bn-1]+1;
R[bn]=n;
}
for(rint i=1;i<=bn;i++){
same[i]=-oo; //一开始认为块内每个元素都不相同,打个-oo标记
for(rint j=L[i];j<=R[i];j++)
block[j]=i;
}
while(n--){
int l,r,c;
read(l),read(r),read(c);
Write(work(l,r,c),'\n');
}
}

[总集] LOJ 分块1 – 9的更多相关文章

  1. LOJ分块⑨题汇总

    从零开始的分块学习系列(感谢hzwer) 题目顺序是我建议的做题顺序 先说一句:分块的核心思想(其实本身分块就可以说是一种思想)是:均摊(或者说平衡/权衡?)复杂度,同时这种思想本身不只局限于序列分块 ...

  2. [LOJ] 分块九题 6

    单点插入,单点查询. 优化了的链表. 链表老写错,干脆用vector,也不算慢. 注意链表退化的问题,及时(比如操作根号n次)就重新建块,实测速度可以提高一倍,这还是数据随机的情况,若涉及大量同一位置 ...

  3. [LOJ] 分块九题 4

    https://loj.ac/problem/6280 区间修改,区间求和. 本来线段树的活. //Stay foolish,stay hungry,stay young,stay simple #i ...

  4. [LOJ] 分块九题 3

    https://loj.ac/problem/6279 区间修改,区间查询前驱. TLE无数,我觉得这代码最精髓的就是block=1000. 谜一样的1000. 两个启示: 块内可以维护数据结构,比如 ...

  5. [LOJ] 分块九题 2

    https://loj.ac/problem/6278 区间修改,查询区间第k大. 块内有序(另存),块内二分. 还是用vector吧,数组拷贝排序,下标搞不来.. //Stay foolish,st ...

  6. [LOJ] 分块九题 1

    https://loj.ac/problem/6277 区间修改,单点查询. //Stay foolish,stay hungry,stay young,stay simple #include< ...

  7. [LOJ] 分块九题 8

    区间查询数值+整体赋值 维护tag代表整个区间被赋成了tag[i] 用pushdown操作,而不是修改了再check. 不压缩代码了,调起来心累,长点有啥不好. //Stay foolish,stay ...

  8. [LOJ] 分块九题 7

    区间加法,区间乘法,单点查询. 洛谷线段树2 屡清加法乘法的关系,定义答案为 a*mut+add 对于整块: 新的乘w,mut和add都要乘w 新的加w,add加w //Stay foolish,st ...

  9. [LOJ] 分块九题 5

    区间开平方,区间查询. lazy标记改为区间是否全是1或者0,这样的区间是没有更新价值的. //Stay foolish,stay hungry,stay young,stay simple #inc ...

随机推荐

  1. nodejs之express入门

    首先安装nodejs,官网下载安装包直接安装, 由于node自带npm,所以npm命令直接即可使用 打开cmd,使用npm install -g express-generator安装express ...

  2. layer 1.8.5 引用样式失效

    在layer.min.js里,默认引用的官网的样式,目前,网址失效.所以使用本地的样式即可. 将e后面的网址去除,即使用本地的样式.

  3. java和python实现一个加权SlopeOne推荐算法

    一.加权SlopeOne算法公式: (1).求得所有item之间的评分偏差 上式中分子部分为项目j与项目i的偏差和,分母部分为所有同时对项目j与项目i评分的用户数 (2).加权预测评分 项目j与项目i ...

  4. HDU 6089 Rikka with Terrorist (线段树)

    题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=6089 题解 这波强行维护搞得我很懵逼... 扫描线,只考虑每个点能走到左上方(不包括正上方,但包括正左 ...

  5. 图解数据库中的join操作

    1.所有的join都从cross join衍生而来 2.所有join图示 转自Say NO to Venn Diagrams When Explaining JOINs

  6. Java常考面试题整理(六)

    101.HTTP相应的结构是怎么样的? 参考答案: HTTP相应由三个部分组成: 1.状态码(status code):描述了相应的状态,可以用来检查是否成功的完成了请求.请求失败的情况下,状态码可以 ...

  7. Linux常用命令及操作(第二弹)

    Ctrl l清屏 Ctrl d关闭终端 Ctrl Alt T打开终端 pwd 查看当前的目录 Shift Ctrl C复制 Shift Ctrl V粘贴 Shift Ctrl N打开新的终端 F11 ...

  8. pom.xml报Plugin execution not covered by lifecycle configuration错误

    环境     eclipse 4.3.0     maven 3.0.4     m2e 1.4.0      出现场景     以前的老项目,在我的环境(我的环境较新)下,别人老环境不报错. 错误示 ...

  9. extentsreport testng美化报告生成

    一:主要内容 优化testng测试报告,使用extentsreport 解决extentsreport打开后加载不出来样式的问题 二:报告效果 先上图,看下testng extentsreport报告 ...

  10. HearthBuddy 第一次调试

    HearthBuddy https://www.jiligame.com/70639.html 解压缩包,打开hearthbuddy.exe直接运行就可以:不用替换mono.dll直接可用:不需要校验 ...