目錄
聲明:
本系列博客是《算法競賽進階指南》+《算法競賽入門經典》+《挑戰程序設計競賽》的學習筆記,主要是因爲我三本都買了 按照《算法競賽進階指南》的目錄順序學習,包含書中的部分重要知識點、例題答案及我個人的學習心得和對該算法的補充拓展,僅用於學習交流和複習,無任何商業用途。博客中部分內容來源於書本和網絡(我儘量減少書中引用),由我個人整理總結(習題和代碼可全都是我自己敲噠)部分內容由我個人編寫而成 ,如果想要有更好的學習體驗或者希望學習到更全面的知識,請於京東搜索購買正版圖書:《算法競賽進階指南》— 作者李煜東,強烈安利,好書不火系列,謝謝配合。
下方鏈接爲學習筆記目錄鏈接(中轉站)
一、棧
棧後進先出
基礎的棧相信大家都懂,stack可以直接使用STL,或者用一個數組和一個變量(記錄棧頂位置)來實現棧結構。
使用時都要注意判空,不然就會RE!!
0.AcWing 41. 包含min函數的棧 (自己造棧)
劍指Offer的面試類型的題。
關於輸出Min,直接維護一個單調棧,棧頂存的就是當前棧的最小值。
這樣各種操作都是
class MinStack {
public:
/** initialize your data structure here. */
stack<int>stackValue;
stack<int>stackMin;
MinStack() {
}
void push(int x) {
stackValue.push(x);
if(stackMin.empty()||stackMin.top()>=x){
stackMin.push(x);
}
}
void pop() {
if(stackValue.top()==stackMin.top())
stackMin.pop();
stackValue.pop();
}
int top() {
return stackValue.top();
}
int getMin() {
return stackMin.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
1.AcWing 128. 編輯器 (對頂棧)
memset(f,0xcf,sizeof f);
-8084644332
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<map>
#include<vector>
#include<queue>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;
typedef long long ll;//全用ll可能會MLE或者直接WA,全部換成int看會不會A,別動這裏!!!
const int N=2000007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方約等於趨近爲0
int n,m;
int stack_l[N],stack_r[N];//一對對頂棧
int sum[N];//前綴和
int f[N];//最大前綴和
int cntL,cntR;//棧頂元素
int main()
{
scanf("%d",&n);
memset(f,0xcf,sizeof f);
over(i,1,n){
char ch;
cin>>ch;
//ch=getchar();
if(ch=='I'){//插入元素至L
int x;
scanf(" %d",&x);
stack_l[++cntL]=x;
sum[cntL]=sum[cntL-1]+x;
f[cntL]=max(f[cntL-1],sum[cntL]);
}
else if(ch=='Q'){//詢問
int x;
scanf(" %d",&x);
printf("%d\n",f[x]);
}
else if(ch=='D'){//刪除元素
if(!cntL)continue;//判空,要是用STL也必須要判空
cntL--;
}
else if(ch=='L'){
if(!cntL)continue;
stack_r[++cntR]=stack_l[cntL--];
}
else if(ch=='R'){
if(!cntR)continue;
stack_l[++cntL]=stack_r[cntR--];
sum[cntL]=sum[cntL-1]+stack_l[cntL];
f[cntL]=max(sum[cntL],f[cntL-1]);
}
//getchar();
}
return 0;
}
也可以使用STL,我懶得敲了,直接放一個大佬的吧:
作者:秦淮岸燈火闌珊
鏈接:https://www.acwing.com/solution/AcWing/content/1275/
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int t,x,sum[N],f[N],now;
stack<int> a,b,c;
int main()
{
while(scanf("%d\n",&t)!=EOF)//之前在HDU提交,所以是多組數據
{
a=c;//STL特性,這裏就是清空操作
b=c;
f[0]=-1e7;//初始化
sum[0]=0;
for(int i=1;i<=t;i++)
{
char ch=getchar();//讀入
if (ch=='I')//插入操作
{
scanf(" %d",&x);
a.push(x);//將a插入棧中
sum[a.size()]=sum[a.size()-1]+a.top();//前1~a.size()-1的前綴和,加上這個一個新來的,構成1~a.size()
f[a.size()]=max(f[a.size()-1],sum[a.size()]);//看是之前的最大值大,還是新來的最大值大
}
if (ch=='D')
if (!a.empty())//只要棧不爲空,就刪除
a.pop();
if (ch=='L')//左傾思想(博古+文化大革命)(手動滑稽)
if(!a.empty())//只要不爲空
b.push(a.top()),a.pop();//a+b等於整個插入序列,b負責管理當前光標右邊的序列.
if (ch=='R')//右傾思想(陳獨秀)(手動滑稽)
{
if (!b.empty())//b不爲空
{
a.push(b.top());//a負責管理1~當前光標.所以現在a往右了,那麼必然是要加入b棧的開頭,因爲b棧管理當前光標的右邊.
b.pop();
sum[a.size()]=sum[a.size()-1]+a.top();//同樣的還是重新定義.
f[a.size()]=max(f[a.size()-1],sum[a.size()]);//見插入操作.
}
}
if (ch=='Q')
{
scanf(" %d",&x);
printf("%d\n",f[x]);//輸出當前最大值區間.
}
getchar();//換行符讀入
}
}
return 0;
}
2.AcWing 129. 火車進棧
因爲對於每一步我們只有兩種操作,入棧或者棧頂出棧。
選擇出棧得到的序列一定比選擇入棧最後得到的序列的字典序要小,所以DFS爆搜,先搜pop再搜push,這樣就會得到按照字典序排列的答案瞭然後維護好邊界就好。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<stack>
#include<vector>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;
typedef long long ll;//全用ll可能會MLE或者直接WA,全部換成int看會不會A,別動這裏!!!
const int N=1000007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方約等於趨近爲0
int n;
vector<int>ans;
stack<int>st;
//我們只有兩種操作,入棧或者棧頂出棧
//選擇出棧得到的序列一定比選擇入棧最後得到的序列的字典序要小
int cnt=20;
void dfs(int u)
{
if(!cnt)return ;
if(ans.size()==n){
cnt--;
for(auto x:ans)
cout<<x;
cout<<endl;
return ;
}
if(st.size()){
ans.push_back(st.top());
st.pop();
dfs(u);
st.push(ans.back());//回溯
ans.pop_back();
}
if(u<=n){
st.push(u);
dfs(u+1);
st.pop();
}
}
int main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
3.AcWing 130. 火車進出棧問題
方法一:搜索(枚舉/遞歸)面對任何一個狀態,我們只有兩種選擇:
把下一個數進棧;
棧頂的數出棧
方法二:遞推
如果只要求方案數,不需要具體的方案,可以使用遞推直接統計:
設
表示進棧順序爲時可能的出棧順序總數,現在考慮數字1在出棧順序中的位置,如果1排在第k個出棧,那麼整個進出棧的過程爲:
- 整數1進棧;
- 2 ~ k - 1這k - 2個數以某種順序進出棧;
- 整數1出棧,排在第k個;
- k + 1 ~ N 這N - k個數字按照某種順序進出棧;
於是可以得到遞推公式:
方法三:動態規劃
用表示有i個數尚未進棧,目前有j個數在棧中,有個數已經出棧時的方案總數,邊界條件:開始:結束:開始: 結束:開始:結束:F[N,0];
由於每一步只能執行兩種操作:把一個數進棧和把一個數出棧,所以遞推公式爲:
方法四:數學
該問題等價於求第N項數,即,將在第三章介紹。
以上解析均來自《算法競賽進階指南》
二、各種表達式計算
中綴表達式:最常見的表達式,如
前綴表達式:又稱波蘭式,例如
後綴表達式:又稱逆波蘭式,例如
後綴表達式可以在的時間內求值。
後綴表達式求值方式:
建立一個棧,從左往右掃描表達式:
- 遇到數字,入棧
- 遇到運算符,彈出棧中的兩個元素,計算結果後再將結果壓入棧掃描完成之後,棧中只剩下一個數字,最終結果。
中綴表達式轉後綴表達式:
- 建立一個用於儲存運算符的棧,逐一掃描中綴表達式中的元素
- 掃描到數字,輸出該數;
- 遇到左括號,將左括號入棧;
- 遇到右括號,不斷取出棧頂元素並且輸出,直到棧頂爲左括號,彈出左括號舍棄
- 遇到運算符,如果棧頂的運算符優先級大於當前掃描到的運算符,就不斷取出棧頂的元素,最後將新符號入棧;
- 將棧中剩餘的運算符輸出,所有的輸出結果即爲轉化後的後綴表達式。
遞歸法求中綴表達式的值,O(n^2)
int calc(int l, int r) {
// 尋找未被任何括號包含的最後一個加減號
for (int i = r, j = 0; i >= l; i--) {
if (s[i] == '(') j++;
if (s[i] == ')') j--;
if (j == 0 && s[i] == '+') return calc(l, i - 1) + calc(i + 1, r);
if (j == 0 && s[i] == '-') return calc(l, i - 1) - calc(i + 1, r);
}
// 尋找未被任何括號包含的最後一個乘除號
for (int i = r, j = 0; i >= l; i--) {
if (s[i] == '(') j++;
if (s[i] == ')') j--;
if (j == 0 && s[i] == '*') return calc(l, i - 1) * calc(i + 1, r);
if (j == 0 && s[i] == '/') return calc(l, i - 1) / calc(i + 1, r);
}
// 首尾是括號
if (s[l] == '('&&s[r] == ')') return calc(l + 1, r - 1);
// 是一個數
int ans = 0;
for (int i = l; i <= r; i++) ans = ans * 10 + s[i] - '0';
return ans;
}
後綴表達式轉中綴表達式,同時求值,O(n)
// 數值棧
vector<int> nums;
// 運算符棧
vector<char> ops;
// 優先級
int grade(char op) {
switch (op) {
case '(':
return 1;
case '+':
case '-':
return 2;
case '*':
case '/':
return 3;
}
return 0;
}
// 處理後綴表達式中的一個運算符
void calc(char op) {
// 從棧頂取出兩個數
int y = *nums.rbegin();
nums.pop_back();
int x = *nums.rbegin();
nums.pop_back();
int z;
switch (op) {
case '+':
z = x + y;
break;
case '-':
z = x - y;
break;
case '*':
z = x * y;
break;
case '/':
z = x / y;
break;
}
// 把運算結果放回棧中
nums.push_back(z);
}
中綴表達式轉後綴表達式,同時對後綴表達式求值
int solve(string s) {
nums.clear();
ops.clear();
int top = 0, val = 0;
for (int i = 0; i < s.size(); i++) {
// 中綴表達式的一個數字
if (s[i] >= '0' && s[i] <= '9') {
val = val * 10 + s[i] - '0';
if (s[i+1] >= '0' && s[i+1] <= '9') continue;
// 後綴表達式的一個數,直接入棧
nums.push_back(val);
val = 0;
}
// 中綴表達式的左括號
else if (s[i] == '(') ops.push_back(s[i]);
// 中綴表達式的右括號
else if (s[i] == ')') {
while (*ops.rbegin() != '(') {
// 處理後綴表達式的一個運算符
calc(*ops.rbegin());
ops.pop_back();
}
ops.pop_back();
}
// 中綴表達式的加減乘除號
else {
while (ops.size() && grade(*ops.rbegin()) >= grade(s[i])) {
calc(*ops.rbegin());
ops.pop_back();
}
ops.push_back(s[i]);
}
}
while (ops.size()) {
calc(*ops.rbegin());
ops.pop_back();
}
// 後綴表達式棧中最後剩下的數就是答案
return *nums.begin();
}
三、單調棧
單調棧這個東西還是很容易理解的,就是一個棧,維護好他的單調性,可以是單調遞增也可以是單調遞減(或者非嚴格單增等等 )。寫起來非常好寫, 就是如果當前要入棧的元素大於棧頂就push進去,如果小於就一直pop,直到當前元素大於棧頂元素或者棧空爲止,很容易就可以證明/看出來這個棧依照這樣的操作一定能保持單調。那麼這樣的單調棧到底有什麼作用呢 ?比如下面這道題。
輸入樣例:
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0
輸出樣例:
8
4000
這道題要求最大面積,看上去沒什麼思路,其實就是單調棧的最基本的應用。
我畫幾個圖就能非常直觀地感受這道題了,不過在此之前最好先看一下代碼,然後再看圖。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<stack>
#include<vector>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;
typedef long long ll;//全用ll可能會MLE或者直接WA,全部換成int看會不會A,別動這裏!!!
const int N=100007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方約等於趨近爲0
ll q[N],w[N],h;
int n;
int main()
{
while(scanf("%d",&n)&&n){
memset(q,-1,sizeof q);
int top=0;
ll ans=0;
over(i,1,n+1){
if(i!=n+1){
scanf("%lld",&h);
}
else h=0;
if(h>q[top])//高於棧頂元素,保持遞增就入棧
q[++top]=h,w[top]=1;
else {
ll cnt=0;
while(h<=q[top]){//單調被破壞就pop,把所有低於這個元素的全部pop並更新答案。
ans=max(ans,(cnt+w[top])*q[top]);
cnt+=w[top--];
}
q[++top]=h;
w[top]=cnt+1;
}
}
printf("%lld\n",ans);
}
return 0;
}
看完代碼是不是有一點懂了,那麼我們來看圖:
首先圖一是一組數據,畫成圖的樣子。我們維護單調棧的單調性,直到遇見違反單調性的數,我們pop棧頂元素並更新ans。棧頂大於要入棧的元素,就pop,ans=max(ans,s1)。第二個還是大於,同樣的操作,ans=max(ans,s2)。注意這裏的s2就是第二個高度乘以2,因爲第一個棧頂雖然pop了但是對於第二個棧頂來說增加了它的面積,所以用cnt加上這裏的寬度,更新答案。最後恢復單調性,最新的棧頂元素的寬度就是被刪除的元素的寬度與自己的總和。刪完之後雖然棧裏的元素少了幾個但是畫成圖確實沒有少元素,只是高度變了。如下圖圖二:
然後我們繼續上面的操作。最後求得最大值,如圖3。注意我們要在最後加一個高度爲0的數據,爲了避免如圖4的情況出現。
然後就是《算法競賽進階指南》這本書上的代碼:
a[n + 1] = p = 0;
for (int i = 1; i <= n + 1; i++) {
if (a[i] > s[p]) {
s[++p] = a[i], w[p] = 1;
} else {
int width=0;
while (s[p] > a[i]) {
width += w[p];
ans = max(ans, (long long)width * s[p]);
p--;
}
s[++p] = a[i], w[p] = width + 1;
}
}
相信看完圖您一定能非常直觀地理解其中的奧妙。
注:如果您通過本文,有(qi)用(guai)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意爲您解答!( •̀ ω •́ )✧