莫队从入门到精通
食用须知
- 这篇文章作者断断续续写了几个月,可能读起来不连续或者风格术语变化,但是不影响大体
- 部分内容引自 \(OI-wiki\),还有些内容参考了大佬博客,但是已经找不全了,在此一并感谢
- 本文章最初的设计意图是校内讲课用,因此有部分
活泼的话语实际上是给同学写的 - 有错误、不足欢迎指出,可以私信作者或者评论,有时间一定修
- 可能更好的阅读体验,
不要脸的求一波关注和点赞 - 这篇文章越往后的内容可能越精炼,可以当快速复习整理食用,也可作为新手刷题
引子
副将不屑的瞥了他一眼,一抹黑色攀上手中的弯刀,恐怖的毁灭之力倾泻而出!
“对了,最近,我给我这禁墟起了个名字,你要不要听听看?”副将手握黑色弯刀,转头问道。
“你一个莽夫,能起什么好名字。”儒生扬起下巴,“说说看,我可以给你润色一下。”
副将缓缓提起手中的黑色弯刀,那双眼眸中,迸发出一道璀璨的寒芒,他嘴角微微上扬,一字一顿的开口:
“黑,月,斩!!”
颂——!!
一道数十米长的黑色月牙猛地掠过夜空,瞬间将三只呼啸而来的“神秘”身体斩成漫天碎块,黑芒吞噬他们的残躯,化作一阵血雨,飘零在灯火通明的酒楼上空。
噗嗤!
儒生忍不住笑了出来,“黑月斩?这么难听的名字,也只有你詹玉武能想出来了!
”副将咬着牙,恶狠狠的开口,“那你起一个?”
儒生思索片刻,朗声说道:“刀似月牙,泯灭众生……不如,就叫它【泯生闪月】吧!”
基本预处理
首先我们要明确莫队是离线算法,不能做一些强制在线的题,普通莫队也并不支持修改
处理块,块长取值为 len
for(int i=1;i<=n;i++) pos[i]=(i-1)/len+1;;
我们以左端点所在块为第一关键字,右端点为第二关键字排序,可以证明,这样下去,每次操作均摊花费是 len
所以我们块长取$\frac{n}{\sqrt{m} } $ 就好了
然后就是询问间的转移了,基本上是指针的转移了,注意我们转移区间要先扩再缩,不然的话面对一些求解组合数的问题会出神秘事件
while(q[i].l<l) l--,add(l);
while(q[i].r>r) r++,add(r);
while(q[i].l>l) del(l),l++;
while(q[i].r<r) del(r),r--;
ans[q[i].sa]=sum;
莫队的小优化——奇偶化排序
据说适用于除了回滚以外的所有莫队
什么是奇偶化排序?奇偶化排序即对于属于奇数块的询问,r 按从小到大排序,对于属于偶数块的排序,r 从大到小排序,这样我们的 r 指针在处理完这个奇数块的问题后,将在返回的途中处理偶数块的问题,再向 n 移动处理下一个奇数块的问题,优化了 r 指针的移动次数,一般情况下,这种优化能让程序快 30% 左右。(粘过来的,不知道真的假的)
bool cmp(node a,node b)
{
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
普通莫队例题:
P3709 大爷的字符串题
首先题意看似很神秘,实际上我们稍加思考就可以发现他与区间众数有关
一般的数据结构很难维护众数,原因就是无法高效的处理增量,但莫队天生克制逐步增量的东西
考虑维护每个数出现个数(cnt),和出现次数为i的数的个数num,这样的话就可以通过判断num维护众数值的加减
几个经验:P1997
int n,m,len,a[N],pos[N],b[N],sum,ans[N];
struct node {
int l,r,id;
}q[N];
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r>b.r;
else return a.r<b.r;
}
int cnt[N],num[N];//每个数字出现个数,次数为这个
void add(int p) {
num[cnt[a[p]]]--; cnt[a[p]]++; num[cnt[a[p]]]++;
if(cnt[a[p]]>z) z++;
}
void del(int p) {
if(!--num[cnt[a[p]]]&&cnt[a[p]]==z) z--;
num[--cnt[a[p]]]++;
}
signed main() {
cin>>n>>m;len=n/sqrt(m*1.0);
for(int i=1;i<=n;i++) b[i]=a[i]=read(),pos[i]=(i-1)/len+1;
sort(b+1,b+1+n);
int le=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+le,a[i])-b;
for(int i=1;i<=m;i++) {
q[i].l=read(),q[i].r=read(),q[i].id=i;
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++) {
while(q[i].l<l) l--,add(l);
while(q[i].r>r) r++,add(r);
while(q[i].l>l) del(l),l++;
while(q[i].r<r) del(r),r--;
ans[q[i].id]=sum;
}
return 0;
}
CF617E XOR and Favorite Number
这题也算个计数题,不知道大家还记不记得普及组时学习的技巧,区间异或和显然等于前缀的异或
P3674 小清新人渣的本愿
来补一下之前自己没填的坑,我们来思索一下,发现a_i值不大,这启发我们把每个数存起来进行考虑,普通bool肯定是不可以的,但是我们用bitset就可以在 \(O(c/w)\) 的时间里处理一次询问,时间3s,那很好了
我们维护一个正bitset b,一个反bitset c \((x->N-x)\)
- 对于减操作,判断 Q=b&(b>>q_i.x)
是否有值即可 - 对于加操作,有一点区别,还记得我们存的反bitset吗
Q=b&(c>>N-q[i].x);
u=(N-v)-(N-x)
u=x-v
u+v=x
- 对于乘积,枚举因数即可
#include<bits/stdc++.h>
//#define int long long
using namespace std;
const int N=2e5+50,P=1e9+7;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
int n,m,opt,l,r,a[N<<1],siz[N<<1],pos[N<<1];
struct node {
int l,r,op,x,id;
}q[N<<1];
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
bitset<N+50> b,c,A,Q;
void add(int p) {
int x=a[p];
if(!siz[x]) b[x]=1,c[N-x]=1;
siz[x]++;
}
void del(int p) {
int x=a[p];
if(siz[x]==1) b[x]=0,c[N-x]=0;
siz[x]--;
}
signed main() {
cin>>n>>m; int len=475;
for(int i=1;i<=n;i++) pos[i]=(i-1)/len+1,a[i]=read();
for(int i=1,x;i<=m;i++) {
opt=read(),l=read(),r=read(),x=read();
q[i]={l,r,opt,x,i};
}
sort(q+1,q+1+m,cmp);
l=1,r=0;
for(int i=1;i<=m;i++) {
while(l>q[i].l) l--,add(l);
while(r<q[i].r) r++,add(r);
while(r>q[i].r) del(r),r--;
while(l<q[i].l) del(l),l++;
if(q[i].op==1) {
Q=b&(b>>q[i].x);
if(Q.count()) A[q[i].id]=1;
}
else if(q[i].op==2) {
Q=b&(c>>N-q[i].x);
if(Q.count()) A[q[i].id]=1;
}
else {
for(int j=1;j*j<=q[i].x;j++) {
if(q[i].x%j==0&&b[j]&&b[q[i].x/j]) A[q[i].id]=1;
}
// if(sqrt(q[i].x)*sqrt(q[i].x)==q[i].x&&siz[(int)sqrt(q[i].x)]>=2)
// A[q[i].id]=1;
}
}
for(int i=1;i<=m;i++)
if(A[i]) cout<<"hana"<<endl;
else cout<<"bi"<<endl;
return 0;
}
带修莫队
我是想把带修莫队讲的通俗易懂一些,然而ta似乎也没有什么很难的点
首先普通莫队不能修改,因为一旦修改有的询问会更改,而有的不会。
所以对于一些简单的修改,我们可以把修改也离线下来,加一个时间维。
注意到这时莫队的上限仍然很低,无法高效地处理修改
我们这时就需要重新分析块长,一般把块长设为
\(n^{\frac 2 3}\),时间复杂度变为\(O(n^{\frac 5 3})\),注意这时必须以左端点块为第一关键字,右端点块为第二关键字,时间维我们可以奇偶化乱排
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.r]^pos[b.r]) return a.r<b.r;
else if(pos[a.r]&1) return a.t>b.t;
else return a.t<b.t;
}
for(int i=1,x,y;i<=m;i++) {
cin>>opt;x=read(),y=read();
if(opt=='R') u[++t]={x,y};
else ++top,q[top]={x,y,t,top};
}
for(int i=1;i<=top;i++) {
while(q[i].l<l) l--,add(a[l]);
while(q[i].r>r) r++,add(a[r]);
while(q[i].l>l) del(a[l]),l++;
while(q[i].r<r) del(a[r]),r--;
while(q[i].t>t) t++,upd(t,l,r);
while(q[i].t<t) upd(t,l,r),t--;
ans[q[i].id]=sum;
}
不过对于一些复杂的修改,带修莫队就无能为力了
P1903 [国家集训队] 数颜色 / 维护队列
板子题,注意先将指针移到对应区间,再修改时间,修改时间时有一个很妙的操作
void add(int p) { if(!cnt[p]) sum++;cnt[p]++; }
void del(int p) { if(cnt[p]==1) sum--;cnt[p]--; }
void upd(int t,int l,int r) {
if(l<=u[t].p&&u[t].p<=r) del(a[u[t].p]),add(u[t].x);
swap(a[u[t].p],u[t].x);//下次修改肯定就改回来了,直接交换
}
CF940F Machine Learning
思路放开了就会很容易,观察mex的性质,发现答案不会超过根号级别,每次暴力找就行
CF1476G Minimum Difference
首先还是上面那个性质,我们发现cnt的值只有根号级别,现在的问题就在于如何处理答案
,有一个比较显然的思路就是维护一个 \(muiltset\)
,添上一个 \(log\) 复杂度,然后双指针求解
我们考虑能不能省掉这个log,发现链式结构就可以解决这个问题
void add(int x) {
int h=la[siz[x]],t=nx[siz[x]];
if(siz[x]&&num[siz[x]]==1) erase(siz[x]);
num[siz[x]]--;
siz[x]++;num[siz[x]]++;
if(num[siz[x]]==1) {
if(num[siz[x]-1]&&siz[x]-1>0) {
h=siz[x]-1;
}
nx[h]=siz[x],la[siz[x]]=h;
if(t)la[t]=siz[x];nx[siz[x]]=t;
// insert(siz[x]);
}
}
void del(int x) {
int h=la[siz[x]],t=nx[siz[x]];
if(siz[x]&&num[siz[x]]==1) erase(siz[x]);
num[siz[x]]--;
siz[x]--,num[siz[x]]++;
if(siz[x]&&num[siz[x]]==1) {
if(num[siz[x]+1]) t=siz[x]+1;
nx[h]=siz[x],la[siz[x]]=h;
if(t)la[t]=siz[x];nx[siz[x]]=t;
// insert(siz[x]);
}
}
int h=nx[0],j=0;
while(h) b[++j]=h,h=nx[h];
// Copy();
int p1=1,p2=0,ans=1e9,sum=0;
for(;p1<=j;p1++) {
while(p2<j&&sum<q[i].k) p2++,sum+=num[b[p2]];
if(sum>=q[i].k) ans=min(ans,b[p2]-b[p1]);
sum-=num[b[p1]];
}
A[q[i].id]=ans;
回滚莫队
回滚莫队是一种神奇的莫队,正常的莫队是通过增加和删除操作完成区间的转移,但一些情况下,我们可能很难进行这其中的一个操作,即只能轻松进行增加或者删除操作的一种
回滚莫队的核心思想就是:既然只能实现一个操作,那么就只使用一个操作,剩下的交给回滚解决。
大致流程为:
-
按左端点所在块为第一关键字,若相同,按右端点大小排序(若增加即为从小到大,删除即为从大到小)
-
若左端点到了一个新块,重新初始化
-
左右端点在一个块内,暴力
-
拓展右端点,这部分答案为ans
-
拓展左端点,这部分答案为tmp
-
回滚左端点
-
复杂度同普通莫队,同时注意到这时就不能用奇偶化排序了
JOISC 2014 Day1 历史研究
容易发现删除操作很难,但是增加很容易
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else return a.r<b.r;
}
int ans,tmp;
void add(int x,int &y) {
++cnt[x];
y=max(y,cnt[x]*b[x]);
}
void del(int x) { --cnt[x]; }
void init() {
len=n/sqrt(n);
tot=n/len;
for(int i=1;i<=tot;i++) L[i]=(i-1)*len+1,R[i]=i*len;
R[tot]=n;
for(int i=1;i<=tot;i++)
for(int j=L[i];j<=R[i];j++)
pos[j]=i;
}
for(int i=1;i<=m;i++) {
if(pos[q[i].l]!=pos[q[i-1].l]) {
while(r>R[pos[q[i].l]]) del(a[r]),--r;
while(r<R[pos[q[i].l]]) ++r,add(a[r],ans);
while(l<R[pos[q[i].l]]+1) del(a[l]),++l;
ans=0;
}
// cout<<q[i].l<<" "<<q[i].r<<" "<<l<<" "<<r<<" "<<ans<<endl;
// for(int j=1;j<=3;j++) cout<<cnt[j]<<" ";cout<<endl;
if(pos[q[i].l]==pos[q[i].r]) {
for(int j=q[i].l;j<=q[i].r;j++) add(a[j],A[q[i].id]);
for(int j=q[i].l;j<=q[i].r;j++) del(a[j]);
continue;
}
while(r<q[i].r) ++r,add(a[r],ans);
tmp=ans;
while(l>q[i].l) --l,add(a[l],tmp);
// cout<<l<<" "<<r<<" "<<tmp<<endl;
A[q[i].id]=tmp;
while(l<R[pos[q[i].l]]+1) del(a[l]),++l;
}
P5906 【模板】回滚莫队&不删除莫队
自己做去
P4137 Rmq Problem / mex
容易发现mex删除很容易但是增加很困难
这种是只支持删除的回滚,我自己理解了一种写法,仅供大家参考
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else return a.r>b.r;
}
for(int i=1;i<=m;i++) {
if(pos[q[i].l]^pos[q[i-1].l]) {
B=pos[q[i].l];
l=L[B],r=q[i].r;
for(int j=0;j<=n;j++) t[j]=0;
for(int j=L[B];j<=q[i].r;j++)
if(a[j]<=n) t[a[j]]++;
for(int j=0;j<=n;j++) if(!t[j]) { tmp=ans=j;break; }
}
if(pos[q[i].l]==pos[q[i].r]) {
for(int j=q[i].l;j<=q[i].r;j++)
if(a[j]<=n) w[a[j]]++;
for(int j=0;j<=n;j++) if(!w[j]) { A[q[i].id]=j;break; }
for(int j=q[i].l;j<=q[i].r;j++)
if(a[j]<=n) w[a[j]]--;
continue;
}
while(r>q[i].r) add(r,1),r--;
tmp=ans;
while(l<q[i].l) add(l,2),l++;
A[q[i].id]=tmp;
while(l>L[B]) l--,add(l,3);
}
树上莫队
现在主流的树上莫队基本上都是将欧拉序跑下来然后在序列上跑正常莫队
欧拉序是一个什么东东呢?
void dfs(int p,int f) {
st[p]=++cnt,o[cnt]=p;
for(int i:v[p]) {
if(i==f) continue;
dfs(i,p);
}
ed[p]=++cnt,o[cnt]=p;
}
注意要记录每个点两次被遍历的位置
那这样有什么用捏?
假如题目要求我们处理一条路径(从 x 到 y )
(有些题目要求处理子树,那么跑dfs序即可,因为子树中的dfs序是连续的 )
这里我们规定 \(st[x]<st[y]\),先说结论
-
\(LCA(x,y)==x\),此时y在x子树中,那么我们就把询问压成 \(st[x]\) 到 $st[y] $
-
否则,将询问压成 \(ed[x]\) 到 \(st[y]\),并对 \(LCA(x,y)\) 特殊处理
证明:
-
首先两种情况都会有出现两次的点,我们发现对于路径而言,这些点毫无价值,我们去除这些贡献
-
对于第一种情况,显然路径上的点只出现一次
-
对于第二种情况,我们显然发现LCA的贡献没被考虑到,单独处理即可
证毕。
SP10707 COT2 - Count on a tree II
树上莫队板子题,但是大家可能会疑惑上面所讲的没有贡献的点如何处理,毕竟我们现在压成了一个序列,总不能记录次数吧(其实也可以起),我们可以用一个vis数组来存储目前这个位置的状态(有贡献 or 无贡献),当我们再次遍历到这个位置的时候就将状态取反
//o 代表的是欧拉序
void Solve(int x) {
vis[x]?del(c[x]):add(c[x]);vis[x]=vis[x]^1;
}
for(int i=1;i<=m;i++) {
while(r<q[i].r) r++,Solve(o[r]);
while(l>q[i].l) l--,Solve(o[l]);
while(r>q[i].r) Solve(o[r]),r--;
while(l<q[i].l) Solve(o[l]),l++;
if(q[i].lca) Solve(q[i].lca);
A[q[i].id]=ans;
if(q[i].lca) Solve(q[i].lca);
}
P4074 [WC2013] 糖果公园
这个题目加上了修改操作,改为树上带修莫队即可,值得一提的是笔者写这个题的时候排序没有用带修莫队的排序,而是用的普通莫队的排序,然后还过了,导致笔者丝毫没有意识到这件事然后写另一个题时卡了半天常,最后发现排序锅了,这启发我们写莫队题要清醒(选择大于努力)
//^ v ^ 开心做题 快乐学习
//好戏,开场!
#include<bits/stdc++.h>
// #define int long long
using namespace std;
const int N=2e6+500,P=1e9+21;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
int n,m,T,V[N],w[N],c[N],pos[N];long long A[N],tmp;
vector<int > v[N];
int tim,tot;
struct Fix { int p,x; }u[N];
struct node {
int l,r,t,lca,id;
}q[N];
int op,x,y;
bool cmp(node a, node b){
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.r]^pos[b.r]) return a.r<b.r;
else if(pos[a.r]&1) return a.t>b.t;
else return a.t<b.t;
}
int id[N],st[N],ed[N];
int siz[N],dep[N],top[N],fa[N],son[N];
void dfs1(int p,int f) {
id[st[p]=++id[0]]=p;
dep[p]=dep[fa[p]=f]+1,siz[p]++;
for(int i:v[p]) {
if(i^f) {
dfs1(i,p);
siz[p]+=siz[i];
if(siz[i]>siz[son[p]]) son[p]=i;
}
}
id[ed[p]=++id[0]]=p;
}
void dfs2(int p,int t) {
top[p]=t;
if(son[p]) dfs2(son[p],t);
for(int i:v[p]) {
if(i==fa[p]||i==son[p]) continue;
dfs2(i,i);
}
}
int Lca(int x,int y) {
while(top[x]^top[y]) {
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}
bitset<N > vis;
int cnt[N];
void add(int x) { cnt[x]++,tmp+=1ll*w[cnt[x]]*V[x]; }
void del(int x) { tmp-=1ll*w[cnt[x]]*V[x],cnt[x]--; }
void solve(int x) {
vis[x]?del(c[x]):add(c[x]);vis[x]=vis[x]^1;
}
void upd(int t) {
int p=u[t].p;
if(vis[p]) {
del(c[p]),add(u[t].x);
}
swap(u[t].x,c[p]);
}
signed main() {
cin>>n>>m>>T;
for(int i=1;i<=m;i++) V[i]=read();
for(int i=1;i<=n;i++) w[i]=read();
for(int i=1;i<n;i++) {
int x=read(),y=read();
v[x].push_back(y),v[y].push_back(x);
}
dfs1(1,0),dfs2(1,1);
int len=pow(id[0],0.666666666666);
for(int i=1;i<=id[0];i++)
pos[i]=(i-1)/len+1;
for(int i=1;i<=n;i++) c[i]=read();
for(int i=1;i<=T;i++) {
op=read(),x=read(),y=read();
if(!op) ++tim,u[tim]={x,y};
else {
if(st[x]>st[y]) swap(x,y);
int lca=Lca(x,y);
if(lca==x) ++tot,q[tot]={st[x],st[y],tim,0,tot};
else ++tot,q[tot]={ed[x],st[y],tim,lca,tot};
}
}
sort(q+1,q+1+tot,cmp);
int l=1,r=0,t=0;
for(int i=1;i<=tot;i++) {
while(r<q[i].r) r++,solve(id[r]);
while(l>q[i].l) l--,solve(id[l]);
while(l<q[i].l) solve(id[l]),l++;
while(r>q[i].r) solve(id[r]),r--;
while(t>q[i].t) upd(t),t--;
while(t<q[i].t) t++,upd(t);
int lca=q[i].lca;
if(lca) add(c[lca]);
A[q[i].id]=tmp;
if(lca) del(c[lca]);
}
for(int i=1;i<=tot;i++) printf("%lld\n",A[i]);
return 0;
}
相同类型,基本算是经验:SP32952 Ada and Football(就是笔者卡了半天常的那个)
CF375D Tree and Queries
这个题就是子树内问题了,我们直接dfs序即可
题目要求 $ \ge k $ 的数的个数,我们莫队统计每个数的出现次数是简单的,但是这个怎么办的
还记得笔者前文提到过吗:莫队天生克制逐步增量的东西,你考虑实际上每个数出现次数加减是一个增量过程,我们设 \(num_i\) 为出现次数大于i的数的个数,在增量过程中维护即可
int cnt[N],num[N];
void add(int x) { cnt[x]++,++num[cnt[x]]; }
void del(int x) { --num[cnt[x]],cnt[x]--; }
实际上因为莫队的逐步增量特性,莫队的可拓展性很高
在考场中,即使我们不会正解,也可以用莫队来写一些暴力,重点就在于思考增量怎么写,增量代价是多少
笔者曾在模拟赛中利用莫队拿到了全场最高的暴力分(可能是大家都会正解没人写暴力) ,其中增量代价也并不是 \(O(1)\),而是 \(O(log n)\) ,读者在考场上也可以先思考普通暴力,然后观察能不能转为莫队
莫队二次离线
因为笔者时间原因(和实力原因),二离板子(第十四分块前提)和第十四分块并没有写,只带来了两道比较简单的题目,而且目前读者对二离的理解还不够深刻,有些地方可能讲解不清,还请读者见谅
有些时候,我们会遇到一些看起来很适合莫队的题目,但是转移复杂度不是 \(O(1)\),导致复杂度不对
并且这时候我们惊奇的发现增量的贡献可以差分,我们用 \(f(x,l,r)\) 表示x关于 \([l,r]\) 的贡献
即 \(f(x,l,r)=f(x,1,r)-f(x,1,l-1)\)
这时候我们考虑将增量操作都离线下来,最后再一起处理
我们观察到当区间 \([l,r]\) 扩展到 \([l,r+1]\) 时
\(f(r+1,l,r)=f(r+1,1,r)-f(r+1,1,l-1)\)
前一项显然是可以预处理的,后一项发现l-1是固定的,可以把每个这样的增量都离线到对应的 \([l - 1]\) 上,最后从小到大处理即可。其他几个方向也是类似的
这样,把增量操作离线下来,就叫二次离线啦
P5501 [LnOI2019] 来者不拒,去者不追
设 \(f(x,l,r)\) 为 \([l,r]\) 中比a_x大的数的和,\(g(x,l,r)\) 为 \([l,r]\) 中比 \(a_x\) 小的个数
增量 \(F(r+1,l,r)=f(x,l,r)+a_{r+1}*g(x,l,r)\)
即 \(F(r+1,l,r)=f(x,1,r)-f(x,1,l-1)+a_{r+1}*[ g(r+1,1,r)-g(r+1,1,l-1)]\)
其中 \(f(r+1,1,r)\) 和 \(g(r+1,1,r)\) 可以预处理,剩下的我们离线
我们这里有一个优化空间技巧:我们发现每次指针移动都是连续的,所以我们只需要存连续的一段即可,空间复杂度从 \(O(n \sqrt n)\) 降到了 \(O(n)\)
然后我们发现操作变为了:在集合中加入一个数,查询一个数在集合中排名,查询集合中比某个数大的数之和
其中加入操作只有 \(O(n)\) 次,而查询操作却有 \(O(n \sqrt n)\) 次,
使用 \(O(\sqrt n)\) 修改, \(O(1)\) 查询的值域分块即可,算上莫队,总时间复杂度\(O(n \sqrt n)\)
其他几个方向也是类似的
//^ v ^ 开心做题 快乐学习
//好戏,开场!
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+500,P=1e9+21;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
int n,m,lsh[N],a[N],pos[N],lim,len,qz[N];
int f[N],g[N],S[N],A[N];
struct BIT {
int t[N];
void add(int x,int k) { for(;x<=lim;x+=x&-x) t[x]+=k; }
int query(int x) { int sum=0; for(;x;x-=x&-x) sum+=t[x]; return sum; }
}F,G;
struct Query {
int l,r,id,op;
}q[N];
bool cmp(Query a,Query b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
void LSH() {
len=sqrt(n);
for(int i=1;i<=n;i++) a[i]=read(),lim=max(lim,a[i]),pos[i]=(i-1)/len+1,qz[i]=qz[i-1]+a[i];
for(int i=1;i<=n;i++) {
F.add(a[i],a[i]); f[i]=f[i-1]+F.query(lim)-F.query(a[i]);
G.add(a[i],1); g[i]=g[i-1]+a[i]*(G.query(a[i]-1));
// cout<<F.query(lim)-F.query(a[i])<<" "<<a[i]*G.query(a[i]-1)<<endl;
}
}
vector<Query > v[N];
int L[N],R[N],qf[N],qg[N],tf[N],tg[N];
int nf(int x) { return qf[x]+tf[pos[x]]; }
int ng(int x) { return qg[x]+tg[pos[x]]; }
void Block() {
len=sqrt(lim);
// cout<<lim<<" "<<len;
int bcnt=lim/len;if(bcnt*len<lim) bcnt++;
for(int i=1;i<=bcnt;i++) {
L[i]=(i-1)*len+1,R[i]=i*len;
for(int j=L[i];j<=R[i];j++) pos[j]=i;
}R[bcnt]=lim;
// cout<<pos[2]<<" ";
for(int i=1;i<=n;i++) {
for(int j=a[i]+1;j<=R[pos[a[i]]];j++) qg[j]++;
for(int j=pos[a[i]]+1;j<=bcnt;j++) tg[j]++;
for(int j=a[i]-1;j>=L[pos[a[i]]];j--) qf[j]+=a[i];
for(int j=pos[a[i]]-1;j;j--) tf[j]+=a[i];
// for(int j=1;j<=lim;j++) cout<<ng(j)<<" "<<nf(j)<<" ";
for(auto t:v[i]) {
for(int j=t.l;j<=t.r;j++) {
S[t.id]+=t.op*(nf(a[j])+a[j]*(ng(a[j])));
}
}
// cout<<endl;
}
}
signed main() {
cin>>n>>m;LSH();
for(int i=1;i<=m;i++) {
q[i]={read(),read(),i,0};
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++) {
// cout<<q[i].id<<" ";
if(r<q[i].r) {
S[i]+=f[q[i].r]-f[r];
S[i]+=g[q[i].r]-g[r];
v[l-1].push_back({r+1,q[i].r,i,-1});
r=q[i].r;
}
if(l>q[i].l) {
S[i]-=f[l-1]-f[q[i].l-1];
S[i]-=g[l-1]-g[q[i].l-1];
v[r].push_back({q[i].l,l-1,i,1});
l=q[i].l;
}
if(r>q[i].r) {
S[i]-=f[r]-f[q[i].r];
S[i]-=g[r]-g[q[i].r];
v[l-1].push_back({q[i].r+1,r,i,1});
r=q[i].r;
}
if(l<q[i].l) {
S[i]+=f[q[i].l-1]-f[l-1];
S[i]+=g[q[i].l-1]-g[l-1];
v[r].push_back({l,q[i].l-1,i,-1});
l=q[i].l;
}
}
// cout<<S[1]<<endl;
Block();
for(int i=1;i<=m;i++) S[i]+=S[i-1],A[q[i].id]=S[i]+qz[q[i].r]-qz[q[i].l-1];
for(int i=1;i<=m;i++) printf("%lld\n",A[i]);
return 0;
}
P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
题目要求逆序对数
我们设 \(f(x,l,r)\) 为区间 \([l,r]\) 中比 $ a_x$ 大的数,设 \(g(x,l,r)\) 为区间 \([l,r]\) 中比 \(a_x\) 小的数
然后4个转移方向随便推一下,发现这个依旧可以使用上面提到的值域分块,大家仿照着推一推,这道笔者就留作给大家的练习吧,贴一下代码
//^ v ^ 开心做题 快乐学习
//好戏,开场!
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+500,P=1e9+21;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
int n,m,lsh[N],a[N],pos[N],lim,len;
int f[N],g[N],S[N],A[N];
struct BIT {
int t[N];
void add(int x,int k) { for(;x<=lim;x+=x&-x) t[x]+=k; }
int query(int x) { int sum=0; for(;x;x-=x&-x) sum+=t[x]; return sum; }
}SZ;
struct Query {
int l,r,id,op;
}q[N];
bool cmp(Query a,Query b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
void LSH() {
int b[N];
len=sqrt(n);
for(int i=1;i<=n;i++) a[i]=b[i]=read(),pos[i]=(i-1)/len+1;
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;
lim=len;
for(int i=1;i<=n;i++) {
SZ.add(a[i],1);
f[i]=i-SZ.query(a[i])+f[i-1];
g[i]=SZ.query(a[i]-1)+g[i-1];
}
}
vector<Query > v[N];
int L[N],R[N],qz[N],tag[N];
int num(int x) { return qz[x]+tag[pos[x]]; }
void Block() {
len=sqrt(lim);
int bcnt=lim/len;bcnt++;
for(int i=1;i<=bcnt;i++) {
L[i]=(i-1)*len+1;R[i]=i*len;
for(int j=L[i];j<=R[i];j++) pos[j]=i;
} R[bcnt]=lim;
for(int i=1;i<=n;i++) {
int x=a[i];
for(int j=x;j<=R[pos[x]];j++) {
qz[j]++;
}
for(int j=pos[x]+1;j<=bcnt;j++) tag[j]++;
for(auto t:v[i]) {
if(t.op==1)
for(int j=t.l;j<=t.r;j++) S[t.id]+=num(lim)-num(a[j]);
else if(t.op==-1)
for(int j=t.l;j<=t.r;j++) S[t.id]-=num(lim)-num(a[j]);
else if(t.op==2)
for(int j=t.l;j<=t.r;j++) S[t.id]+=num(a[j]-1);
else if(t.op==-2)
for(int j=t.l;j<=t.r;j++) S[t.id]-=num(a[j]-1);
}
}
}
signed main() {
cin>>n>>m;LSH();
for(int i=1;i<=m;i++) {
q[i]={read(),read(),i,0};
}
sort(q+1,q+1+m,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++) {
if(r<q[i].r) {
S[i]+=(f[q[i].r]-f[r]);
v[l-1].push_back({r+1,q[i].r,i,-1});
r=q[i].r;
}
if(l>q[i].l) {
S[i]-=(g[l-1]-g[q[i].l-1]);
v[r].push_back({q[i].l,l-1,i,2});
l=q[i].l;
}
if(r>q[i].r) {
S[i]-=(f[r]-f[q[i].r]);
v[l-1].push_back({q[i].r+1,r,i,1});
r=q[i].r;
}
if(l<q[i].l) {
S[i]+=(g[q[i].l-1]-g[l-1]);
v[r].push_back({l,q[i].l-1,i,-2});
l=q[i].l;
}
}
Block();
for(int i=1;i<=m;i++) {
S[i]+=S[i-1];A[q[i].id]=S[i];
}
for(int i=1;i<=m;i++) printf("%lld\n",A[i]);
return 0;
}
补充一些好题
由于这个课件笔者断断续续写了好久,所以后来做了一些好题没来得及放,就当做补充啦
P5268 [SNOI2017] 一个简单的询问
原式是这个 \(\sum\limits_{x=0}^\infty get(l_1,r_1,x)*{get}(l_2,r_2,x)\)
显然可以化成 \((get(1,r_1,x)-get(1,l_1-1,x))*(get(1,r_2,x)-get(1,l_2-1,x))\)
为了方便我们简写为 \(g(r,x)\)
然后变为 \(\sum\limits_{x=1}^\infty g(r_1,x)g(r_2,x)-g(r_1,x)g(l_2-1,x)-g(l_1-1,x)g(r_2,x)+g(l_1-1,x)g(l_2-1,x)\)
将一个询问拆为4个跑即可,这个形式的莫队处理增量其实也不太一样
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+50,P=1e9+7;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
int n,m,l,r,a[N],b[N],A[N];
int len,tot,pos[N],cl[N],cr[N],ans;
struct node { int l,r,id,op; }q[N];
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
void add(int p,int op) {
int x=a[p];
if(op==1) {
ans+=cr[x];cl[x]++;
}
else ans+=cl[x],cr[x]++;
}
void del(int p,int op) {
int x=a[p];
if(op==1) {
ans-=cr[x];cl[x]--;
}
else ans-=cl[x],cr[x]--;
}
signed main() {
cin>>n;
len=sqrt(n);
for(int i=1;i<=n;i++) a[i]=read(),pos[i]=(i-1)/len+1;
pos[n+1]=n;
cin>>m;
for(int i=1;i<=m;i++) {
int l1=read(),r1=read(),l2=read(),r2=read();
l1--,l2--;
q[++tot]={min(r1,r2),max(r1,r2),i,1};
if(l1) q[++tot]={min(l1,r2),max(l1,r2),i,-1};
if(l2) q[++tot]={min(l2,r1),max(l2,r1),i,-1};
if(l1&&l2) q[++tot]={min(l1,l2),max(l1,l2),i,1};
}
sort(q+1,q+1+tot,cmp);
l=0,r=0;
for(int i=1;i<=tot;i++) {
while(r<q[i].r) r++,add(r,2);
while(l<q[i].l) l++,add(l,1);
while(l>q[i].l) del(l,1),l--;
while(r>q[i].r) del(r,2),r--;
A[q[i].id]+=ans*q[i].op;
}
for(int i=1;i<=m;i++) printf("%lld\n",A[i]);
return 0;
}
P4689 [Ynoi Easy Round 2016] 这是我自己的发明
可以先思考一下根始终为1的情况,发现这不就是上一个题目的换皮吗,就换成了dfn序而已
然后我们考虑换根
- \(rt=v\) ,此时v对应区间为整棵树
- rt不在v的子树里,此时发现v的子树不变,区间为 \([dfn_v,dfn_v+siz_v-1]\)
- rt在v的某个儿子x子树内,那么 v 的子树就是整棵树去掉x这个儿子 的子树
这时候可能会出现子树在我们跑出来的dfn序上不连续的情况,我们把dfn序复制一遍即可
听说还有狠人把询问拆成了16个或9个,感觉有点意思
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e6+50,P=1e9+7;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
long long A[N];
int n,m,l,r,a[N],b[N],opt,l1,r1,l2,r2;
int len,tot,pos[N],cl[N],cr[N],ans,o[N];
vector<int > v[N];
struct node { int l,r,id,op; }q[N];
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
void add(int p,int op) {
int x=a[p];
if(op==1) {
ans+=cr[x];cl[x]++;
}
else ans+=cl[x],cr[x]++;
}
void del(int p,int op) {
int x=a[p];
if(op==1) {
ans-=cr[x];cl[x]--;
}
else ans-=cl[x],cr[x]--;
}
void Add(int l1,int r1,int l2,int r2) {
++A[0];
l1--,l2--;
q[++tot]={min(r1,r2),max(r1,r2),A[0],1};
if(l1) q[++tot]={min(l1,r2),max(l1,r2),A[0],-1};
if(l2) q[++tot]={min(l2,r1),max(l2,r1),A[0],-1};
if(l1&&l2) q[++tot]={min(l1,l2),max(l1,l2),A[0],1};
}
void Solve() {
sort(q+1,q+1+tot,cmp);
l=0,r=0;
for(int i=1;i<=tot;i++) {
while(r<q[i].r) r++,add(o[r],2);
while(l<q[i].l) l++,add(o[l],1);
while(l>q[i].l) del(o[l],1),l--;
while(r>q[i].r) del(o[r],2),r--;
A[q[i].id]+=ans*q[i].op;
}
for(int i=1;i<=A[0];i++) printf("%lld\n",A[i]);
}
int rt,dfn[N],siz[N],fa[N],top[N],dep[N],son[N];
void dfs1(int p,int f) {
dep[p]=dep[fa[p]=f]+1;siz[p]++;
for(int i:v[p]) {
if(i==f) continue;
dfs1(i,p);
siz[p]+=siz[i];
if(siz[son[p]]<siz[i]) son[p]=i;
}
}
void dfs2(int p,int t) {
top[p]=t;
dfn[p]=++dfn[0],o[dfn[0]]=p;
if(son[p]) dfs2(son[p],t);
for(int i:v[p]) {
if(i==son[p]||i==fa[p]) continue;
dfs2(i,i);
}
}
int get(int x,int y) {
while(top[x]^top[y]) {
if(dep[x]<dep[y]) swap(x,y);
if(fa[top[x]]==y) break;
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
return fa[top[x]]==y?top[x]:son[y];
}
signed main() {
cin>>n>>m;
for(int i=1;i<=n;i++) b[i]=a[i]=read();
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;
len=sqrt(n);
for(int i=1;i<=n+n;i++) pos[i]=(i-1)/len+1;
for(int i=1;i<n;i++) {
l=read(),r=read();
v[l].push_back(r),v[r].push_back(l);
}
dfs1(1,0);
dfs2(1,1);
for(int i=1;i<=n;i++) o[i+n]=o[i];
rt=1;
for(int i=1;i<=m;i++) {
opt=read();
if(opt&1) rt=read();
else {
l=read(),r=read();
if(rt==l) l1=1,r1=n;
else if(dfn[rt]>=dfn[l]&&dfn[rt]<=dfn[l]+siz[l]-1) {
l=get(l,rt);
l1=dfn[l]+siz[l],r1=n+dfn[l]-1;
}
else l1=dfn[l],r1=dfn[l]+siz[l]-1;
if(rt==r) l2=1,r2=n;
else if(dfn[rt]>=dfn[r]&&dfn[rt]<=dfn[r]+siz[r]-1) {
r=get(r,rt);
l2=dfn[r]+siz[r],r2=n+dfn[r]-1;
}
else l2=dfn[r],r2=dfn[r]+siz[r]-1;
Add(l1,r1,l2,r2);
}
}
Solve();
return 0;
}
P4688 [Ynoi Easy Round 2016] 掉进兔子洞
题意即为三个集合的大小之和除去三个集合的交集大小的3倍,肯定不能暴力做吗,我们考虑用bitset,我们进行不去重的离散化(doge),不去重的话意味着一个值在bitset里可以有多个相邻下标,那我们就可以用bitset表示多个相同的数,再取交集就好了
然后lxl卡我们空间啊,分组做就好了
#include<bits/stdc++.h>
//#define int long long
using namespace std;
const int T_T=1e4,N=1e5+50,P=1e9+7;
int read() {
int x=0;short f=1;char s=getchar();
while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}
while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}
return x*f;
}
int n,m,l,r,a[N],b[N];
int len,tot,L[N],R[N],pos[N],cnt[N],A[N];
struct node {
int l,r,id;
}q[N];
bool cmp(node a,node b) {
if(pos[a.l]^pos[b.l]) return a.l<b.l;
else if(pos[a.l]&1) return a.r<b.r;
else return a.r>b.r;
}
bitset<N > B[10050],tmp;
void add(int p) {
int x=a[p];
cnt[x]++;
tmp[x+cnt[x]-1]=1;
}
void del(int p) {
int x=a[p];
cnt[x]--;
tmp[x+cnt[x]]=0;
}
void init() {
len=n/sqrt(m);tot=n/max(1,len);
for(int i=1;i<=tot;i++) L[i]=(i-1)*len+1,R[i]=i*len;
R[tot]=n;
for(int i=1;i<=tot;i++)
for(int j=L[i];j<=R[i];j++)
pos[j]=i;
}
signed main() {
cin>>n;cin>>m;
for(int i=1;i<=n;i++) b[i]=a[i]=read();
sort(b+1,b+1+n);init();
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+n,a[i])-b;
while(m>0) {
int siz=min(m,T_T);
tmp.reset();
for(int i=1;i<=n;i++) cnt[i]=0;
for(int i=1;i<=siz;i++) B[i].set(),A[i]=0;
for(int i=1;i<=siz;i++)
for(int j=1;j<=3;j++) {
l=read(),r=read();
A[i]+=(r-l)+1;
q[(i-1)*3+j]={l,r,i};
}
sort(q+1,q+1+3*siz,cmp);
l=1,r=0;
for(int i=1;i<=3*siz;i++) {
while(l>q[i].l) l--,add(l);
while(r<q[i].r) r++,add(r);
while(l<q[i].l) del(l),l++;
while(r>q[i].r) del(r),r--;
B[q[i].id]&=tmp;
}
for(int i=1;i<=siz;i++) printf("%d\n",A[i]-3*B[i].count());
m-=T_T;
}
return 0;
}
后记
以后可能会接着完善哦,比如补几个二离的板子或者加几个好题,可以关注我的博客:OvO