聽上去有丶厲害,實際也很巧妙
學習了這兩篇:ReMoon - 單調棧的應用 --- 笛卡爾樹與虛樹
ACM算法日常 - 算法合集 | 神奇的笛卡爾樹 - HDU 1506
板子:

struct Cartesian { int root; int ls[N],rs[N]; vector<int> v; void clear() { root=0; v.clear(); for(int i=1;i<n;i++) ls[i]=rs[i]=0; } void build(int *a) { for(int i=1;i<n;i++) { int j=0; //a[v.back()]<a[i] 大根 //a[v.back()]>a[i] 小根 while(v.size() && a[v.back()]<a[i]) j=v.back(),v.pop_back(); if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } };
~ 簡介 ~
雖然名字中帶有“樹”,但是笛卡爾樹其實是對於一個序列的轉化,並通過這個轉化獲得更多此序列的信息
對於一個簡單的序列:$2,8,5,7,1,4$,我們可以建立如下的笛卡爾樹($pos$表示原序列中的位置,$val$表示該位置的值)
笛卡爾樹有這樣的基本性質:
對於樹上的任意一點$x$和左右兒子$left,right$,有:
1. $pos[left]<pos[x]<pos[right]$
2. $val[x]<val[left],val[right]$
即一般講解所說的$pos$滿足二叉查找樹,$val$滿足堆
直觀點說,就是這兩條延伸性質:
以樹上任意一點$x$為根構成的子樹中,
1. 各節點的$pos$是連續的,且對$pos$的中序遍歷即為原序列順序(由$pos$滿足二叉查找樹可得)
2. $x$點的$val$為全子樹最小(由$val$滿足堆可得)
~ 建樹 ~
有了對笛卡爾樹結構的了解,現在考慮怎么建立這棵樹
【方法一】優先滿足$val$
要想優先滿足$val$的條件,那就必須從頂向下建樹了
利用上面的延伸性質2,每次選取當前區間$[l,r]$中$val$的最小值所在的$pos$(記$pos=i$)作為子樹的根節點
然后對於$[l,i-1],[i+1,r]$遞歸地不斷重復上述過程
其中選取區間$val$最小值所在的$pos$可以使用線段樹優化
總復雜度$O(nlogn)$

#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=100005; const int INF=1<<30; int n; int val[N]; int sz; int t[N<<2]; inline void Add(int i) { int k=i+sz-1; t[k]=i; k>>=1; while(k) { int left=t[k<<1],right=t[k<<1|1]; t[k]=(val[left]<val[right]?left:right); k>>=1; } } inline int Query(int k,int l,int r,int a,int b) { if(a>r || b<l) return 0; if(a>=l && b<=r) return t[k]; int mid=(a+b)>>1; int left=Query(k<<1,l,r,a,mid),right=Query(k<<1|1,l,r,mid+1,b); return (val[left]<val[right]?left:right); } void Init() { sz=1; while(sz<n) sz<<=1; val[0]=INF; for(int i=1;i<(sz<<1);i++) t[i]=0; for(int i=1;i<=n;i++) Add(i); } int ls[N],rs[N]; inline int Build(int l,int r) { if(l>r) return 0; int pos=Query(1,l,r,1,sz); ls[pos]=Build(l,pos-1); rs[pos]=Build(pos+1,r); return pos; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&val[i]); Init(); int root=Build(1,n); /* for(int i=1;i<=n;i++) printf("i=%d: ls=%d rs=%d\n",i,ls[i],rs[i]);*/ return 0; }
【方法二】優先滿足$pos$
由於對於子樹的先序遍歷是原序列順序,所以考慮按$i=1\text{ ~ }n$的順序依次加入節點並調整樹的結構,使得當前的樹為子序列$[1,i]$所構成的笛卡爾樹
由於$pos$滿足二叉排序樹,而$i$在區間$[1,i]$中$pos$最大,所以$i$插入的位置為 序列$[1,i-1]$所構成的笛卡爾樹的根節點 一直向右兒子走、直到走到了空節點
這樣插入后,$i$的$pos$已經滿足要求了,但是$val$卻不一定滿足堆
於是考慮怎么調整當前的樹
若$i$的$val$不滿足要求,即存在某(些)祖先$j$,使得$j$為根的子樹中$val$全大於$val[i]$;顯然我們需要通過調整$i$的位置,使得$i$成為$j$的祖先
是這樣操作的:
0. 剛剛插入完成后,可能樹是這樣的
1. 將$i$向上一層移動;這時由於$pos[k]<pos[i]$,所以$k$成為$i$的左兒子,$k'$依然是$k$的左兒子
2. 繼續將$i$向上一層移動,相似的,$j$也應當屬於$i$的左子樹;不妨讓$j$為$i$的左兒子,$k$為$j$的右兒子(使用這種調整方法,$j,k,k'$相互間與原來的連邊相同)
以上的調整操作都是在$[1,i-1]$序列構成的笛卡爾樹的最右鏈(即從根節點一直向右兒子走的這條路徑)上進行的
在處理完后,我們對比一下調整前后的樹結構,發現只有很少的地方出現了變化:
1. $k$的右兒子變成了空節點
2. $j$的父親變成了$i$,且$j$是$i$的左兒子
3. $i$繼承了原來$j$的父親
事實上,即使$i$到$j$的路徑很長很長,一共也只有這三個地方發生了變化,所以我們的調整不是很復雜
現在最大的問題變成,如何找到$j$
目光回到最右鏈上,由於$val$滿足堆,於是最右鏈上的各節點$val$是單調遞增的;可以考慮用單調棧維護,棧中裝的是最右鏈上節點的$pos$
而我們要找的$j$,就是$val[j]<val[i]$、且最靠近棧底的元素
原理理解了之后,重新整理一下思路,盡量簡單清楚地建笛卡爾樹:
1. 用單調棧維護最右鏈
2. 每次插入當前的$i$,在單調棧中不停彈出棧頂,直到棧頂$fa$滿足$val[fa]<val[i]$,則最后一次彈出的就是$j$
3. 將$i$作為$fa$的右兒子,$j$作為$i$的左兒子
是不是很簡單owo
復雜度$O(n)$,是相當優秀的一種方法

#include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int N=100005; int n; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]>a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); Build(); /* for(int i=1;i<=n;i++) printf("i=%d ls=%d rs=%d\n",i,ls[i],rs[i]);*/ return 0; }
所以在一些情況下,笛卡爾樹的題目可以不用建樹,直接用單調棧就夠了
~應用~
最簡單的一個應用是求元素的左右延伸區間
具體點說,就是對於一個數列$a$,詢問以$a[i]$為區間最大(小)值的最長區間
使用笛卡爾樹,就可以通過$O(n)$的預處理做到$O(1)$查詢:進行中序遍歷,每個節點$x$的子樹的$pos$最小、最大值就是答案
模板題:HDU 1506 ($Largest\ Rectangle\ in\ a\ Histogram$)

#include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; typedef long long ll; const int N=100005; int n; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { v.clear(); memset(ls,0,sizeof(ls)); memset(rs,0,sizeof(rs)); for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]>a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; void dfs(int x) { l[x]=r[x]=x; if(ls[x]) { dfs(ls[x]); l[x]=l[ls[x]]; } if(rs[x]) { dfs(rs[x]); r[x]=r[rs[x]]; } } int main() { scanf("%d",&n); while(n) { for(int i=1;i<=n;i++) scanf("%d",&a[i]); Build(); dfs(root); ll ans=0; for(int i=1;i<=n;i++) ans=max(ans,ll(a[i])*(r[i]-l[i]+1)); printf("%lld\n",ans); scanf("%d",&n); } return 0; }
一個稍微高級一點的應用,就是給出分治的邊界
一道不錯的題:Luogu P4755 ($Beautiful\ Pair$)
官方題解已經很完善了:FlierKing - 題解 P4755 【Beautiful Pair】
簡單點說,就是每次取當前區間$[l,r]$的最大值$a_i$,那么$i$即為笛卡爾樹中 此區間對應子樹的根節點
於是將區間分成兩部分$[l,i-1],[i+1,r]$的操作,就可以轉化成笛卡爾樹上的分治
同時,這個題解將“統計$[l,r]$中$a_i\leq x$的數量”這個主席樹問題,離線后通過拆分轉化為樹狀數組問題,設計十分巧妙

#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; typedef pair<int,int> pii; const int N=100005; int n; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]<a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; inline void dfs(int x) { if(ls[x]) { dfs(ls[x]); l[x]=l[ls[x]]; } else l[x]=x; if(rs[x]) { dfs(rs[x]); r[x]=r[rs[x]]; } else r[x]=x; } vector<pii> add[N]; inline void Solve(int x) { int lp=x-l[x],rp=r[x]-x; if(lp<rp) for(int i=l[x];i<=x;i++) { add[r[x]].push_back(pii(a[x]/a[i],1)); add[x-1].push_back(pii(a[x]/a[i],-1)); } else for(int i=x;i<=r[x];i++) { add[x].push_back(pii(a[x]/a[i],1)); add[l[x]-1].push_back(pii(a[x]/a[i],-1)); } if(ls[x]) Solve(ls[x]); if(rs[x]) Solve(rs[x]); } vector<int> pos; int t[N]; inline int lowbit(int x) { return x&(-x); } inline void Add(int k,int x) { for(int i=k;i<=n;i+=lowbit(i)) t[i]+=x; } inline int Query(int k) { int res=0; for(int i=k;i;i-=lowbit(i)) res+=t[i]; return res; } int main() { scanf("%d",&n); pos.push_back(0); for(int i=1;i<=n;i++) scanf("%d",&a[i]),pos.push_back(a[i]); sort(pos.begin(),pos.end()); pos.resize(unique(pos.begin(),pos.end())-pos.begin()); Build(); dfs(root); Solve(root); ll ans=0; for(int i=1;i<=n;i++) { int p=lower_bound(pos.begin(),pos.end(),a[i])-pos.begin(); Add(p,1); for(int j=0;j<add[i].size();j++) { int lim=lower_bound(pos.begin(),pos.end(),add[i][j].first)-pos.begin(); if(pos[lim]>add[i][j].first) lim--; ans=ans+add[i][j].second*Query(lim); } } printf("%lld\n",ans); return 0; }
學完之后立馬就現場碰到基本一樣的題...
牛客ACM 883G ($Removing\ Stones$,2019牛客暑期多校訓練營(第三場))
只不過對於當前區間,是遍歷較小的那一半、並在另一半二分

#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=300005; int n; int a[N]; ll p[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]<a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; inline void dfs(int x) { if(ls[x]) { dfs(ls[x]); l[x]=l[ls[x]]; } else l[x]=x; if(rs[x]) { dfs(rs[x]); r[x]=r[rs[x]]; } else r[x]=x; } ll ans=0; void Solve(int x) { ll sum=0; int lp=x-l[x]+1,rp=r[x]-x+1; int left,right,mid; if(lp<rp) for(int i=x;i>=l[x];i--) { sum+=a[i]; left=x,right=r[x]+1,mid; while(left<right) { mid=(left+right)>>1; if(sum+p[mid]-p[x]<2LL*a[x]) left=mid+1; else right=mid; } ans+=r[x]-left+1; } else for(int i=x;i<=r[x];i++) { sum+=a[i]; left=l[x],right=x,mid; while(left<right) { mid=(left+right)>>1; if(sum+p[x-1]-p[mid-1]>=2LL*a[x]) left=mid+1; else right=mid; } if(sum+p[x-1]-p[left-1]<2LL*a[x]) left--; ans+=left-l[x]+1; } if(ls[x]) Solve(ls[x]); if(rs[x]) Solve(rs[x]); } void Clear() { ans=0; v.clear(); for(int i=1;i<=n;i++) ls[i]=rs[i]=0; } int main() { int T; scanf("%d",&T); while(T--) { Clear(); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]),p[i]=p[i-1]+a[i]; Build(); dfs(root); Solve(root); printf("%lld\n",ans); } return 0; }
一道比較明顯的題:HDU 6701 ($Make\ Rounddog\ Happy$,$2019\ Multi-University\ Training\ Contest\ 10$)
由於需要對$a_l,...,a_r$求max,所以能比較自然地想到笛卡爾樹上分治
然后就是處理區間內數字不同的限制;不過也並不困難,只要正向、反向各掃一遍就能預處理出來$i$向左、向右不出現重復數字的最大延伸長度了

#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=300005; int n,k; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { v.clear(); for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]<a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; void dfs(int x) { l[x]=r[x]=x; if(ls[x]) dfs(ls[x]),l[x]=l[ls[x]]; if(rs[x]) dfs(rs[x]),r[x]=r[rs[x]]; } int cnt[N]; int L[N],R[N]; ll ans=0; void Solve(int x) { int lp=x-l[x],rp=r[x]-x; if(lp<rp) { for(int i=l[x];i<=x;i++) ans+=max(0,min(R[i],r[x])-max(a[x]-k+i-1,x)+1); } else { for(int i=x;i<=r[x];i++) ans+=max(0,min(k-a[x]+i+1,x)-max(L[i],l[x])+1); } if(ls[x]) Solve(ls[x]); if(rs[x]) Solve(rs[x]); } int main() { int T; scanf("%d",&T); while(T--) { scanf("%d%d",&n,&k); ans=0; for(int i=1;i<=n;i++) ls[i]=rs[i]=0; for(int i=1;i<=n;i++) scanf("%d",&a[i]); Build(); dfs(root); int j=1; for(int i=1;i<=n;i++) { cnt[a[i]]++; while(cnt[a[i]]>1) { R[j]=i-1; cnt[a[j]]--; j++; } } for(int i=j;i<=n;i++) R[i]=n,cnt[a[i]]--; j=n; for(int i=n;i>=1;i--) { cnt[a[i]]++; while(cnt[a[i]]>1) { L[j]=i+1; cnt[a[j]]--; j--; } } for(int i=j;i>=1;i--) L[i]=1,cnt[a[i]]--; Solve(root); printf("%lld\n",ans); } return 0; }
標算是左偏樹,不過用笛卡爾樹+倍增也能搞過去:HDU 5575 ($Discover\ Water\ Tank$,$2015\ ACM/ICPC$上海)
首先可以根據隔板的高度,對於$n-1$個隔板建立一個大根笛卡爾樹
有了這棵笛卡爾樹,我們可以考慮利用它來划分出分治區間
比如,對於笛卡爾樹根節點對應原序列的位置$pos_{root}$,相當於將$1\text{~}n$的區間划分成兩部分$[1,pos_{root}],[pos_{root}+1,n]$,且每部分的水位最高都不超過$h[pos_{root}]$;其余節點的划分同理
我們先將每個查詢分配到划分樹上,具體方法是,先倍增出每個划分樹節點的父親關系,然后對於每個查詢$\{x,y,w\}$,從$[x,x]$對應的區間,向上找到樹上最深的 水位限制大於等於$y$的祖先,並將這個查詢扔到那個節點的vector中
於是考慮樹形dp
對於一個區間,要不將它整體灌滿,要不不灌滿、並向下遞歸;所以對於每個划分樹上的節點,分別記錄灌滿和不灌滿的 最多正確詢問數
注意$y$的邊界即可(我的處理是將每個$y++$)

#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; struct Query { int x,y,w; Query(int a,int b,int c) { x=a,y=b,w=c; } }; inline bool operator <(Query A,Query B) { return A.y<B.y; } inline bool operator >(Query A,Query B) { return A.y>B.y; } const int INF=1<<30; const int N=200005; const int LOG=20; int n,m; int h[N]; struct Cartesian { int root; int ls[N],rs[N]; vector<int> v; void clear() { root=0; v.clear(); for(int i=1;i<n;i++) ls[i]=rs[i]=0; } void build() { for(int i=1;i<n;i++) { int j=0; while(v.size() && h[v.back()]<h[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } }tree; int tot; int lb[N],rb[N],lim[N]; int ls[N],rs[N]; int fa[N][LOG]; int place[N]; void Build(int x,int y,int l,int r,int f) { lb[x]=l,rb[x]=r; fa[x][0]=f; if(l==r) { place[l]=x; return; } ls[x]=++tot; lim[tot]=h[y]; Build(tot,tree.ls[y],l,y,x); rs[x]=++tot; lim[tot]=h[y]; Build(tot,tree.rs[y],y+1,r,x); } int sum[N],sub[N]; vector<Query> v[N]; void dfs(int x) { if(ls[x]) dfs(ls[x]); if(rs[x]) dfs(rs[x]); int empty=0,full=0; for(int i=0;i<v[x].size();i++) { Query tmp=v[x][i]; if(tmp.w==0) empty++; } sub[x]=sub[ls[x]]+sub[rs[x]]+empty; for(int i=0;i<v[x].size();) { int j=i; while(j<v[x].size() && v[x][i].y==v[x][j].y) { Query tmp=v[x][j]; if(tmp.w==0) empty--; if(tmp.w==1) full++; j++; } i=j; sub[x]=max(sub[x],sum[ls[x]]+sum[rs[x]]+full+empty); } sum[x]=sum[ls[x]]+sum[rs[x]]+full; } int main() { int T; scanf("%d",&T); for(int kase=1;kase<=T;kase++) { scanf("%d%d",&n,&m); for(int i=1;i<n;i++) scanf("%d",&h[i]); tree.clear(); tree.build(); for(int i=1;i<=tot;i++) { v[i].clear(); ls[i]=rs[i]=fa[i][0]=0; sum[i]=sub[i]=0; } tot=1; lim[1]=INF; Build(1,tree.root,1,n,0); for(int i=1;i<LOG;i++) for(int j=1;j<=tot;j++) fa[j][i]=fa[fa[j][i-1]][i-1]; for(int i=1;i<=m;i++) { int x,y,w; scanf("%d%d%d",&x,&y,&w); Query tmp(x,++y,w); int p=place[x]; for(int j=LOG-1;j>=0;j--) if(fa[p][j] && y>lim[fa[p][j]]) p=fa[p][j]; if(y>lim[p]) p=fa[p][0]; v[p].push_back(tmp); } for(int i=1;i<=tot;i++) sort(v[i].begin(),v[i].end()); dfs(1); printf("Case #%d: %d",kase,sub[1]); putchar('\n'); } return 0; }
一般都是銀牌題難度的樣子吧,平常見的不多,遇到再補充
Nowcoder 209390 (Sort the String Revision,2020牛客暑期多校第四場)
(完)