笛卡尔树学习笔记
\(~~~~\) 因为做不动例题所以就来水学习笔记了
\(~~~~\) 以下记 \(ls_i\) 表示 \(i\) 节点的左儿子,\(rs_i\) 表示 \(i\) 结点的右儿子。
一、简介
\(~~~~\) 笛卡尔树是一种二叉树,每个结点都有一个键值二元组 \((k,w)\) 。在笛卡尔树中,\(k\) 满足 BST(二叉搜索树)的性质,而 \(w\) 满足堆的性质。即:
\(~~~~\) 有一个事实是,如果每个结点的 \(k\) 值互不相同,\(w\) 值互不相同,那么这棵笛卡尔树的形态唯一确定,这也是洛谷模板题用到的一个事实。
\(~~~~\) 此外,在大部分题目中,元素的下标将作为 \(k\) ,因此我们可以得到构建的笛卡尔树的某一子树内的 \(k\) 值是连续区间。(这是显然的,因为该元素一定是某一个区间内的最值)
二、构建
\(~~~~\) 笛卡尔树一般使用 \(\mathcal{O(n)}\) 的栈构建方法。
\(~~~~\) 将原序列按 \(k\) 升序排序,首先用一个栈维护该笛卡尔树的右链(右链:指从根节点开始一直向右走的那条链)。每次加入新点时先将其加入右链,此时它定然满足 \(k\) 的限制,但不一定满足 \(w\) 的限制,因此将该点沿右链上移,直到第一个满足其 \(w\) 限制的位置,然后将原来右链的下部分移到其左子树即可。
\(~~~~\) 这一部分的代码如下:
查看代码
for(int i=1;i<=n;i++)
{
int k=top;
while(k&&arr[sta[k]]>arr[i]) k--;
if(k) rs[sta[k]]=i;
if(k<top) ls[i]=sta[k+1];
sta[++k]=i;top=k;
}
三、例题
\(~~~~\) 笛卡尔树的基本操作只有上面这些,所以它一般会套上其他数据结构食用(然后就不会了
1、Largest Rectangle in a Histogram
题意
\(~~~~\) 求若干个宽为一的如图放置的长方形中最大矩形面积。
题解
\(~~~~\) 这题 DP,单调栈都可以做,下面只提笛卡尔树的解法。
\(~~~~\) 首先按下标和高度为键值二元组构建小根堆的笛卡尔树。
\(~~~~\) 那么一个结点的子树大小就是该最小值可以作为高度的长度,因此 \(\mathcal{O(n)}\) 遍历整棵树,然后用 权值 \(\times\) 子树大小后取 \(\max\) 即可。
代码
查看代码
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
int h[100005];
int sta[100005],top;
int ls[100005],rs[100005],siz[100005];
ll Ans=0;
void dfs(int u)
{
siz[u]=1;
if(ls[u]) dfs(ls[u]),siz[u]+=siz[ls[u]];
if(rs[u]) dfs(rs[u]),siz[u]+=siz[rs[u]];
Ans=max(Ans,1ll*siz[u]*h[u]);
}
int main() {
int n;
while(scanf("%d",&n)&&n)
{
Ans=0;top=0;
for(int i=1;i<=n;i++) ls[i]=rs[i]=0;
for(int i=1;i<=n;i++) scanf("%d",&h[i]);
for(int i=1;i<=n;i++)
{
int k=top;
while(k&&h[sta[k]]>h[i]) k--;
if(k) rs[sta[k]]=i;
if(k<top) ls[i]=sta[k+1];
sta[++k]=i;top=k;
}
dfs(sta[1]);
printf("%lld\n",Ans);
}
return 0;
}
2、洛谷P4755 Beautiful Pair
题意
\(~~~~\) 求满足 \(1\leq i\leq j\leq n\) 且 \(a_ia_j\leq \max_{k=i}^j a_k\) 的二元组 \((i,j)\) 的个数。
\(~~~~\) \(1\leq n\leq 10^5\)
题解
\(~~~~\) 由于这是笛卡尔树的例题,不难想到构建一棵以下标和权值为二元组的且满足大根堆的二元组。则我们可以得到每个数在哪个区间内作为最大值,设 \(a_{i}\) 其在 \([l_i,r_i]\) 之内为最大值,则现在我们要求从 \([l_i,i-1]\) 和 \([i+1,r_i]\) 之内任意取两个数 \(a_j,a_k\) 满足 \(a_j\times a_k\leq a_i\) 的数对个数。然后再分治去解决 \([l_i,i-1]\) 和 \([i+1,r_i]\) 即可。
\(~~~~\) 如何数这样的数对个数?此时我们考虑枚举 \([l_i,i-1]\) 和 \([i+1,r_i]\) 中较短的一边,当枚举到 \(x\) 时需要查询在另一个区间内 \(\leq \dfrac{a_i}{x}\) 的数的个数,此时可以拆为询问两个以 \(1\) 为左端点的区间中 \(\leq\) 该数的数的个数相减,离线下来用 BIT 维护即可。
\(~~~~\) 至于复杂度,显然每次分治最劣会使两边区间长度一样,则此时每次区间长度减去一半,故每个数最多被枚举 \(\log n\) 次,也就是最多有 \(n \log n\) 个询问,再加上 BIT,可以用 \(n \log^2 n\) 的时间复杂度通过此题。
代码
查看代码
#define ll long long
#define PII pair<int,int>
#define mp(a,b) make_pair(a,b)
vector< PII > V[100005];
int a[100005],b[100005],sta[100005],top;
int ls[100005],rs[100005],siz[100005];
int L[100005],R[100005];
void dfs(int u)
{
siz[u]=1;
if(ls[u])
{
L[ls[u]]=L[u];
R[ls[u]]=u-1;
dfs(ls[u]);
siz[u]+=siz[ls[u]];
}
if(rs[u])
{
L[rs[u]]=u+1;
R[rs[u]]=R[u];
dfs(rs[u]);
siz[u]+=siz[rs[u]];
}
int from,to,from1,to1;
if(siz[ls[u]]<=siz[rs[u]]) from=L[u],to=u,from1=u,to1=R[u];
if(siz[ls[u]]>siz[rs[u]]) from=u,to=R[u],from1=L[u],to1=u;
for(int i=from;i<=to;i++)
{
V[from1-1].push_back(mp(a[u]/a[i],-1));
V[to1].push_back(mp(a[u]/a[i],1));
}
}
struct BIT{
int tr[100005];
inline int lowbit(int x){return x&(-x);}
void add(int x,int val){for(;x<=100000;x+=lowbit(x)) tr[x]+=val;}
ll query(int x)
{
ll ret=0;
for(;x;x-=lowbit(x)) ret+=tr[x];
return ret;
}
}BIT;
int main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),b[i]=a[i];
sort(b+1,b+1+n);
int cnt=unique(b+1,b+1+n)-b-1;
for(int i=1;i<=n;i++)
{
int k=top;
while(k&&a[sta[k]]<a[i]) k--;
if(k) rs[sta[k]]=i;
if(k<top) ls[i]=sta[k+1];
sta[++k]=i;top=k;
}
L[sta[1]]=1,R[sta[1]]=n;
dfs(sta[1]);
for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+cnt,a[i])-b;
ll Ans=0;
for(int i=1;i<=n;i++)
{
BIT.add(a[i],1);
for(int j=0;j<V[i].size();j++)
{
int q=V[i][j].first,Type=V[i][j].second;
int To=upper_bound(b+1,b+1+cnt,q)-b-1;
Ans+=1ll*Type*BIT.query(To);
}
}
printf("%lld",Ans);
return 0;
}
3、[HNOI2016]序列
题意
\(~~~~\) 给出 \(n\) 个数的序列和 \(q\) 次询问,每次询问序列 \([l,r]\) 的所有子序列的最小值的和。
\(~~~~\) \(1\leq n,q\leq 10^5\)
题解
\(~~~~\) 预处理出每个数 \(a_i\) 作为最小值的区间 \([l_i,r_i]\) ,则 \(a_i\) 会对该区间内每一个 \(L\in[l_i,i],R\in[i,r_i]\) 的序列 \([L,R]\) 产生贡献。 如果我们构建一个平面直角坐标系,以 \(l\) 为横坐标,\(r\) 为纵坐标,则此时 \(a_i\) 会对左下角为 \((l_i,i)\) 右上角为 \((i,r_i)\) 的矩形内所有点产生贡献。同理,对于一个询问 \([l,r]\) ,它就相当于询问在上述坐标系中以 \((l,l)\) 为左下角,\((r,r)\) 为右上角的矩形内的点的和(注意到对于任意点 \((l,r)\) ,若 \(r>l\) ,则 \((l,r)\) 的值一定为 \(0\))。
\(~~~~\) 考虑如何处理上述问题,即实现矩形内加法和矩形内求和的问题,显然由于本题所有询问均在修改之后,因此可以离线处理,故在进行矩形加法时可以打上差分标记,在求和时打上询问的标记(即询问二维前缀和的类似方式)。则此时考虑某个在询问标记 \((i,j)\) 左下角的修改标记 \((x,y)\) 对其产生的影响。不难看出这样会产生 \((i-x+1)\times (j-y+1)\times val_i\) (\(val_i\) 为该修改的值)的贡献,拆开上式: \(xy\times val_i-(i-1)\times y\times val_i-(j-1)\times x\times val_i+(i-1)(j-1)\times val_i\) ,故对于一个修改标记,在修改时维护 \(xy\times val_i,y\times val_i,x\times val_i,val_i\) 即可,然后运用 BIT 进行二维数点。时间复杂度为大常数 \(\mathcal{O(n\log n)}\) 。
\(~~~~\) 什么,你问笛卡尔树在哪里?预处理区间时用笛卡尔树即可。
代码
查看代码
#define ll long long
#define fi first
#define se second
#define PII pair<int,int>
#define PLL pair<ll,ll>
#define mp(a,b) make_pair(a,b)
int n,m,q,arr[100005];
int sta[100005],top;
int ls[100005],rs[100005];
int L[100005],R[100005];
vector< pair<int,ll> >V[100005];
vector< pair<PII,int> >Q[100005];
void Add(int x,int y,int a,int b,ll val)
{
V[a+1].push_back(mp(b+1,val));
V[x].push_back(mp(y,val));
V[x].push_back(mp(b+1,-val));
V[a+1].push_back(mp(y,-val));
}
void Query(int x,int y,int a,int b,int id)
{
Q[a].push_back(mp(mp(b,1),id));
Q[x-1].push_back(mp(mp(y-1,1),id));
Q[x-1].push_back(mp(mp(b,-1),id));
Q[a].push_back(mp(mp(y-1,-1),id));
}
void dfs(int u)
{
Add(L[u],u,u,R[u],arr[u]);
if(ls[u])
{
L[ls[u]]=L[u];
R[ls[u]]=u-1;
dfs(ls[u]);
}
if(rs[u])
{
L[rs[u]]=u+1;
R[rs[u]]=R[u];
dfs(rs[u]);
}
}
void Pre()
{
for(int i=1;i<=n;i++)
{
int k=top;
while(k&&arr[sta[k]]>arr[i]) k--;
if(k) rs[sta[k]]=i;
if(k<top) ls[i]=sta[k+1];
sta[++k]=i;top=k;
}
L[sta[1]]=1,R[sta[1]]=n;
dfs(sta[1]);
}
ll Ans[100005];
struct BIT{
ll tr1[100005],tr2[100005],tr3[100005],tr4[100005];
inline int lowbit(int x){return x&(-x);}
void Add(int x,ll val1,ll val2,ll val3,ll val4)
{
for(;x<=100000;x+=lowbit(x))
{
tr1[x]+=val1;tr2[x]+=val2;
tr3[x]+=val3;tr4[x]+=val4;
}
}
pair< PLL,PLL > Query(int x)
{
pair< PLL,PLL > ret;
for(;x;x-=lowbit(x))
{
ret.fi.fi+=tr1[x]; ret.fi.se+=tr2[x];
ret.se.fi+=tr3[x]; ret.se.se+=tr4[x];
}
return ret;
}
}BIT;
void Solve()
{
for(int i=1;i<=n;i++)
{
for(int j=0;j<V[i].size();j++)
{
int x=i,y=V[i][j].first,val=V[i][j].second;
BIT.Add(y,1ll*val,1ll*(x-1)*val,1ll*(y-1)*val,1ll*(x-1)*(y-1)*val);
}
for(int k=0;k<Q[i].size();k++)
{
int j=Q[i][k].fi.fi,id=Q[i][k].se;
pair< PLL,PLL > ret=BIT.Query(j);
ll val,xval,yval,xyval;
val=ret.fi.fi,xval=ret.fi.se;
yval=ret.se.fi,xyval=ret.se.se;
Ans[id]+=Q[i][k].fi.se*(1ll*i*j*val-1ll*j*xval-1ll*i*yval+1ll*xyval);
}
}
}
int main() {
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&arr[i]);
Pre();
for(int i=1,l,r;i<=m;i++)
{
scanf("%d %d",&l,&r);
Query(l,l,r,r,i);
}
Solve();
for(int i=1;i<=m;i++) printf("%lld\n",Ans[i]);
return 0;
}