2024.7.26 集訓筆記

zhuluoan發表於2024-10-07

單調棧

給定一個長度為 \(n\) 的數列 \(a\),對每個數字求出其右/左邊第一個值大於等於它的數字的位置。

考慮從左到右掃整個序列,維護一個棧,裡面存放可能成為答案的數字,當遍歷到一個新的數 \(a_i\) 的時候,可以發現棧中 \(\leq a_i\) 的數就再也不可能成為答案了,那就把它們彈掉,此時棧頂就是答案,之後加入 \(a_i\)

由於棧中的元素是單調不升的,故得名單調棧。

這麼做的複雜度:每個元素只會入棧出棧一次,所以複雜度是線性的。

點選檢視程式碼
Rep(i,n,1)
{
	while(top>0&&a[s[top]]<=a[i]) top--;
	ans[i]=s[top];
	s[++top]=i;
}

e.g. 1. 最大值求和

給出一個 \(n\) 個數的序列,對所有 \(1 \leq l \leq r \leq n\)\(\max(a_l,a_{l+1},\dots ,a_{r-1},a_r)\) 並求和。\(n \leq 10^6,a_i \leq 10^3\)

考慮一個數字會在哪些區間被算到,對於一個數字 \(a_p\),用單調棧求出左面和右面第一個比它大的位置 \(l_p\)\(r_p\),那麼當 \(l_p < L \leq p \leq R < r_p\) 的時候,區間 \([L,R]\)\(\max\) 就是 \(a_p\),依據乘法原理,\(a_p\) 的貢獻就是 \(a_p \times (p-l_p) \times (r_p-p)\),求和即可。

注意序列中有相同數字的時候,要欽定(比如)左邊的比右邊的大。

e.g. 2. 序列最大價值

給出一個 \(n\) 個數字的序列,求 \(1\leq l \leq r\leq n\) 使得 \((r-l+1) \times \min(a_l,a_{l+1},\dots ,a_{r-1},a_r)\) 最大。\(n \leq 10^6,a_i \leq 10^3\)

還是和剛才一樣對每個數字維護其在哪個極大區間成為最小值,然後算一算即可。

e.g. 3. 洛谷P4147 玉蟾宮

有一個矩陣,每個位置是 \(0\) 或者 \(1\)。求最大的全 \(1\) 子矩陣。\(n \times m \leq 10^6\)

列舉每一行,對每個位置維護其向上極長的 \(1\) 的段的長度,然後每行轉化為剛剛那個問題即可。

單調佇列

有一個長為 \(n\) 的序列 \(a\),以及一個大小為 \(k\) 的視窗。現在這個從左邊開始向右滑動,每次滑動一個單位,求出每次滑動後視窗中的最大值和最小值。

沿用單調棧的思路,從左到右掃描每一個 \(i\),從棧頂彈數,不過由於棧底的數有可能不在滑動視窗裡了,所以還要從棧底弾掉,棧不支援此操作,考慮使用佇列。複雜度 \(\mathcal{O}(n)\)

點選檢視程式碼
l=1,r=0;
For(i,1,n)
{
	while(l<=r&&a[q[r]]<=a[i]) r--;
	q[++r]=i;
	while(l<=r&&i-q[l]>=k) l++;
	if(i>=k) cout<<a[q[l]]<<" "; 
}

二維字首和

給你一個 \(n\)\(m\) 列的矩陣 \(a\)。接下來有 \(q\) 次查詢,給定引數 \(x_1,y_1,x_2,y_2\)。請輸出以 \((x_1, y_1)\) 為左上角, \((x_2,y_2)\) 為右下角的子矩陣的和。

\(b_{i,j}\)\((1,1)\sim (i,j)\) 這個矩陣的和,由容斥原理,可得遞推式 \(b_{i,j}=b_{i-1,j}+b_{i,j-1}-b_{i-1,j-1}+a_{i,j}\)

如果要求 \((x_1,y_1)~(x_2,y_2)\) 的矩陣和,那麼 \(ans=b_{x_2,y_2}-b_{x_1-1,y_2}-b_{x_2,y_1-1}+b_{x_1-1,x_2-1}\)

二維差分

給你一個 \(n\)\(m\) 列的矩陣 \(b\)\(q\) 次操作,每次給定引數 \(x1,y1,x2,y2,v\),將子矩陣 \((x_1,y_1)\sim (x_2,y_2)\) 加上 \(v\),最後輸出矩陣 \(b\)

由於字首和和差分是互逆運算,設 \(a\)\(b\) 的差分陣列,對字首和的遞推式做代數變換可得 \(a_{i,j}=b_{i,j}+b_{i-1,j-1}-b_{i-1,j}-b_{i,j-1}\)

將子矩陣 \((x_1,y_1)\sim (x_2,y_2)\) 加上 \(v\),其實就等價於

\[\left\{ \begin{array}{} a_{x_1,y_1}+v \\ a_{x_2+1,y_1}-v \\ a_{x_1,y_2+1}-v \\ a_{x_2+1,y_2+1}+v \end{array}\right. \]

最後對於 \(a\) 陣列做一遍字首和就可以得到 \(b\) 陣列了。

二維字首和/差分看起來很麻煩但其實畫個圖很好理解。

基數排序

首先要知道計數排序。其實就是把一個 \(\texttt{int}\) 型別的數拆成低 \(16\) 位和高 \(16\) 位,然後先對低 \(16\) 位做計數排序,再排高 \(16\) 位就行了,複雜度 \(\mathcal{O}(n+32768)\)

對於 \(\texttt{long long}\) 型別也是一樣的原理。

可以再 \(\mathcal{O}(\log n)\) 的時間內支援刪除,插入,查詢最值操作,一般用 STL 中的優先佇列實現。

e.g. 1. 堆排序

把所有數字 \(\texttt{push}\) 進去然後依次 \(\texttt{pop}\) 出來即可。

時間複雜度 \(\mathcal{O}(n \log n)\)

e.g. 2. 洛谷 P3871 [TJOI2010] 中位數

每次插入一個數字,然後詢問所有數字的中位數。

維護一個大根堆一個小根堆,使得大根堆維護的是前一半的元素,小根堆維護剩下的。每次插入視情況放到小根堆/大根堆裡面。

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std ;
#define ull unsigned long long
#define ll long long
#define db double
#define random(a,b) (rand()%((b)-(a)+1)+(a))
#define se second
#define fi first
#define pr pair<int,int>
#define pb push_back
#define ph push
#define ft front
#define vec vector
#define For(i,a,b) for(int i=a;i<=b;i++)
#define Rep(i,a,b) for(int i=a;i>=b;i--)
#define mem(a,b) memset(a,b,sizeof(a))
#define NO cout<<"NO"<<"\n";
#define YES cout<<"YES"<<"\n";
#define No cout<<"No"<<"\n";
#define Yes cout<<"Yes"<<"\n";
#define str string
const int N=1e5+10;
int T,n,a,m;
struct Less
{
	int h[N],cnt,sz;
	void up(int p)
	{
		for(;p>1&&h[p]<h[p>>1];p>>=1)
		{
			swap(h[p],h[p>>1]);
		}
	}
	void push(int x)
	{
		h[++cnt]=x;
		sz++;
		up(cnt);
	}
	int size()
	{
		return sz;
	}
	int top()
	{
		return h[1];
	}
	void down()
	{
		int s=2;
		while(s<=cnt)
		{
			if(s<cnt&&h[s|1]<h[s]) s++;
			if(h[s]<h[s>>1])
			{
				swap(h[s],h[s>>1]);
				s<<=1;
			}
			else break;
		}
	}
	void pop()
	{
		h[1]=h[cnt--];
		down();
		sz--;
	}
}q1;
struct Greater
{
	int h[N],cnt,sz;
	void up(int p)
	{
		for(;p>1&&h[p]>h[p>>1];p>>=1)
		{
			swap(h[p],h[p>>1]);
		}
	}
	void push(int x)
	{
		h[++cnt]=x;
		sz++;
		up(cnt);
	}
	int size()
	{
		return sz;
	}
	int top()
	{
		return h[1];
	}
	void down()
	{
		int s=2;
		while(s<=cnt)
		{
			if(s<cnt&&h[s|1]>h[s]) s++;
			if(h[s]>h[s>>1])
			{
				swap(h[s],h[s>>1]);
				s<<=1;
			}
			else break;
		}
	}
	void pop()
	{
		h[1]=h[cnt--];
		down();
		sz--;
	}
}q2;
void Push(int x)
{
	if(!q1.size()&&!q2.size())
	{
		q1.push(x);
		return;
	}
	else if(!q2.size())
	{
		q2.push(x);
		if(q1.top()<q2.top())
		{
			swap(q1.h[1],q2.h[1]);
		} 
		return;
	}
	else if(x>=q1.top())
	{
		q1.push(x);
		if(abs(q1.size()-q2.size())>1)
		{
			q2.push(q1.top());
			q1.pop();
		}
		return;
	}
	else
	{
		q2.push(x);
		if(abs(q1.size()-q2.size())>1)
		{
			q1.push(q2.top());
			q2.pop();
		}
	}
}
int mid()
{
	if((q1.size()+q2.size())&1)
	{
		return q1.size()>q2.size()?q1.top():q2.top();
	}
	else return q2.top();
}
void solve()
{
    cin>>n;
    For(i,1,n)
    {
    	cin>>a;
    	Push(a);
	}
	cin>>m;
	while(m--)
	{
		str opt;
		cin>>opt;
		if(opt=="mid")
		{
			cout<<mid()<<"\n";
		}
		else
		{
			cin>>a;
			Push(a);
		}
	}
}
int main ( )
{
	ios::sync_with_stdio(false);
    cin.tie(0);
    srand(time(0));
//    cin >> T ;
//    while ( T -- )
		solve();
	return 0;
}

e.g. 3. 洛谷 P1631 序列合併

有兩個長度為 \(N\)單調不降序列 \(A,B\),在 \(A,B\) 中各取一個數相加可以得到 \(N^2\) 個和,求這 \(N^2\) 個和中最小的 \(N\) 個。

\(c_{i,j}=A_i \times B_j\)。維護一個堆,第 \(1\) 大的數一定是 \(c\) 中第一列的數,所以將這 \(N\) 個數壓入堆中,取出堆中的最大值是 \(c_{i,j}\),那麼 \(c_{i+1,j}\) 也可能成為答案,加入堆中,重複 \(N\) 次即可。

e.g. 4. 洛谷 P1090 [NOIP2004 提高組] 合併果子

\(n\) 堆石子,每次你可以選擇任意不同的兩堆合併,代價是兩堆石子的個數之和。求合併成一堆的最小代價。

考慮貪心,每次取兩堆最小的石子合併,將新的石子堆放回,遞迴成為一個 \(i-1\) 的問題,考慮用堆最佳化,時間複雜度 \(\mathcal{O}(n \log n)\)

e.g. 5. 洛谷 P2672 [NOIP2015 普及組] 推銷員

一條街上有n個白點,座標依次是 \(x_1 \sim x_n\),有個人一開始在 \(0\)。選擇第 \(i\) 個點塗黑要付出 \(a_i\) 的代價(必須走到這個點)。最後必須回到 \(0\)。選出某k個點塗黑的代價是這 \(k\) 個點的 \(a\) 的和,加上兩倍的座標最大值。

對每個 \(k=1 \sim n\) 求,塗黑恰好 \(k\) 個不同的點的最大代價。

猜一個結論,\(k=i\) 的答案一定是在 \(k=i-1\) 的最優解的基礎上再選一個最優的點得來的,發現是對的,那麼就可以寫出 \(\mathcal{O}(n^2)\) 的暴力。

考慮最佳化。預處理一個 \(pre\) 陣列,\(pre_i\) 表示 \(i \sim n\) 中選擇一個新的最遠點可以得到的最大值。假設現在要求 \(k=i\) 的答案,\(k=i-1\) 時選擇的最遠點的位置是 \(pos\),分類討論:

  • 如果要在 \(pos\) 右邊選點,那麼答案就是 \(pre_{pos+1}\)
  • 如果在左邊選點,那麼就要遍歷 \(1 \sim pos-1\) 查詢 \(a_i\) 最大的點,這可以用堆最佳化,那麼答案就是堆頂元素。

將左右兩邊的答案比較一下如果左邊的更大,那就選左邊的,將堆頂元素刪去即可,否則選右邊的,此時最遠點發生了變化,那麼就要把 \(pos+1 \sim pos'-1\) 的元素入隊,就做完了。

並查集

每次合併兩個不相交集合,或者詢問兩個元素是否在同一個集合裡。

e.g. 1. 洛谷 P1197 [JSOI2008] 星球大戰

給一張圖,每次刪掉一個點及相連的邊,求剩下的圖中的聯通塊數。

我們倒著從空圖往回做,就變成了加邊求聯通塊數的問題。

e.g. 2. 洛谷 P1525 [NOIP2010 提高組] 關押罪犯

有一張圖,邊有邊權。你要給每個點確定是黑色或者白色,使得兩端顏色相同的邊的邊權最大值最小。

solution 1

一個直觀想法是儘可能讓邊權大的邊,兩端顏色不同。因此我們從大到小加邊,由於不能確定兩端點是什麼顏色,考慮用並查集維護一個相對關係,就是敵人的敵人是朋友這種感覺,如果一條邊兩端點在同一集合,直接輸出即可。

solution 2

考慮二分,假設二分到一個 \(\texttt{mid}\),如果大於 \(\texttt{mid}\) 的邊構成一個二分圖就是可行的。

e.g. 3. P2024 [NOI2001] 食物鏈

有三種生物 \(A,B,C\),其中 \(A\)\(B\)\(B\)\(C\)\(C\)\(A\)
\(n\) 個生物,每個要麼是 \(A\),要麼 \(B\),要麼 \(C\)
每次告訴你誰吃誰或者誰和誰同類,或者問你誰吃誰/誰和誰同類是否一定不成立。

使用帶權並查集,維護節點 \(x\) 與其父親 \(fa\) 的關係,記為 \(d_x\)

  • 如果 \(d_x=0\),那麼 \(x\) 與其父親 \(fa\) 是同類。
  • 如果 \(d_x=1\),那麼 \(x\)\(fa\)
  • 如果 \(d_x=2\),那麼 \(x\)\(fa\) 吃。

一些神奇的性質就是:

  • \(A \stackrel{s_1}{\longrightarrow} B \stackrel{s_2}{\longrightarrow} C \Rightarrow A \stackrel{s_1+s_2}{\longrightarrow} C\)
  • \(A \stackrel{s_1}{\longrightarrow} B \Rightarrow B \stackrel{-s_1}{\longrightarrow} A\)

(以上運算都是在模 \(3\) 意義下進行的)

之後再合併/路徑壓縮的時候用上述性質討論一下就可以了,不再贅述。

e.g. 4. P8779 [藍橋杯 2022 省 A] 推導部分和

有一列數字,每次告訴你一個區間的和,或者問能否確定一個區間的和。

考慮兩兩元素之間的縫隙,共有 \(n+1\) 個縫隙(加上第一個元素之前的和第 \(n\) 個元素之後的)。依次編號,告訴你一個區間的和本質上就是在講從一個縫隙走到另一個縫隙的距離。例如 \(l \sim r\) 的和是 \(s\) 就是第 \(l\) 個縫隙到 \(r+1\) 個縫隙的距離是 \(s\)。考慮帶權並查集,與上一題的做法基本類似。

將某些區間問題視作關於縫隙的圖是一類特殊技巧,有必要掌握一下。

數論基礎

取模

同餘:如果 \(a\)\(b\)\(m\) 取模得到的結果相同,那麼說 \(a\)\(b\) 在模 \(m\) 意義下相等,或者說二者同餘,記作 \(a \equiv b \pmod {m}\)

如果 \(m\)\(a\) 的因數,記作 \(m \mid a\)

因數

如果 \(a \mid b\) 並且 \(a \mid c\),那麼 \(a \mid (bx+cy)\)

\(\gcd(a,b)\)\((a,b)\)

顯然 \(d \mid (a,b)\) 等價於,\(d \mid a\)\(d \mid b\)

顯然 \((a,b)=(a+b,b)=(a-b,b)=(a \bmod b,b)\)

如果 \((a,b)=1\),那麼稱作 \(a\)\(b\) 互質

\(\operatorname{lcm}(a,b)\) 記為 \([a,b]\)

\([a,b]=\frac{a \times b}{(a,b)}\)

相關文章