笛卡尔树 学习笔记

突然发现这玩意居然在提高组大纲里 /jk,赶紧突击了一下。

【模板】笛卡尔树 - 洛谷

给定一个 \(1 \sim n\) 的排列 \(p\),构建其笛卡尔树。

即构建一棵二叉树,满足:

  1. 每个节点的编号满足二叉搜索树的性质。
  2. 节点 \(i\) 的权值为 \(p_i\),每个节点的权值满足小根堆的性质。

\(1 \le n \le 10^7\)

通过题中描述,不难发现,Treap 本质上也是一棵笛卡尔树,所以 Treap 的某些性质和笛卡尔树相通。

广义上来说,笛卡尔树是一种特殊的二叉搜索树,每个结点 \(i\) 保存着一组数对 \((x_i,y_i)\)

其中,若只考虑 \(x\),它是一棵二叉搜索树。若只考虑 \(y\),它满足堆的性质(此处默认为小根堆)

显然,题中所给的点的编号为 \(x_i\),排列即为 \(y_i\)

建树

由于 \(x_i\) 递增,我们可以 \(\mathcal O(n)\) 地构建笛卡尔树,过程如下:

因为 \(x_i\) 要满足二叉搜索树的性质且已经递增,故顺序插入 \((x_i,y_i)\) 时,显然要尽量往当前节点的右子节点走,一直走到一个满足 \(y_u\lt y_i\) 的节点 \(u\),将 \(i\) 设为 \(rson(u)\),再将剩余子树设为 \(i\) 的左子树。

由此,我们可以维护一条根一直向右走到叶子结点的链,用一个单调栈来维护。

当栈顶 \(y\gt y_i\) 时不断弹出(注意,由于我们要将子树接在 \(i\) 的左子树上,所以不能真的弹出去,这个过程用一个变量模拟),直到栈空或栈顶 \(y\le y_i\),设此时栈中有 \(cur\) 个元素:

  • \(cur\gt 0\),则 \(rson(stk_{cur})=i\)

  • \(cur\lt top\),则 \(lson(i)=stk_{cur+1}\)

由于每个结点至多入栈出栈一次,故时间复杂度为 \(\mathcal O(n)\)

Code

核心代码,稍微有点压行,总共 4 行:

for(int i = 1;i <= n;++ i) {
	for(cur = top;cur&&a[stk[cur]] > a[i];-- cur);
	if(cur)rs[stk[cur]] = i;
	if(cur < top)ls[i] = stk[cur + 1];
	stk[top = cur + 1] = i;
}

例题

[TJOI2011] 树的序

给定一个 \(1\sim n\) 的排列 \(p\),顺序插入 \(p_{1\sim n}\) 形成一棵二叉搜索树,重排 \(p\),使得 \(p\) 字典序最小的同时二叉搜索树形态不发生改变。

\(1\le n\le 10^6\)

稍微想想就能发现,其实要求的就是 BST 的前序遍历序列。

但是 BST 的构造很烦人,随便一组 \(p_i=i\) 就能把常规方法卡到 \(\mathcal O(n^2)\)

考虑笛卡尔树优化一下。仿照上道模板题,由于 \(p\) 满足二叉搜索树的性质,故 \(x\) 应为 \(p\)

画一下图,理解理解就能发现,把编号作为 \(y\) 插入一定是正确的。

胡的证明:因为原题中顺序插入 BST,所以每个结点的编号必然大于其父亲的编号。

然后。。就没了,套模板就行。

Code

// Problem: P1377 [TJOI2011] 树的序
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1377
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>

const int maxn = 1e6 + 5;
int n,a[maxn],stk[maxn],rt,top,cur,ls[maxn],rs[maxn];

void dfs(int u) {
	printf("%d ",u);
	if(ls[u])dfs(ls[u]);
	if(rs[u])dfs(rs[u]);
	return ;
}

int main() {
	scanf("%d",&n);
	for(int i = 1,x;i <= n;++ i)
		scanf("%d",&x),a[x] = i;
	
	for(int i = 1;i <= n;++ i) {
		for(cur = top;cur&&a[stk[cur]] > a[i];-- cur);
		if(cur)rs[stk[cur]] = i;
		else rt = i;
		if(cur < top)ls[i] = stk[cur + 1];
		stk[top = cur + 1] = i;
	}
	
	dfs(rt);// 降智了,这里直接写 stk[1] 就行了 /kel,压根不用 rt 维护根
	return 0;
}

[COCI2008-2009#4] PERIODNI

题意好长,不写了

统计方案,肯定是道 DP 题。

我们把底下连通的部分称作「地基」,上面的小块称为「散块」。不断地递归拆分地基和散块,会发现这个过程就是构建笛卡尔树的过程。

\((x_i,y_i)=(i,h_i)\),建立笛卡尔树,在上面跑 DP 即可。

具体而言,令 \(f(i,j)\) 表示 \(i\) 的子树中选了 \(j\) 个点的方案数,前面「地基」的高度和为 \(h\)

先合并两个子节点的答案,再考虑计算,假设子节点内共选 \(k\) 个结点。

那么一定有 \(size(i)-k\) 列没有被填,则方案数为 \(C_{size(i)-k}^{j-k}\times A_{h_u-h}^{j-k}\)

再转移一遍即可。至于时间复杂度,同经典树上背包,为 \(\mathcal O(nk+\max\{h_i\})\)

Code

有些地方和解析里不太一致,见谅。

// Problem: P6453 [COCI2008-2009#4] PERIODNI
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P6453
// Memory Limit: 31 MB
// Time Limit: 5000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
typedef long long ll;

const int maxn = 505;
const int maxm = 1e6 + 5;
const ll mod = 1e9 + 7;

ll power(ll x,ll y) {
	ll ans = 1;
	for(;y;y >>= 1) {
		if(y & 1)(ans *= x) %= mod;
		(x *= x) %= mod;
	}
	return ans;
}

ll f[maxn][maxn],frac[maxm],inv[maxm];

ll C(int n,int m) {
	if(n < 0||m < 0||n < m)return 0;
	return frac[n] * inv[m] % mod * inv[n - m] % mod;
}

int n,m,a[maxn],stk[maxn],top,cur,son[maxn][2],sz[maxn];

void dfs(int u,int h) {
	sz[u] = f[u][0] = 1;
	for(int i = 0;i < 2;++ i) {
		if(!son[u][i])continue ;
		dfs(son[u][i] , a[u]);
		sz[u] += sz[son[u][i]];
		for(int j = std::min(m , sz[u]);~ j;-- j)
			for(int k = 1;k <= std::min(sz[son[u][i]] , j);++ k)
				(f[u][j] += f[u][j - k] * f[son[u][i]][k] % mod) %= mod;
	}
	for(int j = std::min(m , sz[u]);~ j;-- j)
		for(int k = 1;k <= std::min(j , a[u] - h);++ k)
			(f[u][j] += f[u][j - k] * frac[k] % mod * C(a[u] - h , k) % mod * C(sz[u] - j + k , k) % mod) %= mod;
	return ;
}

int main() {
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;++ i)
		scanf("%d",&a[i]);
	
	frac[0] = 1;
	for(int i = 1;i < maxm;++ i)
		frac[i] = 1ll * i * frac[i - 1] % mod;
	inv[maxm - 1] = power(frac[maxm - 1] , mod - 2);
	for(int i = maxm - 2;~ i;-- i)
		inv[i] = 1ll * (i + 1) * inv[i + 1] % mod;
	
	for(int i = 1;i <= n;++ i) {
		for(cur = top;cur&&a[stk[cur]] > a[i];-- cur);
		if(cur)son[stk[cur]][1] = i;
		if(cur < top)son[i][0] = stk[cur + 1];
		stk[top = cur + 1] = i;
	}
	
	dfs(stk[1] , 0);
	
	printf("%lld\n",f[stk[1]][m]);
	return 0;
}

写到这里已经晚上 23:18 了,我必须考虑一下我一笔没动的 whk 作业该怎么办了,先润了,有更多例题或者文章中的事实性错误请联系我,虽然我不会管(逃

哦对了今天疯狂星期四,有没有 fuge v50

posted @ 2022-10-19 21:41  ImALAS  阅读(67)  评论(0编辑  收藏  举报