分块与莫队相关题目
- P13976 数列分块入门 1
- P13977 数列分块入门 2
- P13978 数列分块入门 3
- P13979 数列分块入门 4
- P13980 数列分块入门 5
- P13981 数列分块入门 6
- P13982 数列分块入门 7
- P13983 数列分块入门 8
- P13984 数列分块入门 9
- 作诗
- 「SDOI2009」HH的项链(这个是弱化版)
- 「CQOI2018」异或序列
- 小Z的袜子
- Mato的文件管理
P13976 数列分块入门 1
1. 题意
有一个长度为 \(n\) 的数列 \(a\),接下来有 \(n\) 次操作:
0 l r c:把区间 \([l,r]\) 内的所有数加上 \(c\)。1 l r c:询问 \(a_r\) 的值,其中 \(l\) 和 \(c\) 忽略。
注意查询操作问的是 第 \(r\) 个位置的值,不是区间和,也不是区间查询。
例如输入:
1 0 1 0
真正有用的是 r=1,表示查询 \(a_1\)。
2. 为什么用分块
操作特点:
- 区间加法:可能加一个很长的区间;
- 单点查询:只查一个位置。
如果每次区间加法都暴力 for(i=l;i<=r;i++) a[i]+=c,最坏一次 \(O(n)\),总复杂度 \(O(n^2)\),会超时。
使用分块:
- 把数组分成若干块,每块长度约为 \(\sqrt n\);
- 对于完整被覆盖的块,不逐个修改,而是给这个块打一个“整体加法标记”
lazy; - 对于左右两端不完整的块,直接暴力修改
a[i]; - 查询时,答案就是:
其中:
a[r]是元素自身被零散修改过的值;lazy[bl[r]]是 \(r\) 所在块被整体加过的值。
3. 变量含义
代码中:
int n;
cin>>n;
int len=sqrtl(n);
vector<int>a(n+1),bl(n+1),lazy(n/len+2,0);
a[i]:第 \(i\) 个位置自身的基础值;bl[i]:第 \(i\) 个位置属于哪一个块,块编号从 1 开始;lazy[i]:第 \(i\) 个块被整体加了多少;len:每一块的块长,约为 \(\sqrt n\)。
因为代码开头有:
#define int long long
所以这里的 int 实际是 long long,a、lazy 等都可以承受较大的累加值。
4. 分块初始化
for(int i=1;i<=n;i++)
{
cin>>a[i];
if(i%len==1) bl[i]=bl[i-1]+1;
else bl[i]=bl[i-1];
}
块长 len = sqrt(n)。
判断块首:
if(i%len==1)
含义是:当 \(i\) 是每一块的第一个位置时,块编号加 1。
例如 \(n=4\),len=2:
| i | i%2 | bl[i] |
|---|---|---|
| 1 | 1 | bl[1]=bl[0]+1=1 |
| 2 | 0 | bl[2]=bl[1]=1 |
| 3 | 1 | bl[3]=bl[2]+1=2 |
| 4 | 0 | bl[4]=bl[3]=2 |
所以:
- 位置 1、2 属于第 1 块;
- 位置 3、4 属于第 2 块。
bl[0] 默认是 0,所以 bl[1]=1。
lazy 的大小:
vector<int> lazy(n/len+2,0);
块的数量最多约为:
所以开 n/len+2 足够,防止下标越界。
5. 区间加法 opt=0
if(op==0)
{
int L=bl[l],R=bl[r];
if(L==R) for(int i=l;i<=r;i++) a[i]+=c;
else
{
for(int i=L+1;i<R;i++) lazy[i]+=c;
for(int i=l;bl[i]==L;i++) a[i]+=c;
for(int i=r;bl[i]==R;i--) a[i]+=c;
}
}
分两种情况。
情况一:l 和 r 在同一块
if(L==R) for(int i=l;i<=r;i++) a[i]+=c;
因为只修改块内一部分,不能给整个块打标记,否则会把块内其他不需要修改的位置也加上 \(c\)。
所以直接暴力修改区间 \([l,r]\)。
情况二:l 和 r 不在同一块
int L=bl[l],R=bl[r];
此时区间分成三部分:
- 左边散块:从
l到L块末尾; - 中间完整块:第
L+1块到第R-1块; - 右边散块:从
R块开头到r。
中间完整块打标记
for(int i=L+1;i<R;i++) lazy[i]+=c;
这些块被完整覆盖,所以只给它们的 lazy 加 \(c\),不修改 a。
左散块暴力修改
for(int i=l;bl[i]==L;i++) a[i]+=c;
从 l 开始,一直改到块编号不再是 L 为止,也就是改到第 L 块的末尾。
右散块暴力修改
for(int i=r;bl[i]==R;i--) a[i]+=c;
从 r 开始向左,一直改到块编号不再是 R 为止,也就是改到第 R 块的开头。
这样:
- 完整块只改
lazy,复杂度低; - 两端不完整块直接改
a,保证正确。
6. 单点查询 opt=1
else cout<<a[r]+lazy[bl[r]]<<"\n";
查询的是 \(a_r\) 的当前值。
一个位置 \(r\) 的值由两部分组成:
a[r]:它自身被零散修改时加上的值;lazy[bl[r]]:它所在块被整体加过的值。
所以答案就是:
a[r] + lazy[bl[r]]
注意 opt=1 时,输入中的 l 和 c 忽略,真正使用的是 r。
例如:
1 0 1 0
这里 r=1,查询 \(a_1\)。
P13977 数列分块入门 2
1. 题目
维护长度为 \(n\) 的数列 \(a\),支持:
0 l r c:把区间 \([l,r]\) 内所有数加上 \(c\);1 l r c:询问区间 \([l,r]\) 中有多少个数严格小于 \(c^2\)。
注意查询阈值是 \(c^2\),不是 \(c\)。
2. 分块 + 块内排序
把数组分成若干块,每块长度约为 \(m\)。
每个位置记录:
a[i]:第 \(i\) 个位置自身的基础值;bl[i]:第 \(i\) 个位置属于哪一块;lazy[j]:第 \(j\) 块被整体加了多少;b[j]:第 \(j\) 块内所有a[i]排序后的结果。
对于第 \(i\) 个位置的实际值:
因为同一块内所有元素共享同一个 lazy,所以块内排序时只排 a[i] 即可。
3. 块划分
int m=max(1LL,(long long)sqrt(n)/2);
if((i-1)%m==0)bl[i]=bl[i-1]+1;
else bl[i]=bl[i-1];
当 \(i\) 是某一块的第一个位置时,块编号加一。
第 \(L\) 块的起始位置为:
最后一块可能不足 \(m\) 个元素,用 b[L].size() 控制长度。
4. 区间加法 op=0
分两种情况。
情况一:l 和 r 在同一块
if(L==R)
{
for(int i=l;i<=r;i++)a[i]+=c;
for(int i=(L-1)*m+1,j=0;j<b[L].size();i++,j++)b[L][j]=a[i];
sort(b[L].be,b[L].en);
}
因为只修改块内一部分,不能给整块打 lazy,否则会影响块内其他位置。
所以直接暴力修改 a[i],然后用修改后的 a 重建该块的 b[L] 并重新排序。
情况二:l 和 r 不在同一块
int L=bl[l],R=bl[r];
for(int i=l;bl[i]==L;i++)a[i]+=c;
重建并排序 b[L];
for(int i=r;bl[i]==R;i--)a[i]+=c;
重建并排序 b[R];
for(int j=L+1;j<R;j++)lazy[j]+=c;
- 左散块:从
l到第L块末尾,暴力修改a,重建排序; - 右散块:从
r到第R块开头,暴力修改a,重建排序; - 中间完整块:只给
lazy[j]加c,不修改a。
重建块时:
for(int i=(L-1)*m+1,j=0;j<b[L].size();i++,j++)b[L][j]=a[i];
把第 \(L\) 块内所有 a[i] 重新拷贝到 b[L],再排序。
5. 区间查询 op=1
先计算阈值:
c*=c;
查询条件:
即:
散块查询
对于左右不完整的块,直接暴力扫描:
for(int j=l;bl[j]==L;j++)if(a[j]+lazy[L]<c)ans++;
for(int j=r;bl[j]==R;j--)if(a[j]+lazy[R]<c)ans++;
完整块查询
对于中间完整块 \(j\),需要统计:
即:
令:
int d=c-lazy[j];
由于 b[j] 已经排序,用 lower_bound 找到第一个 \(\ge d\) 的位置,前面的元素个数就是答案:
ans+=lower_bound(b[j].be,b[j].en,d)-b[j].be;
P13978 数列分块入门 3
1. 题意
维护长度为 \(n\) 的数列 \(a\),支持:
0 l r c:把区间 \([l,r]\) 内所有数加上 \(c\);1 l r c:查询区间 \([l,r]\) 中严格小于 \(c\) 的最大元素,即 \(c\) 的前驱;不存在输出 \(-1\)。
数据范围 \(n\le 200000\),值域约 \([-2^{31},2^{31}-1]\)。
2. 分块思想
把数组分成若干块,块长取
每块维护:
a[i]:第 \(i\) 个位置的基础值;bl[i]:第 \(i\) 个位置所属块编号;tag[j]:第 \(j\) 块被整体加了多少;b[j]:第 \(j\) 块内所有a[i]排序后的结果。
一个位置的真实值是
注意 b[j] 中存的是 不含 tag[j] 的 a 值,所以查询整块时,条件
等价于
这样可以在 b[j] 中二分。
3. 块划分
if((i-1)%len==0)bl[i]=bl[i-1]+1;
else bl[i]=bl[i-1];
b[bl[i]].push_back(a[i]);
当 (i-1)%len==0 时,i 是某一块的第一个位置,块编号加一。
第 \(L\) 块的起始位置为
最后一块可能不足 len 个元素,用 b[L].size() 控制长度。
4. 区间加法 op=0
4.1 同一块 L==R
for(int i=l;i<=r;i++)a[i]+=c;
for(int i=(L-1)*len+1,j=0;j<b[L].size();i++,j++)b[L][j]=a[i];
sort(b[L].be,b[L].en);
只修改块内一部分,不能给整块打 tag,否则会影响块内其他位置。
所以暴力修改 a[i],然后用修改后的 a 重建该块的 b[L] 并重新排序。
4.2 跨块 L!=R
for(int i=l;bl[i]==L;i++)a[i]+=c;
重建并排序 b[L];
for(int i=r;bl[i]==R;i--)a[i]+=c;
重建并排序 b[R];
for(int i=L+1;i<R;i++)tag[i]+=c;
- 左散块:从
l到第L块末尾,暴力修改a,重建排序; - 右散块:从
r到第R块开头,暴力修改a,重建排序; - 中间完整块:只给
tag[i]加c,不修改a。
重建块时:
for(int i=(L-1)*len+1,j=0;j<b[L].size();i++,j++)b[L][j]=a[i];
把第 \(L\) 块内所有 a[i] 重新拷贝到 b[L],再排序。
5. 查询前驱 op=1
查询条件:
即
5.1 同一块 L==R
for(int i=l;i<=r;i++)
{
if(a[i]+tag[L]<c)ans=max(ans,a[i]+tag[L]);
}
if(ans==-inf)ans=-1;
暴力扫描区间内每个位置,取满足条件的最大值。
5.2 跨块 L!=R
for(int i=l;bl[i]==L;i++)if(a[i]+tag[L]<c)ans=max(ans,a[i]+tag[L]);
for(int i=r;bl[i]==R;i--)if(a[i]+tag[R]<c)ans=max(ans,a[i]+tag[R]);
for(int i=L+1;i<R;i++)
{
int d=c-tag[i];
auto x=lower_bound(b[i].be,b[i].en,d);
if(x==b[i].be)continue;
x--;
ans=max(ans,*x+tag[i]);
}
if(ans==-inf)ans=-1;
- 左右散块:暴力扫描,取满足条件的最大值;
- 中间完整块:在排序数组
b[i]中二分。
对于第 \(i\) 块,令
在 b[i] 中找第一个 \(\ge d\) 的位置 x:
- 如果
x==b[i].begin(),说明块内所有a都 \(\ge d\),没有满足 \(a<d\) 的元素,跳过; - 否则
x--,此时*x是块内严格小于 \(d\) 的最大a值,加上tag[i]就是该块能提供的最大前驱候选。
最后若 ans 仍为 -inf,说明不存在前驱,输出 -1。
P13979 数列分块入门 4
1. 题意回顾
维护长度 \(n\) 的数列 \(a\),共 \(n\) 次操作:
0 l r c:区间 \([l,r]\) 全部加 \(c\);1 l r c:查询 \(\left(\sum_{i=l}^{r} a_i\right) \bmod (c+1)\),答案要求非负。
注意:
- 查询是 区间和,不是单点;
- 取模是 \(c+1\),不是 \(c\);
- 答案必须 非负(C++ 中负数取模会保留负号)。
2. 数据结构:分块
把长度 \(n\) 的数组切成若干块,块长取
每个位置和每个块维护以下信息:
| 变量 | 含义 |
|---|---|
a[i] |
第 \(i\) 个位置的基础值 |
bl[i] |
第 \(i\) 个位置所属块编号(从 1 开始) |
lazy[j] |
第 \(j\) 个块被整体加了多少 |
sum[j] |
第 \(j\) 个块当前的元素和(已经包含 lazy[j]) |
LL[j] |
第 \(j\) 块的左端点 |
RR[j] |
第 \(j\) 块的右端点 |
对任意位置 \(i\),它的真实值是
对任意块 \(j\),它的元素和是
关键约定:sum[j] 已经包含了 lazy[j] 的贡献,所以查询整块时直接取 sum[j] 即可,不能再加 lazy[j]。
3. 初始化
int len=sqrtl(n);
vector<int>a(n+1),bl(n+1);
vector<int>lazy(n/len+5,0),sum(n/len+5,0);
vector<int>RR(n/len+5,0),LL(n/len+5,0);
块数最多约为 \(\lceil n/len\rceil\le n/len+1\),所以开 n/len+5 足够大。
for(int i=1;i<=n;i++)
{
cin>>a[i];
if((i-1)%len==0)
{
bl[i]=bl[i-1]+1;
LL[bl[i]]=i;
}
else bl[i]=bl[i-1];
RR[bl[i]]=i;
sum[bl[i]]+=a[i];
}
逐行解释:
(i-1)%len==0:表示 \(i\) 是某一块的第一个位置;- 此时块编号加 1,并记录该块左端点
LL。 - 用
(i-1)%len而不是i%len,是为了在len=1时也正确。若写i%len==1,当len=1时永远不成立,会导致所有元素被划进第 0 块,程序出错。
- 此时块编号加 1,并记录该块左端点
RR[bl[i]]=i:块编号每确定一次,就把这一块右端点更新为当前位置。循环结束时RR[j]就是第 \(j\) 块的最后一个位置。sum[bl[i]]+=a[i]:块初始和就是块内元素初值之和。
4. 区间加法 op=0
int L=bl[l],R=bl[r];
L、R 分别是 \(l\) 和 \(r\) 所属块。
4.1 情况一:L == R(同一块)
if(L==R)
{
for(int i=l;i<=r;i++)
{
a[i]+=c;
sum[L]+=c;
}
}
只修改块内一部分,不能给整块打 lazy,否则块内其他位置也会被错误加上 \(c\)。
所以逐个修改 a[i],同时让 sum[L] 每个元素加 \(c\)(共加 \(r-l+1\) 次 \(c\))。
4.2 情况二:L != R(跨块)
区间分成三部分:
- 左散块:从 \(l\) 到第 \(L\) 块末尾;
- 中间完整块:第 \(L+1\) 块到第 \(R-1\) 块;
- 右散块:从第 \(R\) 块开头到 \(r\)。
中间完整块:只打 lazy
for(int i=L+1;i<R;i++)
{
lazy[i]+=c;
sum[i]+=c*(RR[i]-LL[i]+1);
}
这些块被完整覆盖,给 lazy[i] 加 \(c\) 就够了。
但由于 sum[i] 是含 lazy[i] 的块和,必须同步更新:
左散块:暴力修改
for(int i=l;bl[i]==L;i++)
{
a[i]+=c;
sum[bl[i]]+=c;
}
从 \(l\) 开始,一直改到块编号不是 \(L\) 为止。
每个 a[i] 加 \(c\) 后,所在块的 sum 也加 \(c\)。
右散块:暴力修改
for(int i=r;bl[i]==R;i--)
{
a[i]+=c;
sum[bl[i]]+=c;
}
从 \(r\) 向左,一直改到块编号不是 \(R\) 为止。
同样同步更新 sum。
为什么左散块和右散块不会重复
当 \(L<R\) 时:
- 左散块是 \(l\sim RR[L]\);
- 右散块是 \(LL[R]\sim r\)。
中间块 \(L+1\sim R-1\) 已经单独处理。三部分互不重叠,正好拼成 \([l,r]\)。
5. 区间查询 op=1
int L=bl[l],R=bl[r],ans=0;
5.1 情况一:L == R
if(L==R)for(int i=l;i<=r;i++)ans+=a[i]+lazy[bl[i]];
同一块内直接暴力扫描,每个位置真实值是 a[i]+lazy[bl[i]]。
5.2 情况二:L != R
for(int i=L+1;i<R;i++)ans+=sum[i];
ans%=c+1;
for(int i=l;bl[i]==L;i++)ans+=a[i]+lazy[bl[i]];
ans%=c+1;
for(int i=r;bl[i]==R;i--)ans+=a[i]+lazy[bl[i]];
- 中间完整块:
sum[i]已经是含lazy的块和,直接累加; - 左右散块:暴力累加每个元素真实值
a[i]+lazy[bl[i]]。
6. 取模处理(关键)
6.1 中间每段累加后立即取模
for(int i=L+1;i<R;i++)ans+=sum[i];
ans%=c+1;
for(int i=l;bl[i]==L;i++)ans+=a[i]+lazy[bl[i]];
ans%=c+1;
for(int i=r;bl[i]==R;i--)ans+=a[i]+lazy[bl[i]];
为什么中间要提前取模?
sum[i]本身可能非常大(多次累加 \(c\) 后可以达到 \(2^{55}\) 量级);ans累加 \(\sqrt n\) 个块后可能超过long long上限;- 提前取模可以把
ans控制在 \([-(c+1)+1, c]\) 范围内,避免溢出。
6.2 最后统一处理负数
ans%=c+1;
if(ans<0)ans+=c+1;
C++ 中,负数取模会保留负号:
而数学上的非负余数应当是 \(3\)。加一次 \(c+1\) 即可修正:
- 若
ans<0,ans+=c+1后落入 \([1,c]\); - 若
ans>=0,不执行,已经正确。
P13980 数列分块入门 5
1. 题意
维护长度 \(n\) 的数列 \(a\),支持:
0 l r:对区间 \([l,r]\) 内每个 \(a_i\) 执行 \(a_i\leftarrow\lfloor\sqrt{a_i}\rfloor\);1 l r:查询区间 \([l,r]\) 的元素和。
数据范围 \(n\le 300000\),\(0\le a_i\le 2^{31}-1\),答案不超过 \(2^{63}-1\)。
2. 核心观察
对于非负整数 \(a\):
- \(0\) 开方后仍是 \(0\);
- \(1\) 开方后仍是 \(1\);
- 只有 \(a>1\) 时开方才会改变值。
并且一个 \(a\le 2^{31}-1\) 的数最多开方约 \(5\) 次就会变成 \(1\):
所以每个元素的有效开方次数非常少。
一旦某个块内所有元素都 \(\le1\),这个块再也不会因开方而改变,可以整块跳过。
3. 分块设计
取块长
每块维护:
| 变量 | 含义 |
|---|---|
a[i] |
第 \(i\) 个位置当前值 |
bl[i] |
第 \(i\) 个位置所属块编号 |
L[j]、R[j] |
第 \(j\) 块的左右端点 |
sum[j] |
第 \(j\) 块当前元素和 |
cnt[j] |
第 \(j\) 块中 \(>1\) 的元素个数 |
flag[j] |
第 \(j\) 块是否存在 \(>1\) 的元素 |
flag[j]==0 表示第 \(j\) 块所有元素都 \(\le1\),整块开方可以直接跳过。
4. 初始化
for(int i=1;i<=n;i++)
{
cin>>a[i];
bl[i]=bl[i-1];
if((i-1)%len==0)
{
bl[i]++;
L[bl[i]]=i;
}
R[bl[i]]=i;
sum[bl[i]]+=a[i];
if(a[i]>1)
{
cnt[bl[i]]++;
flag[bl[i]]=1;
}
}
(i-1)%len==0:\(i\) 是某一块的第一个位置,块编号加一,记录左端点;R[bl[i]]=i:不断更新右端点,循环结束后就是块的真实右端点;sum[bl[i]]+=a[i]:维护块和;- 若
a[i]>1,则cnt加一,flag置 1。
5. 区间开方 op=0
5.1 同一块
if(bl[l]==bl[r])add(l,r);
直接对区间 \([l,r]\) 暴力开方。
5.2 跨块
add(l,R[bl[l]]);
add(L[bl[r]],r);
for(int j=bl[l]+1;j<bl[r];j++)if(flag[j])add(L[j],R[j]);
- 左散块:\(l\) 到第
bl[l]块末尾; - 右散块:第
bl[r]块开头到 \(r\); - 中间完整块:若
flag[j]为真才开方,否则整块跳过。
6. add(l,r) 函数
inline void add(int l,int r)
{
if(!flag[bl[l]])return;
for(int j=l;j<=r;j++)
{
sum[bl[j]]-=a[j];
if(a[j]>1)cnt[bl[j]]--;
a[j]=sqrt(a[j]);
if(a[j]>1)cnt[bl[j]]++;
sum[bl[j]]+=a[j];
}
if(cnt[bl[l]]==0)flag[bl[l]]=0;
else flag[bl[l]]=1;
}
这个函数处理的区间 \([l,r]\) 始终在同一块内,调用方式保证了这一点。
逐行解释:
-
if(!flag[bl[l]])return;
如果该块已经全部 \(\le1\),开方不会改变任何值,直接返回。 -
对每个元素 \(a_j\):
sum[bl[j]]-=a[j]:先从块和中减去旧值;- 若旧值 \(>1\),
cnt[bl[j]]--; a[j]=sqrt(a[j]):开方(long long会自动截断浮点结果,即向下取整);- 若新值 \(>1\),
cnt[bl[j]]++; sum[bl[j]]+=a[j]:把新值加回块和。
-
最后根据
cnt[bl[l]]更新flag[bl[l]]。
7. 区间求和 op=1
int ans=0;
if(bl[l]==bl[r])ans=ask(l,r);
else
{
ans=ask(l,R[bl[l]])+ask(L[bl[r]],r);
for(int j=bl[l]+1;j<bl[r];j++)ans+=sum[j];
}
cout<<ans<<"\n";
- 同一块:暴力求和
ask(l,r); - 跨块:左散块 + 右散块 + 中间完整块直接取
sum[j]。
ask(l,r) 就是简单的循环累加,处理的区间也始终在同一块内。
P13981 数列分块入门 6
1. 题意
维护一个初始长度为 \(n\) 的数列,共 \(n\) 次操作:
0 l r:在第 \(l\) 个数字前插入数字 \(r\);1 c:询问当前数列中第 \(c\) 个数字的值。
数据随机生成,\(n\le300000\),值域 \([-2^{31},2^{31}-1]\)。
2. 为什么用分块
如果直接用 vector 在中间插入:
a.insert(a.begin()+l-1, r);
单次插入是 \(O(n)\),\(n\) 次插入最坏 \(O(n^2)\),会超时。
分块动态维护:
- 把数列切成若干块,每块用一个
vector存; - 插入时先定位到第 \(l\) 个数字所在块,只在该块内插入;
- 如果某块过大,就重建:把所有块展开,再按块大小重新分块;
- 查询时定位到第 \(c\) 个数字所在块,直接取块内元素。
3. 变量含义
int n,s,a[N];
vector<int>o[100010];
n:初始数字个数;s:块大小,取sqrt(n)+1;a[i]:初始数列;o[j]:第 \(j\) 块内存放的数字,块编号从 \(1\) 开始。
块大小:
s=sqrt(n)+1;
取 \(\sqrt n\) 左右,保证块数约为 \(\sqrt n\)。
4. 初始分块
int now=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
if((i-1)%s==0) now++;
o[now].push_back(a[i]);
}
(i-1)%s==0:当前元素是某一块的第一个;now++:块编号加一;o[now].push_back(a[i]):把元素放进当前块。
例如 \(n=4\),\(s=\lfloor\sqrt4\rfloor+1=3\),初始块:
- 第 1 块:位置 \(1,2,3\);
- 第 2 块:位置 \(4\)。
块编号连续,且每块大小不超过 \(s\)。
5. 插入操作 op=0
int l,r;
cin>>l>>r;
for(int i=1;;i++)
{
if(o[i].size()>=l)
{
o[i].insert(o[i].be+l-1,r);
if(o[i].size()>2*s) rebuild();
break;
}
l-=o[i].size();
}
目标:在第 \(l\) 个数字前插入 \(r\)。
- 从第 1 块开始遍历;
- 如果当前块大小
< l,说明第 \(l\) 个数字不在这一块,令l -= o[i].size(),继续下一块; - 如果当前块大小
>= l,说明第 \(l\) 个数字就在这一块内,且是块内第l个位置; - 在该块的第
l-1下标处插入r(因为vector下标从 0 开始); - 插入后如果该块大小超过
2*s,调用rebuild()重建,防止某块过大。
o[i].be 是 o[i].begin() 的宏。
6. 重建 rebuild()
void rebuild()
{
vector<int> tmp;
for(int j=1;j<100010&&!o[j].empty();j++)
{
for(int x:o[j]) tmp.push_back(x);
o[j].clear();
}
for(int j=0;j<tmp.size();j++) o[j/s+1].push_back(tmp[j]);
}
作用:把所有非空块按顺序展开到一个临时数组 tmp,然后重新按每 s 个元素分块。
- 第一层循环遍历所有非空块,把元素加入
tmp,并清空原块; - 第二层循环按
j/s+1重新分配块编号:j=0..s-1进入第 1 块;j=s..2s-1进入第 2 块;- 以此类推。
重建后每块大小约为 \(s\),保证后续插入和查询效率。
7. 查询操作 op=1
int r;
cin>>r;
for(int i=1;;i++)
{
if(o[i].size()>=r)
{
cout<<o[i][r-1]<<'\n';
break;
}
r-=o[i].size();
}
目标:查询当前数列中第 \(r\) 个数字。
- 从第 1 块开始遍历;
- 如果当前块大小
< r,说明第 \(r\) 个数字不在这一块,令r -= o[i].size(),继续下一块; - 如果当前块大小
>= r,说明第 \(r\) 个数字就在这一块内,且是块内第r个位置; - 输出
o[i][r-1]。
P13982 数列分块入门 7
1. 题意
维护长度 \(n\) 的数列 \(a\),支持三种操作:
0 l r c:区间 \([l,r]\) 每个数加 \(c\);1 l r c:区间 \([l,r]\) 每个数乘 \(c\);2 l r c:查询 \(a_r \bmod 10007\),输出非负余数,\(l\) 和 \(c\) 忽略。
数据范围 \(n\le300000\),\(a_i,c\in[-2^{31},2^{31}-1]\)。
2. 核心难点:加法与乘法标记的顺序
如果只用分块,完整块需要打懒标记。但加法和乘法混在一起时,标记不能简单分开存,因为:
- 先加后乘:\((x+a)\times b=x\times b+a\times b\);
- 先乘后加:\(x\times a+b\)。
为了让两种操作都能快速处理,统一把块内变换写成:
即每个位置的真实值等于:
mul[j]:第 \(j\) 块的乘法标记,初始为 1;tag[j]:第 \(j\) 块的加法标记,初始为 0。
初始时 mul=1, tag=0,真实值就是 \(a[i]\)。
3. 标记下放
当需要对块内部分元素单独操作时,必须先把块标记“下放”到每个元素上,然后重置标记。
下放公式:
代码:
for(int i=L[j];i<=R[j];i++)
a[i]=((a[i]*mul[j]+tag[j])%mod+mod)%mod;
mul[j]=1;tag[j]=0;
外层 ( ... %mod+mod)%mod 保证结果非负。
4. 区间加法 op=0
4.1 同一块 LL==RR
for(int i=L[LL];i<=R[LL];i++)a[i]=((a[i]*mul[LL]+tag[LL])%mod+mod)%mod;
mul[LL]=1;tag[LL]=0;
for(int i=l;i<=r;i++)a[i]=(a[i]+c_mod)%mod;
先下放整个块的标记,然后只对 \([l,r]\) 暴力加 \(c\)。
4.2 跨块 LL!=RR
下放左块,重置;
修改左散块 [l, R[LL]];
下放右块,重置;
修改右散块 [L[RR], r];
for(int i=LL+1;i<RR;i++)tag[i]=(tag[i]+c_mod)%mod;
中间完整块被整体加 \(c\)。
当前变换是 \(x\cdot mul+tag\),加 \(c\) 后变为:
所以只需:
tag[i]=(tag[i]+c_mod)%mod;
5. 区间乘法 op=1
5.1 同一块
先下放标记,然后:
for(int i=l;i<=r;i++)a[i]=(a[i]*c_mod)%mod;
5.2 跨块
下放左块,修改左散块;
下放右块,修改右散块;
for(int i=LL+1;i<RR;i++)
{
mul[i]=mul[i]*c_mod%mod;
tag[i]=tag[i]*c_mod%mod;
}
中间完整块整体乘 \(c\)。
当前变换是 \(x\cdot mul+tag\),乘 \(c\) 后变为:
所以:
mul[i]=mul[i]*c_mod%mod;
tag[i]=tag[i]*c_mod%mod;
这是本题最关键的一步:乘法作用于整个块时,tag 也要乘 \(c\),否则顺序就错了。
6. 单点查询 op=2
题目查询 \(a_r\),\(l\) 和 \(c\) 忽略。代码中:
int pos=r;
int ans=(a[pos]*mul[bl[pos]]+tag[bl[pos]])%mod;
ans=(ans%mod+mod)%mod;
cout<<ans<<"\n";
真实值就是:
再取非负余数。
7. 取模处理
题目要求输出非负余数,而 C++ 中负数取模保留负号。代码统一使用:
(x%mod+mod)%mod
对于输入的 \(c\):
int c_mod=(c%mod+mod)%mod;
保证 \(c\_mod\in[0,10006]\)。
下放标记时同理,保证 a[i] 始终在 \([0,10006]\)。
8. 小结
这题的核心是把块内变换统一写成:
- 加法:\(tag\leftarrow tag+c\);
- 乘法:\(mul\leftarrow mul\cdot c,\ tag\leftarrow tag\cdot c\);
- 查询:\(a[i]\cdot mul+tag\);
- 部分块操作前先下放标记并重置。
P13983 数列分块入门 8
1. 题意
维护长度 \(n\) 的数列 \(a\),共 \(n\) 次操作,每次给定 \(l,r,c\):
- 先查询区间 \([l,r]\) 中有多少个元素等于 \(c\);
- 再把区间 \([l,r]\) 中所有元素改为 \(c\)。
数据范围 \(n\le300000\),\(a_i,c\in[-2^{31},2^{31}-1]\)。
2. 分块维护
把数组切成若干块,块长约为 \(\sqrt n\)。
每个位置和每个块维护:
| 变量 | 含义 |
|---|---|
a[i] |
第 \(i\) 个位置的具体值 |
bl[i] |
第 \(i\) 个位置所属块编号 |
l[j]、r[j] |
第 \(j\) 块的左右端点 |
lz[j] |
第 \(j\) 块的整块赋值标记 |
islz[j] |
第 \(j\) 块是否存在整块赋值标记 |
如果 islz[j]=1,表示第 \(j\) 块内所有元素都被统一赋值为 lz[j],此时不需要访问 a 的具体值;
如果 islz[j]=0,表示第 \(j\) 块内元素可能不一致,需要访问 a。
3. 标记下放 putlz(x)
inline void putlz(int x)
{
if(islz[x])
{
islz[x]=0;
for(int i=l[x];i<=r[x];i=-~i)a[i]=lz[x];
}
}
作用:把第 \(x\) 块的整块标记下放到每个 a[i],然后清除标记。
- 只有
islz[x]=1时才需要下放; - 下放后块内元素都变成
lz[x],之后可以单独修改其中一部分; - 下放后
islz[x]=0。
4. 查询等于 \(c\) 的个数 getnum(tl,tr,c)
4.1 同一块
if(bl[tl]==bl[tr])
{
if(islz[bl[tl]])return lz[bl[tl]]==c?tr-tl+1:0;
for(int i=tl;i<=tr;i=-~i)sum+=(a[i]==c);
return sum;
}
- 如果整块有标记,整块值都是
lz,区间内值也全是lz,直接判断; - 否则暴力扫描区间。
4.2 跨块
if(islz[bl[tl]])sum+=(lz[bl[tl]]==c?r[bl[tl]]-tl+1:0);
else for(int i=tl;i<=r[bl[tl]];i=-~i)sum+=(a[i]==c);
if(islz[bl[tr]])sum+=(lz[bl[tr]]==c?tr-l[bl[tr]]+1:0);
else for(int i=l[bl[tr]];i<=tr;i=-~i)sum+=(a[i]==c);
for(int i=bl[tl]+1;i<bl[tr];i=-~i)
{
if(islz[i])sum+=(lz[i]==c?r[i]-l[i]+1:0);
else for(int j=l[i];j<=r[i];j=-~j)sum+=(a[j]==c);
}
- 左散块:若整块有标记,直接判断;否则暴力;
- 右散块:同理;
- 中间完整块:若整块有标记,整块元素相同,直接判断;否则暴力扫描整个块。
5. 区间赋值 build(tl,tr,c)
inline void build(int tl,int tr,int c)
{
putlz(bl[tl]);
putlz(bl[tr]);
if(bl[tl]==bl[tr])
{
for(int i=tl;i<=tr;i=-~i)a[i]=c;
return;
}
for(int i=tl;i<=r[bl[tl]];i=-~i)a[i]=c;
for(int i=l[bl[tr]];i<=tr;i=-~i)a[i]=c;
for(int i=bl[tl]+1;i<bl[tr];i=-~i)
{
islz[i]=1;
lz[i]=c;
}
}
步骤:
-
下放左右块标记:
因为要单独修改左右块中的一部分,必须先putlz,否则会覆盖掉未修改部分的标记信息。 -
同一块:
直接暴力把 \([tl,tr]\) 赋值为 \(c\)。 -
跨块:
- 左散块:\(tl\) 到第
bl[tl]块末尾赋值为 \(c\); - 右散块:第
bl[tr]块开头到 \(tr\) 赋值为 \(c\); - 中间完整块:直接打整块标记
islz[i]=1, lz[i]=c,不修改a。
- 左散块:\(tl\) 到第
6. 分块初始化
m=sqrt(n);
for(int i=1;i<=m;i=-~i)
{
l[i]=r[i-1]+1;
r[i]=m*i;
}
if(r[m]<n)
{
m=-~m;
l[m]=r[m-1]+1;
r[m]=n;
}
- 前 \(m\) 块每块长度为 \(m\);
- 如果最后还有剩余元素,再加一块覆盖
r[m]+1..n; - 然后
bl[j]标记每个位置属于哪一块。
7. 小结
本题核心是整块赋值懒标记:
- 用
islz[j]表示第 \(j\) 块是否被整体赋值; - 用
lz[j]存整块被赋的值; - 查询时,有标记的整块可以 \(O(1)\) 判断,无标记的散块暴力;
- 赋值时,左右散块先下放再暴力修改,中间完整块直接打标记。
整体复杂度 \(O(n\sqrt n)\)
P13984 数列分块入门 9
1. 题意
维护长度 \(n\) 的数列,共 \(n\) 次询问:
- 每次给出 \(l,r\),求 \([l,r]\) 内的众数;
- 若多个数出现次数相同,取数值最小的那个。
数据范围 \(n\le300000\),\(a_i\in[-2^{31},2^{31}-1]\)。
2. 算法:回滚莫队(只向右扩展的莫队)
题目要求区间众数,众数不支持删除(删除一个元素后,原来的最大频率可能无法立即得到新的众数)。
所以普通莫队不行,需要用回滚莫队:
- 对询问按「左端点所属块」排序,同一块内按右端点排序;
- 对每个左端点块,维护一个只向右扩展的稳定区间 \([L,R]\),其中 \(L\) 是该块右边界 +1;
- 对于块内每个询问:
- 若 \(r\) 也在同一块内,直接暴力计算;
- 否则先扩展 \(R\) 到询问的 \(r\),再临时加入左散块 \([l,L-1]\),得到答案后回滚左散块。
这样每个询问的右端点只向右移动,左散块最多 \(O(\sqrt n)\) 长度,总复杂度 \(O(n\sqrt n)\)。
3. 离散化
for(int i=1;i<=n;i++)
{
cin>>a[i];
ls[i]=a[i];
}
sort(ls+1,ls+1+n);
for(int i=1;i<=n;i++)if(b[ls[i]]==0)
{
tot++;
b[ls[i]]=tot;
c[tot]=ls[i];
}
for(int i=1;i<=n;i++)a[i]=b[a[i]];
b[x]把原值 \(x\) 映射到 \([1,tot]\) 中的编号;c[id]把编号还原为原值;- 离散化后,数组下标就是值域,
cnt[]可以直接按下标统计频次,同时编号越小原值越小,正好对应「相同频率取最小值」的规则。
4. 询问排序
struct query
{
int l,r,L,i;
inline friend bool operator<(query a,query b)
{
return a.L==b.L?a.r<b.r:a.L<b.L;
}
}o[N];
o[i].L=(o[i].l-1)/m 是左端点所在块编号。
排序规则:
- 左端点块编号小的在前;
- 同一块内,右端点小的在前。
这正是回滚莫队的要求。
5. 块内暴力 ask(l,r)
inline int ask(int l,int r)
{
int ans=0,maxx=0;
for(int i=l;i<=r;i++)
{
cnt1[a[i]]++;
if(cnt1[a[i]]>maxx||(cnt1[a[i]]==maxx&&a[i]<ans))
{
maxx=cnt1[a[i]];
ans=a[i];
}
}
for(int i=l;i<=r;i++)cnt1[a[i]]--;
return ans;
}
当 \(r\) 和 \(l\) 在同一个块内时,区间长度 \(\le m\),直接暴力统计,用 cnt1 单独计数,结束后回滚。
cnt1[a[i]]>maxx:出现次数更多,更新;cnt1[a[i]]==maxx&&a[i]<ans:次数相同但值更小,更新;- 结束后把
cnt1清零,不影响后续。
6. 主循环
int L=1,R=0,ans0,maxx0,ans,maxx;
for(int i=1;i<=n;i++)
{
if(o[i].L==(o[i].r-1)/m)
{
anss[o[i].i]=ask(o[i].l,o[i].r);
continue;
}
if(L<=o[i].l)
{
L+=m;
memset(cnt,0,sizeof(cnt));
R=L-1;
ans0=0,maxx0=0;
}
while(R<o[i].r)
{
R++;
cnt[a[R]]++;
if(cnt[a[R]]>maxx0||(cnt[a[R]]==maxx0&&a[R]<ans0))
{
maxx0=cnt[a[R]];
ans0=a[R];
}
}
ans=ans0,maxx=maxx0;
for(int j=o[i].l;j<L;j++)
{
cnt[a[j]]++;
if(cnt[a[j]]>maxx||cnt[a[j]]==maxx&&a[j]<ans)
{
maxx=cnt[a[j]];
ans=a[j];
}
}
anss[o[i].i]=ans;
for(int j=o[i].l;j<L;j++)cnt[a[j]]--;
}
逐段解释:
6.1 r 和 l 同块
if(o[i].L==(o[i].r-1)/m)
{
anss[o[i].i]=ask(o[i].l,o[i].r);
continue;
}
区间长度不超过 \(m\),直接暴力。
6.2 切换左端点块
if(L<=o[i].l)
{
L+=m;
memset(cnt,0,sizeof(cnt));
R=L-1;
ans0=0,maxx0=0;
}
L是稳定区间的左边界,初始为 1;- 当
L <= o[i].l时,说明当前稳定区间的左边界已经落到询问的 \(l\) 左边,需要把L推进到下一个块的右边界 +1; - 清空
cnt,重置R=L-1,重新开始扩展。
由于询问按左块排序,L 最多推进 \(\sqrt n\) 次,每次 memset 代价 \(O(N)\),总 \(O(N\sqrt n)\)。
6.3 向右扩展 R
while(R<o[i].r)
{
R++;
cnt[a[R]]++;
if(cnt[a[R]]>maxx0||(cnt[a[R]]==maxx0&&a[R]<ans0))
{
maxx0=cnt[a[R]];
ans0=a[R];
}
}
把稳定区间右端扩展到 \(r\),边扩展边更新稳定区间的众数 ans0 与最大频率 maxx0。
同一左块内询问按 \(r\) 升序,所以 R 只向右移动,总共移动 \(O(n)\) 次。
6.4 临时加入左散块
ans=ans0,maxx=maxx0;
for(int j=o[i].l;j<L;j++)
{
cnt[a[j]]++;
if(cnt[a[j]]>maxx||cnt[a[j]]==maxx&&a[j]<ans)
{
maxx=cnt[a[j]];
ans=a[j];
}
}
anss[o[i].i]=ans;
for(int j=o[i].l;j<L;j++)cnt[a[j]]--;
左散块 \([l,L-1]\) 是本次询问相对于稳定区间多出来的部分。
临时把它加入 cnt,更新答案,然后撤销(把频次减回去),保证 cnt 始终只描述稳定区间 \([L,R]\)。
6.5 记录答案
anss[o[i].i]=ans;
按原询问顺序存答案,最后统一输出。
7. 输出
for(int i=1;i<=n;i++)cout<<c[anss[i]]<<'\n';
anss[i] 存的是离散化后的编号,c[] 还原成原值。
8. 小结
本题核心是回滚莫队:
- 离散化,使编号顺序与原值顺序一致;
- 按左端点块排序,同块内按右端点排序;
- 对每个左块,维护只向右扩展的稳定区间 \([L,R]\);
- 对每个询问,先扩展 \(R\),再临时加入左散块 \([l,L-1]\),得到答案后回滚;
- 同块询问直接暴力。
关键修正:把 N 从 1e5+5 改为 3e5+5,否则 \(n>100000\) 时越界。修正后总复杂度 \(O(n\sqrt n)\),可以 AC。
作诗
1. 题意
给定长度 \(n\) 的数组 \(a\),\(a_i\le c\),共 \(m\) 次询问(强制在线):
-
每次给出 \(L,R\),用上一次答案
ans解码:\[l=((L+ans)\bmod n)+1,\quad r=((R+ans)\bmod n)+1 \]若 \(l>r\) 则交换;
-
询问 \([l,r]\) 中有多少种数出现了正偶数次。
数据范围 \(n,c,m\le10^5\)。
2. 核心思路:分块 + 预处理整块答案
把数组按块长 \(S\) 分块。每块维护左右端点 L[j]、R[j],每个位置记录所属块 bl[i]。
关键预处理:
即从第 \(i\) 块开头到第 \(j\) 块结尾的答案。
预处理方法:
for(int i=1;i<=cnt;i++)
{
ans=0;
memset(sum,0,sizeof(sum));
for(int l=L[i];l<=n;l++)
{
if(sum[a[l]])
{
if(sum[a[l]]%2==0)ans--;
else ans++;
}
sum[a[l]]++;
if(l==R[bl[l]])f[i][bl[l]]=ans;
}
}
固定左块 \(i\),向右扫描,sum[x] 是当前区间中 \(x\) 的出现次数,ans 是当前区间中正偶数次种类数:
- 旧次数为奇数:加入后变偶数,
ans++; - 旧次数为正偶数:加入后变奇数,
ans--; - 旧次数为 0:加入后变 1,奇数,
ans不变。
扫描到某块末尾时,记录 f[i][bl[l]]=ans。
3. 查询
3.1 小段暴力
if(bl[l]==bl[r]||bl[l]+1==bl[r])
{
ans=0;
for(int j=l;j<=r;j++)oo.insert(a[j]);
for(auto x:oo)ans=ans+(ask(l,r,x)%2==0);
cout<<ans<<"\n";
}
bl[l]==bl[r]:同一块,长度 \(\le S\);bl[l]+1==bl[r]:相邻块,长度 \(\le 2S\)。
直接收集区间内出现的数到 set,对每个数用 ask(l,r,x) 统计次数,若为正偶数则答案加一。
ask(l,r,c) 用 pos[c] 存所有出现位置,通过两次 upper_bound 得到 \(c\) 在 \([l,r]\) 中的出现次数。
3.2 跨多块
ans=f[bl[l]+1][bl[r]-1];
for(int j=l;bl[j]==bl[l];j++)oo.insert(a[j]);
for(int j=r;bl[j]==bl[r];j--)oo.insert(a[j]);
for(auto x:oo)
{
int cnt=ask(L[bl[l]+1],R[bl[r]-1],x);
if(cnt>0&&cnt%2==0)ans--;
ans=ans+(ask(l,r,x)%2==0);
}
- 先取中间完整块 \([bl[l]+1, bl[r]-1]\) 的答案
f; - 收集左右散块中出现过的数到
oo; - 对每个散块中出现的数 \(x\):
- 若 \(x\) 在中间完整块中出现正偶数次,
ans中已经计入了 \(x\),先减掉; - 再计算 \(x\) 在整个 \([l,r]\) 中的出现次数,若为正偶数,加回来。
- 若 \(x\) 在中间完整块中出现正偶数次,
这样就把左右散块的影响正确合并到中间整块答案上。
4. 强制在线
l=(l+ans)%n+1;
r=(r+ans)%n+1;
if(l>r)swap(l,r);
ans 是上一次询问的答案,初始为 0。
每次查询后 ans 更新为本次答案,供下一次解码使用。
5. 小结
本题是「区间正偶数次种类数」的经典分块题,核心是:
- 预处理
f[i][j]:第 \(i\) 块到第 \(j\) 块的答案; - 查询时,中间整块直接用
f; - 左右散块中出现的数逐个调整:先减去它在中间块中的贡献,再加上它在整个区间中的贡献;
- 用
pos+upper_bound快速统计某数在任意区间内的出现次数; - 强制在线,用上一次答案解码。
「SDOI2009」HH的项链(这个是弱化版)
1. 题意
给定长度 \(n\) 的数列,\(m\) 次询问,每次询问区间 \([l,r]\) 中有多少种不同的数。
数据范围:
- \(n,m\le 10^5\)
- 需要较快读入,最大数据约 20MB。
2. 为什么用莫队
普通暴力每次询问扫描 \([l,r]\),最坏 \(O(nm)\),会超时。
莫队算法适合这种离线区间询问:
- 把所有询问按一定顺序排序;
- 用两个指针 \(l,r\) 表示当前维护的区间;
- 通过移动指针,逐步从上一个询问区间转移到下一个询问区间;
- 移动过程中用
add维护答案。
排序后,左右指针总移动次数约为:
对于 \(n,m\le10^5\),可以通过。
3. 维护的信息
int cnt,sum[N<<1];
sum[x]:数 \(x\) 在当前区间 \([l,r]\) 中出现的次数;cnt:当前区间中不同数的个数,即sum[x]>0的 \(x\) 的个数。
add(x,d):
inline void add(int x,int d)
{
if(sum[x])cnt--;
sum[x]+=d;
if(sum[x])cnt++;
}
含义:
- 如果
x原来已经在区间中出现过,即sum[x]>0,那么它已经贡献过cnt,先减掉; - 更新
sum[x]+=d; - 如果更新后
sum[x]>0,说明 \(x\) 仍在区间中,重新贡献cnt。
对于加入操作 d=1:
- 若
x原来没出现,sum[x]从 0 变 1,cnt加一; - 若
x原来已出现,cnt先减后加,不变。
对于删除操作 d=-1:
- 若删除后
x仍出现,cnt不变; - 若删除后
x消失,sum[x]变 0,cnt减一。
4. 询问排序
s=sqrt(n);
bl[i]=i/s+1;
struct node
{
int l,r,i;
inline friend bool operator<(node a,node b)
{
return bl[a.l]^bl[b.l]?bl[a.l]<bl[b.l]:(bl[a.l]&1?a.r<b.r:a.r>b.r);
}
}o[N];
排序规则:
- 左端点所在块编号小的在前;
- 同一块内:
- 若块编号为奇数,右端点升序;
- 若块编号为偶数,右端点降序。
这就是奇偶优化,可以减少右指针来回移动的距离。
o[i].i 记录原始询问编号,最后按原顺序输出答案。
5. 指针移动
int l=1,r=0;
for(register int i=1;i<=m;i++)
{
while(l<o[i].l)add(a[l],-1),l++;
while(l>o[i].l)l--,add(a[l],1);
while(r>o[i].r)add(a[r],-1),r--;
while(r<o[i].r)r++,add(a[r],1);
ans[o[i].i]=cnt;
}
四个 while 分别表示:
- 当前左端点小于目标左端点:删除左端元素,左指针右移;
- 当前左端点大于目标左端点:左指针左移,加入新元素;
- 当前右端点大于目标右端点:删除右端元素,右指针左移;
- 当前右端点小于目标右端点:右指针右移,加入新元素。
移动完成后,当前区间正好是 [o[i].l, o[i].r],cnt 就是答案。
6. 快速读入与输出
namespace OI
{
static char buf[1000002],*pa(buf),*pb(buf);
#define gc pa==pb&&(pb=(pa=buf)+fread(buf,1,1000000,stdin),pa==pb)?EOF:*pa++
inline int read()noexcept
{
int x=0,c=gc;bool f=0;
while(c<48||c>57)c==45?f=1,c=gc:c=gc;
while(c>=48&&c<=57)x=(x<<3)+(x<<1)+(c^48),c=gc;
return f?-x:x;
}
#undef gc
int chr[60],cntter;
inline void write(int x)noexcept
{
if(x<0)putchar(45),x=-x;
if(!x)return putchar(48),void();
cntter=0;
while(x)chr[++cntter]=(x%10)^48,x/=10;
while(cntter)putchar(chr[cntter--]);
}
}
read()用fread一次性读入大块数据,比cin快很多;write()用字符数组逆序输出整数,比cout快。
题目最大数据约 20MB,使用快速 IO 可以避免读入超时。
7. 小结
本题是莫队算法模板题:
- 离线读入所有询问;
- 按左端点分块、右端点奇偶优化排序;
- 用两个指针维护当前区间;
sum[x]统计出现次数,cnt统计不同数个数;- 移动指针时通过
add动态维护答案; - 最后按原询问顺序输出。
「CQOI2018」异或序列
1. 题意
给定长度 \(n\) 的数列 \(a\),\(m\) 次询问,每次询问区间 \([l,r]\) 内有多少个子区间异或和等于 \(k\)。
即统计满足
的二元组 \((x,y)\) 数量。
数据范围 \(n,m\le10^5\),\(0\le k,a_i\le10^5\)。
2. 前缀异或转换
设前缀异或数组:
则区间 \([x,y]\) 的异或和为:
条件 \(p_y\oplus p_{x-1}=k\) 等价于:
于是询问 \([l,r]\) 转化为:
在前缀下标区间 \([l-1,r]\) 中,有多少对 \((i,j)\) 满足:
代码中:
for(int i=1;i<=n;i++)a[i]^=a[i-1];
把 a[i] 变成前缀异或 \(p_i\),且 a[0]=0 对应 \(p_0\)。
询问处理时:
int nl=q[i].l-1,nr=q[i].r;
即维护前缀下标区间 \([l-1,r]\)。
3. 莫队维护
用莫队维护当前前缀下标区间 \([L,R]\):
tot[x]:当前区间中前缀异或值 \(x\) 出现次数;sum:当前区间中满足 \(p_i\oplus p_j=k\) 的无序对数量。
初始区间为 \([0,0]\),包含 \(p_0=0\):
tot[0]=1;
int l=0,r=0;
加入元素 add(x)
inline void add(int x){sum+=tot[a[x]^k];tot[a[x]]++;}
加入 \(p_x\) 时,能与它配对的是所有满足
的已有元素。
已有元素中出现次数为 tot[a[x]^k],所以新增配对数就是它。
删除元素 del(x)
inline void del(int x){tot[a[x]]--;sum-=tot[a[x]^k];}
删除 \(p_x\) 时,先把它从 tot 中减掉,再减去它与剩余元素组成的配对数。
注意顺序:先 tot[a[x]]--,再 sum-=tot[a[x]^k]。
当 \(k=0\) 时,配对对象就是 \(p_x\) 自身,先减掉自身再统计剩余相同值个数,正好是删除后减少的配对数。
4. 询问排序
s=max(1LL,(long long)sqrt(n));
for(int i=1;i<=n;i++)bl[i]=(i-1)/s+1;
块长取 \(\sqrt n\),保证莫队总移动次数约 \(O(n\sqrt m)\)。
排序规则:
return bl[a.l]^bl[b.l]?bl[a.l]<bl[b.l]:
(bl[a.l]&1?a.r<b.r:a.r>b.r);
- 左端点所在块不同,按块编号升序;
- 同一块内,奇数块右端点升序,偶数块右端点降序。
这是莫队的奇偶优化,能进一步减少右指针来回移动。
5. 指针移动
while(l<nl)del(l++);
while(l>nl)add(--l);
while(r<nr)add(++r);
while(r>nr)del(r--);
四个方向:
- 左端点小于目标:删除左端元素,左指针右移;
- 左端点大于目标:左指针左移,加入新元素;
- 右端点小于目标:右指针右移,加入新元素;
- 右端点大于目标:删除右端元素,右指针左移。
移动完成后,sum 就是当前询问答案,存入 ans[id]。
6. 小结
本题核心是:
- 用前缀异或把子区间异或和转成两点异或;
- 询问 \([l,r]\) 转为前缀下标区间 \([l-1,r]\);
- 用莫队维护当前前缀下标区间;
tot[x]统计前缀异或值出现次数,sum维护满足 \(p_i\oplus p_j=k\) 的对数;- 加入/删除时通过
tot[x^k]更新答案; - 块长取 \(\sqrt n\),配合奇偶优化。
小Z的袜子
1. 题意
有 \(n\) 只袜子,每只有颜色 \(c_i\)。\(m\) 次询问,每次问区间 \([L,R]\) 中随机抽两只袜子颜色相同的概率,输出最简分数 \(A/B\)。
若 \(L=R\),概率为 \(0\),输出 0/1。
数据范围 \(n,m\le50000\)。
2. 概率公式
区间长度为:
从 \(len\) 只袜子中任选两只有:
种选法。
若某种颜色出现了 \(t\) 次,从中选两只同色的方案数为:
所以同色对总数为:
概率:
化简后输出 \(S/g\) 和 \(\binom{len}{2}/g\),其中:
3. 莫队维护 \(S\)
用莫队维护当前区间 \([L,R]\) 中的同色对数 ans。
cnt[x]:当前区间中颜色 \(x\) 出现的次数。
加入颜色 x
inline void add(int x){ans+=cnt[a[x]];cnt[a[x]]++;}
加入一只颜色为 \(a[x]\) 的袜子前,区间中已有 cnt[a[x]] 只同色袜子,它们都能与这只新袜子组成同色对,所以 ans += cnt[a[x]],然后 cnt 加一。
删除颜色 x
inline void del(int x){cnt[a[x]]--;ans-=cnt[a[x]];}
删除一只颜色为 \(a[x]\) 的袜子后,剩余同色袜子数为 cnt[a[x]]-1,减少的同色对数为删除前同色数减一,即 cnt[a[x]]-1。
先 cnt[a[x]]--,再 ans -= cnt[a[x]],正好减掉这个量。
4. 离散化
for(int i=1;i<=n;i++)b.push_back(a[i]);
sort(b.be,b.en);
b.erase(unique(b.be,b.en),b.en);
for(int i=1;i<=n;i++)a[i]=lower_bound(b.be,b.en,a[i])-b.be;
颜色值可能很大,离散化到 \([0,llen-1]\),方便用数组 cnt 统计。
5. 询问排序
s=max(1LL,(long long)sqrt(n));
bl[i]=bl[i-1];
if((i-1)%s==0)bl[i]++;
块长取 \(\sqrt n\)。
inline friend bool operator<(query a,query b)
{
return bl[a.l]^bl[b.l]?bl[a.l]<bl[b.l]:(bl[a.l]&1?a.r<b.r:a.r>b.r);
}
- 左端点所在块不同,按块编号升序;
- 同一块内,奇数块右端点升序,偶数块右端点降序(奇偶优化)。
6. 排序范围修正
q.resize(m+1);
for(int i=1;i<=m;i++){cin>>q[i].l>>q[i].r;q[i].id=i;}
sort(q.be+1,q.en);
q[0] 是默认的 {0,0,0},不参与排序和处理。
只对 q[1..m] 排序,保证排序后 q[1..m] 恰好是全部真实询问。
7. 指针移动
int L=1,R=0;
for(int i=1;i<=m;i++)
{
while(L>q[i].l)add(--L);
while(R<q[i].r)add(++R);
while(L<q[i].l)del(L++);
while(R>q[i].r)del(R--);
...
}
初始区间为空 [1,0]。
通过四个方向移动,把当前区间调整为 [q[i].l, q[i].r]。
8. 计算答案
int len=q[i].r-q[i].l+1;
if(len==1)
{
res1[q[i].id]=0;
res2[q[i].id]=1;
continue;
}
int g=gcd(ans,len*(len-1)/2);
res1[q[i].id]=ans/g;
res2[q[i].id]=len*(len-1)/2/g;
len==1时无法选出两只,直接输出0/1;- 否则分子为
ans,分母为 \(\binom{len}{2}\),用gcd化简; ans=0时gcd(0,x)=x,得到0/1,正确。
9. 小结
本题是莫队算法求区间同色对的模板题:
- 离线读入所有询问;
- 按左端点分块、右端点奇偶优化排序;
- 用两个指针维护当前区间;
cnt[x]统计颜色出现次数,ans维护同色对数;- 每次询问用
gcd化简概率输出; - 注意
len==1特判和排序范围。
Mato的文件管理
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23551619

浙公网安备 33010602011771号