1. 程式人生 > 實用技巧 >git stash 的用法

git stash 的用法

莫隊演算法

由於本人太懶,很多圖都是從別人那裡抓過來的,很多圖都是比較熟悉得啦

莫隊演算法是由莫濤大神提出的玄學(毒瘤暴力騙分)演算法,適用於各種毒瘤的區間問題(比如區間求一些很複雜的柿子)。但莫隊以其優秀的複雜度和簡短的程式碼還是廣受 OIer 喜歡(髒心病狂)的。

小A: 莫隊不就是四層while嗎? 有啥不好寫的?

小B:那你是不知道莫隊除了普通的莫隊還有帶修莫隊,樹上莫隊,樹上帶修莫隊(這模板題都是黑的),回滾莫隊,莫隊二次離線(這玩意可是 Ynoi 搞出來的)。。。。

下面請聽我口胡(本人對上述幾種毒瘤演算法還不是很透徹) 出鍋了還請各位大佬見諒。

一:前置知識

1.分塊的基本思想(為了保證莫隊的複雜度)

2.STL中的 \(sort\) 的用法(手寫各種關鍵字排序)

3.基礎(毒瘤)的卡常技巧 (包括指令集,臭氧)

4.離散化(很多毒瘤題都會用到)

5.要有總夠的耐心(有時候你會被一道題卡到吐,卡成一頁的提交記錄)。

二:普通莫隊

莫隊到底能來幹什麼? (這不廢話嗎?)

用興趣的可以看一下莫濤大神的知乎 %%%。

我們先來看一道例題

Luogu P1972 HH的項鍊

不過這題卡了莫隊(我***,好不容易找到例題)右轉資料弱化版 SP3267 DQUERY - D-query

我們考慮暴力怎麼打,很簡單,開一個數組記錄每個數值出現的次數,在暴力列舉每個區間,找不為零的數值的個數,輸出答案就可以。複雜度 O(n^2),本題跑不起。

假如每次查詢的區間左右端點都是從 1-n,那麼你就炸了。

但,我們可以嘗試優化

優化1:每次列舉一個數值的時候,判斷一下出現的次數是否為 \(0\),如果為 \(0\) 直接加一 ,反之刪除的時候判斷一下出現的次數是否大於 \(0\) 如果等於 \(0\) 了,那麼他對答案的貢獻就會減一

優化2: 我們主要的複雜度來自列舉的每一個區間,所以我們嘗試來優化這一個過程。我們搞兩個指標 \(L\)\(R\) 每次查詢時移動這兩個指標,直到與查詢區間重合,這樣我們就可以快速統計出答案來了。

我們來模擬一下這個操作:(這絕不是因為本人太懶,才搬別人的呢

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

我們初始化 L =1、R = 0(如果 L = 0,那麼我們還需要刪除一個數值 0,使其出現次數變成-1,導致一些奇奇怪怪錯誤)如下圖:

我們發現 L 已經是第一個查詢區間的左端點,無需移動。現在我們將 R 右移一位,發現新數值1:

R繼續右移,發現新數值2:

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

當 R 再次右移時,發現此時的新位置中的數值2出現過,數值總數不增:

接下來是兩個7,由於7沒出現過,所以總數+1:

繼續右移發現3:

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

至此,Q1區間所有數值統計完成,結果為5。

現在我們又看一下Q2區間的情況:

首先我們發現, L 指標 在 \(Q_2\)區間左端點的左邊,我們需要將它右移,同時刪除原位置的統計資訊。將 L

右移一位到位置2,刪除位置1處的數值1。但由於操作後的區間中仍然有數值1存在,所以總數不減。

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

當 L 指標繼續右移時,發現一個問題:原位置上的數值是2,但是刪除這個2後,此時的區間 [L,R] 中再也沒有2了(回顧之前的內容,這種情況就是刪除後 cnt[2] = 0),那麼總數就要-1,因為有一個數值已經不在該區間內出現了,而本題需要統計的就是區間內的數值個數。此步驟如下圖:

再右移一位,發現無需減總數,而且L已經移到了 \(Q_2\)區間的左端點,無需繼續移下去(如下圖)。當然

R 還是要移動的,只不過沒圖了,我相信大家應該知道做法的 QAQ.

R 的最後位置:

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

Code

void add(int x)//加入一個數
{
    
	if(num[a[x]] == 0) tmp++;//如果當前這個數沒有出現過,那麼對答案的貢獻就要加一
	num[a[x]]++;//這個數出現的次數加一
}
void del(int x)//刪除一個數
{
	num[a[x]]--;//這個數出現的次數減一
	if(num[a[x]] == 0) tmp--;//如果這個數不在出現了,那麼對答案的貢獻就要減一
}
int main()
{
	for(int i = 1; i <= t; i++)
	{
		while(l < q[i].l) del(l++);//左指標在左端點左邊,要刪除當前的數,先刪在移動指標
		while(l > q[i].l) add(--l);//左指標在左端點右邊,要加入當前的數,先移動指標在加入
		while(r < q[i].r) add(++r);//右指標在右端點左邊,要加入當前的數,先移動指標在加入
		while(r > q[i].r) del(r--);//右指標在左端點右邊,要刪除當前的數,先刪在移動指標
		q[i].ans = tmp;
	}
}

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

這實際上還不能算是莫隊,他少了最精髓的部分(最最最最最最毒瘤的一部分

優化2實際上還是可以被一些特殊情況卡掉的

比如下面這張圖:

這樣,我們每次移動左右兩個指標的時候,都要從頭移到尾(反覆橫跳)。這樣複雜度就會退化為 O(nt)

這他喵的跑的比暴力還慢。儘管如此,各種神仙玩家開始對他優化。

考慮上面的情況,我們要儘可能的少移動左右兩個指標,那我們怎麼辦?

排個序不就完了,我們給他排個序,每次讓左右指標在小幅度範圍內移動,這樣莫隊的複雜度就有了基本的保證。

三:莫隊的玄學優化方式

1.關鍵字排序

我們為了避免莫隊被上面的特殊情況卡掉,專門搞出來了個特殊的排序方式。

我們嘗試把分塊和莫隊結合起來(鬼知道當初發明人怎麼想出來的)。

我們把序列分成 \(\sqrt {n}\) 塊,從1到 \(\sqrt{n}\) 標號,然後就瞎搞。一種方式就是按左端點所在的塊的編號排序,如果所在的塊的編號相同,就按右端點排序。在進行我們的左右指標反覆橫跳大法。

時間複雜度O(玄學)。

下面聽我口胡,來證明他的複雜度是O(n \(\sqrt{n}\))。

他的複雜度主要來自於三個方面。

1.排序預處理 sort一下即可,複雜度O(n logn)。

2.左指標的移動

設每個塊 \(i\) 中分佈有 \(x_i\)個左端點,由於莫隊的新增、刪除操作複雜度為O(1),那麼處理塊 \(i\) 的最壞時間複雜度是O(\(x_i \sqrt{n}\)),指標跨越整塊的時間複雜度為O(\sqrt{n}),最壞需要跨越n次;

總複雜度O(\(\displaystyle\sum_{i} x_i \sqrt{n}\) + \(n \sqrt{n}\))=O(\(n \sqrt{n}\))。

3.右指標的移動

設每個塊 \(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} $), 簡稱 O(玄學)。

經過看似簡單的排序之後,莫隊的複雜度猛降一個 $\sqrt{n} $ 這麼多,愛了愛了。

但由於經過排序,莫隊就變成了典型的離線演算法,且這種演算法不帶修改(瞎說,發明人已經yy出了帶修莫隊,這下文會講)。如果遇到強制線上的題目,你就要採用其他演算法(當然了,你也可以自己yy出在線莫隊)。

溫馨提示,下面可能會引起大賢者模式,請各位同學繫好安全帶

下面的優化方式太過玄學,搞不懂得可以先記住,等被卡到吐的時候,會用就行了。

2.氧氣 OR 臭氧

事實證明,開了o2的莫隊跑的飛快,1e6都有可能過。有時甚至比不開o2的版本快4-5倍。

當然了,在實際比賽中是不能開o2的,也許你需要的是這個 #pragma GCC optimize(2) (

3.莫隊玄學奇偶排序

這是最玄學的一部分了。。。。

這個和莫隊主體差不多(鬼知道,當初是怎麼想出這個來的)。看似一點用都沒有,實則平均幫你每個點優化200ms(流批

我們把原來的排序操作扔掉,改成

bool cmp4(const node &c, const node &d) //超級無敵壓行寫法
{
   return (c.block^d.block)?c.block<d.block:((c.block&1)?c.r<d.r:c.r>d.r);
}
inline bool cmp3(node a,node b)//樸素寫法
{
    if(pos[a.l] == pos[b.l])
    {
        if(a.block & 1)//左端點在奇數塊按右端點升序排列
        {
            return a.r < b.r;
        }
        else
        {
            return a.r > b.r;//左端點在偶數塊按右端點降序排列
        }
    }
    return a.l < b.l;
}

普通排序:1.59s

奇偶排序:1.17s

實測大資料點平均優化 100 - 200ms。

氧氣加奇偶排序:589ms

開了o2的莫隊和普通莫隊簡直是兩種演算法,平均優化了 200ms+。

下面聽我口胡他的原理:

這種優化主要優化在右指標跳完奇數塊往回跳的時候,就能把偶數塊跳完,然後跳完偶數塊的往回跳的時候,又可以把奇數塊跳完。理論上是可以使複雜度減半的。(不過能優化就是很爽)。

當跳奇數塊的時候,右指標假設指在這裡

當你往左移動右指標的時候,發現下一個詢問區間的右端點就在你旁邊,而不是像普通排序一樣離你天涯海角。

聽不懂也沒關係啦,會用就行了(一般題是不會卡普通排序的,除非是ynoi的毒瘤題)。

下面的優化就比較神了。

4.常數壓縮

我們可以利用運算子優先順序的知識,把這個和while

void add(int x)
{
    
	if(num[a[x]] == 0) tmp++;
	num[a[x]]++;
}
void del(int x)
{
	num[a[x]]--;
	if(num[a[x]] == 0) tmp--;
}
	while(l < q[i].l) del(l++);
	while(l > q[i].l) add(--l);
	while(r < q[i].r) add(++r);
	while(r > q[i].r) 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。但我太菜了,搞不通這種寫法,所以就只能像上面那麼寫了。(可能是我被卡的經歷不夠多

不過這麼寫,C_錐應該會喜歡吧

有能力的童鞋可以試試這麼寫,蚊子再小也是肉(

5.快讀和快輸

大多數莫隊題的輸入和輸出還是很大的。快讀和快輸也挺重要的。

快讀和快輸想必大家都會吧(最起碼寫個琛式快讀啊)。

卡常數的部分就可以愉快的結束了。

那我們就來看幾道例題吧

1.HH的項鍊

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 2e5+10;
int n,t,l,r,len,tmp;
int a[30100],num[1000010],pos[1000010];
inline int read()
{
	int s = 0,w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
	return s * w;
}
struct node
{
	int l,r;
	int id,ans;
}q[N];
bool comp(node a,node b)//這裡我比較懶用的普通排序
{
	if(pos[a.l] != pos[b.l]) return pos[a.l] < pos[b.l];
	return a.r < b.r;
}
bool cmp(node a,node b)
{
	return a.id < b.id;
}
void add(int x)
{
	if(num[a[x]] == 0) tmp++;
	num[a[x]]++;
}
void del(int x)
{
	num[a[x]]--;
	if(num[a[x]] == 0) tmp--;
}
int main()
{
	n = read(); len = sqrt(n);
	for(int i = 1; i <= n; i++) a[i] = read();
	t = read();
	for(int i = 1; i <= t; i++)
	{
		q[i].l = read(); q[i].r = read();
		q[i].id = i;
	}
	for(int i = 1; i <= n; i++) pos[i] = (i-1) / len + 1;
	sort(q+1,q+t+1,comp);
	l = 1, r = 0, tmp = 0;
	for(int i = 1; i <= t; i++)
	{
		while(l < q[i].l) del(l++);
		while(l > q[i].l) add(--l);
		while(r < q[i].r) add(++r);
		while(r > q[i].r) del(r--);
		q[i].ans = tmp;
	}
	sort(q+1,q+t+1,cmp);
	for(int i = 1; i <= t; i++) printf("%d\n",q[i].ans);
	return 0;
}

2.小z的襪子

頹柿子

\(\displaystyle\sum_{i=1}^{n} C_{num_i}^{2}\over C_{len}^{2}\)

= \(\displaystyle \sum_{i=1}^{n} num_i \times (num_i -1) \over len \times (len - 1)\)

= \(\displaystyle \sum_{i=1}^{n}num_i^2-num_i \over len^2 - len\)

然後我們就可以像上面那個題一樣維護一下出現次數的平方水過去了。

但我太菜了,被卡了好幾回,這也讓我認識到了莫隊的可怕

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
#define LL long long//一定要開long long 我在這裡栽了好幾回
const int N = 50010;
LL n,t,len,l,r,tmp;
LL pos[N],a[N],num[N];
inline LL read()
{
	LL s = 0,w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
	return s * w;
}
struct node
{
	LL l,r,len,id;
	LL f1,f2;
}q[50010];
bool comp(node a,node b)//奇偶排序
{
	if(pos[a.l] == pos[b.l])
	{
		if(pos[a.l] & 1) return a.r < b.r;
		else return a.r > b.r;
	}
	return pos[a.l] < pos[b.l];
}
bool cmp(node a,node b)
{
	return a.id < b.id;
}
LL gcd(LL a, LL b)
{
	if(b == 0) return a;
	else return gcd(b,a%b);
}
void add(int x)//加一個數
{
	tmp -= num[a[x]] * num[a[x]];//先減去原來的貢獻
	num[a[x]]++;
	tmp += num[a[x]] * num[a[x]];//加上現在的貢獻
}
void del(int x)//減一個數
{
	tmp -= num[a[x]] * num[a[x]];
	num[a[x]]--;
	tmp += num[a[x]] * num[a[x]];
}
int main()
{
	n = read(); t = read(); len = sqrt(n);
	for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 1; i <= n; i++) pos[i] = (i-1) / len + 1;
	for(int i = 1; i <= t; i++)
	{
		q[i].l = read(); q[i].r = read(); q[i].id = i; 
		q[i].len = q[i].r - q[i].l + 1;
		q[i].f2 = q[i].len * (q[i].len - 1);//處理一下分母
	}
	sort(q+1,q+t+1,comp);
	l = 1, r = 0, tmp = 0;
	for(int i = 1; i <= t; i++)
	{
		if(q[i].l == q[i].r) continue;
		while(l < q[i].l) del(l++);
		while(l > q[i].l) add(--l);
		while(r < q[i].r) add(++r);
		while(r > q[i].r) del(r--);
		q[i].f1 = tmp - q[i].len;
		LL d = gcd(q[i].f1,q[i].f2);//如果分子為0的話就會RE
		q[i].f1 /= d; q[i].f2 /= d;//約分一下
	}
	sort(q+1,q+t+1,cmp);
	for(int i = 1; i <= t; i++)
	{
		if(q[i].f1 == 0 || l == r) cout<<0<<"/"<<1<<"\n";//特判一下l==r的情況
		else cout<<q[i].f1<<"/"<<q[i].f2<<"\n";
	}
	return 0;
}

先留幾道基礎的題吧(

P3901數列找不同 莫隊裡面顏色最低的了

CF220B Little Elephant and Array 莫隊加離散化模板

四:帶修莫隊

普通的莫隊是不支援修改的,但我們下面介紹的黑科技能讓你爽翻天(暈頭晃腦)。

我們來通過一道題看一下帶修莫隊都能來做什麼。

P1903 [國家集訓隊]數顏色 / 維護佇列

這題按道理是普通莫隊是不能做的,但某位神仙yy出了帶修莫隊(說不定,莫隊還能線上了呢),這道題就變成了板子題。

雖然正解是樹桃樹,但依舊不能阻止我們AC———by treaker

帶修莫隊比普通的莫隊多了一個時間維。

也就是說,我們在移動指標的時候還要在填上一個時間指標,表示當前修改了多少次。

如果當前修改的次數比我們詢問的區間修改的次數要少,我們就要進行修改,

反之要撤銷這次修改。

我們的做法是把修改操作編號,稱為"時間戳",而查詢操作的時間戳沿用之前最近的修改操作的時間戳。跑主演算法

時定義當前時間戳為 \(t\),對於每個查詢操作,如果當前時間戳相對太大了,說明已進行的修改操作比要求的多,就

把之前改的改回來,反之往後改。只有噹噹前區間和查詢區間左右端點、時間戳均重合時,才認定區間完全重合,

此時的答案才是本次查詢的最終答案。 -------正規解釋

通俗地講,就是再弄一指標,在修改操作上跳來跳去,如果當前修改多了就改回來,改少了就改過去,直到次數恰當為止。

那麼我們怎麼排序呢

我們還是和原來的一樣,只不過多了一種情況,如果當前左端點和右端點都相同的時候就按修改的次數排序,其他的就和普通莫隊差不多了。

程式碼如下:

bool comp(node a,node b)
{
	if(shu[a.l] == shu[b.l])//左端點相同,按右端點排序
	{
		if(shu[a.r] == shu[b.r]) return a.tim < b.tim;//左右端點都相同的話按時間戳排序
		else
		{
			if(shu[a.l] & 1) return shu[a.r] < shu[b.r];//奇偶排序
			else return shu[a.r] > shu[b.r];
		}
	}
	return shu[a.l] < shu[b.l];
}

例題程式碼簡化版:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 150000 ;
const int M = 1000010;
int cnt[M],col[N],tmp[N],n,m;
int l,r,t,ans,mxt,tot,block;
struct ask{
	int l,r;
	int t;//修改了幾次 
	int ord;//第幾條命令
	int ans; 
}a[N];
int read(){
    int a=0,f=0;char c=getchar();
    for(;c<'0'||c>'9';c=getchar())if(c=='-')f=1;
    for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
    return f?-a:a;
}
struct chenge{
     int pre;//之前是什麼顏色 
	 int c;//變成什麼顏色 
	 int pos;//修改的地方 
}b[N];
int comp (ask a,ask b){
	if(a.l / block == b.l / block){
		if(a.r/block == b.r/block){
			return a.t <b.t;
		}else {
		      return a.r/block <  b.r/block;
		}
	}else{
		return a.l/block < b.l/block;
	} 
}
int cmp (ask a,ask b){
	return a.ord < b.ord;
}
void add(int c){
	if(c == 0) return;
	cnt[c]++;
	if(cnt[c] == 1) ans++;
}
void del(int c){
	if(c == 0) return;
	cnt[c]--;
	if(cnt[c] == 0) ans--;
}


int main(){
	n = read(),m = read();
	block = (int)ceil(pow(n,2.0/3));
	for(int i = 1; i <= n; i++) col[i] = read();
	for(int i = 1; i <= n; i++) tmp[i] = col[i];
	for(int i = 1; i <= m; i++){
		char opt;
		int x,y;
		cin>>opt;
		x= read(),y = read();
		if(opt == 'Q'){
			tot++;
			a[tot].l = x;
			a[tot].r = y;
			a[tot].t = mxt;//記錄一下他之前修改了幾次
			a[tot].ord = tot;//記錄一下他是第幾次查詢操作
		} 
		else {
			mxt ++;
			b[mxt].pos = x;//記錄修改操作的位置
			b[mxt].c = y;//記錄修改成什麼顏色
			b[mxt].pre = tmp[x];//他之前是什麼顏色
			tmp[x] = y;
		}
	}
	sort(a+1,a+tot+1,comp);
	int l = 1;r = 0; ans = 1; t = 0 ;
	for(int i = 1; i <= tot; i++){
		while(a[i].t > t){//要進行修改
			t++;
			if( l <= b[t].pos && r >= b[t].pos){//如果第t次修改在這段區間,則答案會發生更改
				del(col[b[t].pos]);//刪去現在的顏色的貢獻
				add(b[t].c);//改成現在顏色的貢獻
			}
			col[b[t].pos] = b[t].c;//把當前顏色交換一下
		}
		while(t > a[i].t){
			if(l <= b[t].pos && r >= b[t].pos){//要撤銷的修改在這段區間,則答案會發生變化
				del(col[b[t].pos]);//撤銷操作
				add(b[t].pre);
			}
			col[b[t].pos] = b[t].pre;//把他改回原來的顏色
			t--;
		}
		while(a[i].l < l) l--,add(col[l]);//普通莫隊的指標移動
		while(a[i].r > r) r++,add(col[r]);
		while(a[i].l > l) del(col[l]),l++;
		while(a[i].r < r) del(col[r]),r--;
		a[i].ans = ans;
	}
	sort(a+1,a+tot+1,cmp);
	for(int i = 1; i <= tot; i++){
		printf("%d\n",a[i].ans);
	}
	return 0;
} //之前寫的,馬蜂差評

由於某毒瘤對此題的資料進行了加強,再加上我們上面那種寫法常數過大。

所以,我們有必要進行一下優化,改的面目全非

1.我們嘗試把塊的大小調為 \(n^{2\over 3}\)

有神仙證明了當塊的大小為 \(\sqrt {n^4 \times t} ^ {3}\) 理論複雜度達到最優,但是蒟蒻我太菜了,證明不出來。

不過一個顯然的問題(一點也不顯然)就是當塊的大小為 \(n^{2\over 3}\) 時,要優於取 \(\sqrt n\) 的情況,總體複雜度

為O(玄學),但塊的大小為 \(\sqrt n\) 的時候,複雜度會退化為 O(n^2).

具體的方程式是這個:

總之,能優化就很爽。

2.修改常數。

我們把上面程式碼給成這幅鬼樣子

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
char opt;
const int N = 150000 ;
const int M = 1000010;
int n,m,cntq,cntm,block,tmp,l,r,t;
int c[N],num[M],shu[N],ans[N];
struct node
{
	int l,r;
	int id,ans,tim;
}q[N];
struct modify
{
	int pos,c;
}b[N];
inline int read()//莫隊必備快讀
{
	int s = 0,w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
	return s * w;
}
bool comp(node a,node b)//奇偶排序
{
	if(shu[a.l] == shu[b.l])
	{
		if(shu[a.r] == shu[b.r]) return a.tim < b.tim;
		else
		{
			if(shu[a.l] & 1) return shu[a.r] < shu[b.r];
			else return shu[a.r] > shu[b.r];
		}
	}
	return shu[a.l] < shu[b.l];
}
void add(int x)
{
	if(++num[x] == 1) tmp++;
}
void del(int x)
{
	if(--num[x] == 0) tmp--;
}
void chenge(int t,int i)
{
	if(b[t].pos >= q[i].l && b[t].pos <= q[i].r)
	{
		if(--num[c[b[t].pos]] == 0) tmp--;//減少函式呼叫量,這樣可以省去不少時間
		if(++num[b[t].c] == 1) tmp++;
	}
	swap(c[b[t].pos],b[t].c);//少開一個數組,來優化常數
    //這裡交換函式是一個小小的技巧吧,因為我們可能在前面查詢需要將數字修改,但是本次查詢不用,則要修改回來。
//這是直接交換他們的值,要是需要修改,則直接交換值,要是需要修改回來,則把值交換回來就行了。交換兩次就等同於沒有交換.
}
int main()
{
	n = read(); m = read();
	block = (int)ceil(pow(n,2.0/3));//調整塊的大小
	for(int i = 1; i <= n; i++) shu[i] = i / block;
 	for(int i = 1; i <= n; i++) c[i] = read();
	for(int i = 1; i <= m; i++)
	{
		cin>>opt;
		if(opt == 'Q')
		{
			q[++cntq].l = read();
			q[cntq].r = read();
			q[cntq].id = cntq;
			q[cntq].tim = cntm;
		}
		else if(opt == 'R')
		{
			b[++cntm].pos = read();
			b[cntm].c = read();
		}
	}
	sort(q+1,q+cntq+1,comp);
	l = 1, r = 0, t = 0, tmp = 0;
	for(int i = 1; i <= cntq; i++)
	{
		while(l < q[i].l) del(c[l++]);
		while(l > q[i].l) add(c[--l]);
		while(r < q[i].r) add(c[++r]);
		while(r > q[i].r) del(c[r--]);
		while(t < q[i].tim) chenge(++t,i);//修改操作
		while(t > q[i].tim) chenge(t--,i);
		ans[q[i].id] = tmp;
	}
	for(int i = 1; i <= cntq; i++) printf("%d\n",ans[i]);
	return 0;
}

總之,這題卡卡就過去了,但你實在卡不過去,我也沒有任何辦法QAQ(可能你像jbh那樣人傻常數大吧)。

留一道例題吧

CF940F Machine Learning

五 樹上莫隊

前置芝士:

1.樹分塊 2. 尤拉序

這些不會沒關係,下面會講的。聽我口胡。


最近什麼演算法都能上樹(就連隔壁jmh都會了上樹)。

一般的我們莫隊只能解決的是序列問題,所以我們想辦法要把樹變成一個序列。怎麼做?

其實很簡單,把樹拍成一個序列,直接給他的節點編一下號,這樣就能轉化為我們熟悉的序列問題。

那我們的莫隊演算法就可以上樹了(大霧)。

假如,我們按之前的習慣,用dfs序來編號的話,可能會有些差錯。

比如:

這樣你就會發現區間對應不上,普通的dfs序是完全不行(GG了)的。

這個時候就用到了,我們前置知識裡的尤拉序。

尤拉序是什麼呢?,我們來拓展一下。


1.什麼是尤拉序

就是從根節點,按dfs序繞回原點所經過所有點的順序。(通俗地講就是出棧入棧的順序)。

2.怎麼求呢

dfs的時候加進去,遍歷完所有子樹後,在把他加進去,總共加兩邊。

void dfs(int x,int fa)
{
	ord[++cnt] = x;//第一次加入
	for(int i = head[x]; i; i = e[i].net)//遍歷他所有的子樹
	{
		int to = e[i].to;
		if(to == fa) continue;
		dfs(to,fa);
	}
	ord[++cnt] = x;//最後再把它加回來
}

我們結合一張圖來理解一下

尤拉序為A-B-D-D-E-G-G-E-B-C-F-H-H-F-C-A

尤拉序就拓展完啦,太深奧的我還不會。


我們對上面那個圖求一下尤拉序。就是這樣

我們可以發現他每個點都出現了兩次(這不廢話嗎)。

再看看他出現的兩個位置有什麼特點。

以2為例,他出現的位置在2和9,他中間的編號為 \(4\times 2\) , \(7 \times 2\) ,\(5\times 2\)

在看一下這棵樹,發現這些編號都是2子樹上的節點。。。

這樣,他就有了一條性質 樹的尤拉序上兩個相同編號(設為 \(x\))之間的所有編號都出現兩次,且都位於 \(x\)子樹上

這由我們求尤拉序的過程可以得證,因為我們是遍歷完他所有的子樹,才把他這個節點入隊的。

那麼,我們怎麼把路徑轉化為區間呢,我們看一下下面這張圖

我們在尤拉序中找到路徑1→10起點(1)終點(10)的位置。我們發現,我們完全可以在找到對應的區間(綠色

部分),而由於其中有一些點出現了兩次,這些出現了兩次的點可以證明不在路徑上(路徑不會經過一個點兩次,

而如果只經過一次則不會出現兩個相同的編號),所以出現了兩次的點我們不予算入。

那我們在嘗試找一下2—> 6 的區間,嗯? 為毛沒有 \(1\) 啊, \(1\) 可是他們祖先啊,看來我們還要在改進一下。

具體方法:設每個點的編號 \(a\) 首次出現的位置 \(first[a]\) 最後出現的位置為 \(last[a]\)

那麼對於 x -> y, 設 \(first[x] <= first[y]\) (不滿足就 \(swap\) ,這樣就保證如果 \(x\) , \(y\) 在一條鏈上的時候, \(x\)

一定是 \(y\) 的祖先,或等於 \(y\),如果 \(lca(x,y) == x\) (例如 1 -> 10 ) 就直接把 \([first[x] ,first[y]]\) ,拿過來用。

反之(如上圖 2 -> 6 的情況) 就用 \([last[x], first[y]\) 。至於不用 \(first[x],first[y]\) 是因為 \(first[x] - last[x]\)

裡面的編號是 \(x\) 的子樹中的節點 ,他們是不會出現在這條路徑上的。即 裡面的編號出現了兩次,考慮了等同於

沒考慮。但這個區間不包括他們的最近公共祖先,查詢時直接加上就可以了。

注意: 陣列要開兩倍大,千萬不要在這TLE 了,(來自前人的忠告)。


剩下的就和普通的莫隊差不多了,一個小技巧吧,就是由於一個點出現兩次就等同於沒有考慮,所以我們

開個標記陣列(表示該節點是否被訪問),沒訪問就加,訪問過就刪,每個操作將標記亦或 \(1\) 完美解決所有的問題。並且程式碼的長度也縮短了不少。


例題:

SP10707 COT2 - Count on a tree II

很水得啦,就是普通的樹上莫隊的模板題。

除了上樹操作,其他的和普通的莫隊沒什麼區別。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 1e5+10;//記憶體1.4GB,絲毫不慌開大點
int n,u,v,x,y,m,tot,l,r,id,tmp,block,o;
int dep[N],siz[N],fa[N],head[N],top[N],son[N],first[N],last[N],a[N];
int shu[N<<1],ord[N<<1],cnt[200010],b[N];
bool vis[N];
struct node
{
	int l,r;
	int id,ans,lca;
}q[N];
struct bian//用來存邊的結構體
{
	int to,net;
}e[N<<1];
inline int read()
{
	int s = 0,w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
	return s * w;
}
void add(int x,int y)//加邊操作
{
	e[++tot].to = y;
	e[tot].net = head[x];
	head[x] = tot;
}
bool comp(node a ,node b)//奇偶排序
{
	if(shu[a.l] == shu[b.l])
	{
		if(shu[a.l] & 1) return a.r < b.r;
		else return a.r > b.r;
	}
	return shu[a.l] < shu[b.l];
}
bool cmp(node a,node b)
{
	return a.id < b.id;
}
void YYCH()//數值離散化(這是一個比較坑的點,題目描述中根本沒給顏色的範圍)
{
	sort(b+1,b+o+1);
	int t = unique(b+1,b+o+1)-b-1;
	for(int i = 1; i <= n; i++) a[i] = lower_bound(b+1,b+t+1,a[i])-b;
}
void get_tree(int x)//樹剖預處理,以及求尤拉序
{
	dep[x] = dep[fa[x]] + 1, siz[x] = 1;
	ord[++id] = x; first[x] = id;//記錄一下每個節點第一次和第二次出現的位置,ord陣列存尤拉序
	for(int i = head[x]; i; i = e[i].net)//預處理重兒子
	{
		int to = e[i].to;
		if(to == fa[x]) continue;
		fa[to] = x;
		get_tree(to);
		siz[x] += siz[to];
		if(siz[to] > siz[son[x]]) son[x] = to;
	}
	ord[++id] = x; last[x] = id;
}
void dfs(int x,int topp)//處理每條重鏈的top
{
	top[x] = topp;
	if(son[x]) dfs(son[x],topp);
	for(int i = head[x]; i; i = e[i].net)
	{
		int to = e[i].to;
		if(to == fa[x] || to == son[x]) continue;
		dfs(to,to);
	}
}
int lca(int x,int y)//樹剖求Lca
{
	while(top[x] != top[y])
	{
		if(dep[top[x]] < dep[top[y]]) swap(x,y);
		x = fa[top[x]];
	}
	return dep[x] <= dep[y] ? x : y;
}
void add(int x)//加點操作
{
	if(!cnt[x]) tmp++;//如果這個顏色之前沒出現過,那麼他對答案的貢獻就要加一
	cnt[x]++;//這個數出現的次數加1
}
void del(int x)
{
	cnt[x]--;//這個數出現的次數減1
	if(!cnt[x]) tmp--;//如果這個數出現次數變為0,那麼對答案對貢獻就是-1
}
void work(int pos)
{
	if(vis[pos] == 1) del(a[pos]);//如果是第二次訪問就刪除
	else add(a[pos]);//如果是第一次被訪問就加入這個點的貢獻
	vis[pos] ^= 1;
}
int main()
{
	n = read(); m = read(); block = sqrt(n*2);//對尤拉序進行分塊 
	for(int i = 1; i <= n; i++) a[i] = read(), b[++o] = a[i];//輸入每個點的顏色	
	for(int i = 1; i <= (n<<1); i++) shu[i] = (i-1) / block + 1; //分塊
	for(int i = 1; i <= n-1; i++)//建樹
	{
		u = read(); v = read();
		add(u,v); add(v,u);
	}
	get_tree(1); dfs(1,1); YYCH();//預處理
	for(int i = 1; i <= m; i++)
	{
		x = read(); y = read();
		if(first[x] > first[y]) swap(x,y);//保證x和y在同一條鏈上的時候,x是y的祖先
		int Lca = lca(x,y);
		if(Lca == x)//在一條鏈上的情況
		{
			q[i].l = first[x];
			q[i].r = first[y];
			q[i].id = i;
		}
		else//不在同一條鏈上的情況
		{
			q[i].l = last[x];
			q[i].r = first[y];
			q[i].lca = Lca;//記錄一下這兩個點的lca
			q[i].id = i;
		}
	}
	sort(q+1,q+m+1,comp);
	l = 1, r = 0, tmp = 0;
	for(int i = 1; i <= m; i++)
	{
		while(l < q[i].l) work(ord[l++]);//對尤拉序所對應的點就行操作
		while(l > q[i].l) work(ord[--l]);
		while(r < q[i].r) work(ord[++r]);
		while(r > q[i].r) work(ord[r--]);
		if(q[i].lca) work(q[i].lca);//算一下他lca的貢獻
		q[i].ans = tmp;
		if(q[i].lca) work(q[i].lca);
	}
	sort(q+1,q+m+1,cmp);
	for(int i = 1; i <= m; i++) printf("%d\n",q[i].ans);
	return 0;
}

不是說莫隊的程式碼挺短的嗎?,為什麼一上樹賊長。(又長又難調)。

樹上莫隊理解了就好寫出來了(但是難調啊)。

耐心的·調幾個小時·還是可以調出來的()。

至於,後面的莫隊演算法,先咕著吧,等我學會了在更。