莫隊算法——從入門到黑題

衆所周知,莫隊是由莫濤大神提出的,一種玄學毒瘤暴力騙分區間操做算法,它以簡短的框架、簡單易記的板子和優秀的複雜度聞名於世。然而因爲莫隊算法應用的毒瘤,不少可作的莫隊模板題都有着較高的難度評級,令不少初學者望而卻步。然而,若是你真正理解了莫隊的算法原理,那麼它用起來仍是很簡單的。固然某些套左套右的毒瘤除外


0、前置芝士:

莫隊算法仍是比較獨立的。不過你仍是得了解了解如下的一些知識:

\(1\)、分塊的基本思想(開根號等)php

\(2\)、STL中sort的用法(手寫cmp函數或重載運算符實現結構體的多關鍵字排序)git

\(3\)、基(du)礎(liu)的卡常技巧(包含#pragma GCC optimize系列)算法

\(4*\)、倍增/樹剖 求LCA(樹上莫隊所需)數組

\(5*\)、數值離散化(用於應付不少題目)框架

至此所有完畢。撒花~~(霧

誒,別走啊qwq,我可不是在勸退qwq,若是你認爲本身不懂這些東西也不要緊,往下看吧qwq


一、莫隊算法是個啥

來歷:

前面已經介紹過了(逃函數

有興趣的同窗能夠看一下莫濤大神的知乎優化

然而這個算法究竟是用來搞什麼操做的呢?咱們先看個例題:

Luogu P1972 [SDOI2009]HH的項鍊

題目描述

HH 有一串由各類漂亮的貝殼組成的項鍊。HH 相信不一樣的貝殼會帶來好運,因此每次散步完後,他都會隨意取出一段貝殼,思考它們所表達的含義。HH 不斷地收集新的貝殼,所以,他的項鍊變得愈來愈長。有一天,他忽然提出了一個問題:某一段貝殼中,包含了多少種不一樣的貝殼?這個問題很難回答……由於項鍊實在是太長了。因而,他只好求助睿智的你,來解決這個問題。spa

輸入輸出格式

輸入格式:

第一行:一個整數N,表示項鍊的長度。3d

第二行:N 個整數,表示依次表示項鍊中貝殼的編號(編號爲0 到1000000 之間的整數)。指針

第三行:一個整數M,表示HH 詢問的個數。

接下來M 行:每行兩個整數,L 和R(1 ≤ L ≤ R ≤ N),表示詢問的區間。

輸出格式:

M 行,每行一個整數,依次表示詢問對應的答案。

輸入輸出樣例

輸入樣例#1:

6
1 2 3 4 3 5
3
1 2
3 5
2 6

輸出樣例#1:

2
2
4

說明

數據範圍:

對於100%的數據,N <= 500000,M <= 500000。


題意簡明易懂:給你一個長度不大於\(n≤5×10^5\)的序列,其中數值都小於等於\(10^6\),有\(m≤5×10^5\)次詢問,每次詢問區間\([l,r]\)中數值個數(也就是去重後數字的個數)。

不過這個例題卡了莫隊,因此請左轉數據弱化版:SP3267 DQUERY - D-query

題目到手,咱們開始分析本題的算法。這題最簡單作法無非暴力——用一個\(cnt\)數組記錄每一個數值出現的次數,再暴力枚舉\(l\)\(r\)統計次數,最後再掃一遍cnt數組,統計\(cnt\)不爲零的數值個數,輸出答案便可。設最大數值爲\(s\),那麼這樣作的複雜度爲\(O(m(n+s))∽O(n^2)\),對於本題實在跑不起。

咱們能夠嘗試優化一下:

優化1:每次枚舉到一個數值\(num\),增長出現次數時判斷一下\(cnt_{num}\)是否爲0,若是爲0,則這個數值以前沒有出現過,如今出現了,數值數固然要+1。反之在從區間中刪除\(num\)後也判斷一下\(cnt_{num}\)是否爲0,若是爲0數值總數-1。這樣咱們優化掉了一個\(O(ms)\),但仍是跑不起。

優化2:咱們弄兩個指針 \(l\)\(r\) ,每次詢問不直接枚舉,而是移動 \(l\)\(r\) 指針到詢問的區間,直到\([l,r]\)與詢問區間重合。在統計答案時,咱們也只在兩個指針處加減\(cnt\),而後咱們就能夠用優化1中的方法快速地統計答案啦\(qwq\)


優化2具體步驟以下:

假設這個序列是這樣子的:(其中\(Q1\)\(Q2\)是詢問區間)

咱們初始化\(l=1\)\(r=0\)(若是\(l=0\),那麼咱們還須要刪除一個數值\(0\),使其出現次數變成-1,致使一些奇奇怪怪錯誤),以下圖(因爲畫圖軟件中\(l\)\(1\)看不出區別,我只好在圖中使用\(L\)\(R\)來表示qwq):

咱們發現 \(l\) 已是第一個查詢區間的左端點,無需移動。如今咱們將 \(r\) 右移一位,發現新數值1:

\(r\) 繼續右移,發現新數值2:

繼續右移,發現新數值4:

\(r\) 再次右移時,發現此時的新位置中的數值2出現過,數值總數不增:

接下來是兩個7,因爲7沒出現過,因此總數+1:

繼續右移發現3:

繼續右移,但接下來的兩個數值都出現過,總數不增。

至此,\(Q1\)區間全部數值統計完成,結果爲5。

如今咱們又看一下\(Q2\)區間的狀況:

首先咱們發現, \(l\) 指針在\(Q2\)區間左端點的左邊,咱們須要將它右移,同時刪除原位置的統計信息。

\(l\)右移一位到位置2,刪除位置1處的數值1。但因爲操做後的區間中仍然有數值1存在,因此總數不減。

接下來的兩位也是如此,直接刪掉便可,總數不減。

\(l\) 指針繼續右移時,發現一個問題:原位置上的數值是2,可是刪除這個2後,此時的區間\([l,r]\)中再也沒有2了(回顧以前的內容,這種狀況就是刪除後\(cnt_2 = 0\)),那麼總數就要-1,由於有一個數值已經不在該區間內出現了,而本題須要統計的就是區間內的數值個數。此步驟以下圖:

再右移一位,發現無需減總數,並且\(l\)已經移到了\(Q2\)區間的左端點,無需繼續移下去(以下圖)。固然 \(r\) 仍是要移動的,只不過沒圖了,我相信你們應該知道作法的\(qwq\)

\(r\)的最後位置:

至於刪除操做,也是同樣的作法,只不過要先刪除當前位置的數值,才能移動指針。

有了以上的內容,這段代碼就能夠很容易寫出啦qwq:

int aa[maxn], cnt[maxn], l = 1, r = 0, now = 0; //每一個位置的數值、每一個數值的計數器、左指針、右指針、當前統計結果(總數)
void add(int pos) {//添加一個數
    if(!cnt[aa[pos]]) ++now;//在區間中新出現,總數要+1
    ++cnt[aa[pos]];
}
void del(int pos) {//刪除一個數
    --cnt[aa[pos]];
    if(!cnt[aa[pos]]) --now;//在區間中再也不出現,總數要-1
}
void work() {//優化2主過程
    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);//輸出統計結果
    }
}

優化2完結撒花✿✿ヽ(°▽°)ノ✿\(qwq\)


誒等等,什麼叫作「優化2完結撒花」??!

難道這不就是莫隊嗎??!

我會很嚴肅的告訴你:這還不是莫隊,可是看到這裏,你已經把莫隊的基礎打好了。還請繼續看下去:


剛剛的優化2,在普通的狀況下表現很好,可是若是區間是這樣:

優化2基本上就萎了\(qwq\)。此時\(l\)\(r\)指針在整個序列中移來移去,從頭至尾,又從尾到頭。咱們發現左右指針最壞狀況下均移動了\(O(nm)\)次,\(O(1)\)更新答案,總時間複雜度仍然是\(O(nm)\),在最壞狀況下跑得比慢的一批的優化1還慢。儘管如此,咱們仍是能夠繼續優化。

繼續優化?怎麼優化?

咱們能夠考慮把全部查詢區間按左端點排序,從而使左指針最多移動\(O(n)\)次。但這樣的話右端點又是無序的,右指針又讓總體複雜度打回原形。看上去,這個複雜度已經不能再優化了。在這個時候,莫隊算法的出現,給無數OIer帶來了光明(霧)。

至此,你能夠把莫隊算法理解爲一種暴力,優雅而不失複雜度的暴力,只不過它的剪枝極爲巧妙,達到了理想的效果。


二、莫隊算法的基礎實現

一、預處理

莫隊算法優化的核心是分塊和排序。咱們將大小爲\(n\)的序列分爲\(\sqrt{n}\)個塊,從\(1\)\(\sqrt{n}\)編號,而後根據這個對查詢區間進行排序。一種方法是把查詢區間按照左端點所在塊的序號排個序,若是左端點所在塊相同,再按右端點排序。排完序後咱們再進行左右指針跳來跳去的操做,雖然看似沒多大用,但帶來的優化實際上極大。


那麼這樣作的實際複雜度是多少呢?下面瞎胡亂搞證實它的複雜度是\(O(n\sqrt{n})\)

0.區間排序

建個結構體,用sort跑一遍便可。平均複雜度\(O(n\log n)\)

1.左指針的移動

設每一個塊 \(i\) 中分佈有 \(x_i\)個左端點,因爲莫隊的添加、刪除操做複雜度爲\(O(1)\),那麼處理塊\(i\)的最壞時間複雜度是\(O(x_i\sqrt{n})\),指針跨越整塊的時間複雜度爲O(\sqrt{n}),最壞須要跨越\(n\)次;總複雜度\(O(\sum x_i \sqrt{n}+n\sqrt{n})=O(n\sqrt{n})\)

2.右指針的移動

設每一個塊 \(i\) 中分佈有 \(x_i\)個左端點,因爲左端點同塊的區間右端點有序,那麼對於這\(x_i\)個區間,右端點最壞只需總共\(O(n)\)的時間跳(最壞需跳完整個序列),總共\(\sqrt{n}\)個塊,總複雜度\(O(n\sqrt{n})\)

至此可得出,莫隊算法的總時間複雜度爲\(O(n\sqrt{n}) + O(n\sqrt{n}) + O(n\log n) = O(n\sqrt{n})\)


可見,通過一番看似雞肋的排序以後,這個算法的複雜度猛降了一個根號之多,對於一些不須要寫大常數莫隊而數據範圍巨大的題目來講(如例題),整整一個根號的提高意味着運行時間質的飛躍。

不過通過排序打亂原序以後,這個算法就變成了典型的離線算法,並且這種算法不支持修改。若是遇到強制在線的題目,還要另尋他法。


參考代碼:

查詢區間結構體的排序函數:

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

二、定策略

雖然說莫隊實質是優化後的暴力,但有時候,有些用暴力枚舉很容易處理的數據用莫隊並不容易處理(只能在左右指針處更新),這時候就要咱們定好一個更新策略。
通常來講,咱們只要找到指針移動一位之後,統計數據與當前數據的差值,找出規律(能夠用數學方法或打表),而後每次移動時用這個規律更新就行啦qwq。至於例題……在後面會有噠qwq!

三、碼代碼與查錯

莫隊代碼不長(或者說是很短),但很容易寫錯一些細節。好比自加自減運算符的優先級問題、排序關鍵字問題、分塊大小與sqrt精度問題、還有某些題目中用到的離散化的鍋。因此每次碼完莫隊都別先測樣例(甚至能夠先不編譯),先靜態查錯一陣,真的能夠幫助你大大減小錯誤的發生。

後面兩點都特別簡單可是沒有特定的模式,其重要性不容小覷。


三、(重點)莫隊的玄學卡常技巧

WARNING:如下內容可能引出大賢者模式,請謹慎思考。

一、#pragma GCC optimize(2)

能夠用實踐證實,開了O2的莫隊簡直跑得飛快,連\(1e6\)都能無壓力跑過,甚至能夠比不開O2的版本快上4~5倍乃至更多。然而部分OI比賽中O2是禁止的,若是不由O2的話,那仍是開着吧qwq

實在不行,就optimize(3)(逃

二、莫隊玄學奇偶性排序

這個是最玄學的……無力吐槽

這個和莫隊的主算法有殊途同歸之妙……看起來卵用都沒有,實際上能夠幫你每一個點平均優化200ms(可怕)

主要操做:把查詢區間的排序函數

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);
}

也就是說,對於左端點在同一奇數塊的區間,右端點按升序排列,反之降序。這個東西也是看着沒用,但實際效果顯著。

它的主要原理即是右指針跳完奇數塊往回跳時在同一個方向能順路把偶數塊跳完,而後跳完這個偶數塊又能順帶把下一個奇數塊跳完。理論上主算法運行時間減半,實際狀況有所誤差。(不過能優化得很爽就對了)

三、移動指針的常數壓縮

咱們能夠根據運算優先級的知識,把這個:

void add(int pos) {
    if(!cnt[aa[pos]]) ++now;
    ++cnt[aa[pos]];
}
void del(int pos) {
    --cnt[aa[pos]];
    if(!cnt[aa[pos]]) --now;
}

和這個:

while(l < ql) del(l++);
while(l > ql) add(--l);
while(r < qr) add(++r);
while(r > qr) del(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--]];

能優化將近200ms(怎麼又是這個數字)

並且這個優化看上去滿滿的很差搞,但實際上頗有用。不過用它來優化千萬要創建在熟練的基礎上,否則會大大加強調試難度,不如不用。

四、手寫快讀、快輸

大多數莫隊題的輸入輸出量仍是很大的……I/O優化與否,運行時間差別也很大。並且值得注意的是莫隊經典題中基本沒有輸入輸出負數的狀況,不考慮負數又能優化一點小小的常數。

卡常部分到此結束,撒花✿✿ヽ(°▽°)ノ✿(腦補歡呼音效)


講到如今,例題的代碼已經不難寫出。下面給出參考代碼:

#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;
}

四、莫隊算法的擴展——帶修改的莫隊

拋出個例題:Luogu P1903 [國家集訓隊]數顏色 / 維護隊列

題目描述

墨墨購買了一套N支彩色畫筆(其中有些顏色可能相同),擺成一排,你須要回答墨墨的提問。墨墨會向你發佈以下指令:

一、 \(Q\) \(L\) \(R\)表明詢問你從第\(L\)支畫筆到第\(R\)支畫筆中共有幾種不一樣顏色的畫筆。

二、 \(R\) \(P\) \(Col\) 把第\(P\)支畫筆替換爲顏色\(Col\)

爲了知足墨墨的要求,你知道你須要幹什麼了嗎?

輸入輸出格式

輸入格式:

第1行兩個整數\(N\)\(M\),分別表明初始畫筆的數量以及墨墨會作的事情的個數。

第2行N個整數,分別表明初始畫筆排中第i支畫筆的顏色。

第3行到第2+M行,每行分別表明墨墨會作的一件事情,格式見題幹部分。

輸出格式:

對於每個Query的詢問,你須要在對應的行中給出一個數字,表明第L支畫筆到第R支畫筆中共有幾種不一樣顏色的畫筆。

輸入輸出樣例

輸入樣例#1:

6 5
1 2 3 4 5 5
Q 1 4
Q 2 6
R 1 2
Q 1 4
Q 2 6

輸出樣例#1:

4
4
3
4

說明

對於\(100%\)的數據,\(N≤50000\)\(M≤50000\),全部的輸入數據中出現的全部整數均大於等於1且不超過\(10^6\)

前面說過,莫隊算法是離線算法,不支持修改,強制在線須要另尋他法。的確,遇到強制在線的題目莫隊基本上萎了,可是對於某些容許離線的帶修改區間查詢來講,莫隊仍是能大展拳腳的。作法就是把莫隊直接加上一維,變爲帶修莫隊。

那麼加上一維什麼呢?具體怎麼實現?咱們的作法是把修改操做編號,稱爲"時間戳",而查詢操做的時間戳沿用以前最近的修改操做的時間戳。跑主算法時定義當前時間戳爲 \(t\) ,對於每一個查詢操做,若是當前時間戳相對太大了,說明已進行的修改操做比要求的多,就把以前改的改回來,反之日後改。只有噹噹前區間和查詢區間左右端點、時間戳均重合時,才認定區間徹底重合,此時的答案纔是本次查詢的最終答案。

通俗地講,就是再弄一指針,在修改操做上跳來跳去,若是當前修改多了就改回來,改少了就改過去,直到次數恰當爲止。

這樣,咱們當前區間的移動方向從四個(\([l-1,r]\)\([l+1,r]\)\([l,r-1]\)\([l,r+1]\))變成了六個(\([l-1,r,t]\)\([l+1,r,t]\)\([l,r-1,t]\)\([l,r+1,t]\)\([l,r,t-1]\)\([l,r,t+1]\)),可是代碼並無增長多少,仍是很好背的(霧

帶修改莫隊的排序:

其實排序的主要方法仍是跟普通莫隊沒兩樣,只不過是加了個關鍵字而已。

排序函數:

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

可是實測有時排序寫錯還會快些。。。也許是評測機的鍋吧。

主算法中的修改操做

修改操做其實也沒啥值得注意的,就跟移\(l\)\(r\)指針同樣,加個對總數的特判就好了。

不過有個代碼長度的小優化——移完 \(t\),作完一處修改後,有可能要改回來,因此咱們還要把原值存好備用。但其實咱們也能夠不存,只要在修改後把修改操做的值和原值swap一下,那麼改回來時也只要swap一下,swap兩次至關於沒搞,就改回來了\(qwq\)(因此不仍是存了嘛)

分塊大小和複雜度

有的\(dalao\)證實了當塊的大小設\(\sqrt[3]{n^4t}\)時理論複雜度達到最優,可是小蒟蒻我並不能推出來。不過能夠證實,塊大小取\(n^{\frac{2}{3}}\)優於取\(\sqrt{n}\)的狀況,整體複雜度\(O(n^{\frac{5}{3}})\)。而塊大小取\(\sqrt{n}\)時會退化成\(O(n^2)\),不建議使用。

例題參考代碼:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
#define maxn 50500
#define maxc 1001000
int a[maxn], cnt[maxc], ans[maxn], belong[maxn];
struct query {
    int l, r, time, id;
} q[maxn];
struct modify {
    int pos, color, last;
} c[maxn];
int cntq, cntc, n, m, size, bnum;
int cmp(query a, query b) {
    return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.r] ^ belong[b.r]) ? belong[a.r] < belong[b.r] : a.time < b.time);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
inline 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;
}
int main() {
    n = read(), m = read();
    size = pow(n, 2.0 / 3.0);
    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) 
        a[i] = read();
    for(int i = 1; i <= m; ++i) {
        char opt[100];
        scanf("%s", opt);
        if(opt[0] == 'Q') {
            q[++cntq].l = read();
            q[cntq].r = read();
            q[cntq].time = cntc;
            q[cntq].id = cntq;
        }
        else if(opt[0] == 'R') {
            c[++cntc].pos = read();
            c[cntc].color = read();
        }
    }
    sort(q + 1, q + cntq + 1, cmp);
    int l = 1, r = 0, time = 0, now = 0;
    for(int i = 1; i <= cntq; ++i) {
        int ql = q[i].l, qr = q[i].r, qt = q[i].time;
        while(l < ql) now -= !--cnt[a[l++]];
        while(l > ql) now += !cnt[a[--l]]++;
        while(r < qr) now += !cnt[a[++r]]++;
        while(r > qr) now -= !--cnt[a[r--]];
        while(time < qt) {
            ++time;
            if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
            swap(a[c[time].pos], c[time].color);
        }
        while(time > qt) {
            if(ql <= c[time].pos && c[time].pos <= qr) now -= !--cnt[a[c[time].pos]] - !cnt[c[time].color]++;
            swap(a[c[time].pos], c[time].color);
            --time;
        }
        ans[q[i].id] = now;
    }
    for(int i = 1; i <= cntq; ++i) 
        printf("%d\n", ans[i]);
    return 0;
}

五、莫隊算法的擴展——樹上莫隊

前面咱們所使用的莫隊都是在一維的序列上進行,即便加了一維的時間軸,可是主題仍是一維序列。那麼樹上統計問題可否用莫隊來處理呢?答案是確定的。

不要認爲這個東西很高級,實際上它仍是個序列。

問題一:子樹統計

子樹統計算是這裏面最簡單的了。在原樹上跑一遍dfs序,而後發現一顆子樹其實就是裏面一段固定區間……

邊跑dfs邊弄子樹對應的左右端點便可。

這裏序列的長度=結點的個數。

實際上子樹上的統計徹底不須要莫隊,傳個標記就能夠\(O(nlogn)\)

問題二:路徑上的統計

好比說這個題目:SP10707 COT2 - Count on a tree II

題目描述

給定一個n個節點的樹,每一個節點表示一個整數,問u到v的路徑上有多少個不一樣的整數。

輸入格式

第一行有兩個整數n和m(n=40000,m=100000)。

第二行有n個整數。第i個整數表示第i個節點表示的整數。

在接下來的n-1行中,每行包含兩個整數u v,描述一條邊(u,v)。

在接下來的m行中,每一行包含兩個整數u v,詢問u到v的路徑上有多少個不一樣的整數。

輸出格式

對於每一個詢問,輸出結果。

輸入輸出樣例

輸入樣例#1:

8 2
105 2 9 3 8 5 7 7
1 2
1 3
1 4
3 5
3 6
3 7
4 8
2 5
7 8

輸出樣例#1:

4
4

這還不簡單嗎?dfs序一遍找區間……誒?區間呢qwq?

參照上圖,我能夠負責地告訴你:普通dfs序是徹底不行的(由於區間沒有對應關係)。

可是還好咱們有歐拉序,這是一種特殊的dfs序,能夠解決不少普通dfs序解決不了的問題(就好比咱們的樹上莫隊)。

那歐拉序有什麼特色呢?怎麼求它?

仍是那張圖,咱們對它求一遍歐拉序:

這是個什麼東西?!它怎麼求得的暫且不談(不過你也應該已經知道了),先看看它的性質:

咱們看一看每一個編號出現的次數——兩次,無一例外。再看看它出現的兩個位置有什麼特色:

咱們以編號\(2\)爲例,它出如今位置2和9,它中間的編號有\(4×2\)\(7×2\)\(5×2\)

再觀察這棵樹,誒,這些編號不都是\(2\)的子樹上的結點嗎??!

就這樣,咱們得出它的一條性質:樹的歐拉序上兩個相同編號(設爲\(x\))之間的全部編號都出現兩次,且都位於\(x\)子樹上。(前半句話其實能夠由後半句話間接證實)

它的求法也很簡單,在剛dfs到一個點時加入序列,最後退出時也加入一遍。如今知道這個性質的來源了吧qwq

那麼爲何用歐拉序能夠把路徑搬到區間上呢?咱們來看一下這張圖:

咱們在歐拉序中找到路徑\(1\rightarrow10\)起點(1)終點(10)的位置。咱們發現,咱們徹底能夠在找到對應的區間(綠色部分),而因爲其中有一些點出現了兩次,這些出現了兩次的點能夠證實不在路徑上(路徑不會通過一個點兩次,而若是隻通過一次則不會出現兩個相同的編號),因此出現了兩次的點咱們不予算入。

那咱們嘗試找一下\(2\rightarrow 6\)對應的區間吧。唔,這還不簡單嗎,不就是二、四、7……三、6……嗯?1哪去了?1呢?^1但是他們的\(lca\)啊!!看來這樣單純的找區間仍是不行的,還有其餘特殊方法。

具體作法:設每一個點的編號\(a\)首次出現的位置\(first[a]\),最後出現的位置爲\(last[a]\),那麼對於路徑\(x\rightarrow y\),設\(first[x]<=first[y]\)(不知足則swap,這個操做的意義在於,若是\(x\)\(y\)在一條鏈上,則\(x\)必定是\(y\)的祖先或等於\(y\)),若是\(lca(x,y)=x\),則直接把\([first[x],first[y]]\)的區間扯過來用,反之使用\([last[x],first[y]]\)區間(爲何不用\([first[x],first[y]]\)?由於\((first[x],last[x])\)不會在路徑上,根據性質,裏面的編號都會出現兩次,考慮了等於沒考慮),但這個區間內不包含\(x\)\(y\)的最近公共祖先,查詢的時候加上便可。

注意,這裏序列長度爲\(2×n\),千萬不要在這T了啊……qwq


作完了這些,樹上莫隊的其餘東西就和普通莫隊差很少啦。值得注意的是,咱們又能夠像上文的帶修莫隊那樣優化代碼長度——因爲無需考慮的點會出現兩次,咱們能夠弄一個標記數組(標記結點是否被訪問),沒訪問就加,訪問過就刪,每次操做把標記·異或個1,完美解決全部添加、刪除、去雙問題。

例題參考代碼:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
#define maxn 200200
#define isdigit(x) ((x) >= '0' && (x) <= '9')
inline 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;
}
int aa[maxn], cnt[maxn], first[maxn], last[maxn], ans[maxn], belong[maxn], inp[maxn], vis[maxn], ncnt, l = 1, r, now, size, bnum; //莫隊相關
int ord[maxn], val[maxn], head[maxn], depth[maxn], fa[maxn][30], ecnt;
int n, m;
struct edge {
    int to, next;
} e[maxn];
void adde(int u, int v) {
    e[++ecnt] = (edge){v, head[u]};
    head[u] = ecnt;
    e[++ecnt] = (edge){u, head[v]};
    head[v] = ecnt;
}
void dfs(int x) {
    ord[++ncnt] = x;
    first[x] = ncnt;
    for(int k = head[x]; k; k = e[k].next) {
        int to = e[k].to;
        if(to == fa[x][0]) continue;
        depth[to] = depth[x] + 1;
        fa[to][0] = x;
        for(int i = 1; (1 << i) <= depth[to]; ++i) fa[to][i] = fa[fa[to][i - 1]][i - 1];
        dfs(to);
    }
    ord[++ncnt] = x;
    last[x] = ncnt;
}
int getlca(int u, int v) {
    if(depth[u] < depth[v]) swap(u, v);
    for(int i = 20; i + 1; --i) 
        if(depth[u] - (1 << i) >= depth[v]) u = fa[u][i];
    if(u == v) return u;
    for(int i = 20; i + 1; --i)
        if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
    return fa[u][0];
}
struct query {
    int l, r, lca, 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);
}
void work(int pos) {
    vis[pos] ? now -= !--cnt[val[pos]] : now += !cnt[val[pos]]++;
    vis[pos] ^= 1;
}
int main() {
    n = read(); m = read();
    for(int i = 1; i <= n; ++i) 
        val[i] = inp[i] = read();
    sort(inp + 1, inp + n + 1);
    int tot = unique(inp + 1, inp + n + 1) - inp - 1;
    for(int i = 1; i <= n; ++i)
        val[i] = lower_bound(inp + 1, inp + tot + 1, val[i]) - inp;
    for(int i = 1; i < n; ++i) adde(read(), read());
    depth[1] = 1;
    dfs(1);
    size = sqrt(ncnt), bnum = ceil((double) ncnt / size);
    for(int i = 1; i <= bnum; ++i)
        for(int j = size * (i - 1) + 1; j <= i * size; ++j) belong[j] = i;
    for(int i = 1; i <= m; ++i) {
        int L = read(), R = read(), lca = getlca(L, R);
        if(first[L] > first[R]) swap(L, R);
        if(L == lca) {
            q[i].l = first[L];
            q[i].r = first[R];
        }
        else {
            q[i].l = last[L];
            q[i].r = first[R];
            q[i].lca = lca;
        }
        q[i].id = i;
    }
    sort(q + 1, q + m + 1, cmp);
    for(int i = 1; i <= m; ++i) {
        int ql = q[i].l, qr = q[i].r, lca = q[i].lca;
        while(l < ql) work(ord[l++]);
        while(l > ql) work(ord[--l]);
        while(r < qr) work(ord[++r]);
        while(r > qr) work(ord[r--]);
        if(lca) work(lca);
        ans[q[i].id] = now;
        if(lca) work(lca);
    }
    for(int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
    return 0;
}

六、莫隊算法的擴展——回滾莫隊

莫隊維護區間統計信息雖然方便,但在某些場合下卻很是雞肋。好比以下這題:

AT1219 [JOI2013]歴史の研究

題目描述

IOI國曆史研究的第一人——JOI教授,最近得到了一份被認爲是古代IOI國的住民寫下的日記。JOI教授爲了經過這份日記來研究古代IOI國的生活,開始着手調查日記中記載的事件。

日記中記錄了連續N天發生的時間,大約天天發生一件。

事件有種類之分。第i天\((1<=i<=N)\)發生的事件的種類用一個整數\(X_i\)表示,\(X_i\)越大,事件的規模就越大。

JOI教授決定用以下的方法分析這些日記:

\(1\).選擇日記中連續的一些天做爲分析的時間段

\(2\).事件種類t的重要度爲t×(這段時間內重要度爲t的事件數)

\(3\).計算出全部事件種類的重要度,輸出其中的最大值

如今你被要求製做一個幫助教授分析的程序,每次給出分析的區間,你須要輸出重要度的最大值。

輸入格式

第一行兩個空格分隔的整數\(N\)\(Q\),表示日記一共記錄了\(N\)天,詢問有\(Q\)次。

接下來一行N個空格分隔的整數\(X_1\)...\(X_N\)\(X_i\)表示第\(i\)天發生的事件的種類

接下來Q行,第i行\((1<=i<=Q)\)有兩個空格分隔整數\(A_i\)\(B_i\),表示第i次詢問的區間爲\([A_i,B_i]\)

輸入輸出樣例

輸入樣例#1:

5 5
9 8 7 8 9
1 2
3 4
4 4
1 4
2 4

輸出樣例#1:

9
8
8
16
16

題目到手很快就能想到用莫隊維護這個最大值,添加值很好作,直接加個計數器,而後乘一下取個max就完事了。而後刪除……不會。想到的惟一辦法就是在當前計數器清零後往前枚舉,找到一個可行的最大值再替換。這樣的複雜度會多一維,達到\(O(n^2\sqrt{n})\),還不如直接n方暴力,說不定就能過百萬了呢

此時,因爲莫隊的無敵(霧),有神犇發明了一個玄學高效的算法,複雜度最壞\(O(n\sqrt{n})\),並且常數碾壓同爲\(O(n\sqrt{n})\)的塊狀數組作法。

咱們觀察莫隊的性質:左端點在同一塊中的全部查詢區間右端點單調遞增。這樣,對於左端點在同一塊中的每一個區間,咱們均可以\(O(n)\)解決全部的右端點,且不用回頭刪除值(單調遞增)。考慮枚舉每一個塊,總共須要枚舉\(\sqrt{n}\)個塊,這部分的總複雜度\(O(n\sqrt{n})\)

又對於每一個塊內的左端點:假設每一個塊內的每一個左端點都從塊右端開始統計,每次都從新開始暴力統計一次,作完每一個左端點複雜度\(O(\sqrt{n})\),共\(n\)個左端點,總複雜度\(O(n\sqrt{n})\)

咱們發現這兩部分是很容易結合起來的。作法就是枚舉每一個塊,每次把\(l\)\(r\)指針置於塊尾+1的位置和塊尾(至於爲何+1還請看前面),先暴力處理掉左右端點在一塊的特殊狀況(\(O(\sqrt{n})\)),而後右端點暴力向右推,左端點一個個解決,在移動左指針前紀錄一下當前狀態,移動保存值後復原便可,也無需刪除。以上的問題完美解決。(豈不美滋滋??#滑稽#)

注意暴力和正常推指針時的\(cnt\)不要共用,並且每作一個新塊都要把\(cnt\)清零。這樣回滾莫隊代碼不難寫出啦(難調啊):

例題參考代碼:

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
#define maxn 100100
#define maxb 5050
#define ll long long
int aa[maxn], typ[maxn], cnt[maxn], cnt2[maxn], belong[maxn], lb[maxn], rb[maxn], inp[maxn];
ll ans[maxn];
struct query {
    int l, r, id;
} q[maxn];
int n, m, size, bnum;
#define isdigit(x) ((x) >= '0' && (x) <= '9')
inline 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;
}
int cmp(query a, query b) {
    return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : a.r < b.r; 
}
int main() {
    n = read(), m = read();
    size = sqrt(n);
    bnum = ceil((double) n / size);
    for(int i = 1; i <= bnum; ++i) {
        lb[i] = size * (i - 1) + 1;
        rb[i] = size * i;
        for(int j = lb[i]; j <= rb[i]; ++j) belong[j] = i;
    }
    rb[bnum] = n;
    for(int i = 1; i <= n; ++i) inp[i] = aa[i] = read();
    sort(inp + 1, inp + n + 1);
    int tot = unique(inp + 1, inp + n + 1) - inp - 1;
    for(int i = 1; i <= n; ++i) typ[i] = lower_bound(inp + 1, inp + tot + 1, aa[i]) - inp;
    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 i = 1;
    for(int k = 0; k <= bnum; ++k) {
        int l = rb[k] + 1, r = rb[k];
        ll now = 0;
        memset(cnt, 0, sizeof(cnt));
        for( ; belong[q[i].l] == k; ++i) {
            int ql = q[i].l, qr = q[i].r;
            ll tmp;
            if(belong[ql] == belong[qr]) {
                tmp = 0;
                for(int j = ql; j <= qr; ++j) cnt2[typ[j]] = 0;
                for(int j = ql; j <= qr; ++j) {
                    ++cnt2[typ[j]]; tmp = max(tmp, 1ll * cnt2[typ[j]] * aa[j]);
                }
                ans[q[i].id] = tmp;
                continue;
            }
            while(r < qr) {
                ++r; ++cnt[typ[r]]; now = max(now, 1ll * cnt[typ[r]] * aa[r]);
            }
            tmp = now;
            while(l > ql){
                --l; ++cnt[typ[l]]; now = max(now, 1ll * cnt[typ[l]] * aa[l]);
            } 
            ans[q[i].id] = now;
            while(l < rb[k] + 1) {
                --cnt[typ[l]];
                l++;
            }
            now = tmp;
        }
    }
    for(int i = 1; i <= m; ++i) printf("%lld\n", ans[i]);
    return 0;
}

注意這裏分塊時有個坑點:向上取整的ceil不要寫成floor,這樣在普通莫隊中會多出一個塊0,徹底不影響AC,但在回滾莫隊中就是WA,WA到我渾身不得勁qwq

回滾莫隊完結撒花✿✿ヽ(°▽°)ノ✿qwq


七、練習題

這裏放的主要是莫隊裸題,沒有與其餘算法的綜合應用,但部分有思惟難度。如須要綜合應用題請左轉【Luogu OJ】 右轉【BZOJ】

雖然莫隊算法思想很簡單,但與它有關的應用仍是很經(du)典(liu)的。下面是一些經(du)典(liu)的例題:

一、【基礎題】Luogu P2709 小B的詢問

這題的話,手推公式很容易就能作出來,並且題目無坑點,甚至無需卡常

代碼不給了qwq,和例題差不了兩句話qwq

二、【進階題】Luogu P1494 [國家集訓隊]小Z的襪子

這題須要一些基礎但較爲複雜的數學演算,打表基本不靠譜(另外還要注意約分)

留給你們自行推理(代碼仍是很簡單噠qwq)

三、【進階題】Luogu P3709 大爺的字符串題

內個,題意別看了,題目要求的是區間衆數的出現次數(出題人語文很差)(固然你也能夠直接把題意推出來qwq)

即使題意明瞭了,這題仍是有點綜合性的(區間衆數這東西並很差求)

真是一道好(du)題(liu)(逃

四、【最終挑戰】Luogu P4074 [WC2013]糖果公園

承諾的黑題終於來啦,撒花✿✿ヽ(°▽°)ノ✿

然而這題並不難,樹上帶修莫隊模板題,相信你很快就能切掉它qwq

參考代碼:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
#define maxn 200200
#define ll long long
int cnt[maxn], aa[maxn], belong[maxn], inp[maxn], n, m, Q, ncnt, size, bnum, w[maxn], v[maxn], ccnt, qcnt;
int val[maxn], fa[maxn][30], depth[maxn], head[maxn], ecnt;
int fir[maxn], la[maxn], vis[maxn];
int l = 1, r = 0, t = 0;
ll now, ans[maxn];
struct edge {
    int to, next;
} e[maxn];
void adde(int u, int v) {
    e[++ecnt] = (edge){v, head[u]};
    head[u] = ecnt;
    e[++ecnt] = (edge){u, head[v]};
    head[v] = ecnt;
}
void dfs(int x) {
    aa[++ncnt] = x;
    fir[x] = ncnt;
    for(int k = head[x]; k; k = e[k].next) {
        int to = e[k].to;
        if(depth[to]) continue;
        depth[to] = depth[x] + 1;
        fa[to][0] = x;
        for(int i = 1; (1 << i) <= depth[to]; ++i) fa[to][i] = fa[fa[to][i - 1]][i - 1];
        dfs(to);
    }
    aa[++ncnt] = x;
    la[x] = ncnt;
}
int getlca(int u, int v) {
    if(depth[u] < depth[v]) swap(u, v);
    for(int i = 20; i + 1; --i) if(depth[fa[u][i]] >= depth[v]) u = fa[u][i];
    if(u == v) return u;
    for(int i = 20; i + 1; --i) if(fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
    return fa[u][0];
}
struct query {
    int l, r, id, lca, t;
} q[maxn];
int cmp(query a, query b) {
    return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.r] ^ belong[b.r]) ? belong[a.r] < belong[b.r] : a.t < b.t );
}
inline void add(int pos) {
    now += 1ll * v[val[pos]] * w[++cnt[val[pos]]];
}
inline void del(int pos) {
    now -= 1ll * v[val[pos]] * w[cnt[val[pos]]--];
}
inline void work(int pos) {
    vis[pos] ? del(pos) : add(pos);
    vis[pos] ^= 1;
}
struct change {
    int pos, val;
} ch[maxn];
void modify(int x) {
    if(vis[ch[x].pos]) {
        work(ch[x].pos);
        swap(val[ch[x].pos], ch[x].val);
        work(ch[x].pos);
    }
    else swap(val[ch[x].pos], ch[x].val);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
inline 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;
}
int main() {
    n = read(), m = read(), Q = read();
    for(int i = 1; i <= m; ++i) v[i] = read();
    for(int i = 1; i <= n; ++i) w[i] = read();
    for(int i = 1; i < n; ++i) {
        int u = read(), v = read();
        adde(u, v);
    }
    for(int i = 1; i <= n; ++i) val[i] = read();
    depth[1] = 1;
    dfs(1);
    size = pow(ncnt, 2.0 / 3.0);
    bnum = ceil((double)ncnt / size);
    for(int i = 1; i <= bnum; ++i)
        for(int j = size * (i - 1) + 1; j <= i * size; ++j) belong[j] = i;
    for(int i = 1; i <= Q; ++i) {
        int opt = read(), a = read(), b = read();
        if(opt) {
            int lca = getlca(a, b);
            q[++qcnt].t = ccnt;
            q[qcnt].id = qcnt;
            if(fir[a] > fir[b]) swap(a, b);
            if(a == lca) q[qcnt].l = fir[a], q[qcnt].r = fir[b];
            else q[qcnt].l = la[a], q[qcnt].r = fir[b], q[qcnt].lca = lca;
        }
        else {
            ch[++ccnt].pos = a;
            ch[ccnt].val = b;
        }
    }
    sort(q + 1, q + qcnt + 1, cmp);
    for(int i = 1; i <= qcnt; ++i) {
        int ql = q[i].l, qr = q[i].r, qt = q[i].t, qlca = q[i].lca;
        while(l < ql) work(aa[l++]);
        while(l > ql) work(aa[--l]);
        while(r < qr) work(aa[++r]);
        while(r > qr) work(aa[r--]);
        while(t < qt) modify(++t);
        while(t > qt) modify(t--);
        if(qlca) work(qlca);
        ans[q[i].id] = now;
        if(qlca) work(qlca);
    }
    for(int i = 1; i <= qcnt; ++i) printf("%lld\n", ans[i]);
    return 0;
}

五、其它

本身找qwq,百度是個很好的東西qwq


尾聲

耗時將近兩天的長篇大論終於要結束啦,再來無恥的求一波贊qwq(逃

感謝各位堅持着看過來(霧)的dalao,文章若有錯誤歡迎指出哦qwq

NOIP 2019 RP++

HNOI 2019 RP++

相關文章
相關標籤/搜索