笛卡尔树学习笔记

\(~~~~\) 因为做不动例题所以就来水学习笔记了

\(~~~~\) 以下记 \(ls_i\) 表示 \(i\) 节点的左儿子,\(rs_i\) 表示 \(i\) 结点的右儿子。

一、简介

\(~~~~\) 笛卡尔树是一种二叉树,每个结点都有一个键值二元组 \((k,w)\) 。在笛卡尔树中,\(k\) 满足 BST(二叉搜索树)的性质,而 \(w\) 满足堆的性质。即:

\[\large k_{ls_i}\leq k_{i} \leq k_{rs_i}\\ \large (w_{i}\leq w_{ls_i} ~ \land ~ w_i\leq w_{rs_i}) \lor (w_i\geq w_{ls_i} \land w_i \geq w_{rs_i}) \]

\(~~~~\) 有一个事实是,如果每个结点的 \(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;
}
posted @ 2021-06-28 19:36  Azazеl  阅读(104)  评论(0编辑  收藏  举报