AIGC标识 分块与莫队相关题目

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]] \]

其中:

  • 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);

块的数量最多约为:

\[\left\lceil \frac{n}{len} \right\rceil \le \frac{n}{len}+1 \]

所以开 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];

此时区间分成三部分:

  1. 左边散块:从 l 到 L 块末尾;
  2. 中间完整块:第 L+1 块到第 R-1 块;
  3. 右边散块:从 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\) 的值由两部分组成:

  1. a[r]:它自身被零散修改时加上的值;
  2. 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\) 个位置的实际值:

\[a[i]+lazy[bl[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\) 块的起始位置为:

\[(L-1)\times m+1 \]

最后一块可能不足 \(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;

查询条件:

\[a[i]+lazy[bl[i]] < c^2 \]

即:

\[a[i] < c^2-lazy[bl[i]] \]

散块查询

对于左右不完整的块,直接暴力扫描:

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\),需要统计:

\[a[i]+lazy[j] < c^2 \]

即:

\[a[i] < c^2-lazy[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. 分块思想

把数组分成若干块,块长取

\[len=\sqrt n \]

每块维护:

  • a[i]:第 \(i\) 个位置的基础值;
  • bl[i]:第 \(i\) 个位置所属块编号;
  • tag[j]:第 \(j\) 块被整体加了多少;
  • b[j]:第 \(j\) 块内所有 a[i] 排序后的结果。

一个位置的真实值是

\[a[i]+tag[bl[i]] \]

注意 b[j] 中存的是 不含 tag[j] 的 a 值,所以查询整块时,条件

\[a[i]+tag[j]<c \]

等价于

\[a[i]<c-tag[j] \]

这样可以在 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\) 块的起始位置为

\[(L-1)\times len+1 \]

最后一块可能不足 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

查询条件:

\[a[i]+tag[bl[i]]<c \]

即

\[a[i]<c-tag[bl[i]] \]

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\) 块,令

\[d=c-tag[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)\),答案要求非负。

注意:

  1. 查询是 区间和,不是单点;
  2. 取模是 \(c+1\),不是 \(c\);
  3. 答案必须 非负(C++ 中负数取模会保留负号)。

2. 数据结构:分块

把长度 \(n\) 的数组切成若干块,块长取

\[len=\sqrt n \]

每个位置和每个块维护以下信息:

变量 含义
a[i] 第 \(i\) 个位置的基础值
bl[i] 第 \(i\) 个位置所属块编号(从 1 开始)
lazy[j] 第 \(j\) 个块被整体加了多少
sum[j] 第 \(j\) 个块当前的元素和(已经包含 lazy[j])
LL[j] 第 \(j\) 块的左端点
RR[j] 第 \(j\) 块的右端点

对任意位置 \(i\),它的真实值是

\[a[i]+lazy[bl[i]] \]

对任意块 \(j\),它的元素和是

\[sum[j]=\sum_{i=LL[j]}^{RR[j]}\big(a[i]+lazy[j]\big) \]

关键约定: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 块,程序出错。
  • 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(跨块)

区间分成三部分:

  1. 左散块:从 \(l\) 到第 \(L\) 块末尾;
  2. 中间完整块:第 \(L+1\) 块到第 \(R-1\) 块;
  3. 右散块:从第 \(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] 的块和,必须同步更新:

\[sum[i]\mathrel{+}=c\times \big(RR[i]-LL[i]+1\big) \]

左散块:暴力修改

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++ 中,负数取模会保留负号:

\[(-7)\bmod 5=-2 \]

而数学上的非负余数应当是 \(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\):

\[2^{31}-1\to46340\to215\to14\to3\to1 \]

所以每个元素的有效开方次数非常少。
一旦某个块内所有元素都 \(\le1\),这个块再也不会因开方而改变,可以整块跳过。


3. 分块设计

取块长

\[len=2\sqrt n \]

每块维护:

变量 含义
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]\) 始终在同一块内,调用方式保证了这一点。

逐行解释:

  1. if(!flag[bl[l]])return;
    如果该块已经全部 \(\le1\),开方不会改变任何值,直接返回。

  2. 对每个元素 \(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]:把新值加回块和。
  3. 最后根据 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\)。

为了让两种操作都能快速处理,统一把块内变换写成:

\[x\longmapsto x\cdot mul+tag \]

即每个位置的真实值等于:

\[a[i]\cdot mul[bl[i]]+tag[bl[i]] \]

  • mul[j]:第 \(j\) 块的乘法标记,初始为 1;
  • tag[j]:第 \(j\) 块的加法标记,初始为 0。

初始时 mul=1, tag=0,真实值就是 \(a[i]\)。


3. 标记下放

当需要对块内部分元素单独操作时,必须先把块标记“下放”到每个元素上,然后重置标记。

下放公式:

\[a[i]\leftarrow a[i]\cdot mul[j]+tag[j]\pmod{10007} \]

代码:

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\) 后变为:

\[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\) 后变为:

\[(x\cdot mul+tag)\cdot c=x\cdot(mul\cdot c)+(tag\cdot 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";

真实值就是:

\[a[pos]\cdot mul[bl[pos]]+tag[bl[pos]] \]

再取非负余数。


7. 取模处理

题目要求输出非负余数,而 C++ 中负数取模保留负号。代码统一使用:

(x%mod+mod)%mod

对于输入的 \(c\):

int c_mod=(c%mod+mod)%mod;

保证 \(c\_mod\in[0,10006]\)。
下放标记时同理,保证 a[i] 始终在 \([0,10006]\)。


8. 小结

这题的核心是把块内变换统一写成:

\[x\longmapsto x\cdot mul+tag \]

  • 加法:\(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\):

  1. 先查询区间 \([l,r]\) 中有多少个元素等于 \(c\);
  2. 再把区间 \([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;
    }
}

步骤:

  1. 下放左右块标记:
    因为要单独修改左右块中的一部分,必须先 putlz,否则会覆盖掉未修改部分的标记信息。

  2. 同一块:
    直接暴力把 \([tl,tr]\) 赋值为 \(c\)。

  3. 跨块:

    • 左散块:\(tl\) 到第 bl[tl] 块末尾赋值为 \(c\);
    • 右散块:第 bl[tr] 块开头到 \(tr\) 赋值为 \(c\);
    • 中间完整块:直接打整块标记 islz[i]=1, lz[i]=c,不修改 a。

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 是左端点所在块编号。
排序规则:

  1. 左端点块编号小的在前;
  2. 同一块内,右端点小的在前。

这正是回滚莫队的要求。


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. 小结

本题核心是回滚莫队:

  1. 离散化,使编号顺序与原值顺序一致;
  2. 按左端点块排序,同块内按右端点排序;
  3. 对每个左块,维护只向右扩展的稳定区间 \([L,R]\);
  4. 对每个询问,先扩展 \(R\),再临时加入左散块 \([l,L-1]\),得到答案后回滚;
  5. 同块询问直接暴力。

关键修正:把 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]。

关键预处理:

\[f[i][j]=\text{区间 }[L[i],R[j]]\text{ 中正偶数次种类的个数} \]

即从第 \(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);
}
  1. 先取中间完整块 \([bl[l]+1, bl[r]-1]\) 的答案 f;
  2. 收集左右散块中出现过的数到 oo;
  3. 对每个散块中出现的数 \(x\):
    • 若 \(x\) 在中间完整块中出现正偶数次,ans 中已经计入了 \(x\),先减掉;
    • 再计算 \(x\) 在整个 \([l,r]\) 中的出现次数,若为正偶数,加回来。

这样就把左右散块的影响正确合并到中间整块答案上。


4. 强制在线

l=(l+ans)%n+1;
r=(r+ans)%n+1;
if(l>r)swap(l,r);

ans 是上一次询问的答案,初始为 0。
每次查询后 ans 更新为本次答案,供下一次解码使用。


5. 小结

本题是「区间正偶数次种类数」的经典分块题,核心是:

  1. 预处理 f[i][j]:第 \(i\) 块到第 \(j\) 块的答案;
  2. 查询时,中间整块直接用 f;
  3. 左右散块中出现的数逐个调整:先减去它在中间块中的贡献,再加上它在整个区间中的贡献;
  4. 用 pos + upper_bound 快速统计某数在任意区间内的出现次数;
  5. 强制在线,用上一次答案解码。

「SDOI2009」HH的项链(这个是弱化版)

1. 题意

给定长度 \(n\) 的数列,\(m\) 次询问,每次询问区间 \([l,r]\) 中有多少种不同的数。

数据范围:

  • \(n,m\le 10^5\)
  • 需要较快读入,最大数据约 20MB。

2. 为什么用莫队

普通暴力每次询问扫描 \([l,r]\),最坏 \(O(nm)\),会超时。

莫队算法适合这种离线区间询问:

  1. 把所有询问按一定顺序排序;
  2. 用两个指针 \(l,r\) 表示当前维护的区间;
  3. 通过移动指针,逐步从上一个询问区间转移到下一个询问区间;
  4. 移动过程中用 add 维护答案。

排序后,左右指针总移动次数约为:

\[O(n\sqrt m) \]

对于 \(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];

排序规则:

  1. 左端点所在块编号小的在前;
  2. 同一块内:
    • 若块编号为奇数,右端点升序;
    • 若块编号为偶数,右端点降序。

这就是奇偶优化,可以减少右指针来回移动的距离。

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 分别表示:

  1. 当前左端点小于目标左端点:删除左端元素,左指针右移;
  2. 当前左端点大于目标左端点:左指针左移,加入新元素;
  3. 当前右端点大于目标右端点:删除右端元素,右指针左移;
  4. 当前右端点小于目标右端点:右指针右移,加入新元素。

移动完成后,当前区间正好是 [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. 小结

本题是莫队算法模板题:

  1. 离线读入所有询问;
  2. 按左端点分块、右端点奇偶优化排序;
  3. 用两个指针维护当前区间;
  4. sum[x] 统计出现次数,cnt 统计不同数个数;
  5. 移动指针时通过 add 动态维护答案;
  6. 最后按原询问顺序输出。

「CQOI2018」异或序列

1. 题意

给定长度 \(n\) 的数列 \(a\),\(m\) 次询问,每次询问区间 \([l,r]\) 内有多少个子区间异或和等于 \(k\)。

即统计满足

\[l\le x\le y\le r,\qquad a_x\oplus a_{x+1}\oplus\cdots\oplus a_y=k \]

的二元组 \((x,y)\) 数量。

数据范围 \(n,m\le10^5\),\(0\le k,a_i\le10^5\)。


2. 前缀异或转换

设前缀异或数组:

\[p_0=0,\qquad p_i=a_1\oplus a_2\oplus\cdots\oplus a_i \]

则区间 \([x,y]\) 的异或和为:

\[a_x\oplus\cdots\oplus a_y=p_y\oplus p_{x-1} \]

条件 \(p_y\oplus p_{x-1}=k\) 等价于:

\[p_y=p_{x-1}\oplus k \]

于是询问 \([l,r]\) 转化为:

在前缀下标区间 \([l-1,r]\) 中,有多少对 \((i,j)\) 满足:

\[l-1\le i<j\le r,\qquad p_i\oplus p_j=k \]

代码中:

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\) 时,能与它配对的是所有满足

\[p_y\oplus p_x=k\iff p_y=p_x\oplus k \]

的已有元素。
已有元素中出现次数为 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--);

四个方向:

  1. 左端点小于目标:删除左端元素,左指针右移;
  2. 左端点大于目标:左指针左移,加入新元素;
  3. 右端点小于目标:右指针右移,加入新元素;
  4. 右端点大于目标:删除右端元素,右指针左移。

移动完成后,sum 就是当前询问答案,存入 ans[id]。


6. 小结

本题核心是:

  1. 用前缀异或把子区间异或和转成两点异或;
  2. 询问 \([l,r]\) 转为前缀下标区间 \([l-1,r]\);
  3. 用莫队维护当前前缀下标区间;
  4. tot[x] 统计前缀异或值出现次数,sum 维护满足 \(p_i\oplus p_j=k\) 的对数;
  5. 加入/删除时通过 tot[x^k] 更新答案;
  6. 块长取 \(\sqrt n\),配合奇偶优化。

小Z的袜子

1. 题意

有 \(n\) 只袜子,每只有颜色 \(c_i\)。\(m\) 次询问,每次问区间 \([L,R]\) 中随机抽两只袜子颜色相同的概率,输出最简分数 \(A/B\)。

若 \(L=R\),概率为 \(0\),输出 0/1。

数据范围 \(n,m\le50000\)。


2. 概率公式

区间长度为:

\[len=R-L+1 \]

从 \(len\) 只袜子中任选两只有:

\[\binom{len}{2}=\frac{len(len-1)}{2} \]

种选法。

若某种颜色出现了 \(t\) 次,从中选两只同色的方案数为:

\[\binom{t}{2}=\frac{t(t-1)}{2} \]

所以同色对总数为:

\[S=\sum_{\text{颜色}} \binom{t}{2} \]

概率:

\[P=\frac{S}{\binom{len}{2}} \]

化简后输出 \(S/g\) 和 \(\binom{len}{2}/g\),其中:

\[g=\gcd\left(S,\binom{len}{2}\right) \]


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. 小结

本题是莫队算法求区间同色对的模板题:

  1. 离线读入所有询问;
  2. 按左端点分块、右端点奇偶优化排序;
  3. 用两个指针维护当前区间;
  4. cnt[x] 统计颜色出现次数,ans 维护同色对数;
  5. 每次询问用 gcd 化简概率输出;
  6. 注意 len==1 特判和排序范围。

Mato的文件管理

posted @ 2026-11-04 20:55  Aries2011  阅读(4)  评论(0)    收藏  举报