1. 程式人生 > 實用技巧 >[CISCN2019 華北賽區 Day1 Web2]ikun

[CISCN2019 華北賽區 Day1 Web2]ikun

單調棧

定義

顧名思義,就是棧中儲存元素的某種資訊是單調的棧。
單調棧可以幹什麼呢?
可以線性尋找一個元素左邊(或右邊)第一個滿足某種條件的元素。
比較常見的問題是:給定一個序列,對於每個數尋找其左邊(或右邊)第一個比它大(或比它小)的數。

演算法流程

單調棧是怎麼實現的呢?我們以尋找每個數右邊第一個比它大的數為例。
我們從左往右掃這個序列,維護一個棧,儲存當前還沒找到比它大的數的元素
可以發現棧中的元素有兩個資訊是單調的:下標和數值。
下標單調是由於我們是從左往右掃這個序列,所以入棧的元素一定是按照下標單增的;而且棧是 \(LIFO\) 的結構,所以每次出棧的元素的下標一定是棧中最大的,彈出後仍滿足下標的單調性。
數值單增是由於我們棧內維護的是未找到比它大的數的元素

,所以棧內的元素的數值應該是單減的。如果出現了單增的情況,即當前我們掃到了第 \(j\) 個數,且此時棧中有個數的下標為 \(i\),滿足 \(a[i]<a[j](\)別忘了一定滿足 \(i<j)\),那麼不就說明第 \(i\) 個數 \(a[i]\) 已經找到了它右面第一個比它大的數 \(a[j]\) 了嘛?所以此時 \(i\) 被彈出棧,使得棧中元素保持單減的性質。
到這裡整個演算法流程就出來了:
我們從左往右掃整個序列,如果棧為空或當前這個數的數值小於等於棧頂元素的數值,那麼我們將它壓入棧頂;否則一直將棧頂彈出,直到棧為空或者當前這個數的數值小於等於棧頂元素的數值。
舉個例子吧:
\(a[i]\)
表示第 \(i\) 個數是幾,\(b[i]\) 表示第 \(i\) 數右邊第一個比它大的數的下標是幾,棧中維護的是數的下標
\(a[i]:3,7,2,1,5\)
我們掃到第 \(1\) 個數 \(3\),發現此時棧為空,入棧。棧中元素:\(1\)
我們掃到第 \(2\) 個數 \(7\),發現此時棧頂元素 \(a[1]=3<7\),說明棧頂元素找到了第一個比它大的數,我們更新 \(b[1]=2\),彈出 \(1\),此時棧為空,那麼壓入 \(2\)。棧中元素:\(2\)
我們掃到第 \(3\) 個數 \(2\),發現此時棧頂元素 \(a[2]=7>2\),不更新,我們將 \(3\)
壓入棧。棧中元素:\(2,3\)
我們掃到第 \(4\) 個數 \(1\),發現此時棧頂元素 \(a[3]=2>1\),不更新,我們將 \(4\) 壓入棧。棧中元素:\(2,3,4\)
我們掃到第 \(5\) 個數 \(5\),發現此時棧頂元素 \(a[4]=1<5\),說明棧頂元素找到了第一個比它大的數,我們更新 \(b[4]=5\),彈出 \(4\),繼續看棧頂。此時棧頂元素 \(a[3]=2<5\),說明棧頂元素找到了第一個比它大的數,我們更新 \(b[3]=5\),彈出 \(3\),繼續看棧頂。此時棧頂元素 \(a[2]=7>5\),不更新,我們將 \(5\) 壓入棧。棧中元素:\(2,5\)
我們掃完之後發現棧內還有元素 \(2,5\),說明這幾個數它右面沒有比它大的數了,我們令 \(b[2]=b[5]=0\),然後將棧清空。
附上簡短的程式碼:

int a[N],b[N],st[N];
void work()
{
	int top=0;
	for(int i=1;i<=n;i++)
	{
		while(top!=0&&a[st[top]]<a[i])  //如果棧不為空且棧頂元素的數值小於當前數,說明棧頂元素找到了右邊第一個大於它的數 
		{
			b[st[top]]=i;               
			top--;
		}
		st[++top]=i;                    //入棧 
	}
	while(top!=0)                           //掃完之後棧未空,說明這些數右面沒有比它大的數了 
	{
		b[st[top]]=0;                   //索性賦為0吧,具體根據題目定 
		top--; 
	} 
} 

例題

SP1805 HISTOGRA - Largest Rectangle in a Histogram

簡化一下題面:
在一條水平線上有 \(n\) 個寬為 \(1\) 的矩形,求包含於這些矩形的最大子矩形面積。
題解
考慮到答案矩形的高度是由它底邊範圍內最低的矩形的高度所決定。
樸素的做法是預處理區間最小值,然後列舉左右端點然後算面積,時間複雜度 \(O(n^2)\),顯然我們是無法接受的。
我們不妨看作是讓每個小矩形分別向左和向右擴充套件,如果遇到高度比它高的就繼續擴充套件,否則就停止擴充套件。
這樣一來,擴充套件所成的大矩形的高度我們是知道的,就是這個小矩陣的高度,因為其左右擴充套件的矩形的高度均不小於它的高度
關鍵就是要求擴充套件所成的大矩形的長度。我們可以貪心地想到,在一個矩形高度確定的情況下,底邊長度越大面積越大,換句話說,就是要一直擴充套件到無法再擴展才會更優。
那麼這個問題就轉化成了:給你一個序列,問每個數左右兩邊第一個比它小的數的距離乘這個數的值最大是多少。
我們可以用單調棧來求。時間複雜度 \(O(n)\)

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=1e6;
int n,top;
ll a[N],L[N],R[N],st[N];
ll work()
{
	for(int i=1;i<=n+1;i++)          //找每個數右邊第一個比它小的數,這裡迴圈到n+1就能保證每個數都被彈出棧 
	{
		while(top&&a[st[top]]>a[i]) R[st[top--]]=i;
		st[++top]=i;
	}
	for(int i=n;i>=0;i--)            //找每個數左邊第一個比它小的數,這裡迴圈到0就能保證每個數都被彈出棧 
	{
		while(top&&a[st[top]]>a[i]) L[st[top--]]=i;
		st[++top]=i;
	}
	ll ans=0;
	for(int i=1;i<=n;i++)            //計算每個小矩形所能擴充套件的最大矩形 
	    ans=max(ans,(ll)(R[i]-L[i]-1)*a[i]);  
	return ans; 
}
int main()
{
	while(1)
	{
		scanf("%d",&n);
		if(!n) break;
		for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
		printf("%lld\n",work()); 
	}
	return 0;
}

luogu P1950 長方形

簡化一下題面:
給出一個 \(n*m\) 的矩形,只包含 \('.'\)\('*'\) 兩種符號,問有多少個子矩形內只包含 \('.'\)
題解
為了方便起見,我們將 \('.'\) 記為 \(0\),將 \('*'\) 記為 \(1\)
我們先預處理每個格子最多能向上延伸幾個格子。如果遇到 \(1\) 則記為 \(0\)
先貼上神奇的讀入函式:

bool read()                            //手寫的read,媽媽再也不用擔心我讀入字串了 
{
	char ch=getchar();
	while(ch!='.'&&ch!='*') ch=getchar();
	if(ch=='.') return 0;              
	return 1;
}

然後是預處理每個格子最多能向上延伸多少個格子:

for(int i=1;i<=n;i++)
{
	for(int j=1;j<=n;j++)
	{
		map[i][j]=read();           //讀入矩形 
		if(map[i][j]) S[i][j]=0;    //如果當前格子是1,則不能向上延伸,記為0 
		else S[i][j]=S[i-1][j]+1;   //否則就接著上一個格子繼續延伸 
	}
}

然後我們列舉長方形的底邊在哪條邊上,這樣我們就相當於固定了長方形底邊的高度,這樣就轉化成了上一個題目,只不過上個題是讓求最大面積,本題是讓求方案數。
我們利用單調棧求出每個點左邊第一個小於等於它的數 \(L[i]\) 和右邊第一個小於它的數 \(R[i]\),那麼被這一列所限制的長方形數為 \((i-L[i])*(R[i]-i)*h_i\),將所有列相加就是以當前行為底邊所能構造的長方形數了 。
解釋一下為什麼是一個小於等於,一個小於
如果出現了相鄰的且高度相等的小矩形,那麼兩個小於等於是會算重了的,而兩個小於又會漏情況,所以只能一個小於等於,一個小於,這樣才能做到不重不漏。
不重:當且僅當在同一行存在兩個數 \(L[i]=L[j]\) 並且 \(R[i]=R[j]\) 的時候,才有可能會算重矩形。但是這種情況是不存在的,因為 \(L[i]\) 滿足了左邊第一個小於等於的,\(R[i]\) 滿足了右邊第一個小於的,顯然無法構造出這樣的情況。
不漏:對於一個矩形,總有一個 \(L[i]\)\(R[i]\) 能框住矩形的兩邊,故這個矩形一定能被算到。
時間複雜度 \(O(mn)\)

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
bool read()                 //手寫讀入頂呱呱                                
{
	char ch=getchar();
	while(ch!='.'&&ch!='*') ch=getchar();
	if(ch=='.') return 0;
	return 1;
}
const int N=1005;
int n,m,top;
ll ans;
int S[N][N],L[N],R[N],st[N];
bool map[N][N];              
ll work(int x)
{
	for(int i=1;i<=m+1;i++) //往右找第一個小於它的數,迴圈到m+1能保證所有數出棧 
	{
		while(top&&S[x][i]<S[x][st[top]]) R[st[top--]]=i;
		st[++top]=i;
	}
	for(int i=m;i>=0;i--)   //往左找第一個小於它的數,迴圈到0能保證所有數出棧 
	{
		while(top&&S[x][i]<=S[x][st[top]]) L[st[top--]]=i;
		st[++top]=i;
	}
	ll cnt=0;
	for(int i=1;i<=m;i++)   //把被每個小矩形所限制的長方形的構造方案數相加,就是被這一行所限制的長方形構造方案數 
	    cnt+=(i-L[i])*(R[i]-i)*S[x][i];
    return cnt;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			map[i][j]=read();
			if(map[i][j]) S[i][j]=0;    //預處理出每個格子連續向上為0的最長長度,也就是小矩形的高度 
			else S[i][j]=S[i-1][j]+1;
		}
	}
	for(int i=1;i<=n;i++)   //列舉每一行,算被每一行所限制的長方形的構造方案數     
	    ans+=work(i);
	printf("%lld\n",ans);
	return 0;
}