普通莫队算法 形式 假设 𝑛   = 𝑚 n = m [ 𝑙 , 𝑟 ] [ l , r ] 𝑂 ( 1 ) O ( 1 ) [ 𝑙   − 1 , 𝑟 ] , [ 𝑙   + 1 , 𝑟 ] , [ 𝑙 , 𝑟   + 1 ] , [ 𝑙 , 𝑟   − 1 ] [ l − 1 , r ] , [ l + 1 , r ] , [ l , r + 1 ] , [ l , r − 1 ] [ 𝑙 , 𝑟 ] [ l , r ] 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
解释 离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
排序方法 对于区间 [ 𝑙 , 𝑟 ] [ l , r ] 𝑙 l 𝑟 r 
实现 
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 void   move ( int   pos ,   int   sign )   { 
   // update nowAns 
} 
void   solve ()   { 
   BLOCK_SIZE   =   int ( ceil ( pow ( n ,   0.5 ))); 
   sort ( querys ,   querys   +   m ); 
   for   ( int   i   =   0 ;   i   <   m ;   ++ i )   { 
     const   query   & q   =   querys [ i ]; 
     while   ( l   >   q . l )   move ( -- l ,   1 ); 
     while   ( r   <   q . r )   move ( ++ r ,   1 ); 
     while   ( l   <   q . l )   move ( l ++ ,   -1 ); 
     while   ( r   >   q . r )   move ( r -- ,   -1 ); 
     ans [ q . id ]   =   nowAns ; 
   } 
} 
复杂度分析 以下的情况在 𝑛 n 𝑚 m 
首先是分块这一步,这一步的时间复杂度是 𝑂 ( √ 𝑛   ⋅ √ 𝑛 l o g  √ 𝑛   + 𝑛 l o g  𝑛 )   = 𝑂 ( 𝑛 l o g  𝑛 ) O ( n ⋅ n log  n + n log  n ) = O ( n log  n ) 
接着就到了莫队算法的精髓了,下面我们用通俗易懂的初中方法来证明它的时间复杂度是 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
证明  证:令每一块中 𝐿 L m a x 1 , m a x 2 , m a x 3 , ⋯ , m a x ⌈ √ 𝑛 ⌉ max 1 , max 2 , max 3 , ⋯ , max ⌈ n ⌉ 
 由第一次排序可知,m a x 1   ≤ m a x 2   ≤ ⋯   ≤ m a x ⌈ √ 𝑛 ⌉ max 1 ≤ max 2 ≤ ⋯ ≤ max ⌈ n ⌉ 
 显然,对于每一块暴力求出第一个询问的时间复杂度为 𝑂 ( 𝑛 ) O ( n ) 
 考虑最坏的情况,在每一块中,𝑅 R 𝑛 n 𝐿 L m a x 𝑖 − 1 max i − 1 m a x 𝑖 max i m a x 𝑖 max i m a x 𝑖 − 1 max i − 1 
 考虑 𝑅 R 𝑅 R 𝑂 ( 𝑛 ) O ( n ) 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
 重点分析 𝐿 L 𝑂 ( m a x 𝑖   − m a x 𝑖 − 1 ) O ( max i − max i − 1 ) 𝑂 ( √ 𝑛   ⋅ ( m a x 𝑖   − m a x 𝑖 − 1 ) ) O ( n ⋅ ( max i − max i − 1 ) ) 
 将每一块 𝐿 L 
 对于 𝐿 L 
 𝑂 ( √ 𝑛 ( m a x 1 − 1 ) + √ 𝑛 ( m a x 2 − m a x 1 ) + √ 𝑛 ( m a x 3 − m a x 2 ) + ⋯ + √ 𝑛 ( m a x ⌈ √ 𝑛 ⌉ − m a x ⌈ √ 𝑛 ⌉ − 1 ) ) = 𝑂 ( √ 𝑛 ⋅ ( m a x 1 − 1 + m a x 2 − m a x 1 + m a x 3 − m a x 2 + ⋯ + m a x ⌈ √ 𝑛 ⌉ − 1 − m a x ⌈ √ 𝑛 ⌉ − 2 + m a x ⌈ √ 𝑛 ⌉ − m a x ⌈ √ 𝑛 ⌉ − 1 ) ) = 𝑂 ( √ 𝑛 ⋅ ( m a x ⌈ √ 𝑛 ⌉ − 1 ) ) O ( n ( max 1 − 1 ) + n ( max 2 − max 1 ) + n ( max 3 − max 2 ) + ⋯ + n ( max ⌈ n ⌉ − max ⌈ n ⌉ − 1 ) ) = O ( n ⋅ ( max 1 − 1 + max 2 − max 1 + max 3 − max 2 + ⋯ + max ⌈ n ⌉ − 1 − max ⌈ n ⌉ − 2 + max ⌈ n ⌉ − max ⌈ n ⌉ − 1 ) ) = O ( n ⋅ ( max ⌈ n ⌉ − 1 ) ) (裂项求和)
 由题可知 m a x ⌈ √ 𝑛 ⌉ max ⌈ n ⌉ 𝑛 n 𝐿 L 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
综上所述,莫队算法的时间复杂度为 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
但是对于 𝑚 m 𝑚   < 𝑛 m < n 
怎么分块呢?
我们设块长度为 𝑆 S 𝑛 n 𝑛 𝑆 n S 𝑛 2 𝑆 n 2 S 𝑚 𝑆 m S 𝑂 ( 𝑛 2 𝑆 + 𝑚 𝑆 ) O ( n 2 S + m S ) 𝑆 S 𝑛 √ 𝑚 n m 𝑂 ⎛ ⎜ ⎜ ⎜ ⎜ ⎜ ⎝ 𝑛 2 𝑛 √ 𝑚 + 𝑚 ( 𝑛 √ 𝑚 ) ⎞ ⎟ ⎟ ⎟ ⎟ ⎟ ⎠   = 𝑂 ( 𝑛 √ 𝑚 ) O ( n 2 n m + m ( n m ) ) = O ( n m ) 
事实上,如果块长度的设定不准确,则莫队的时间复杂度会受到很大影响。例如,如果 𝑚 m √ 𝑛 n √ 𝑛 n 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 𝑂 ( 𝑛 5 / 4 ) O ( n 5 / 4 ) 
莫队算法看起来十分暴力,很大程度上是因为莫队算法的分块排序方法看起来很粗糙。我们会想到通过看上去更精细的排序方法对所有区间排序。一种方法是把所有区间 [ 𝑙 , 𝑟 ] [ l , r ] ( 𝑙 , 𝑟 ) ( l , r ) 
假设 𝑛 , 𝑚 n , m 𝑛 n [ 𝑎 √ 𝑛 , 𝑏 √ 𝑛 ] ( 1   ≤ 𝑎 , 𝑏   ≤ √ 𝑛 ) [ a n , b n ] ( 1 ≤ a , b ≤ n ) 𝑛 n √ 𝑛 n 𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
莫队算法还有一个特点:当 𝑛 n 𝑚 m 𝑚 m 
例题 & 代码 例题 「国家集训队」小 Z 的袜子   题目大意:
 有一个长度为 𝑛 n { 𝑐 𝑖 } { c i } 𝑚 m 𝑙 , 𝑟 l , r 𝑙 l 𝑟 r 
过程 思路:莫队算法模板题。
对于区间 [ 𝑙 , 𝑟 ] [ l , r ] 𝑙 l 𝑟 r 
然后从序列的第一个询问开始计算答案,第一个询问通过直接暴力算出,复杂度为 𝑂 ( 𝑛 ) O ( n ) 
具体做法:
对于区间 [ 𝑖 , 𝑖 ] [ i , i ] 
我们设 𝑐 𝑜 𝑙 [ 𝑖 ] c o l [ i ] 𝑖 i 𝑎 𝑛 𝑠 a n s 𝑘 k 𝑎 𝑛 𝑠 a n s ( 𝑐 𝑜 𝑙 [ 𝑘 ] + 1 2 )   − ( 𝑐 𝑜 𝑙 [ 𝑘 ] 2 ) ( c o l [ k ] + 1 2 ) − ( c o l [ k ] 2 ) 𝑎 𝑛 𝑠 a n s ( 𝑐 𝑜 𝑙 [ 𝑘 ] 2 )   − ( 𝑐 𝑜 𝑙 [ 𝑘 ] − 1 2 ) ( c o l [ k ] 2 ) − ( c o l [ k ] − 1 2 ) 𝑎 𝑛 𝑠 ( 𝑟 − 𝑙 + 1 2 ) a n s ( r − l + 1 2 ) 
这里有个优化:( 𝑎 2 )   = 𝑎 ( 𝑎 − 1 ) 2 ( a 2 ) = a ( a − 1 ) 2 
所以 ( 𝑎 + 1 2 )   − ( 𝑎 2 )   = ( 𝑎 + 1 ) 𝑎 2   − 𝑎 ( 𝑎 − 1 ) 2   = 𝑎 2   ⋅ ( 𝑎   + 1   − 𝑎   + 1 )   = 𝑎 2   ⋅ 2   = 𝑎 ( a + 1 2 ) − ( a 2 ) = ( a + 1 ) a 2 − a ( a − 1 ) 2 = a 2 ⋅ ( a + 1 − a + 1 ) = a 2 ⋅ 2 = a 
所以 ( 𝑐 𝑜 𝑙 [ 𝑘 ] + 1 2 )   − ( 𝑐 𝑜 𝑙 [ 𝑘 ] 2 )   = 𝑐 𝑜 𝑙 [ 𝑘 ] ( c o l [ k ] + 1 2 ) − ( c o l [ k ] 2 ) = c o l [ k ] 
算法总复杂度:𝑂 ( 𝑛 √ 𝑛 ) O ( n n ) 
下面的代码中 deno 表示答案的分母 (denominator),nume 表示分子 (numerator),sqn 表示块的大小:√ 𝑛 n arr 是输入的数组,node 是存储询问的结构体,tab 是询问序列(排序后的),col 同上所述。
注意:由于 ++l 和 --r 的存在,下面代码中的移动区间的 4 个 while 循环的位置很关键,不能随意改变它们之间的位置关系。 
关于四个循环位置的讨论  莫队区间的移动过程,就相当于加入了 [ 1 , 𝑟 ] [ 1 , r ] [ 1 , 𝑙   − 1 ] [ 1 , l − 1 ] 
 对于 𝑙   ≤ 𝑟 l ≤ r [ 1 , 𝑙   − 1 ] [ 1 , l − 1 ] [ 𝑙 , 𝑟 ] [ l , r ] [ 𝑟   + 1 ,   + ∞ ) [ r + 1 , + ∞ )  对于 𝑙   = 𝑟   + 1 l = r + 1 [ 1 , 𝑟 ] [ 1 , r ] [ 𝑟   + 1 ,   + ∞ ) [ r + 1 , + ∞ )  对于 𝑙   > 𝑟   + 1 l > r + 1 [ 𝑟   + 1 , 𝑙   − 1 ] [ r + 1 , l − 1 ]  因此,如果某时刻出现 𝑙   > 𝑟   + 1 l > r + 1 set 维护区间中的所有数,就会出现「需要删除 set 中不存在的元素」的问题。
 代码中的四个 while 循环一共有 4 !   = 2 4 4 ! = 24 1 2 12 1 2 12 
 循环顺序 正确性 反例或注释 l--,l++,r--,r++错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l--,l++,r++,r--错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l--,r--,l++,r++错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l--,r--,r++,l++正确 证明较繁琐 l--,r++,l++,r--正确 l--,r++,r--,l++正确 l++,l--,r--,r++错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l++,l--,r++,r--错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l++,r++,l--,r--错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l++,r++,r--,l--错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l++,r--,l--,r++错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ l++,r--,r++,l--错误 𝑙   < 𝑟   < 𝑙 ′   < 𝑟 ′ l < r < l ′ < r ′ 
 全部 24 种排列中只有 6 种是正确的,其中有 2 种的证明较繁琐,这里只给出其中 4 种的证明。
 这 4 种正确写法的共同特点是,前两步先扩大区间(l-- 或 r++),后两步再缩小区间(l++ 或 r--)。这样写,前两步是扩大区间,可以保持 𝑙   ≤ 𝑟   + 1 l ≤ r + 1 𝑙   ≤ 𝑙 ′   ≤ 𝑟 ′   ≤ 𝑟 l ≤ l ′ ≤ r ′ ≤ r [ 𝑙 ′ , 𝑟 ′ ] [ l ′ , r ′ ] 𝑙   ≤ 𝑟   + 1 l ≤ r + 1 
实现 参考代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 #include   <algorithm> 
#include   <cmath> 
#include   <iostream> 
using   namespace   std ; 
constexpr   int   N   =   50005 ; 
int   n ,   m ,   maxn ; 
int   c [ N ]; 
long   long   sum ; 
int   cnt [ N ]; 
long   long   ans1 [ N ],   ans2 [ N ]; 
struct   query   { 
   int   l ,   r ,   id ; 
   bool   operator < ( const   query   & x )   const   {    // 重载<运算符 
     if   ( l   /   maxn   !=   x . l   /   maxn )   return   l   <   x . l ; 
     return   ( l   /   maxn )   &   1   ?   r   <   x . r   :   r   >   x . r ; 
   } 
}   a [ N ]; 
void   add ( int   i )   { 
   sum   +=   cnt [ i ]; 
   cnt [ i ] ++ ; 
} 
void   del ( int   i )   { 
   cnt [ i ] -- ; 
   sum   -=   cnt [ i ]; 
} 
long   long   gcd ( long   long   a ,   long   long   b )   {   return   b   ?   gcd ( b ,   a   %   b )   :   a ;   } 
int   main ()   { 
   cin . tie ( nullptr ) -> sync_with_stdio ( false ); 
   cin   >>   n   >>   m ; 
   maxn   =   sqrt ( n ); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   cin   >>   c [ i ]; 
   for   ( int   i   =   0 ;   i   <   m ;   i ++ )   cin   >>   a [ i ]. l   >>   a [ i ]. r ,   a [ i ]. id   =   i ; 
   sort ( a ,   a   +   m ); 
   for   ( int   i   =   0 ,   l   =   1 ,   r   =   0 ;   i   <   m ;   i ++ )   {    // 具体实现 
     if   ( a [ i ]. l   ==   a [ i ]. r )   { 
       ans1 [ a [ i ]. id ]   =   0 ,   ans2 [ a [ i ]. id ]   =   1 ; 
       continue ; 
     } 
     while   ( l   >   a [ i ]. l )   add ( c [ -- l ]); 
     while   ( r   <   a [ i ]. r )   add ( c [ ++ r ]); 
     while   ( l   <   a [ i ]. l )   del ( c [ l ++ ]); 
     while   ( r   >   a [ i ]. r )   del ( c [ r -- ]); 
     ans1 [ a [ i ]. id ]   =   sum ; 
     ans2 [ a [ i ]. id ]   =   ( long   long )( r   -   l   +   1 )   *   ( r   -   l )   /   2 ; 
   } 
   for   ( int   i   =   0 ;   i   <   m ;   i ++ )   { 
     if   ( ans1 [ i ]   !=   0 )   { 
       long   long   g   =   gcd ( ans1 [ i ],   ans2 [ i ]); 
       ans1 [ i ]   /=   g ,   ans2 [ i ]   /=   g ; 
     }   else 
       ans2 [ i ]   =   1 ; 
     cout   <<   ans1 [ i ]   <<   '/'   <<   ans2 [ i ]   <<   '\n' ; 
   } 
   return   0 ; 
} 
普通莫队的优化 过程 我们看一下下面这组数据
// 设块的大小为 2 (假设)
1 1
2 100
3 1
4 100
手动模拟一下可以发现,r 指针的移动次数大概为 300 次,我们处理完第一个块之后,𝑙   = 2 , 𝑟   = 1 0 0 l = 2 , r = 100 
什么是奇偶化排序?奇偶化排序即对于属于奇数块的询问,r 按从小到大排序,对于属于偶数块的排序,r 从大到小排序,这样我们的 r 指针在处理完这个奇数块的问题后,将在返回的途中处理偶数块的问题,再向 n 移动处理下一个奇数块的问题,优化了 r 指针的移动次数,一般情况下,这种优化能让程序快 30% 左右。
实现 排序代码:
小细节  如果使用 sort 比较两个结构体,不能出现 𝑎   < 𝑏 a < b 𝑏   < 𝑎 b < a 常见错误 。
对于压行版,如果没有 r == x.r 的特判,当 l 属于同一奇数块且 r 相等时,会出现上面小细节中的问题(自己手动模拟一下),对于不压行版,如果写成小于(大于)等于,则也会出现同样的问题。
参考资料 2025/10/13 16:54:57 ,更新历史 在 GitHub 上编辑此页! Ir1d , StudyingFather , countercurrent-time , Backl1ght , H-J-Granger , mgt , NachtgeistW , akakw1 , c-forrest , CCXXXI , Enter-tainer , Tiphereth-A , AngelKitty , cjsoft , diauweb , Early0v0 , ezoixx130 , GekkaSaori , greyqz , Konano , LovelyBuggies , Makkiy , MicDZ , minghu6 , ouuan , P-Y-Y , PotassiumWings , SamZhangQingChuan , sshwy , Suyun514 , weiyong1024 , Xeonacid , AtomAlpaca , cesonic , Chrogeek , GavinZhengOI , Gesrua , GuanghaoYe , Henry-ZHR , HeRaNO , iamtwz , kenlig , konnyakuxzy , ksyx , kxccc , leoleoasd , lychees , Menci , MingqiHuang , Peanut-Tang , renyancheng , SukkaW , Sweetlemon68 , thallium , ZnPdCo CC BY-SA 4.0  和 SATA