【莫队/树上莫队/回滚莫队】原理详解及例题:小B的询问(普通莫队),Count on a tree II(树上莫队),kangaroos(回滚莫队)
文章目錄
- 問題引入
- 介紹莫隊算法及其實現過程
- 時間復雜度
- 莫隊算法適用范圍
- 莫隊奇偶優化
- 普通莫隊:小B的詢問
- 樹上莫隊:SP10707 COT2 - Count on a tree II
- 回滾莫隊:[PA2011]Kangaroos
upd:2021-08-11:重新對博客進行了外觀美化修正,以及新增樹上莫隊
upd:2021-08-19:新增回滾莫隊
問題引入
給定一個大小為NNN的數組,數組中所有元素的大小≤N\le N≤N。你需要回答MMM個查詢。
每個查詢的形式是L,RL,RL,R。你需要回答在范圍[L,R][L,R][L,R]中至少重復222次的數字的個數
如果按照以往的想法,就會是O(n2)O(n^2)O(n2)的暴力枚舉
for ( int i = 1;i <= Q;i ++ ) {scanf ( "%d %d", &l, &r );for ( int j = l;j <= r;j ++ ) {count[a[j]] ++;if ( count[a[j]] == 3 )result ++;}}就算加一些優化,用l,rl,rl,r采取指針轉移,但總歸上還是在[1,n][1,n][1,n]區間內進行移動
最壞多半也是逼近于O(n2)O(n^2)O(n2)?
void add ( int x ) {count[a[x]] ++;if ( count[a[x]] == 3 )result ++; } void removed ( int x ) {count[a[x]] --;if ( count[a[x]] == 2 )result --; } for ( int i = 1;i <= m;i ++ ) {scanf ( "%d %d", &l, &r );while ( curl < l )removed ( curl ++ );while ( curl > l )add ( -- curl );while ( curr > r )removed ( curr -- );while ( curr < r )add ( ++ curr );printf ( "%d\n", result ); }add ?添加該位置的元素到當前集合內,并且更新答案
remove 從當前集合內刪除該位置的元素,并更新答案
那么這個時候莫隊算法就重磅登場了
為什么叫做莫隊算法呢?
據說算法是由之前的國家隊隊長莫濤發明的,他的隊友平日里稱他為莫隊,所以稱之為莫隊算法
介紹莫隊算法及其實現過程
莫隊算法就是一個離線算法,僅僅調整了處理查詢的順序
實現過程如下:
-
將給定的輸入數組分為n\sqrt{n}n??????塊。每一塊的大小為 nn\frac{n}{\sqrt{n}}n?n??
每個LLL????落入其中的一塊,每個RRR也落入其中的一塊
如果某查詢的LLL???落在第iii??塊中,則該查詢屬于第iii?塊??
-
所有的詢問首先按照所在塊的編號升序排列(所在塊的編號是指詢問的L屬于的塊)
如果編號相同,則按R值升序排列
-
莫隊算法將依次處理第111塊中的查詢,然后處理第222塊.........直到最后一塊
有很多的查詢屬于同一塊
e.g.
假設我們有333??個大小為333?的塊(0?2,3?5,6?8)(0-2,3-5,6-8)(0?2,3?5,6?8): {0,3}{1,7}{2,8}{7,8}{4,8}{4,4}{1,2}\{0,3\} \{1, 7\} \{2, 8\} \{7, 8\} \{4, 8\} \{4, 4\} \{1, 2\}{0,3}{1,7}{2,8}{7,8}{4,8}{4,4}{1,2}
先根據所在塊的編號重新排列它們
- 第111塊:{0,3}{1,7}{2,8}{1,2}\{0, 3\} \{1, 7\} \{2, 8\} \{1, 2\}{0,3}{1,7}{2,8}{1,2}
- 第222塊:{4,8}{4,4}\{4, 8\} \{4, 4\}{4,8}{4,4}
- 第333?塊:{7,8}\{7, 8\}{7,8}
接下來按照R的值重新排列
- 第一塊:{1,2}{0,3}{1,7}{2,8}\{1, 2\} \{0, 3\} \{1, 7\} \{2, 8\}{1,2}{0,3}{1,7}{2,8}
- 第二塊:{4,4}{4,8}\{4, 4\} \{4, 8\}{4,4}{4,8}
- 第三塊: {7,8}\{7, 8\}{7,8}
上述過程只是重新排列了查詢的順序
時間復雜度
我們說了這么多,選用莫隊算法無非就是想把時間復雜度給降下來
接下來我們來看看真正莫隊的時間復雜度是多少,其實我看了很多博客也是有點懵逼
上面的代碼就是起一個鋪墊作用,所有查詢的復雜性是由444個``while`循環決定的
前222??個while循環可以理解為左指針curl的移動總量
后222??個 while循環可以理解為右指針curr的移動總量
這兩者的和將是總復雜性
先算右指針
對于每個塊,查詢是遞增的順序排序,所以右指針curr按照遞增的順序移動
在下一個塊的開始時,指針可能在最右端,將移動到下一個塊中的最小的RRR處
又可以從本塊最左端移動到最右端
這意味著對于一個給定的塊,右指針移動的量是O(n)O(n)O(n)(curr可以從111跑到最后的nnn)
我們有O(n)O(\sqrt{n})O(n?)?塊,所以總共是O(nn)O(n\sqrt{n})O(nn?)??
接下來看看左指針怎樣移動
對于每個塊,所有查詢的左指針落在同一個塊中,從一個查詢移動到下一個查詢左指針會移動
但由于前一個LLL?與下一個LLL在同一塊中,此移動是O(n)O(\sqrt{n})O(n?)???(塊的大小)
在每一塊中左指針的移動總量是O(Qn)O(Q\sqrt{n})O(Qn?)??,(QQQ是落在那個塊的查詢的數量)
對于所有的塊,總的復雜度為O(m?n)O(m?\sqrt{n})O(m?n?)??
綜上,總復雜度為O((n+m)?n)=O(n?n)O((n+m)?\sqrt{n})=O(n?\sqrt n)O((n+m)?n?)=O(n?n?)?
實在無法理解就跳過吧(如果有通俗易懂的解釋歡迎評論)
莫隊算法適用范圍
首先莫隊算法是一個離線算法,所以如果問題是在線操作帶修或者強制特殊的順序
莫隊就失去了它的效應
其次一個重要的限制性:add和remove的操作
當有些題目的add和remove耗時很大,O(N)O(\sqrt N)O(N?)????時就應該思考能否卡過
因為莫隊本身就是一種優美的暴力而已
但是還是有很大一部分區間查詢的題可以由莫隊進行完成
莫隊奇偶優化
sqt = sqrt( n ) bool cmp( node x, node y ) {return ( x.l / sqt == y.l / sqt ) ? ( ( ( x.l / sqt ) & 1 ) ? x.r < y.r : x.r > y.r ) : x.l < y.l; }普通莫隊:小B的詢問
小B有一個序列,包含N個1~K之間的整數。他一共有M個詢問,
每個詢問給定一個區間[L…R],求Sigma(c(i)^2)的值,
其中i的值從1到K,其中c(i)表示數字i在[L…R]中的重復次數。
小B請你幫助他回答詢問。
輸入格式
第一行,三個整數N、M、K。
第二行,N個整數,表示小B的序列。
接下來的M行,每行兩個整數L、R。
輸出格式
M行,每行一個整數,其中第i行的整數表示第i個詢問的答案。
輸入輸出樣例
輸入
6 4 3
1 3 2 1 1 3
1 4
2 6
3 5
5 6
輸出
6
9
5
2
說明/提示
對于全部的數據,1<=N、M、K<=50000
簡單題解
說了是算法模板入門題,肯定不會把你拒之門外,還是要讓你摸摸門的
這個題就是要簡單處理一下∑ci2∑c_i^2∑ci2??,當ci±1c_i±1ci?±1?時,答案會發生怎樣的轉化?
完全平方公式大家都會吧!!!👇
(c[i]?1)2=c[i]2?2?c[i]+1(c[i]-1)^2=c[i]^2-2*c[i]+1(c[i]?1)2=c[i]2?2?c[i]+1?
(c[i]+1)2=c[i]2+2?c[i]+1(c[i]+1)^2=c[i]^2+2*c[i]+1(c[i]+1)2=c[i]2+2?c[i]+1
#include <cmath> #include <cstdio> #include <algorithm> using namespace std; #define LL l;ong long #define MAXN 50005 struct node {int l, r, num; }G[MAXN]; int n, m, k, apart, curl = 1, curr; int a[MAXN], cnt[MAXN]; LL result; LL ans[MAXN];bool cmp ( node x, node y ) {return ( x.l / apart == y.l / apart ) ? x.r < y.r : x.l < y.l; }void add ( int x ) {result += ( cnt[a[x]] << 1 ) + 1;cnt[a[x]] ++; } void removed ( int x ) {result -= ( cnt[a[x]] << 1 ) - 1;cnt[a[x]] --; }int main() {scanf ( "%d %d %d", &n, &m, &k );for ( int i = 1;i <= n;i ++ )scanf ( "%d", &a[i] );apart = sqrt ( n );for ( int i = 1;i <= m;i ++ ) {scanf ( "%d %d", &G[i].l, &G[i].r );G[i].num = i;}sort ( G + 1, G + m + 1, cmp );for ( int i = 1;i <= m;i ++ ) {int l = G[i].l, r = G[i].r;while ( curl < l ) {removed ( curl ++ );}while ( curl > l ) {add ( -- curl );}while ( curr > r ) {removed ( curr -- );}while ( curr < r ) {add ( ++ curr );}ans[G[i].num] = result;}for ( int i = 1;i <= m;i ++ )printf ( "%lld\n", ans[i] );return 0; }樹上莫隊:SP10707 COT2 - Count on a tree II
顧名思義就是把序列莫隊搬到樹上實現
分塊的大小以及移動的操作與序列莫隊無差別
唯一的區別就在于詢問的l,r
在樹上莫隊詢問的l,rl,rl,r?要用歐拉序進行重新編號
e.g.
原圖的歐拉序為1,2,4,6,6,7,7,5,5,4,2,3,3,1,對于點iii,li:l_i:li?: 第一次訪問iii,ri:r_i:ri?: 最后一次訪問iii
樹上莫隊就是用歐拉序的li,ril_i,r_ili?,ri?代替訪問的iii
對于查詢的(u,v)有兩種情況
-
是直系祖先關系(假設uuu是vvv的祖先)
e.g. : u=2,v=7u=2,v=7u=2,v=7??,拿出[lu,lv][l_u,l_v][lu?,lv?]?代替u,vu,vu,v?
歐拉序為2,4,6,6,7
-
不隸屬同一棵子樹(假設lu<lvl_u<l_vlu?<lv?)
e.g : u=7,v=3u=7,v=3u=7,v=3?,拿出[ru,lv][r_u,l_v][ru?,lv?]?
歐拉序為7,5,5,4,2,3
不在路徑上的點經過了恰好兩次,真正在路徑上的點都恰好只出現一次;對于不同子樹的兩點需要額外加上lca
source
#include <cmath> #include <cstdio> #include <vector> #include <algorithm> using namespace std; #define maxn 40005 #define maxm 100005 vector < int > G[maxn]; int n, Q, B, cnt, ans; bool vis[maxn]; int c[maxn], MS[maxn], tot[maxn], ret[maxm]; int dep[maxn], l[maxn], r[maxn], id[maxn << 1]; int f[maxn][20]; struct node {int l, r, id, lca; }q[maxm];void dfs( int u, int fa ) {dep[u] = dep[fa] + 1, l[u] = ++ cnt, id[cnt] = u;for( int i = 1;i <= 16;i ++ )f[u][i] = f[f[u][i - 1]][i - 1];for( auto v : G[u] ) {if( v == fa ) continue;else f[v][0] = u, dfs( v, u );}r[u] = ++ cnt, id[cnt] = u; }int get_lca( int u, int v ) {if( dep[u] < dep[v] ) swap( u, v );for( int i = 16;~ i;i -- )if( dep[f[u][i]] >= dep[v] ) u = f[u][i];if( u == v ) return u;for( int i = 16;~ i;i -- )if( f[u][i] != f[v][i] ) u = f[u][i], v = f[v][i];return f[u][0]; }void Delete( int x ) { if( -- tot[c[x]] == 0 ) ans --; }void Insert( int x ) { if( ++ tot[c[x]] == 1 ) ans ++; }void modify( int x ) { vis[x] ? Delete( x ) : Insert( x ); vis[x] ^= 1; }int main() {scanf( "%d %d", &n, &Q );for( int i = 1;i <= n;i ++ )scanf( "%d", &c[i] ), MS[i] = c[i];sort( MS + 1, MS + n + 1 );int m = unique( MS + 1, MS + n + 1 ) - MS - 1;for( int i = 1;i <= n;i ++ )c[i] = lower_bound( MS + 1, MS + m + 1, c[i] ) - MS;B = sqrt( n );for( int i = 1, u, v;i < n;i ++ ) {scanf( "%d %d", &u, &v );G[u].push_back( v );G[v].push_back( u );}dfs( 1, 0 );for( int i = 1, u, v;i <= Q;i ++ ) {scanf( "%d %d", &u, &v );q[i].id = i;if( l[u] > l[v] ) swap( u, v );int lca = get_lca( u, v );if( u == lca ) q[i].l = l[u], q[i].r = l[v], q[i].lca = 0;else q[i].l = r[u], q[i].r = l[v], q[i].lca = lca;}sort( q + 1, q + Q + 1, []( node x, node y ) { return ( x.l / B == y.l / B ) ? ( x.r < y.r ) : ( x.l < y.l ); } );int curl = 1, curr = 0;for( int i = 1;i <= Q;i ++ ) {while( curl < q[i].l ) modify( id[curl ++] );while( q[i].l < curl ) modify( id[-- curl] );while( curr < q[i].r ) modify( id[++ curr] );while( q[i].r < curr ) modify( id[curr --] );if( q[i].lca ) modify( q[i].lca );ret[q[i].id] = ans;if( q[i].lca ) modify( q[i].lca );}for( int i = 1;i <= Q;i ++ )printf( "%d\n", ret[i] ); }回滾莫隊:[PA2011]Kangaroos
普通莫隊是能做到快速增添和刪減操作的
但有些題目在區間轉移時,可能會出現增加或者刪除無法實現的問題
在只有增加不可實現或者只有刪除不可實現的時候,就可以使用回滾莫隊
同樣在O(nn)O(n\sqrt n)O(nn?)的時間內解決問題
回滾莫隊的核心思想就是既然只能實現一個操作,那么就只使用一個操作,剩下的交給回滾解決
回滾莫隊分為只使用增加操作的回滾莫隊和只使用刪除操作的回滾莫隊
以只使用添加操作的回滾莫隊為例
- 首先仍是按照區間左端點分塊排序,然后右端點為第二關鍵字
- 枚舉區間左端點的塊
- 對于詢問的左右端點都在同一區間的直接暴力做
- 將左端點都在該塊的按右端點升序排序
- 每次都是右端點右移,加入答案
- 左端點每次都回滾到這個塊的末尾,然后暴力的往前移到本次詢問的左端點處,記錄這一路上的答案,但是不像右端點一樣是永久的
再回退到塊末尾,這樣的時間復雜度,mmm個詢問,每次詢問左端點最多暴力移整個區間n\sqrt nn?,還是根號級別的復雜度
- 到下一塊的時候,l,rl,rl,r一起遍歷了整個區間,扔掉所有的標記那些,然后手動重置為初始局面
只支持刪除的話,我想應該就是右端點rrr遞減,不斷前移,然后還是左端點lll在塊中反復移動清空
可以看一下這道題→\rightarrow→ 回滾莫隊的例題——歷史研究
Kangaroos比較難一點
區間過大,實際上區間相交只關系左右端點的大小關系,所以先離散化區間的端點,最多只有2n2n2n個
然后按詢問左端點分塊,將詢問掛到塊上
接下來就是回滾莫隊問題了
這道題主要是要維護最大連續區間
每次將新增區間的下標扔進去的時候,更新
可以用線段樹維護最大連續區間,但是實際上可以用l[],r[]\rm l[],r[]l[],r[]數組來維護左右端點,省去log\rm loglog
這只是簡單口胡,看不懂可以看代碼,代碼里有詳細注釋
#include <cmath> #include <cstdio> #include <vector> #include <algorithm> using namespace std; #define maxn 200005 #define maxB 320 struct node {int l, r, id;node(){}node( int L, int R, int ID ) {l = L, r = R, id = ID;} }t[maxn]; struct Node {int val, id;Node(){}Node( int Val, int ID ) {val = Val, id = ID;} }x[maxn]; vector < node > q[maxB]; int n, m, ans, top; int block[maxn], L[maxB], R[maxB], l[maxn], r[maxn], ret[maxn];bool operator < ( Node s, Node t ) {return s.val == t.val ? s.id < t.id : s.val < t.val; }struct opt {//記錄操作前的相關信息 方便回滾 //flag 0:左 1:右 記錄被更改的是哪邊 int flag, pos, lst, ans;opt(){}opt( int Flag, int Pos, int Lst, int Ans ) {flag = Flag, pos = Pos, lst = Lst, ans = Ans;} }s[maxn << 4]; //l[i]:在當前已加入區間中i區間所能連的最左邊區間下標 -> [l(i),i]的所有區間都與當前查詢區間有交 //r[i]:在當前已加入區間中i區間所能連的最右邊區間下標 -> [i,r(i)]的所有區間都與當前查詢區間有交 void add( int i ) {if( l[i] || r[i] ) return;//已經算過了i的貢獻 if( ! l[i - 1] and ! r[i + 1] ) {//左右的區間都還沒有被加進來//只能自己這一個區間 長度為1 s[++ top] = opt( 0, i, l[i], ans );s[++ top] = opt( 1, i, r[i], ans );l[i] = r[i] = i, ans = max( ans, 1 );}else if( ! l[i - 1] ) {//i+1的區間被加入了 可以往左延伸和i接上 s[++ top] = opt( 0, r[i + 1], l[r[i + 1]], ans );s[++ top] = opt( 1, i, r[i], ans );r[i] = r[i + 1], l[r[i + 1]] = i;ans = max( ans, r[i] - i + 1 );}else if( ! r[i + 1] ) {//i-1的區間被加入了 可以往右延伸和i接上 s[++ top] = opt( 1, l[i - 1], r[l[i - 1]], ans );s[++ top] = opt( 0, i, l[i], ans );l[i] = l[i - 1], r[l[i - 1]] = i;ans = max( ans, i - l[i] + 1 );}else {//i-1 i+1都被加入 直接左右連接拼起來 s[++ top] = opt( 0, r[i + 1], l[r[i + 1]], ans );s[++ top] = opt( 1, l[i - 1], r[l[i - 1]], ans );s[++ top] = opt( 0, i, l[i], ans );s[++ top] = opt( 1, i, r[i], ans );//都連起來了 也沒有i什么事了 所以隨便記錄i的一邊l/r有值 下次就可以在第一個if語句直接return l[r[i + 1]] = l[i - 1], r[l[i - 1]] = r[i + 1], l[i] = r[i] = i;ans = max( ans, r[i + 1] - l[i - 1] + 1 ); } }void remove( int lst ) {while( top > lst ) {if( ! s[top].flag ) l[s[top].pos] = s[top].lst;else r[s[top].pos] = s[top].lst;ans = s[top --].ans; } }void solve( int id ) {remove( 0 );//移動到新塊 莫隊里的東西清空 for( int i = 1;i <= n;i ++ ) if( t[i].l < L[id] && R[id] < t[i].r )//完全覆蓋該塊且不在塊內的區間一定會對塊內所掛詢問產生貢獻 add( t[i].id );sort( q[id].begin(), q[id].end(), []( node x, node y ) { return x.r < y.r; } );//將塊內詢問按照右端點排序 只增加的回滾莫隊 int lst = top;for( node now : q[id] ) //計算整個區間都在塊內的詢問的答案if( now.r > R[id] ) continue;else {for( int i = L[id];i <= R[id];i ++ )if( now.l <= t[x[i].id].r && t[x[i].id].l <= now.r )//與查詢區間有交 可能會為答案貢獻的區間 add( x[i].id );ret[now.id] = ans;remove( lst ); }remove( 0 );int cur = R[id];//處理r在其他塊遞增的詢問 先把指針撥到塊的最后for( int i = 1;i <= n;i ++ )if( t[i].l <= R[id] && R[id] <= t[i].r )//一定會與后面的查詢有交點 R[id]add( t[i].id );for( node now : q[id] )if( now.r <= R[id] ) continue;else {while( cur < now.r ) add( x[++ cur].id );lst = top;for( int i = R[id];i >= now.l;i -- )//回滾莫隊 滾到當前詢問的左端 add( x[i].id );ret[now.id] = ans;remove( lst );//返回 回滾 抵消 } }int main() {scanf( "%d %d", &n, &m );for( int i = 1, l, r;i <= n;i ++ ) {scanf( "%d %d", &l, &r );x[i] = Node( l, i );x[i + n] = Node( r, i );t[i] = node( l, r, i );}//為了塊的大小能開出來 1e9首先要離散化 sort( x + 1, x + ( n << 1 ) + 1 );for( int i = 1;i <= n;i ++ ) {t[i].l = lower_bound( x + 1, x + ( n << 1 | 1 ), Node( t[i].l, 0 ) ) - x;t[i].r = upper_bound( x + 1, x + ( n << 1 | 1 ), Node( t[i].r, 1e9 ) ) - x - 1;}int N = n << 1;int B = sqrt( N );for( int i = 1;i <= N;i ++ )block[i] = ( i - 1 ) / B + 1;//L,R 記錄塊的左右端點 for( int i = N;i;i -- ) L[block[i]] = i;for( int i = 1;i <= N;i ++ ) R[block[i]] = i;for( int i = 1, l, r;i <= m;i ++ ) {scanf( "%d %d", &l, &r );l = lower_bound( x + 1, x + ( n << 1 | 1 ), Node( l, 0 ) ) - x;r = upper_bound( x + 1, x + ( n << 1 | 1 ), Node( r, 1e9 ) ) - x - 1;q[block[l]].push_back( node( l, r, i ) );}for( int i = 1;i <= block[N];i ++ )if( ! q[i].empty() )solve( i );for( int i = 1;i <= m;i ++ )printf( "%d\n", ret[i] );return 0; }總結
以上是生活随笔為你收集整理的【莫队/树上莫队/回滚莫队】原理详解及例题:小B的询问(普通莫队),Count on a tree II(树上莫队),kangaroos(回滚莫队)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AliOS有什么特性
- 下一篇: 牛客网CSP-S提高组赛前集训营1题解(