根号算法学习笔记
0.前言
本来觉得根号这么 [史] 的东西大概做做就行了,结果发现史全在卡常阶段。为什么我的代码自带大常数!!
1.莫队
莫队纯纯暴力数据结构。其先将所有询问离线下来,然后按照端点排序,通过缩放并暴力维护区间 \([l,r]\) 计算答案。
我们希望某个端点尽量有序,这样该端点的移动就可以做到 \(O(n)\);但一个端点有序另一个可能来回乱跑,为了平衡复杂度,我们希望另一端点的移动区间大小为 \(\sqrt{n}\)。于是,先给所有下标分块,以左端点所在的块第一关键字、右端点为第二关键字排序即可。
复杂度证明: 设块长为 \(B\),在左端点移动复杂度卡满时移动的复杂度是 \(mB\),右端点移动复杂度是 \(\frac{n^2}{B}\),根据均值还是基本来着不等式 \(mB+\frac{n^2}{B}\geqslant 2\sqrt{n^2m}=n\sqrt{m}\),当且仅当 \(mB=\frac{n^2}{B}\)即 \(B=\frac{n}{\sqrt{m}}\) 时取得最小值。
普通莫队
因为移动指针的复杂度就带根号了,所以修改维护信息的复杂度需要做到 \(O(1)\)。有什么东西 \(O(1)\) 修改、根号查询呢,那就是值域分块,所以莫队+值域分块做完了( bitset 的修改也是 \(O(1)\),所以 bitset 也适合与莫队结合)
带修莫队
带修莫队就是将修改版本看作第三维指针,也是暴力转移,但块长取的是 \(n^{\frac{2}{3}}\),复杂度不会证
回滚莫队
这题中需要维护最值,但在缩小区间时无法维护最值,此时就需要上回滚莫队。既然缩小区间不好维护,那么就将每个查询区间分成两个扩张更新的区间即可。
众所周不知,左端点在一块内查询的右端点是有序的,即此时右指针一直在扩张,只需要滚的时候左端点每次都从这一块的最右端开始向左扩展区间就很好维护最值了。
(这样相当于答案数组只保留着右边的信息,每次插入左边的信息后都要撤销以供下次从块右端扩展)
这时候有人要说了,要是块内最右端根本不在查询区间内怎么办,非常好办这样的区间长度不超过 \(\sqrt{n}\) 直接暴力求答案就完事了
莫队二次离线
以区间 \([l,r]\) 扩张到区间 \([l,r+1]\) 为例,这时需要新计算 \(a_{r+1}\) 对 \([l,r]\) 的贡献,但有些时候这无法 \(O(1)\) 做到,可能就需要上二离。
给贡献差分,需要计算 \((a_{r+1}\rightarrow [1,r])-(a_{r+1}\rightarrow [1,l-1])\) ,发现前者是对前缀的贡献,可以预处理并在缩放区间时直接计算;后者可以像二维数点一样插到 vector 中扫 \([1,i]\) 统一计算。因为第二个贡献的复杂度也带根号,所以这个也需要是 \(O(1)\) 转移 所以不能上树状数组!!!
2.分块
也是纯纯暴力。
P2801 教主的魔法
首先显然这要在权值线段树上操作。但是主席树不支持修改,怎么办呢。注意到询问最多 3000 次,可以再加一个根号的复杂度,所以可以分块,给每个块开一棵权值线段树,若是整块修改就直接打偏移量 \(tag\),散点修改直接暴力修改即可(查询同)
P4135 作诗
好像可以不用 bitset。不管了
强制在线无法上莫队,这道题只关注奇偶且没有修改,所以可以上 bitset+分块。出现正偶数次的数相当于所有数减去出现奇数次的数,前者可以按位或求出,后者则是异或求出。但复杂度会爆炸的,整块我们需要 \(O(1)\) 查询。异或可以用前缀和搞,或可以用 ST 表搞(可重复贡献问题),好了完结
P4119 [Ynoi2018] 未来日记
人生第二道黑题,也是卡常卡到破防的题
先考虑查询再考虑修改。一般查询 \(k\) 小值是二分查询某个前缀的值的个数,然后一步一步缩小范围。因为有修改,所以此处不适合主席树二分。仍是考虑某个前缀的值的个数,可以给值域分块,先找出第 \(k\) 小的值在哪个值域块中,再在块中统计
考虑修改。在查询统计散块的过程中,我们需要知道每个 \(a_i\) 的值,所以修改也得落在 \(a_i\) 上。若是修改整块,则相当于整个 \(x\) 集合都变成了 \(y\),非常眼熟好的这是并查集,改一下集合代表元素的值就行;若是散块,相当于要将位于散块的 \(x\) 从整个块的 \(x\) 集合中剥离出去并修改。这简直没法弄。但是散块的元素个数是 \(\sqrt{n}\) 级的,大可暴力重构
3.根号分治
依稀记得我在考场上忘记根号分治于是果断将根号算法排出正解的愚蠢样子。说来也是巧,当晚的加餐就又有了一道根号分治题
根号分治一般是确定一个阙值 \(B\) ,小于这个阙值可以暴力求出所有的 \(nB\) 个状态,大于这个阙值可以保证 \(\frac{n}{B}\) 的范围,所以仍是暴力。直白讲,就是将两种依赖不同复杂度的暴力拼在一起
P7811 [JRKSJ R2] 你的名字。
__卡常题,卡了我一周
取模的另一种形式是 \(a_i-\lfloor\frac{a_i}{k}\rfloor k\) ,而当 \(k\geqslant \sqrt{V}\) 时可以保证 \(\frac{a_i}{k}\) 的个数,于是想到根号分治
设阙值 \(B=\sqrt{V}\) 。当 \(k\leqslant B\) 时,直接对整体取模并维护区间最小值,需要使用 \(O(n)\) 建成的数据结构,比如说线段树(之前没想到线段树建树是 \(O(n)\) 的。但是实测常数太大就是过不了),比如说分块(我再也不看不起分块了)。这样建的复杂度是 \(O(nB)\),查询复杂度是 \(O(\sqrt{n})\),管他呢能过
当 \(k>B\) 时,需要查询 \(min\{a_i|i\in[l,r],a_i\geqslant jk\}-jk\)(其中 \(j\) 是枚举的倍数),即 \(jk\) 在 \([l,r]\) 内的非严格后继。当然这是可以在主席树上二分做的,实测过不了。考虑在值域上从大到小扫描线,令 \(p=jk\) ,插入所有 \(a_i\geqslant p\) 后的数后问题转化为了求区间min问题。因为查询的个数已经是根号级别的了,所以这里的查询需做到 \(O(1)\)。
-
猫树:毋庸置疑。具体原理不在这里展开。猫树可通过二区间合并 \(O(1)\) 查询信息,但是猫树的修改是 \(O(n)\) 级别的,所以需要在猫树上再套分块,猫树维护块之间的最小值,散块暴力查询,最后没用猫树写可能能过
-
ST 表:\(O(1)\) 查询代表,虽然一般不带修。考虑一次单点修改会在 ST 表上影响 \(1+2+4+…+n\) 反正是 \(O(n)\) 级别的区间(别告诉我你现在还是不会等比数列求和),即 ST 表上的修改也是 \(O(n)\) 的,所以也得套个分块,ST 表维护块内最小值,散块用前/后缀最小值数组搞,修改复杂为 \(O(n\sqrt{n})\),查询复杂度近似 \(O(1)\)
好。但是查询的个数是根号级别的,全存下来就会喜提 MLE,怎么办。枚举当前值 \(p\) 的约数 \(p=jk,k>B\),然后按照 \(k\) 值存下来就行了
然后各种卡常。好了完事
代码
#include <bits/stdc++.h>
#define rg register
using namespace std;
const int N=3e5+2;
const int M=1e5+2;
const int B=511;
const int inf=1e6;
int n,m;
struct node { int l,r,k,id; }q[N];
struct NODE { int x,pos; }a[N];
struct Node { int l,r,id; };
vector <Node> q2[M];
int pos[N],mx,nn;
int mn1[N],mn2[N];
int mn[602],ans[N];
struct Segment_Tree
{
inline void init(int k)
{
for (int i=1;i<=nn;i++) mn[i]=inf;
for (int i=1;i<=n;i++) mn[pos[i]]=min(mn[pos[i]],a[i].x%k);
}
inline int query(int l,int r,int k)
{
int res=inf,lid=pos[l],rid=pos[r];
if (l%B!=1) { int rr=min(n,lid*B); lid++; for (int i=l;i<=rr;i++) res=min(res,a[i].x%k); }
if (r!=n&&r%B) { int ll=(--rid)*B+1; for (int i=ll;i<=r;i++) res=min(res,a[i].x%k); }
for (int i=lid;i<=rid;i++) res=min(res,mn[i]);
return res;
}
}Tr;
struct ST
{
int st[602][10],lg[602];
inline void init()
{
lg[0]=-1;
for (rg int i=1;i<=nn;i++) lg[i]=lg[i>>1]+1;
memset(st,0x3f,sizeof st);
}
inline void update(int pos,int k)
{
int l,r;
for (rg int j=0;j<=lg[nn];j++)
{
l=max(1,pos-(1<<j)+1); r=min(nn-(1<<j)+1,pos);
for (rg int i=l;i<=r;i++) st[i][j]=k;
}
}
inline int query(int l,int r)
{
int k=lg[r-l+1];
return min(st[l][k],st[r-(1<<k)+1][k]);
}
}st;
inline bool cmp(node x,node y) { return x.k<y.k; }
inline bool cmp2(NODE x,NODE y) { return x.x<y.x; }
inline int fc(int l,int r,int k)
{
int res=inf;
for (rg int i=l;i<=r;i++) res=min(res,a[i].x%k);
return res;
}
inline int solve(int ll,int rr)
{
int lid=pos[ll],rid=pos[rr],res=inf;
if (ll%B!=1) { lid++; res=mn1[ll]; }
if (rr!=n&&rr%B) { rid--; res=min(res,mn2[rr]); }
if (lid<=rid) res=min(res,st.query(lid,rid));
return res;
}
char *p1, *p2, buf[50000];
#define nc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 50000, stdin), p1 == p2) ? EOF : *p1++)
inline int read()
{
int x=0;char c=0;
while(!isdigit(c)) c=nc();
while(isdigit(c)) x=(x<<3)+(x<<1)+(c^48),c=nc();
return x;
}
int main()
{
n=read(),m=read();
for (rg int i=1;i<=n;i++) { a[i].x=read(); a[i].pos=i; mx=max(mx,a[i].x); }
for (rg int i=1;i<=m;i++) { q[i].l=read(); q[i].r=read(); q[i].k=read(); q[i].id=i; }
sort(q+1,q+1+m,cmp);
for (rg int i=1;i<=n;i++) pos[i]=(i+B-1)/B;
nn=pos[n]; st.init(); int f=n+1;
for (rg int i=1,pre=0;i<=m;i++)
{
if (q[i].r-q[i].l+1<=B) ans[q[i].id]=fc(q[i].l,q[i].r,q[i].k);
else if (q[i].k<=B)
{
if (q[i].k!=pre) Tr.init(q[i].k);
pre=q[i].k; ans[q[i].id]=Tr.query(q[i].l,q[i].r,q[i].k);
}
else { f=i; break; }
}
Tr.init(inf);
for (rg int i=f;i<=n;i++)
{
if (q[i].r-q[i].l+1<=B) ans[q[i].id]=fc(q[i].l,q[i].r,q[i].k);
else { ans[q[i].id]=Tr.query(q[i].l,q[i].r,inf); q2[q[i].k].push_back({q[i].l,q[i].r,q[i].id}); }
}
for (int i=1;i<=n;i++) mn1[i]=mn2[i]=inf;
for (int i=1;i<=nn;i++) mn[i]=inf;
sort(a+1,a+1+n,cmp2);
for (rg int i=mx,_size,id,r,p=n,j,kk;i>B;i--)
{
while (a[p].x==i&&p>=1)
{
id=pos[a[p].pos];
if (mn[id]!=i) st.update(id,i);
for (rg int k=(id-1)*B+1;k<=a[p].pos;k++) mn1[k]=i;
int r=min(n,id*B);
for (rg int k=a[p].pos;k<=r;k++) mn2[k]=i;
mn[id]=i; p--;
}
for (j=1;j<=i;j++)
{
if (i<=B*j) break;
if (i%j) continue;
int k=i/j; _size=q2[k].size();
for (kk=0;kk<_size;kk++) ans[q2[k][kk].id]=min(ans[q2[k][kk].id],solve(q2[k][kk].l,q2[k][kk].r)-i);
}
}
for (int i=1;i<=m;i++) cout<<ans[i]<<"\n";
return 0;
}
P6782 [Ynoi2008] rplexq
由乃 OI 果然毒瘤。
对于一次询问 \(l,r,x\),设 \(cnt_v\) 表示 \(v\) 子树中编号合法的节点个数,那么此次询问答案就是 \(cnt_u\times\sum_{v\in son_u}{cnt_v}-\sum{{cnt_v}^2}\) 除以二。因为 \(cnt_u\) 与 \(\sum{cnt_v}\) 最多相差一,所以可以不额外记录 \(cnt_u\) 直接在最后特判,答案形式就变成了和的平方减去平方的和。
子节点数量较少时可以直接枚举 \(v\) 计算答案,所以考虑根号分治
-
若 \(deg_u\leqslant\sqrt{n}\),暴力枚举子节点计算 \(cnt_v\)。容易发现这是一个二维数点问题,但因为询问次数是 \(O(m\sqrt{n})\) 级别的,所以要求 \(O(1)\) 查询,用分块前缀和实现即可,时间复杂度 \(O(n\sqrt{n}+m\sqrt{n})\)。同时,不能把 \(O(m\sqrt{n})\) 个询问都存下来,考虑 \(v\in son_u\) 的查询区间 \([l,r]\) 是相同的,所以可以将询问存在其父节点 \(u\) 上,空间复杂度为 \(O(m)\)
-
若 \(deg_u>\sqrt{n}\),考虑用莫队统计 \(\sum cnt_v,\sum{{cnt_v}^2}\)。给所有 \(v\) 的子树染色并给节点编号离散化后查询就是 P2709 小B的询问 了。但这样做的节点数量可以高达 \(O(n)\) 个,每次都全部跑莫队复杂度会变得极劣;考虑将最大的 \(\sqrt{n}\) 个子树用第一种二维数点的方式计算,莫队只统计剩下的轻儿子。复杂度证明可以参考树剖的证明,从叶节点到根最多有一个轻儿子,即全树的轻边数量是 \(O(n)\) 的,莫队跑的总点数也是 \(O(n)\) 的,复杂度为 \(O(n\sqrt{n})\)
因为莫队是跑不满的,所以将阙值调小一些跑得更快。感觉这题不是很需要卡常哎(虽然我的程序还是跑得很慢吧)
代码
#include <bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define fst first
#define scd second
#define mkp make_pair
using namespace std;
const int N=2e5+5;
const int B=600;
int n,m,rt;
vector <int> tr[N];
struct node { int l,r,id,tmp; };
vector <node> q[N];
int son[N],siz[N],fa[N];
int f[N],tmp[N],pos[N],r[N];
int num[N],nn,ans1,ans2;
int sum[600],cnt[N],ans[N],tol[N];
pii a[N];
void dfs1(int x,int _fa)
{
siz[x]=1; fa[x]=_fa;
int _size=tr[x].size();
for (int i=0,v;i<_size;i++)
{
if ((v=tr[x][i])==_fa) continue;
dfs1(v,x); siz[x]+=siz[v];
}
}
void dfs0(int x,int ffa)
{
a[++nn]=mkp(x,ffa),num[nn]=x;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==fa[x]) continue;
dfs0(v,ffa);
}
}
bool cmp(int x,int y) { return siz[x]>siz[y]; }
bool cmp2(node x,node y) { return (pos[x.l]==pos[y.l]?x.r<y.r:x.l<y.l); }
void add(int x,int op)
{
ans1+=1+cnt[x]*2*op;
cnt[x]+=op; ans2+=op;
}
node qq[N];
void solve(int x)
{
if (!q[x].size()) return ;
int cnt=0,_size=tr[x].size();
for (int i=0,v;i<_size;i++) if ((v=tr[x][i])!=fa[x]) tmp[++cnt]=v;
sort(tmp+1,tmp+1+cnt,cmp);
nn=0; _size=q[x].size();
for (int i=51;i<=cnt;i++) { dfs0(tmp[i],tmp[i]); f[tmp[i]]=1; }
sort(a+1,a+1+nn); sort(num+1,num+1+nn);
for (int i=0;i<_size;i++)
{
int ll=lower_bound(num+1,num+1+nn,q[x][i].l)-num,rr=upper_bound(num+1,num+1+nn,q[x][i].r)-num-1;
qq[i+1]={ll,rr,q[x][i].id};
}
int l=1,r=0,bl=nn/sqrt(_size)+1;
for (int i=1;i<=nn;i++) pos[i]=(i+bl-1)/bl;
sort(qq+1,qq+1+_size,cmp2); ans1=ans2=0;
for (int i=1;i<=_size;i++)
{
if (qq[i].l>nn||qq[i].r<1) continue;
while (l<qq[i].l) add(a[l++].scd,-1);
while (l>qq[i].l) add(a[--l].scd,1);
while (r<qq[i].r) add(a[++r].scd,1);
while (r>qq[i].r) add(a[r--].scd,-1);
ans[qq[i].id]-=ans1,tol[qq[i].id]+=ans2;
}
}
int qry(int x) { return (x?(r[x]==x?sum[pos[x]]:sum[pos[x]-1]+cnt[x]):0); }
void upd(int x)
{
for (int i=pos[x];i<=pos[n];i++) sum[i]++;
for (int i=x;i<=r[x];i++) cnt[i]++;
}
void calc(int x)
{
int _size;
if (!f[x]&&(_size=q[fa[x]].size())>0) for (int i=0;i<_size;i++) q[fa[x]][i].tmp=qry(q[fa[x]][i].r)-qry(q[fa[x]][i].l-1);
upd(x); _size=tr[x].size();
for (int i=0,v;i<_size;i++) if ((v=tr[x][i])!=fa[x]) calc(v);
if (!f[x]&&(_size=q[fa[x]].size())>0)
for (int i=0;i<_size;i++)
{
int ea=qry(q[fa[x]][i].r)-qry(q[fa[x]][i].l-1)-q[fa[x]][i].tmp;
ans[q[fa[x]][i].id]-=ea*ea,tol[q[fa[x]][i].id]+=ea;
}
_size=q[x].size();
for (int i=0;i<_size;i++) if (q[x][i].l<=x&&x<=q[x][i].r) ans[q[x][i].id]+=tol[q[x][i].id]<<1;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>rt;
for (int i=1,u,v;i<n;i++)
{
cin>>u>>v;
son[u]++,son[v]++;
tr[u].push_back(v);
tr[v].push_back(u);
}
for (int i=1,l,r,x;i<=m;i++)
{
cin>>l>>r>>x;
q[x].push_back({l,r,i,0});
}
dfs1(rt,0);
for (int i=1;i<=n;i++)
{
son[i]=(i==rt?son[i]:son[i]-1);
if (son[i]>50) solve(i);
}
for (int i=1;i<=n;i++) pos[i]=(i+B-1)/B,r[i]=min(n,pos[i]*B);
memset(cnt,0,sizeof cnt); calc(rt);
for (int i=1;i<=m;i++) cout<<((tol[i]*tol[i]+ans[i])>>1)<<"\n";
return 0;
}