清北學堂 2020 國慶J2考前綜合強化 Day5
前置知識
1、分塊的基本思想(開根號等)
2、\(sort\) 的用法(手寫 \(cmp\) 函式或過載運算子實現結構體的多關鍵字排序)
3、卡常
5、離散化(用於應付很多題目)
定義
一種優美的暴力演算法,高效還好寫。(考試出了個題才發現不會莫隊)。其具體的操作大致就是把一個區間離線下來,後根據端點一點一點的跳,具體的過程可以見這篇部落格。下邊以例題來講解莫隊。
例題
莫隊板子題。題目意思就是給你一段序列,每次詢問一段區間,問其中有幾種元素,那麼如果暴力列舉的話肯定過不了,按照莫隊的思想,我們每一次區間詢問的時候,從上一個區間依次跳過來。跳每個點的時候,假如當前端點在查詢的區間之外,那麼每一次我們就讓當前點的個數減一,如果減完,那麼區間元素個數也就是答案就減一。同理,如果端點在區間內部,那麼就需要往外跳,如果跳到一個點,它的值沒有出現過,那麼答案加一,出現過就直接讓它的個數加一就行了。這樣我們就解決了這道題。
程式碼
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #include<cmath> #include<queue> using namespace std; const int L = 1 << 20; char buffer[L],*S,*T; #define gc (S == T && (T = (S = buffer) + fread(buffer,1,L,stdin),S == T) ? EOF : *S++) #define read() ({int s = 0,f = 1;char ch = gc;for(;!isdigit(ch);ch = gc)if(ch == '-')f = -1;for(;isdigit(ch);ch = gc)s = s * 10 + ch - '0';s * f;}) const int maxn = 1e6+10; int bel[maxn],block; int a[maxn]; int cnt[maxn]; int sum,ans[maxn]; struct Node{ int l,r,id; friend bool operator < (const Node& a,const Node& b){ return bel[a.l] == bel[b.l] ? a.r < b.r : bel[a.l] < bel[b.l]; } }q[maxn]; inline void Del(int x){ cnt[a[x]]--; if(!cnt[a[x]])sum--; } inline void Add(int x){ if(!cnt[a[x]])sum++; cnt[a[x]]++; } int main(){ int n = read(); block = sqrt(n); for(int i = 1;i <= n;++i){ a[i] = read(); bel[i] = (i - 1) / block + 1; } int m = read(); for(int i = 1;i <= m;++i){ q[i].l = read();q[i].r = read(); q[i].id = i; } sort(q+1,q+m+1); int l = 1,r = 0; for(int i = 1;i <= m;++i){ int ql = q[i].l,qr = q[i].r; while(l < ql)Del(l++); while(l > ql)Add(--l); while(r < qr)Add(++r); while(r > qr)Del(r--); ans[q[i].id] = sum; } for(int i = 1;i <= m;++i){ printf("%d\n",ans[i]); } return 0; }
Continue
當然,莫隊不僅如此,如果僅僅是這樣的話,那麼這個演算法也不能叫做高效暴力。假如我們的區間是 \([1,2]\),\([99999,100000]\) 交錯進行,那麼就不可行了。如果一直跳,肯定還不如直接跑暴力。所以我們來深入剖析一下莫隊(大霧)。
1、預處理
莫隊演算法優化的核心是分塊和排序。我們將大小為n的序列分為 \(\sqrt n\) 個塊,編號,然後根據這個對查詢區間進行排序。一種方法是把查詢區間按照左端點所在塊的序號排個序,如果左端點所在塊相同,再按右端點排序。排完序後我們再進行左右指標跳來跳去的操作,雖然看似沒多大用,但帶來的優化實際上極大。複雜度 \(O(n\sqrt n)\)
由於我們需要對區間進行排序,所以莫隊就不支援修改了,(當然有帶修改莫隊,但是我還不會)。
排序Code
struct Node{
int l,r,id;
friend bool operator < (const Node& a,const Node& b){
return bel[a.l] == bel[b.l] ? a.r < b.r : bel[a.l] < bel[b.l];
}
}q[maxn];
2、答案計算
莫隊策略就是每跳一個距離就計算一下答案,所以在計算的時候,一般需要推一些式子,然後根據每一次的變化量等一些規律來進行答案的修改。
3、卡常
莫隊通常是被卡的重災區,如果想用莫隊拿更多的分,有時候是需要卡常的。
*1、快讀
最基本的,這裡給一個特殊的(更nb……大霧)
const int L = 1 << 20;
char buffer[L],*S,*T;
#define gc (S == T && (T = (S = buffer) + fread(buffer,1,L,stdin),S == T) ? EOF : *S++)
#define read() ({int s = 0,f = 1;char ch = gc;for(;!isdigit(ch);ch = gc)if(ch == '-')f = -1;for(;isdigit(ch);ch = gc)s = s * 10 + ch - '0';s * f;})
*2、奇偶性排序
玄學奇偶性排序,優化巨大。它的主要原理便是右指標跳完奇數塊往回跳時在同一個方向能順路把偶數塊跳完,然後跳完這個偶數塊又能順帶把下一個奇數塊跳完。理論上主演算法執行時間減半,實際情況有所偏差。
struct Node{
int l,r,id;
friend bool operator < (const Node& a,const Node& b){
return (bel[a.l] ^ bel[b.l]) ? bel[a.l] < bel[b.l] : ((bel[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
}q[maxn];
*3、常數的縮小
有很多種方式,比如 \(register\) ,需要呼叫函式的寫成巨集定義或者直接寫等等。
基礎莫隊差不多就這些了,寫幾個題印象和理解就會深刻許多了。
\(Never\ Give\ Up\)