神奇的莫隊

Part -1: 參考資料

參考資料1
萬分感謝這個大佬,祝他報送清華北大!html


Part 0: 一些介紹

莫隊由莫濤神仙首次提出,是一種區間操做算法。git

即使是板子題,難度也很高(差評)算法

因此,在閱讀後文以前,請你先深呼吸,喝杯咖啡,吃點餅乾,聽聽本身喜歡的歌數組

而後,中止呼吸,放下杯子,扔開餅乾,摘下耳機,接受莫濤大神思想光輝的洗禮優化


Part 1:莫隊算法的引入

先別談莫隊,咱們來回顧一下,遇到區間問題通常怎麼解決?spa

很好,暴力線段樹3d

也就是說,咱們一直在經過維護兩個序列——左序列\([l,mid]\)與右序列\([mid + 1,r]\),從而來維護\([l, r]\),固然,這個操做會一直遞歸下去指針

然而,當題目這麼問:code

令數組\(Q\)大小爲\(n\)且每一個元素\(Q_i < n\),有\(m\)個詢問,每次詢問給定\(l,r\),請找出\([l,r]\)中至少重複出現\(k\)此的數字的個數htm

換句話說:

\(Q_l\)\(Q_r\)內找出現次數多餘\(k\)的數字的個數

of course,你能夠暴力,但你會暴零

那麼咱們試着用線段樹,首先,你須要維護左邊的序列,而後你須要維護右邊的序列,而後……

而後你會發現很難作到短期甚至\(O(1)\)的時間完成對線段樹單一節點的維護,由於你老是要層層遞進向上疊加。

淦!這不是欺負人嗎

咱們先試試暴力吧,用個\(count\)記錄一下出現次數,而後在掃一遍

暴力是萬能的,答案固然正確,可是你的時間複雜度哭了——\(O(n^2)\)

那麼咱們能夠看看是否能夠改進一下,用上\(t(wo)p(oints)\)算法:

假設有兩個指針,\(l\)\(r\),每次詢問的時候用移動\(l\)\(r\)的方式來嘗試和要求區間重合

是否是有點蒙?我舉個栗子

此圖中,兩個Q是待求的區間

初始化\(r = 0,l = 1\)

此時,發現\(l\)和要求的區間左端重合了,而\(r\)沒有,那麼咱們把\(r\)往右邊移動一位

此時,\(r\)發現了一個新的值\(0\),總數記錄一下,繼續右移動

\(r\)又發現了一個新數值\(2\),總數記錄一下,繼續右移動

此處\(2\)被記錄過了,總數值不變

一直到\(r\)與右端點重合,獲得下圖:

第一個區間就算處理完了,咱們來看下一個

首先,\(l\)不在左端點,咱們把它右移

這一次,\(l\)所遇到的數值在區間\([l, r]\)只可以存在,總數不變

下一次也是如此,一直到

你會發現,這時,區間\([l,r]\)將(也就是在下一次移動後)不會有\(2\)存在了,那麼總數就一個\(-1\),而正好本題須要統計的就是區間內數值的個數,總數改變:

如此循環往復,獲得最終答案,因此咱們能夠得出這個代碼

int arr[maxn], cnt[maxn]   // 每一個位置的數值、每一個數值的計數器
int l = 1, r = 0, now = 0; // 左指針、右指針、當前統計結果(總數)
void add(int pos) {             // 添加一個數
    if(!cnt[arr[pos]]) ++ now;  // 在區間中新出現,總數要+1
    ++ cnt[arr[pos]];
}
void del(int pos) {             // 刪除一個數
    -- cnt[arr[pos]];
    if(!cnt[arr[pos]]) -- now;  // 在區間中再也不出現,總數要-1
}
void work() {
    for(int i = 1; i <= q; i ++) {
        int ql, qr;
        scanf("%d%d", &ql, &qr);    
        while(l < ql) del(l++); // 左指針在查詢區間左方,左指針向右移直到與查詢區間左端點重合
        while(l > ql) add(--l); // 左指針在查詢區間左端點右方,左指針左移
        while(r < qr) add(++r); // 右指針在查詢區間右端點左方,右指針右移
        while(r > qr) del(r--); // 不然左移
        printf("%d\n", now);    // 輸出統計結果
    }
}

嗯,幹得漂亮,可是這是莫隊嗎?不是

若是區間特別多,\(l,r\)反覆橫跳,結果皮斷了腿,時間複雜度\(O(nm)\)

那麼如今的問題已經變成了:如何儘可能減小\(l,r\)移動的次數


Part 2:莫隊的正確打開方式

首先,看到儘可能減小\(l,r\)移動的次數,咱們會想到排個序

排序排什麼的順序呢?是排端點嗎?顯然不是,哪怕左端點有序,右端點就會雜亂無章;右端點有序,左端點就會雜亂無章……

這裏,咱們運用一下分塊的思想,把序列分爲\(\sqrt{n}\)塊,把查詢區間按照左端點所在塊的序號排個序,若是左端點所在塊相同,再按右端點排序。

這個算法須要的時間複雜度爲\(sort+move_{\texttt{左指針}}\)

因爲\(sort\)的時間複雜度爲\(O(n\log n)\)\(move_{\texttt{作指針}}\)的時間複雜度爲\(O(n\sqrt{n})\),那麼總的時間複雜度爲\(O(n\sqrt{n})\)

好耶!降了一個根號!鼓掌!

其次,咱們須要考慮一下更新的策略

通常來講,咱們只要找到指針移動一位之後,統計數據與當前數據的差值,找出規律(能夠用數學方法或打表),而後每次移動時用這個規律更新就行

最後給出總代碼:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;

#define maxn 1010000
#define maxb 1010
int aa[maxn], cnt[maxn], belong[maxn];
int n, m, size, bnum, now, ans[maxn];
struct query {
	int l, r, id;
} q[maxn];

int cmp(query a, query b) {
	return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
int read() {
	int res = 0;
	char c = getchar();
	while(!isdigit(c)) c = getchar();
	while(isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar();
	return res;
}
void printi(int x) {
	if(x / 10) printi(x / 10);
	putchar(x % 10 + '0');
}

int main() {
	scanf("%d", &n);
	size = sqrt(n);
	bnum = ceil((double)n / size);
	for(int i = 1; i <= bnum; ++i) 
		for(int j = (i - 1) * size + 1; j <= i * size; ++j) {
			belong[j] = i;
		}
	for(int i = 1; i <= n; ++i) aa[i] = read(); 
	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, cmp);
	int l = 1, r = 0;
	for(int i = 1; i <= m; ++i) {
		int ql = q[i].l, qr = q[i].r;
		while(l < ql) now -= !--cnt[aa[l++]];
		while(l > ql) now += !cnt[aa[--l]]++;
		while(r < qr) now += !cnt[aa[++r]]++;
		while(r > qr) now -= !--cnt[aa[r--]];
		ans[q[i].id] = now;
	}
	for(int i = 1; i <= m; ++i) printi(ans[i]),putchar('\n');
	return 0;
}

Part 3:關於莫隊的一些卡常數

卡常數做爲OIer的屢見不鮮,相信你們必定不陌生了

卡常數包括:

  • 位運算
  • O2
  • 快讀
  • ……

而莫隊的神奇之處在於他的獨特優化:奇偶性排序
原代碼:

int cmp(query a, query b) {
    return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l];
}

改成

int cmp(query a, query b) {
	return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}

別人說跑的很快我還不信,本身跑了一下才知道……

真的跑的很快啊……


Part 4: 能修改的莫隊

我知道,你拿着上面別個大佬寫的代碼(再次膜拜寫這個代碼的大佬orz)興沖沖的去刷題,一路上披荊斬棘,直到你看到了Luogu1903——國家集訓隊-數顏色,你完全傻了眼

媽耶,他要是這麼一修改我豈不是要從新sort?跑了跑了

因爲莫隊自己就是離線的,而你須要修改,得想個辦法讓他在線,具體作法是:「就是再弄一指針,在修改操做上跳來跳去,若是當前修改多了就改回來,改少了就改過去,直到次數恰當爲止。」 (再次感謝這個大佬,,好喜歡這個解釋)

相關文章
相關標籤/搜索