『数据结构』莫队、带修莫队、树上莫队详解
普通莫队简介莫队是一种基于分块思想的离线算法,用于解决区间问题,适用范围如下:
满足以上三个条件就可以在(O(nsqrt{m}+mlogm))的时间复杂度下得到每个询问的解。 算法思想莫队的精髓就在于通过对询问进行排序,并把询问的结果作为下一个询问求解的基础,使得暴力求解的复杂度得到保证。 上文中“适用范围”的第三点“在已知询问$ [l,r]$答案的情况下可以 (O(1))得到([l,r])的答案”即是“把询问的结果作为下一个询问求解的基础”的方法。 例:[国家集训队]小Z的袜子 在这题中,用(cnt_{i})表示当前处理的区间内颜色为(i)的袜子出现的次数,用(mathrm{len})表示当前处理的区间的长度,用(x)表示新增的那只袜子的颜色。 以已知区间([l,r])的答案求解区间([l,r+1])为例。分别处理分子和分母:
因此,将一只颜色为(x)的袜子计入答案的函数就可以写出来了: //fz表示分子,fm表示分母 inline void add(int x) { fz += cnt[x]; cnt[x]++; fm += len; len++; } 同理可以写出将一只颜色为(x)的袜子移出答案的函数: inline void del(int x) { cnt[x]--; fz -= cnt[x]; len++; fm -= len; } 于是,我们就可以得到一个暴力的算法:用(l)和(r)分别记录当前区间的两个端点,然后用下面这段代码来更新答案((q[i].l,q[i].r)代表正在处理的询问的两个端点,(col[p])代表第(p)只袜子的颜色): while (l > q[i].l) add(col[--l]); while (r < q[i].r) add(col[++r]); while (l < q[i].l) del(col[l++]); while (r > q[i].r) del(col[r--]); 然而,这个算法的时间复杂度是(O(nm))的(因为最坏情况下每次(l)和(r)两个指针都要走(O(n))的距离,而一共有(m)次询问),和暴力完全一样甚至跑的更慢。 别忘了,之前我说过,莫队的精髓就在于通过对询问进行排序,使得暴力求解的复杂度得到保证。 我们的目的是使(l)和(r)两个指针走过的总距离尽量的小,这时候就要用到分块的思想了。 把整个区间([1,n])分成若干块,以询问的左端点所在块为第一关键字,以询问的右端点大小为第二关键字,对询问进行排序,那么:
总结:(用(B)表示块的大小)(l)指针每次移动(O(B)),(r)指针每块移动(O(n))。 所以:
因此,总移动次数为(O(mB+n^2/B))。 没错,这就是个双勾函数,所以当(B=sqrt{frac{n^2}{m}})即(frac{n}{sqrt{m}})时复杂度最小,为(O(nsqrt{m}))。 剩下的最后一个问题:初始的当前区间是什么? 只要任意指定一个空区间就好了,如(l=1,r=0)。 所以,整个莫队算法就可以概括为:
总的复杂度为(O(nsqrt{m}+mlogm))。 P.S. 网上很多教程说分块大小取(sqrt{n})最优,复杂度为(O(nsqrt{n})),这是不严谨的,当(n,m)差别较大时使用(sqrt{n})作为分块大小效率会明显偏低。 例题代码[国家集训队]小Z的袜子AC代码: #include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #include<cmath> using namespace std; const int MAXN=50005; int n,m,B,fz,fm,len,col[MAXN],cnt[MAXN],ans[MAXN][2]; struct query { int l,r,id; bool operator < (query & b) { return l / B == b.l / B ? r < b.r : l < b.l; } } q[MAXN]; inline void add(int x) { fz += cnt[x]; cnt[x]++; fm += len; len++; } inline void del(int x) { cnt[x]--; fz -= cnt[x]; len--; fm -= len; } inline int gcd(int a,int b) { return b == 0 ? a : gcd(b,a % b); } int main() { int g,l=1,r=0; scanf("%d%d",&n,&m); B=n/sqrt(m); for (int i=1; i<=n; i++) scanf("%d",&col[i]); for (int i=0; i<m; i++) { scanf("%d%d",&q[i].l,&q[i].r); q[i].id=i; } sort(q,q+m); for (int i=0; i<m; i++) { if (q[i].l == q[i].r) { ans[q[i].id][0]=0; ans[q[i].id][1]=1; continue; } while (l>q[i].l) add(col[--l]); while (r<q[i].r) add(col[++r]); while (l<q[i].l) del(col[l++]); while (r>q[i].r) del(col[r--]); g=gcd(fz,fm); ans[q[i].id][0]=fz / g; ans[q[i].id][1]=fm / g; } for (int i=0; i<m; i++) printf("%d/%dn",ans[i][0],ans[i][1]); return 0; } 其它例题小B的询问 带修莫队前面说过,普通的莫队只能解决没有修改的问题,那么带修改的问题怎么解决呢?带修莫队就是一种支持单点修改的莫队算法。 算法简介还是对询问进行排序,每个询问除了左端点和右端点还要记录这次询问是在第几次修改之后(时间),以左端点所在块为第一关键字,以右端点所在块为第二关键字,以时间为第三关键字进行排序。 暴力查询时,如果当前修改数比询问的修改数少就把没修改的进行修改,反之回退。 需要注意的是,修改分为两部分:
分块大小的选择以及复杂度证明(用(B)表示分块大小,(c)表示修改个数,(q)表示询问个数,(l)块表示以(l div B)分的块,每个(l)块包含(n div B)个(r)块)
所以:总移动次数为(Oleft(frac{cn^2}{B^2}+qB+frac{n^2}{B}right))。 由于一般的题目都不会告诉你修改和询问分别的个数,所以统一用(m)表示,即(Oleft(frac{mn^2}{B^2}+mB+frac{n^2}{B}right))。 (B)可以取[frac{n^{2}}{3^{frac{1}{3}}(9m^{3}n^{2}+sqrt{3}sqrt{27m^{6}n^{4}-m^{3}n^{6}})^{frac{1}{3}}}+frac{(9m^{3}n^{2}+sqrt{3}sqrt{27m^{6}n^{4}-m^{3}n^{6}})^{frac{1}{3}}}{3^{frac{2}{3}}m}] 所以还是不要纠结带修莫队的最佳分块大小好了(cdotscdots)视作(n=m)的话,就可以得到总移动次数为(Oleft(frac{n^3}{B^2}+nB+frac{n^2}{B}right)),那么(B=n^{frac{2}{3}})时取最小值(Oleft(n^{frac{5}{3}}right))。 所以:带修莫队的渐进时间复杂度为(Oleft(nlogn+n^{frac{5}{3}}right))(视作(n=m))。 其它例题CF940F Machine Learning 树上莫队其实,莫队算法除了序列还可以用于树。复杂度同序列上的莫队(不带修(O(nsqrt{m}+mlogm)),带修(Oleft(nlogn+n^{frac{5}{3}}right)))。 例题[WC2013]糖果公园 分块方式这里需要看一道专门为树上莫队设计的题目 [SCOI2005]王室联邦。 用这道题所要求的方式进行分块,并用后文的方式更新答案,就能保证复杂度(复杂度分析见后文)。 那么如何满足每块大小在([B,3B]),块内每个点到核心点路径上的所有点都在块内呢? 这里先提供一种构造方式,再予以证明:
如果你看懂了这个方法的话,每块大小>=B是显然的,下面证明为何每块大小(le 3B): 对于当前节点的每一棵子树:
对于 修改方式修改,就是由询问((cu,cv)) 更新至询问((tu,tv))。 (T(u,v))表示(u)到(v)的路径上除(lca(u,v))外的所有点构成的集合; (S(u,v))代表(u)到(v)的路径,(xor)表示集合对称差。
第二步证明如下: (quad,T(cu,cv) xor T(tu,tv)) (=[S(cu,root) xor S(cv,root)] xor [S(tu,root) xor S(tv,root)]) (=[S(cu,root) xor S(tu,root)] xor [S(cv,root)]) (=T(cu,tu) xor T(cv,tv)) 之所以要把(T(cu,tv))转化成(T(cu,tv)),是因为这样的话就能通过对询问排序来保证复杂度。 关于单点修改树上莫队的单点修改和序列莫队类似,唯一不同就是,修改后是否更新答案通过(mathrm{vis})数组判断。 复杂度分析每块大小在([B,3B)),所以两点间路径长度也在([B,3B)),块内移动就是(O(B))的;编号相邻的块位置必然是相邻的,所以两块间路径长度也是(O(B));然后就和序列莫队的复杂度分析类似了(cdots cdots) 莫队的扩展一般地,若(Q(x_1,x_2,cdots,x_k))为一个询问,(forall iin[1,k]),(x_{i})的规模都为(n),可以在时间(T)内求解(Q(x_1,x_ipm 1,x_n)),共有(m)个询问,那么就可以在(Oleft(kmlogm+nTm^frac{k-1}{k}right))的时间复杂度下离线求解。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |