The Begin

根号算法的优势在于好想好写,且支持维护更多的信息。

比如区间众数这种线段树维护不了的,而我们可以使用分块维护。

当然本篇文章所说的根号算法不只有分块,还有莫队,根号分治等一系列复杂度为 \(\sqrt {n}\) 的算法。

本文肯定会有一个 Ynoi 的大分块的,只是时间问题。

Update on 2025.6.30:完结了,笔者真的写了 Ynoi 系列大分块中的一个。

整除分块

先从最简单的开始吧!

算法简介

形如让我们计算:\(\sum_{i=1}^{n} \lfloor\frac{n}{i}\rfloor\) 这种式子,

我们发现,\(\lfloor\frac{n}{i}\rfloor\) 在 \(i\) 值不同时可能相同,而且是一段连续的区间。

所以我们考虑将这些地方一起计算。

具体地,枚举左端点 \(l\),求出右端点 \(r\),然后令左端点挪动到右端点 \(r\) 上。

经过推导,我们得出了:

\[r=\lfloor \frac{n}{\lfloor\frac{n}{l}\rfloor}\rfloor

\]

直接代入递推计算即可。

模板题:UVA11526 H(n)

套公式直接做即可。

代码

例题:模积和

推式子题,原题让求的是$$\sum_{i=1}^n \sum_{j=1}^m (n \bmod i)(m\bmod j),i≠j$$

长得太丑了,而且 \(i≠j\) 丑到实在不忍直视。假定 \(n\ge m\) ,得到如下:

\[\sum_{i=1}^n \sum_{j=1}^m (n \bmod i)(m\bmod j)-\sum_{i=1}^m (m \bmod i)(n\bmod i)

\]

模数不好表达,考虑利用模数定义转化一下:

\[\sum_{i=1}^n \sum_{j=1}^m (n -i\times \lfloor\frac{n}{i}\rfloor)(m-j\times \lfloor\frac{m}{j}\rfloor) - \sum_{i=1}^m (m -i\times \lfloor\frac{m}{i}\rfloor)(m-i\times\lfloor\frac{n}{i}\rfloor)

\]

然后发现减号前面那一项比较特别,写成这样:

\[\sum_{i=1}^{n}(n-i\times\lfloor\frac{n}{i}\rfloor) \times \sum_{j=1}^{m}(m-j\times\lfloor\frac{m}{j}\rfloor)

\]

然后给它拆开,得到:

\[(n^2- \sum_{i=1}^{n}(i\times\lfloor\frac{n}{i}\rfloor))\times (m^2- \sum_{j=1}^{m}(j\times\lfloor\frac{m}{j}\rfloor))

\]

然后前面这一车就可以拿整除分块算了,后面这一车做类似变化,最终得到一个

\[(n^2- \sum_{i=1}^{n}(i\times\lfloor\frac{n}{i}\rfloor))\times (m^2- \sum_{j=1}^{m}(j\times\lfloor\frac{m}{j}\rfloor))- \sum_{i=1}^{m}(m\times n- n\times i\times\lfloor\frac{m}{i}\rfloor-m\times i\times\lfloor\frac{n}{i}\rfloor+i^2\times\lfloor\frac{n}{i}\rfloor\times\lfloor\frac{m}{i}\rfloor)

\]

这坨式子,发现后面的也是整除分块,接着就是整除分块快速求:

\[\sum_{i=1}^{n}i\times \lfloor\frac{n}{i}\rfloor

\]

令 \(l=i\),通过整除分块计算方式,我们知道 \(r=\lfloor\frac{n}{\lfloor\frac{n}{i}\rfloor} \rfloor\),所以 \(r-l+1\) 就等于 \(\frac{r\times(r+1)}{2}-\frac{(l-1)\times (l)}{2}\)(\(1->l,r\)的前缀和相减),然后后面就是 \(\frac{n}{l}\) ,然后就可以写完了。

后面的 \(i^2\) 有平方和公式 \(\frac{n\times (n+1) \times (2\times n+1)}{6}\) 。

代码

根号分治

算法简介

根号分治是一种平衡复杂度的思想。

比如有一道题,我们可以 \(O(n^2)\) 预处理,\(O(1)\) 回答或者 不预处理,\(O(n)\) 回答。

这两种复杂度我们都不能接受。

我们可以预处理 \(\sqrt n\) 范围的,然后做到 \(O(\sqrt n)\) 预处理 ,\(O(\sqrt n)\) 回答。

上面是我之前学艺不精狗叫,根号分治不仅仅有上面的形式。(现在也学艺不精)

CF1806E Tree Master

我们将树分层,并对每层的节点数分讨:

如果当前层节点数大于 \(\sqrt n\),这样的层数不超过 $ \sqrt n$ 个,我们直接进行搜索即可。

如果当前节点数小于 $ \sqrt n$,直接使用记忆化搜索即可。

总之,直接记搜即可。

#include

#define ll long long

using namespace std;

const int N = 1e5+10;

int n,m,a[N],fa[N],dep[N],sum[N],bel[N];

ll s[N];

ll f[N][500];

ll dfs(int x,int y){

if(x==y) return s[x];

if(sum[dep[x]]<480){

if(f[x][bel[y]]) return f[x][bel[y]];

return f[x][bel[y]]=dfs(fa[x],fa[y])+1ll*a[x]*a[y];

}else return dfs(fa[x],fa[y])+1ll*a[x]*a[y];

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

cin>>n>>m;

for(int i=1;i<=n;i++){cin>>a[i];}

for(int i=2;i<=n;i++){cin>>fa[i];}

for(int i=1;i<=n;i++){

s[i]=s[fa[i]]+1ll*a[i]*a[i];

dep[i]=dep[fa[i]]+1;

bel[i]=++sum[dep[i]];

}

for(int i=1,x,y;i<=m;i++){

cin>>x>>y;

cout << dfs(x,y) << '\n';

}

return 0;

}

P3645 雅加达的摩天楼

想一个最暴力的暴力,建 $ 9\times 10^8$ 条边,然后直接暴力最短路。

发现我们其实根本用不到每一条边,所以尝试不存储状态直接 bfs,然后拿 bitset 表示状态,发现过了?

为什么是对的呢?不应该队列里扔了一堆状态然后暴空间吗?

我们对跳跃能力分讨:

当 \(p> \sqrt n\) 时,对于这只 doge,只有 $ \sqrt n$ 个位置。总计 \(m \sqrt n\) 个状态。

当 \(p<\sqrt n\) 时,对于每个点上状态,只有 $ \sqrt n$ 个不同的跳跃能力。总计 \(n\sqrt n\) 个状态。

所以总状态数是非常正确的,并不会 MLE/TLE。

#include

#define ll long long

using namespace std;

const int N = 3e4+10;

struct node{

int id,p,step;

};

queue que;

bitset vis[N];

vector vec[N];

bool v[N];

int n,m,s,t;

void push(int i,int p,int step){

if(!v[i]){

v[i]=1;

for(int a:vec[i]){

if(!vis[i].test(a)){

vis[i].set(a);

que.push({i,a,step+1});

}

}

}

if(!vis[i].test(p)){

vis[i].set(p);

que.push({i,p,step+1});

}

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

cin>>n>>m;

for(int i=0;i

int b,p;

cin>>b>>p;

if(i==0) s=b;

if(i==1) t=b;

vec[b].push_back(p);

}

if(s==t){cout << 0;return 0;}

v[s]=1;

for(int a:vec[s]){

if(!vis[s].test(a)){

vis[s].set(a);

que.push({s,a,0});

}

}

while(!que.empty()){

auto to=que.front();que.pop();

if(to.id-to.p==t || to.id+to.p==t){

cout << to.step+1;

return 0;

}

if(to.id+to.p

push(to.id+to.p,to.p,to.step);

}

if(to.id-to.p>=0){

push(to.id-to.p,to.p,to.step);

}

}

cout << -1;

return 0;

}

CF710F String Set Queries

分治的标志:总量不超过……

对字符串长度进行根号分治:

长度小于 $ \sqrt n$ 的,上 Trie 树直接维护即可,询问直接暴力查询链和。

长度大于 $ \sqrt n$ 的,全扔去一边,然后用 KMP 维护,每次查询的时候重跑 KMP,然后暴力查询即可。

Trie 树上的操作都是 $ \sqrt n$ 的,总计是 \(O(n \sqrt n)\)。而大于 $ \sqrt n$ 的仅有 $ \sqrt n$ 个,所以总计也是 \(O(n \sqrt n)\) 的。

所以总计的复杂度是 \(O(n \sqrt n)\) 。

#include

#define ll long long

using namespace std;

const int N = 3e5+10;

int n;

struct Trie{

struct node{

int son[26];

int cnt;

}tr[N];

int tot=0;

void insert(char *s,int val){

int u=0;

for(int i=0;s[i];i++){

char ch=s[i];

int &son=tr[u].son[ch-'a'];

if(!son) son=++tot;

u=son;

}

tr[u].cnt+=val;

}

int query(char *s){

int u=0,res=0;

for(int i=0;s[i];i++){

int c=s[i]-'a';

if(!tr[u].son[c]) break;

u=tr[u].son[c];

res+=tr[u].cnt;

}

return res;

}

}T;

struct KMP{

int nxt[N];

void build(const string &s,int len){

memset(nxt,0,sizeof(nxt));

int i=0,j=-1;nxt[0]=-1;

while(i

if(j==-1 || s[i]==s[j]) nxt[++i]=++j;

else j=nxt[j];

}

}

int query(string s,char *qry){

int res=0;

int lens=s.size(),len=strlen(qry);

build(s,s.size());

for(int i=0,j=0;i

while(j>0 && qry[i]!=s[j]) j=nxt[j];

if(qry[i]==s[j]) j++;

if(j==lens){

res++;

j=nxt[j];

}

}

return res;

}

}kmp;

string que[50];

char s[N];

int cnt,val[50];

int main(){

// ios::sync_with_stdio(0);

// cin.tie(0);cout.tie(0);

scanf("%d",&n);

for(int i=1,op;i<=n;i++){

scanf("%d%s",&op,s);

int len=strlen(s);

if(op==1 || op==2){

int v=(op==1 ? 1 : -1);

if(len<=1000){

T.insert(s,v);

}else{

que[cnt]=string(s);

val[cnt++]=v;

}

}else{

ll ans=0;

for(int i=0;i

ans+=T.query(s+i);

}

for(int i=0;i

if(que[i].length()>len) continue;

ans+=kmp.query(que[i],s)*val[i];

}

printf("%lld\n",ans);

fflush(stdout);

}

}

return 0;

}

分块

根号算法的代表,提起根号第一个想到的肯定是我们优雅的分块了!

算法简介

分块是 重构 和 懒标记 的结合,对于修改,通常使用“散块暴力,整块一起”的方法。

分块的作用主要是 平衡复杂度 和 维护线段树 等无法维护的信息。

将序列分块后,令块长为 \(B\) ,可以得到单步复杂度为 \(O(\frac{n}{B}+B)\)。

根据均值不等式可以得到 \(B=\sqrt n\) 时有最优复杂度 \(O(\sqrt n)\)。

接下来手把手教大家写分块模板题:

【模板】线段树 1

分块的预处理

分块预处理需要三个数组,\(L_x , R_x , belong_i\) 分别表示,第 \(x\) 块序列的左端点,右端点,原序列中的第 \(i\) 个点属于哪一块。

分块区间加法

顺应上述的分块思想,对于整块的修改打上 tag ,对于散块就暴力修改。可能还用到了一点点的标记永久化的思想?

分块的区间查询

区间操作都很类似,只要注意一下计算时是否需要加上 tag 就行。

多种运算操作

根线段树一样,定义一个合理的优先级就行。

代码

序列分块

算法简介

最基本的分块,就是对原序列分成若干块。

教主的魔法

说人话:支持区间加法,查询区间内大于等于 k 的数的个数。

对于每一块都排好序,然后散块直接做就行了,对于整块,去二分查找后加上 \(R_i-x+1\) 即可。

代码

由乃打扑克

第一道 Ynoi 题!

思路继承教主的魔法,整块内排序,考虑怎么求得第 k 小值。

去二分这个第 k 小值,然后查这个值是否是第 k 小就行啦。

代码

ycz的妹子

一句话题意:

单点修。

单点插入。

删除第 k 个值。

全局查询和。

线段树写起来显然很累,无论是脑子累或者是手累。

但是分块不一样,直接分块,单点修和单点插入显然,删除第 k 个值记录块内多少个有意义的值,然后直接跳块找即可。

甚至能支持区间查询和!

代码

值域分块

算法简介

如题,可以理解为权值分块

等这场战争结束之后

可撤销并查集+值域分块。

具体做法:离线建立版本树,离散化后值域分块维护每一块每个值出现次数,入树时进行修改或询问,出树时撤销修改。

然后就是 Ynoi 的经典卡常时刻,本题需要卡的是 20MB 的空间。

值域分块数组可以用 short。

块长设大一点。

使用版本树,别写在线!

代码

[Violet] 蒲公英

自然的想到离散化值域分块,因为求众数,要求多少个相同数,所以考虑前缀和。

具体而言,维护两个分块,一个 \(s_{i,j}\) 表示第 \(i\) 个块内的每个数的个数前缀和(对于 \(i\) 统计),一个 \(f_{i,j}\) 表示第 \(i\) 块与第 \(j\) 块之间的众数。这两个数组可以预处理。

对于询问,我们将询问分成三部分:

\(———l——bl———————br——r————\)

其中 \(l,r\) 表示询问区间,\(bl,br\) 表示若干个整块。可能的众数集合是:整块内的众数,\(l\to bl\) 之间的某数,\(br\to r\) 之间的某数。

散块暴力,整块一起,询问区间不能被分为两块时暴力,那么我们就做完了。

代码

[TJOI2007] 可爱的质数/【模板】BSGS

原式为:\(a^l = b \pmod p\),化为 \(a^{Am-n} = b \pmod p\),左右两侧同乘 \(a^n\),式子变为:\(a^{Am} = ba^n \pmod p\)

我们可以预处理出所有的 \(b\times a^n \bmod p\) 存入 hash 中,再去计算 \(a^{Am} \bmod p\) 然后去查 hash 表中的数是否存在相同的数。

用 hash 的时间复杂度是 \(O(\sqrt p)\) 。用 map 是 \(O(\sqrt p \times \log p)\)

代码

多少个1?

若干个 \(1……1111 (N)\) 的形式,思考一下可以转化为 \(\frac{10^n-1}{9}\) 的形式。

那么原题就变为求:

\[\frac{10^n-1}{9}=k\pmod m

\]

略微化简,得到如下式子:

\[10^n = 9\times k +1 \pmod m

\]

求最小整数 \(n\) 满足上述式子,套BSGS板子。

中间需要开 __int128 ,或者使用快速乘。

原理是乘法分配律,可以试着推一把(反正我没推)。

代码

时间轴分块

算法简介

如题而言,对时间轴进行分块,逐个时间处理。

序列

带修也可以离线!

新增一个时间轴,两个修改中间的询问都是一个时间点。

我们先考虑只有一个数怎么做。

发现是查询前缀大于等于自己的数有多少个。

所以我们需要做一个查询排名和单点加法。

接下来考虑怎么做多个数。

我们离线后差分,将区间修改转为单点加法和前缀和即可。

代码

等我写俩题【TBD】

操作分块(阈值分块)

算法简介

平衡修改和询问的复杂度的一个方式吧,具体而言设一个阈值 B,当操作积压到 B 的时候再去处理。

桥梁

观察题目可以发现,我们能想出两种暴力做法:

暴力模拟所有操作,询问时搜索,是 \(O(n^2)\) 的。

离线询问按降序排序,每次回答时重跑时间轴之前的操作,然后拿可撤销并查集维护连通性,也是 \(O(n^2\log n)\) 的。

发现这两种暴力从截然不同的角度出发,一个枚举边,一个枚举操作,所以我们使用分块将这两个均摊一下。

简单而言,定义一个阈值 \(B\),当积压的操作达到 \(B\) 的时候处理当前所有操作,同样使用可持久化并查集维护一下。

复杂度很好玩了,我们会把操作分成 \(\frac{q}{B}\) 块,每块最多 \(B\) 个

将 B 带入上面两个暴力的复杂度,得到最后式子是 \(O(qB\log n + \frac{qm\log m}{B})\) ,\(B\) 取个 \(\sqrt {mlogn}\) 据说跑的飞快,但是我 \(B\) 取了个附近的定值,也跑过了。

代码

根号重构

算法简介

你说得对,但是 单点修+维护抽象信息 为什么不想根号重构被修改的块。

难道你想写什么 奇技淫巧线段树 或者 大力 LCT 吗?

又或者什么 神奇转化+扫描线 吗?

楼房重建

题意:单点修+固定起点查上升子序列长度。

正解是单侧递归线段树或分块重构。

这种看起来很抽象的信息为什么不拿分块!

对于每一块,用 vector 存上升子序列,查询时二分查找即可。

对于修改,将所在块的 vector 清空后直接重构。

两个操作复杂度均为 \(O(\sqrt n)\),显然可以轻松通过本题。

#include

using namespace std;

const int N=1e5+10;

int n,m,a[N],len,bel[N],s=2e9;

vector vec[N];

void init(){

for(int i=1;i<=n;i++){

bel[i]=(i-1)/len+1;

}

}

void modify(int x,int k){

a[x]=k;

double mx=0;

vec[bel[x]].clear();

for(int i=(bel[x]-1)*len+1;bel[i]==bel[x] && i<=n;i++){

if(!a[i]) continue;

double tmp=(double)a[i]/(1.0*i);

if(tmp>mx) vec[bel[x]].push_back(tmp),mx=tmp;

}

}

int find(int x,double val){

return vec[x].end()-upper_bound(vec[x].begin(),vec[x].end(),val);

}

int query(){

int res=0;

double mx=(double)a[s]/(1.0*s);

for(int i=1;i<=bel[n];i++){

res+=find(i,mx);

if(vec[i].empty()) continue;

double tmp=(*(--vec[i].end()));

if(mx

}

return res+1;

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

cin>>n>>m;

len=sqrt(n*log(n));

init();

for(int i=1;i<=m;i++){

int x,k;

cin>>x>>k;

s=min(s,x);

modify(x,k);

cout << query() << '\n';

}

return 0;

}

弹飞绵羊

这次你没法选了,不写 分块 你难道写 LCT 吗?

下文称 \(i\) 到达的位置为 \(to_i\)

有个非常自然的暴力:

询问时直接依次遍历模拟,然后得出答案。询问:\(O(n)\) 修改:\(O(1)\)。

当然稍加思索,如果没有修改,答案就是一段 \(to_i\) 的前缀和,可以预处理。询问:\(O(1)\) 修改 \(O(n)\)。

两个暴力都有了,符合分块平衡复杂度的必要条件了,那就分块呗。

把指向与 \(i\) 一个块内的 \(to_i\) 并列起来,然后做前缀和,查询时直接查询前缀和后跳到下一块。

可能上面说的有点抽象,可以看看代码。(这题还有助于加强对前缀和的理解欸)

复杂度分析:

我们把每块内的 \(to_i\) 并到一起,指向 \(to_{R_i}\),所以查询最多遍历 \(\sqrt n\) 个块。

修改直接重构一整块就行,最多 \(\sqrt n\) 个数。

所以复杂度是 \(O(n\sqrt n)\) 的。你 LCT 跑得过吗?!(跑过了我就去调块长)

#include

#define ll long long

#define B 500

using namespace std;

const int N = 2e5+10;

int n,m,a[N];

int to[N],bel[N],L[N],R[N],tot,cnt[N];

void init(){

tot=(n-1)/B+1;

for(int i=1;i<=n;i++){

bel[i]=(i-1)/B+1;

}

for(int i=1;i<=tot;i++){

L[i]=(i-1)*B+1;

R[i]=min(i*B,n);

}

for(int i=1;i<=tot;i++){

for(int j=R[i];j>=L[i];j--){

if(j+a[j]<=R[i]){

cnt[j]+=cnt[j+a[j]]+1;

to[j]=to[j+a[j]];

}else{

cnt[j]=1;

to[j]=j+a[j];

}

}

}

}

void modify(int x,int k){

a[x]=k;

for(int i=R[bel[x]];i>=L[bel[x]];i--) cnt[i]=to[i]=0;

for(int i=R[bel[x]];i>=L[bel[x]];i--){

if(i+a[i]<=R[bel[x]]){

cnt[i]+=cnt[i+a[i]]+1;

to[i]=to[i+a[i]];

}else{

cnt[i]=1;

to[i]=i+a[i];

}

}

}

int query(int x){

int res=0;

while(x<=n){

res+=cnt[x];

x=to[x];

}

return res;

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

cin>>n;

for(int i=1;i<=n;i++){

cin>>a[i];

}

init();

cin>>m;

for(int i=1;i<=m;i++){

int op;cin>>op;

if(op==1){

int x;cin>>x;x++;

cout << query(x) << '\n';

}else{

int x,y;cin>>x>>y;x++;

modify(x,y);

}

}

return 0;

}

莫队

算法简介

莫队是优雅的暴力,通过挪动左右指针来移动到下一个询问区间。

离线算法,不带修复杂度为 \(O(n\sqrt n)\),带修能做到 \(O(n^{\frac{5}{3}})\)。

需要将询问排序:如果左端点在同一块内,右端点升序,否则左端点升序。

可以玄学优化,左端点在偶数块右端点升序,否则降序。

小Z的袜子

莫队模板题,排完序直接做就行了。

代码

[国家集训队] 数颜色 / 维护队列

带修莫队模板题。

在莫队基础上加一维时间轴,先按时间轴排序,如果是同一时间轴的则按上述莫队正常排序。

考虑时间变动的影响,然后去更新消除影响即可。

复杂度经证明取 \(B=n^{\frac{2}{3}}\) 最优,所以总体复杂度是 \(O(n^{\frac{5}{3}})\)

代码

其他例题

穿插上述各种题目,可能莫队偏多,因为纯考分块的只有lxl大分块。

Rmq Problem / mex

显然值域分块再加上前缀和就可以解决出现最小自然数的问题,配合莫队实现区间蠕动即可。

代码

如果你调半天调不出来,可以看看 while() 是不是写成了 if() 。这玩意我看了半小时

作业

会了上面那个,这个也就很显然了。

值域分块套上莫队,维护两个数组一个是所有和,一个是不同位置的和,写两个询问就好。

代码

异或序列

发现关键性质:给异或做前缀和,一段区间的异或值等于 $arr_r $ ^ \(arr_{l-1}\) 然后莫队的移动我们就可以 \(O(1)\) 了。

代码

区间(range)

不能做除法,O(1) 求数列中定长为 k 的所有区间的数的积模 P(P不保证为质数,不保证有逆元)。

首先会想到前缀积和后缀积,发现正常的前缀后缀积不能拼出来一个长度为 k 的区间。

考虑以 k 为块长分块,每块内做前缀后缀积,然后所有定长为 k 的恰好被分成两段(或者一整段)。

然后就可以直接写了。

代码

小B的询问

莫队的基础题,先把莫队扔上去。

考虑莫队维护区间平方和,难道我们要像线段树一样维护一个区间和,再维护一个区间平方和吗?

我都写莫队了,先把贡献删了,修改后再加回去就行。

代码

小清新人渣的本愿

看完这个题面,已经对日本轻小说产生刻板印象了,果咩。

莫队套 bitset 模板题。

这种抽象东西肯定要拿莫队维护,然后就上值域 bitset 维护。

加法就是平移,减法存一个 \(N-x\) 的值,然后推两下式子就也可以变成平移。(此时有两个 bitset)

乘法就可以直接分解质因数,反正是 \(\sqrt n\) 的。

#include

#define ll long long

#define B 213

using namespace std;

const int N = 1e5+10;

const int M = 1e5;

bitset c1,c2;

struct Q{

int op,l,r,x,id;

bool operator < (const Q &a)const{

return (l/B==a.l/B) ? (l/B&1) ? ra.r : l/B

}

}q[N];

int n,m,a[N],cnt[N];

bool ans[N];

void add(int x){

if(cnt[a[x]]==0) c1[a[x]]=c2[M-a[x]]=1;

cnt[a[x]]++;

}

void del(int x){

cnt[a[x]]--;

if(cnt[a[x]]==0) c1[a[x]]=c2[M-a[x]]=0;

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

cin>>n>>m;

for(int i=1;i<=n;i++){cin>>a[i];}

for(int i=1;i<=m;i++){

cin>>q[i].op>>q[i].l>>q[i].r>>q[i].x;q[i].id=i;

}

sort(q+1,q+1+m);

int l=1,r=0;

for(int i=1;i<=m;i++){

while(l>q[i].l) add(--l);

while(r

while(l

while(r>q[i].r) del(r--);

if(q[i].op==1){

if((c1 & (c1<

ans[q[i].id]=1;

}else if(q[i].op==2){

if((c1 & (c2>>(M-q[i].x))).any())

ans[q[i].id]=1;

}else{

for(int j=1;j*j<=q[i].x;j++){

if(q[i].x%j==0){

if(c1[j]&&c1[q[i].x/j]){

ans[q[i].id]=1;

break;

}

}

}

}

}

for(int i=1;i<=m;i++){

cout << (ans[i] ? "hana" : "bi") << '\n';

}

return 0;

}

曼哈顿交易

论莫队与值域分块的适配度:100% !

虽然本题有奇技淫巧省去值域分块的技巧,但是没必要,复杂度上界不是这个。

对第 \(i\) 种股票开一个桶计数,定义为 \(cnt1_i\),再对 \(cnt1_i\) 开个桶计数为 \(cnt2_i\) 。

然后莫队维护是简单的,查询也是平凡的,就是简单的跳块然后找到所属块后暴力。

#include

#define ll long long

#define B 345

using namespace std;

const int N = 1e5+10;

int n,m,a[N],b[N],len;

struct Q{

int l,r,k,id;

bool operator < (const Q &a)const{

return l/B==a.l/B ? (l/B)&1 ? ra.r : l/B

}

Q(int _l=0,int _r=0,int _k=0,int _id=0):l(_l),r(_r),k(_k),id(_id){}

}q[N];

int ans[N],l=1,r=0;

int bel[N],L[N],R[N],cnt[N],sum[N],tot,cnt2[N];

void init(){

tot=(N-10-1)/B+1;

for(int i=1;i<=N-10;i++){

bel[i]=(i-1)/B+1;

}

for(int i=1;i<=tot;i++){

L[i]=(i-1)*B+1;

R[i]=min(i*B,N-10);

}

}

void add(int x){

sum[bel[cnt[x]]]--;

cnt2[cnt[x]]--;

cnt2[++cnt[x]]++;

sum[bel[cnt[x]]]++;

}

void del(int x){

sum[bel[cnt[x]]]--;

cnt2[cnt[x]]--;

cnt2[--cnt[x]]++;

sum[bel[cnt[x]]]++;

}

int query(int k){

int x;

for(x=1;x<=tot;x++){

if(k-sum[x]<=0) break;

k-=sum[x];

}

if(x==tot+1) return -1;

for(int i=L[x];i<=R[x];i++){

if(k-cnt2[i]<=0) return i;

k-=cnt2[i];

}

return -1;

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

init();

cin>>n>>m;

for(int i=1;i<=n;i++){

cin>>a[i];b[i]=a[i];

}

sort(b+1,b+1+n);len=unique(b+1,b+1+n)-b-1;

for(int i=1;i<=n;i++){a[i]=lower_bound(b+1,b+1+len,a[i])-b;}

for(int i=1,l,r,k;i<=m;i++){

cin>>l>>r>>k;

q[i]=Q(l,r,k,i);

}

sort(q+1,q+1+m);

for(int i=1;i<=m;i++){

while(l>q[i].l) add(a[--l]);

while(r

while(l

while(r>q[i].r) del(a[r--]);

ans[q[i].id]=query(q[i].k);

}

for(int i=1;i<=m;i++){

cout << ans[i] << '\n';

}

return 0;

}

军队

第二分块状物,个人感觉写 分块维护并查集 做法比第二分块难写。

考虑并查集内维护同色块的一个代表,就以第一次出现的为代表即可,将所有的同色块指向代表。

接下来考虑操作。

操作一:

散块直接将所在整块的颜色还原,然后暴力修改后重构即可。

整块分类讨论,没有 \(x\) 颜色不修改,没有 \(y\) 颜色替换,两色都有也是暴力还原后修改,复杂度参考势能分析。

以不同颜色数量为势能,两色都有的时候势能减一,只有 \(O(n)\) 种颜色,总复杂度 \(O(n\sqrt n)\)

操作二:

散块暴力遍历直接加。

给 \(x\) 颜色打 \(tag\),然后对块内和加 \(cnt_x\times v\)。

操作三:

散块暴力遍历直接求和。

整块返回块内和。

然后代码非常的……

#include

#define ll long long

#define B 750

#define L(i) (i-1)*B+1

#define R(i) min(i*B,n)

using namespace std;

constexpr int N = 2.5e5+5;

int n,m,col,c[N];

ll a[N];

struct Query{

int op,l,r,x;

ll y;

}q[N];

ll tag[N],ans[N],sum;

int fa[N],cnt[N],pre[N],tot;

int find(int x){return fa[x]==x ? x : fa[x]=find(fa[x]);}

inline void init(const int &x){

sum=0;

for(int i=1;i<=col;i++) tag[i]=fa[i]=pre[i]=cnt[i]=0;

for(int i=L(x);i<=R(x);i++){

fa[i]=pre[c[i]] ? pre[c[i]] : (pre[c[i]]=i);

cnt[c[i]]++;

sum+=a[i];

}

}

inline void modify1(const int &id,const int &x,const int &y){

if(!pre[x]) return ;

if(!pre[y]){

c[pre[x]]=y;

swap(cnt[y],cnt[x]);

swap(tag[y],tag[x]);

swap(pre[y],pre[x]);

return ;

}else{

cnt[y]+=cnt[x];cnt[x]=0;

for(int i=L(id);i<=R(id);i++) c[i]=c[find(i)];

for(int i=L(id);i<=R(id);i++){

if(c[i]==x) a[i]-=tag[y]-tag[x],c[i]=y;

}

fa[pre[x]]=pre[y];tag[x]=pre[x]=0;

}

}

inline void modify2(const int &id,int l,int r,const int &x,const int &y){

int nl=L(id),nr=R(id);

l=max(nl,l),r=min(nr,r);

for(int i=nl;i<=nr;i++){

c[i]=c[find(i)];

if(c[i]==x || c[i]==y)

a[i]+=tag[c[i]];

}

for(int i=l;i<=r;i++){

if(c[i]==x) c[i]=y;

}

cnt[x]=cnt[y]=pre[x]=pre[y]=tag[x]=tag[y]=0;

for(int i=nl;i<=nr;i++){

if(c[i]==x || c[i]==y){

fa[i]=pre[c[i]] ? pre[c[i]] : (pre[c[i]]=i);

cnt[c[i]]++;

}

}

}

inline void update1(const int &x,const ll &v){

if(!cnt[x]) return ;

tag[x]+=v;

sum+=v*1ll*cnt[x];

}

inline void update2(const int &id,int l,int r,const int &x,const ll &v){

int nl=L(id),nr=R(id);

l=max(nl,l),r=min(nr,r);

for(int i=l;i<=r;i++){

if(c[find(i)]==x) a[i]+=v,sum+=v;

}

}

inline ll query1(){

return sum;

}

inline ll query2(const int &id,int l,int r){

int nl=L(id),nr=R(id);

l=max(nl,l),r=min(nr,r);

ll res=0;

for(int i=l;i<=r;i++){

res+=a[i]+tag[c[find(i)]];

}

return res;

}

int main(){

ios::sync_with_stdio(0);

cin.tie(0);cout.tie(0);

cin>>n>>m>>col;

tot=(n-1)/B+1;

for(int i=1;i<=n;i++) cin>>a[i];

for(int i=1;i<=n;i++) cin>>c[i];

for(int i=1;i<=m;i++){

int op,l,r,x=0,y=0;cin>>op>>l>>r;

if(op<3) cin>>x>>y;

q[i]={op,l,r,x,y};

}

for(int be=1;be<=tot;be++){

init(be);

int nl=L(be),nr=R(be);

for(int i=1;i<=m;i++){

int op=q[i].op,l=q[i].l,r=q[i].r;

if(nl>r || nr

if(op==1){

int x=q[i].x,y=q[i].y;

if(nl>=l && nr<=r) modify1(be,x,y);

else modify2(be,l,r,x,y);

}else if(op==2){

int x=q[i].x,y=q[i].y;

if(nl>=l && nr<=r) update1(x,y);

else update2(be,l,r,x,y);

}else{

if(nl>=l && nr<=r) ans[i]+=query1();

else ans[i]+=query2(be,l,r);

}

}

}

for(int i=1;i<=m;i++){

if(ans[i]) cout << ans[i] <<'\n';

}

return 0;

}

其他

这里记一些 Trick 和 势能分析 证明。

区间操作,若 \(a_i>x\) 则使 \(a_i-x\to a_i\)

分两种情况讨论:

\(2x\ge mx\), 那么全局最大值变为:\(mx=mx-x\)

\(2x < mx\), 我们可以先令 \(\le x\) 的数 +x ,那么全局就都比 x 大了,此时给全局打一个减法 tag,全局最大值依然变为:\(mx=mx-x\) 。

区间操作,将区间所有 \(x\) 变成 \(y\)

分类讨论:

若没有 \(x\) 直接跳过,都是 \(O(1)\)。

若没有 \(y\) 直接swap,都是 \(O(1)\)。

若 \(x,y\) 都有,对整体考虑,每次操作散块可能会增加一种颜色,初始时有 \(n\) 种颜色,所以颜色总数是 \(O(n+m)\) 的。每次暴力重构散块是 \(O(\sqrt n)\),所以我们只需要 \(O((n+m)\sqrt n)\) 的复杂度就能重构散块。

The End 去往那五彩斑斓的世界

故事终于来到最后,你打败了路上的所有小怪,精英怪,你来到了最终 boss 之前。

你攥紧了手中的宝具:[突刺贯穿的第二分块]。

仰望着漆黑的天空,轻抚着狂乱的风暴:

按照故事的剧本,打败最终 boss 之后,你就到了五彩斑斓的世界。

是的,是时候动身了,还有人在等着我们。

fun fact:主播为了找一道能做的大分块找了一个多小时,最终还是回到了这道题

给定长度为 \(n\) 的序列,\(m\) 次操作

把区间 \([l,r]\) 中大于 \(x\) 的数减去 \(x\)。

查询区间 \([l,r]\) 中 \(x\) 的出现次数。

\(n\le 10^6 , m\le 5\times 10^5 ,0\le a_i,x \le 10^5+1\)

拿到手第一刻很懵,相同的数可以并查集缩一起,查询时累加并查集大小就行。

但是我们似乎避不开要枚举值域,那我们就需要分析值域上的性质了。

假定我们现在序列中最大的数是 mx,减去的数是 x,n 为个数,m 为操作数,v 为值域。

分两种情况讨论:

\(2x\ge mx\), 那么全局最大值变为:\(mx=mx-x\)

\(2x < mx\), 我们可以先令 \(\le x\) 的数 +x ,那么全局就都比 x 大了,此时给全局打一个减法 tag,全局最大值依然变为:\(mx=mx-x\) 。

发现全局最大值单调不增!

那么我们最大值上界是 \(10^5\) 的,而操作却有 \(5\times 10^5\) 次。

这是一个很好的均摊,我们直接枚举修改了哪些值即可。总计是 \(O(v)\) 左右的。

那么复杂度有了保证,再想想实现的细节。

对于整块:

修改时直接遍历需要改的值,然后并查集合并。

询问时也是直接回答就行。

修改 \(O(\sqrt n)\) ,询问 \(O(1)\) 的。

对于散块:

修改时将所有值复原,再修改。

询问时也将所有值复原,再遍历。

都是 \(O(\sqrt n)\) 的。

注意值域有 0,这玩意放进并查集里完蛋了,但是 0 不会变成负数,也不会有任何数变成 0。

所以可以直接预处理所有 0 的情况,做前缀和即可。

当然做完上述事情就已经可以 AC 另一道题了:CF896E

fun fact:本题和 lxl 提出的珂朵莉树 是一场比赛的题。

本题 lxl 卡了线性空间,主播跟 lxl 斗智斗勇一中午终于调过。

fun fact:主播写的空间常数略大的线性空间也被卡飞了。

发现块与块之间相互独立,将询问离线下来,去每一个块里跑一遍,然后累加答案就行。

最终复杂度是:\(O(m\sqrt n + n\sqrt v)\) 左右吧,不太会算。

代码

来,出发吧,去选择那独一无二的明天

除非 lxl 来讲课,不然这是主播最后一道 Ynoi 大分块系列题。