其实大部分是暑假就学过的了……
但是笔记是手写的这次做个笔记补档,加些题目,然后学个二次离线。
普通莫队
用途是喜闻乐见的区间查询。
其实思想还是分块,不过暴力得很巧妙罢了。
大致思路:
- 首先对整个序列分块,记录每个位置的所属块
- 将查询离线下来并排序
- 按块查询,使用左右指针完成统计
复杂度就是 (mathcal{O}(nsqrt n)) ,分块复杂度。
例题 SP3267 D-query
给定一个长度为 (n) 的序列,询问区间中不同数字个数。
双倍经验: 小B的询问 求区间出现次数平方和。
预处理:将查询离线,对此进行排序,按左端点所在块的编号升序,再按右端点升序。
然后就可以处理询问了。遍历询问序列,建立指针 (l,r) 表示当前所在区间, (cnt[i]) 记录当前区间每个数的出现次数,(num) 表示出现的不同数的总数。初始时 (l=1,r=0) .
每次对于一个新的询问,将左右指针 (l,r) 移动到和当前查询区间重合,即得到了当前询问的答案。
由于左端点在同一个块的时候,右端点最坏会遍历整个序列,总共有 (sqrt n) 个块,所以复杂度是 (mathcal{O}(nsqrt n))
看起来很暴力,但是是真的很优秀(
一个美妙的优化 :当左端点在同一奇数块的时候,右端点升序;如果在同一偶数块则降序。这样可以让右指针在奇数块询问跳完之后,回来的路上顺便跳完偶数块,实际优化还是挺大的。
//Author:RingweEH
//SP3267 DQUERY - D-query
const int N=3e5,Q=2e5+10,C=1e6+10;
int n,a[N],cnt[C],num,bel[N],siz,bnum,ans[Q];
struct Node
{
int l,r,id;
bool operator < ( Node tmp ) const
{ return (bel[l]^bel[tmp.l]) ? (bel[l]<bel[tmp.l]) : ((bel[l]&1) ? r<tmp.r : r>tmp.r); }
}Que[Q];
void Init()
{
siz=sqrt(n); bnum=ceil((double)n/siz);
for ( int i=1; i<=bnum; i++ )
for ( int j=(i-1)*siz+1; j<=i*siz; j++ )
bel[j]=i;
}
void Del( int x ) { cnt[a[x]]--; num-=(cnt[a[x]]==0); }
void Add( int x ) { cnt[a[x]]++; num+=(cnt[a[x]]==1); }
int main()
{
n=read(); Init();
for ( int i=1; i<=n; i++ ) a[i]=read();
int q=read();
for ( int i=1; i<=q; i++ )
Que[i].l=read(),Que[i].r=read(),Que[i].id=i;
sort( Que+1,Que+1+q );
int l=1,r=0;
for ( int i=1; i<=q; i++ )
{
int ql=Que[i].l,qr=Que[i].r;
while ( l<ql ) Del(l),l++;
while ( l>ql ) l--,Add(l);
while ( r<qr ) r++,Add(r);
while ( r>qr ) Del(r),r--;
ans[Que[i].id]=num;
}
for ( int i=1; i<=q; i++ )
printf( "%d
",ans[i] );
return 0;
}
习题
小Z的袜子
询问在区间 ([l,r]) 随机抽取两个数,相同的概率。(分数表示)
简单推一推式子,发现问题可以转化为平方和,然后再平方差公式一下就好了。注意约分。
void Modify( int x,int num )
{
tmp+=2*sum[c[x]]*num+1; sum[c[x]]+=num;
}
for ( int i=1; i<=q; i++ )
{
while ( l<Que[i].l ) Modify( l++,-1 );
while ( l>Que[i].l ) Modify( --l,1 );
while ( r>Que[i].r ) Modify( r--,-1 );
while ( r<Que[i].r) Modify( ++r,1 );
ll a=tmp-( Que[i].r-Que[i].l+1 );
ll b=(ll)( Que[i].r-Que[i].l+1 )*( Que[i].r-Que[i].l );
ll ggcd=gcd( a,b );
if ( ggcd==0 )
{
ansa[Que[i].id]=0; ansb[Que[i].id]=1; continue;
}
ansa[Que[i].id]=a/ggcd; ansb[Que[i].id]=b/ggcd;
}
CF617E XOR and Favorite Number
求区间中,序列异或和为 (k) 的序列个数。
由于实在是太没代码含量了就口胡一下。
显然可以异或前缀,转化为区间有多少数对异或和为 (k) 。然后发现就是对于左端点,统计有多少 (xoplus k) ,就是裸题了。
大爷的字符串题
给定字符串,询问区间贡献。
定义贡献:
- 每次从区间中取出一个严格上升的序列,最少取的次数
题意杀我。
由于是严格上升,所以只和相同的数个数有关,那么最少取的次数就是区间出现次数最大的数的出现次数。
于是就没了。
void Add( int pos )
{
t[cnt[x[pos]]]--; t[++cnt[x[pos]]]++; res=max( res,cnt[x[pos]] );
}
void Del( int pos )
{
t[cnt[x[pos]]]--;
if ( cnt[x[pos]]==res && !t[cnt[x[pos]]] ) res--;
t[--cnt[x[pos]]]++;
}
带修莫队
其实就是再加一维时间的指针在修改操作上面乱跳(
排序优先级:
- 左端点所在块标号
- 右端点
- 时间
修改:
- 其实不需要搞什么复杂的操作。把操作和原来的值
swap
即可,改回来同理。
分块:
- 听说大小设 (mathcal{O}(n^{frac{2}{3}})) 比较好。
例题 数颜色
区间数颜色,单点修改。
板子题。
尝试了一下毒瘤写法,觉得很离谱
//Author:RingweEH
for ( int i=1; i<=cntq; i++ )
{
int ql=Que[i].l,qr=Que[i].r,qt=Que[i].tim;
while ( l<ql ) res-=!--cnt[a[l++]];
while ( l>ql ) res+=!cnt[a[--l]]++;
while ( r<qr ) res+=!cnt[a[++r]]++;
while ( r>qr ) res-=!--cnt[a[r--]];
while ( tim<qt )
{
++tim;
if ( ql<=c[tim].pos && c[tim].pos<=qr )
res-=!--cnt[a[c[tim].pos]] - !cnt[c[tim].col]++;
swap( a[c[tim].pos],c[tim].col );
}
while ( tim>qt )
{
if ( ql<=c[tim].pos && c[tim].pos<=qr )
res-=!--cnt[a[c[tim].pos]] - !cnt[c[tim].col]++;
swap( a[c[tim].pos],c[tim].col );
--tim;
}
ans[Que[i].id]=res;
}
习题 CF940F Machine Learning
单点修改,区间查 (mex) .
值域很大,有 (1e9) ,数组是开不下了,离散化就好。
剩下的事情和区间数颜色没什么区别,直接做就好了。不会求 (mex) ?统计颜色个数 (cnt) ,再对此统计每个出现次数的出现次数 (tot) 什么东西 ,然后对于每个 (ans) ,直接暴力找就好了。
为什么能暴力求?考虑答案是 (x) ,对于 (x) 之前的数,注意是 出现次数 ,仔细想想就会发现不会超过 (sqrt n) ,和莫队复杂度是同级的。
void Add( int x ) { --tot[cnt[x]]; ++tot[++cnt[x]]; }
void Del( int x ) { --tot[cnt[x]]; ++tot[--cnt[x]]; }
void Modify( int t1,int t2 )
{
if ( c[t1].pos>=q[t2].l && c[t1].pos<=q[t2].r ) Del ( a[c[t1].pos] ),Add( c[t1].x );
swap( c[t1].x,a[c[t1].pos] );
}
int l=1,r=0;
for ( int i=1; i<=qcnt; i++ )
{
while ( l>q[i].l ) Add( a[--l] );
while ( r<q[i].r ) Add( a[++r] );
while ( l<q[i].l ) Del( a[l++] );
while ( r>q[i].r ) Del( a[r--] );
while ( now<q[i].tim ) Modify( ++now,i );
while ( now>q[i].tim ) Modify( now--,i );
for ( ans[q[i].id]=1; tot[ans[q[i].id]]>0; ++ans[q[i].id] );
}
树上莫队
子树统计
算出DFS序即可转化成序列问题。为什么不直接传标记呢
路径统计
还是要转化成序列,不过是括号序( wiki
上说 DFS序
是只记录一次,欧拉序
是每条边都记录一次),也就是进入DFS记一次,出去再记一次。
这样做完之后就可以统计路径了。首先,显然出现两次的数并不在这条路径上,直接忽略。我们设 (fir[x]) 为一个点第一次出现在序列中的位置,(las[x]) 表示最后一次出现的位置。
不难发现,对于路径 (x o y) ,设 (fir[x]leq fir[y]) ,在序列上的对应位置有如下规律:
- 如果 ( ext{lca}(x,y)=x) ,那么对应区间为 ([fir[x],fir[y]])
- 否则,对应区间为 ([las[x],fir[y]]+ ext{lca}(x,y))
注意每个点都出现了两次,空间要开够。
//Author:RingweEH
//SPOJ10707 Count on a tree II
const int N=2e5+1000;
int a[N],cnt[N],fir[N],las[N],bel[N],tmp[N],vis[N],ncnt,ans[N],nw=0;
int ord[N],val[N],head[N],dep[N],fa[N][30],tot=0,n,m;
struct Queries
{
int l,r,lca,id;
bool operator < ( Queries tt )
{ return (bel[l]^bel[tt.l]) ? bel[l]<bel[tt.l] : (bel[l]&1) ? r<tt.r : r>tt.r; }
}q[N];
void Addel( int pos )
{
vis[pos] ? nw-=!--cnt[val[pos]] : nw+=!cnt[val[pos]]++;
vis[pos]^=1;
}
void Init()
{
sort( tmp+1,tmp+n+1 );
int tot1=unique( tmp+1,tmp+1+n )-tmp-1;
for ( int i=1; i<=n; i++ )
val[i]=lower_bound( tmp+1,tmp+tot1+1,val[i] )-tmp;
}
int main()
{
for ( int i=1,l,r,lca; i<=m; i++ )
{
l=read(); r=read(); lca=Get_LCA( l,r );
if ( fir[l]>fir[r] ) swap( l,r );
if ( l==lca ) q[i].l=fir[l],q[i].r=fir[r];
else q[i].l=las[l],q[i].r=fir[r],q[i].lca=lca;
q[i].id=i;
}
int l=1,r=0; sort( q+1,q+1+m );
for ( int i=1; i<=m; i++ )
{
int ql=q[i].l,qr=q[i].r,lca=q[i].lca;
while ( l<ql ) Addel( ord[l++] );
while ( l>ql ) Addel( ord[--l] );
while ( r<qr ) Addel( ord[++r] );
while ( r>qr ) Addel( ord[r--] );
if ( lca ) Addel( lca );
ans[q[i].id]=nw;
if ( lca ) Addel( lca );
}
}
习题
给定一棵 (n) 点树,有点权,初始根为 (1) ,支持:
- 换根
- 求两个子树中点权相等的点对数
没有想到做第一道 Ynoi
竟然是因为莫队补档……
其实这个换根是假的,在DFS序上,
- 如果根是 (u) ,子树是整个序列
- 如果根在 (u) 在初始时的子树内,那么子树就是整个序列去掉 (u) 原来那个子树
- 否则,子树显然不变。
也就是说,询问要么是一个区间,要么是整个序列去掉某个区间,那么直接容斥,把一个询问拆成四个就能很容易地求解了。
for ( int i=1,opt,rt=1,u,v; i<=m; i++ )
{
opt=read();
if ( opt&1 ) { rt=read(); i--; m--; continue; }
u=read(),v=read();
int tx=(l[u]<=l[rt] && r[rt]<=r[u]),ty=(l[v]<=l[rt] && r[rt]<=r[v]);
if ( u==rt ) u=1,tx=0; //为根直接把区间设为整棵树
if ( v==rt ) v=1,ty=0;
if ( tx ) u=mp[u].lower_bound(l[rt])->second;
if ( ty ) v=mp[v].lower_bound(l[rt])->second;
int lx=l[u]-1,ly=l[v]-1,rx=r[u],ry=r[v];
if ( tx && ty ) ans[i]=pre[n];
if ( tx ) ans[i]+=(pre[ry]-pre[ly])*(tx==ty ? -1 : 1);
if ( ty ) ans[i]+=(pre[rx]-pre[lx])*(tx==ty ? -1 : 1);
//计算重合区间
Que[++cntq]=Queries(rx,ry,rx/B,tx==ty ? i : -i);
Que[++cntq]=Queries(rx,ly,rx/B,tx==ty ? -i : i);
Que[++cntq]=Queries(lx,ry,lx/B,tx==ty ? -i : i);
Que[++cntq]=Queries(lx,ly,lx/B,tx==ty ? i : -i);
}
sort( Que+1,Que+1+cntq ); nw=0; memset( c1,0,sizeof(c1) );
int l=1,r=0,ql,qr,qid;
for ( int i=1; i<=cntq; i++ )
{
ql=Que[i].l; qr=Que[i].r; qid=Que[i].id;
while ( l<ql ) l++,c1[a[l]]++,nw+=c2[a[l]];
while ( l>ql ) c1[a[l]]--,nw-=c2[a[l]],l--;
while ( r<qr ) r++,c2[a[r]]++,nw+=c1[a[r]];
while ( r>qr ) c2[a[r]]--,nw-=c1[a[r]],r--;
qid>0 ? ans[qid]+=nw : ans[-qid]-=nw;
}
回滚莫队
主要用途:解决区间端点扩张容易,缩小难维护的情况。
思想就是通过改变维护方式使得区间只增不减。分两类:
- 左右端点在同一块,直接暴力
- 左右端点不在同一块,那么显然可以让右端点单调递增,这样右端点就没有删除操作了;对于左端点,在每次开始时直接移到块尾+1,做每个询问时暴力移动到需要的位置,做完之后重新归位到块尾+1即可。
注意写的时候不要把暴力的计数数组和分块的计数数组混起来。
例题:AT1219 经典模板。
//Author:RingweEH
//AT JOISC 2014 C - 歴史の研究
Init(); int pos=1,l,r,ql,qr,qid; ll nw=0,tt;
for ( int k=1; k<=bnum; k++ )
{
l=rb[k]+1; r=rb[k]; nw=0; memset( cnt,0,sizeof(cnt) );
for ( ; bel[q[pos].l]==k; pos++ )
{
ql=q[pos].l; qr=q[pos].r; qid=q[pos].id;
if ( bel[ql]==bel[qr] )
{
tt=0;
for ( int j=ql; j<=qr; j++ ) cnt2[b[j]]=0;
for ( int j=ql; j<=qr; j++ )
cnt2[b[j]]++,bmax( tt,1ll*cnt2[b[j]]*a[j] );
ans[qid]=tt; continue;
}
while ( r<qr ) ++cnt[b[++r]],bmax(nw,1ll*cnt[b[r]]*a[r]);
tt=nw;
while ( l>ql ) ++cnt[b[--l]],bmax(nw,1ll*cnt[b[l]]*a[l]);
ans[qid]=nw;
while ( l<rb[k]+1 ) --cnt[b[l++]];
nw=tt;
}
}
二次离线莫队
适用范围:
- 可以莫队
- 更新答案时间不是 (mathcal{O}(1)) ,一个数的贡献和区间中别的数有关
大体思路:将更新答案的过程再次离线,降低复杂度。假设更新的暴力复杂度为 (mathcal{O}(k)) ,那么和普通莫队相比,是从 (mathcal{O}(nksqrt n)) 到 (mathcal{O}(nk+nsqrt n)) .
设 (f(x,[l,r])) 表示数 (x) 对区间 ([l,r]) 的贡献。考虑端点变化所产生的影响,设从 ([l,r] o [l,r+k]) ,也就是说:
差分之后就是 (f(x,[l,x-1])=f(x,[1,x-1])-f(x,[1,l-1])) ,转化为一个数对一个前缀的贡献。保存所有询问,从左到右扫描计算即可。但是这样常数巨大,而且空间巨大,非常的不行。
注意到贡献分成两类:
- (f(x,[1,x-1])) 的贡献永远是一个前缀和它后面的一个数的贡献,可以直接预处理
- (f(x,[1,l-1])) 对于一次询问的 (x) 都是不变的,那么打标记就可以只标记左右端点,然后最后再扫一遍暴力处理。
这样就能大大优化时空复杂度,具体可以参考模板题的代码理解。
模板题
查询区间内满足 (count(a_ioplus a_j)=k) 的无序数对个数。其中 (count(x)) 表示 (x) 二进制下 (1) 的个数。
利用异或的优良性质,开个桶记录当前前缀中与 (i) 异或有 (k) 个数位为 (1) 的数的个数。每次加入一个数 (a[i]),就对所有 (count(x)=k) 的 (x) 计入贡献,(++cnt[a[i]oplus x]) 即可。
注意你做的是差量,所以最后对答案要来一遍前缀和。
//Author:RingweEH
const int N=1e5+10;
int bel[N],a[N],n,m,k,cnt[N],pre[N];
struct Queries
{
int l,r,id; ll ans;
bool operator < ( Queries tmp ) { return (bel[l]^bel[tmp.l]) ? l<tmp.l : r<tmp.r; }
}q[N];
struct Node
{
int x,y,pos;
Node ( int _x=0,int _y=0,int _pos=0 ) : x(_x),y(_y),pos(_pos) {}
};
vector<Node> opt[N];
vector<int> buc;
ll ans[N];
#define lowbit(x) ((x)&(-x))
int Count( int x ) { int res=0; for ( ; x; x-=lowbit(x) ) res++; return res; }
void Init()
{
for ( int i=0; i<16384; i++ )
if ( Count(i)==k ) buc.push_back(i);
int siz=sqrt(n);
for ( int i=1; i<=n; i++ ) bel[i]=(i-1)/siz+1;
sort( q+1,q+1+m ); memset( cnt,0,sizeof(cnt) );
for ( int i=1; i<=n; i++ )
{
for ( auto x : buc ) cnt[a[i]^x]++;
pre[i]=cnt[a[i+1]]; //记住你计算的前缀多了1!
}
}
int main()
{
n=read(); m=read(); k=read();
if ( k>14 ) { for ( int i=1; i<=m; i++ ) printf( "0
" ); return 0; }
for ( int i=1; i<=n; i++ ) a[i]=read();
for ( int i=1; i<=m; i++ ) q[i].l=read(),q[i].r=read(),q[i].id=i;
Init(); int l=1,r=0,ql,qr,qid; memset( cnt,0,sizeof(cnt) );
for ( int i=1; i<=m; i++ )
{
ql=q[i].l; qr=q[i].r;
//相当于是r不动,计算固定右端点对区间变化的贡献,l同理
if ( l<ql ) opt[r].push_back( Node(l,ql-1,-i) ); //记录第二类贡献的标记
while ( l<ql ) q[i].ans+=pre[l-1],l++; //累计第一类的贡献
if ( l>ql ) opt[r].push_back( Node(ql,l-1,i) );
while ( l>ql ) q[i].ans-=pre[l-2],l--;
if ( r<qr ) opt[l-1].push_back( Node(r+1,qr,-i) );
while ( r<qr ) q[i].ans+=pre[r],r++;
if ( r>qr ) opt[l-1].push_back( Node(qr+1,r,i) );
while ( r>qr ) q[i].ans-=pre[r-1],r--;
}
for ( int i=1; i<=n; i++ ) //计算 a[1~i] 对所有后面的区间的贡献,暴力统计
{
for ( auto x : buc ) cnt[a[i]^x]++;
for ( int j=0; j<opt[i].size(); j++ )
{
l=opt[i][j].x; r=opt[i][j].y; qid=opt[i][j].pos;
for ( int j=l,tmp; j<=r; j++ )
{
tmp=cnt[a[j]]; tmp-=(j<=i && k==0);
(qid<0) ? q[-qid].ans-=tmp : q[qid].ans+=tmp;
}
}
}
for ( int i=1; i<=m; i++ ) q[i].ans+=q[i-1].ans;
for ( int i=1; i<=m; i++ ) ans[q[i].id]=q[i].ans;
for ( int i=1; i<=m; i++ ) printf( "%lld
",ans[i] );
return 0;
}