笛卡尔树详解带建树模板及例题运用(Largest Submatrix of All 1’s,洗车 Myjnie,Removing Blocks,SPOJ PERIODNI)

本文详细介绍了笛卡尔树这一数据结构,包括其键值和权值的定义、构建方法以及常见的应用。通过实例展示了如何利用笛卡尔树解决全为1的矩阵最大面积问题,并探讨了笛卡尔树在洗车问题和矩阵删除块问题中的应用。文章还讨论了笛卡尔树在期望深度计算中的作用,以及如何利用笛卡尔树思想解决排列权值和的期望问题。
摘要由CSDN通过智能技术生成

笛卡尔树

介绍

笛卡尔树是一种数据结构,每个点由两个值,键值key和权值val,组成

  • 其键值满足二叉树性质

    • 即点的左子树内所有点的键值均小于点的键值,点的键值均小于点的右子树内所有点的键值

      所以笛卡尔树按照中序遍历就会得到原序列

      通常笛卡尔树的键值就是下标

  • 其权值满足堆的性质

    • 可以是大根堆,可以是小根堆

      以小根堆为例,点的权值是该点子树内所有点的权值最小值

      每个点代表一个连续区间的信息

如果某笛卡尔树,键值和权值互不相等,则建出来得笛卡尔树是唯一的

eg

笛卡尔树的构建时间复杂度是 O ( n ) O(n) O(n)

按照键值是下标的原则,顺次加入点

则每次点都会走根的右边,路径是一条右链

但这只能维护笛卡尔树的二叉树性质,无法维护堆的性质

实际上,使用单调栈维护笛卡尔树的右链

找到第一个权值大于新点权值的点

发现用新点代替该点,并使得该点及其一下所有点都成为新点的左子树

恰好维护了笛卡尔树的性质

对应的单调栈操作就会把该点及后面的点全都弹栈

然后新点入栈

build

模板

void build() {
	stack < int > s;
	for( int i = 1;i <= n;i ++ ) {
		while( ! s.empty() and h[s.top()] >= h[i] )
			lson[i] = s.top(), s.pop();
		if( ! s.empty() ) rson[s.top()] = i;
		s.push( i );
	}
	while( ! s.empty() ) rt = s.top(), s.pop();
}

例题

笛卡尔树的应用最常见的就是与直方图的结合,求最长矩阵的面积

eg

Largest Submatrix of All 1’s

POJ3494

此题无非多了一个枚举直方图的底边在第几行,所以用来当模板题没有问题

题意:求全为1的矩阵最大面积

把每一列当成一个建筑

枚举直方图的底边后,计算出从第一行到底边的每一列的高度

只取从底边开始延伸的连续段,半路夭折的段不需要考虑,因为底边是枚举的,那些段一定被计算过了

然后就是模板的笛卡尔树建立

因为笛卡尔树的每个点代表一个连续区间的最小值,所以求存在矩阵的最大值

就考虑矩阵的高度是每个点的可能,那可延伸的高度就是这个点管辖的区间长度,也就等于子树个数(含自身)

一遍dfs就可以计算得出

#include <stack>
#include <cstdio>
#include <iostream>
using namespace std;
#define maxn 2005
int n, m, rt, ans;
int h[maxn], s[maxn], lson[maxn], rson[maxn];
int a[maxn][maxn];

void build() {
	stack < int > s;
	for( int i = 1;i <= n;i ++ ) {
		while( ! s.empty() and h[s.top()] >= h[i] )
			lson[i] = s.top(), s.pop();
		if( ! s.empty() ) rson[s.top()] = i;
		s.push( i );
	}
	while( ! s.empty() ) rt = s.top(), s.pop();
}

int dfs( int x ) {
	if( ! x ) return 0;
	int siz = dfs( lson[x] ) + dfs( rson[x] ) + 1;
	ans = max( ans, siz * h[x] );
	return siz;
}

int main() {
	while( ~ scanf( "%d %d", &m, &n ) ) {
		for( int i = 1;i <= m;i ++ )
			for( int j = 1;j <= n;j ++ )
				scanf( "%d", &a[i][j] );
		ans = 0;
		for( int i = 1;i <= m;i ++ ) {
			for( int j = 1;j <= n;j ++ ) {
				lson[j] = rson[j] = 0;
				if( a[i][j] ) h[j] ++;
				else h[j] = 0;
			}
			build();
			dfs( rt );
		}
		printf( "%d\n", ans );
	}
	return 0;
}

应用

然而,笛卡尔树很多应用其实都非常难

且直白地考察笛卡尔树的并不多

只能说很多题有用到类似笛卡尔树的思想罢了

其实代码通篇看不到真正的笛卡尔树建立使用

所以说,实际上很多题就算不会笛卡尔树的人可能也会做

硬要扯到笛卡尔树,我只能说可能略显牵强罢了

「POI2015」洗车 Myjnie

d p l , r , k : [ l , r ] dp_{l,r,k}:[l,r] dpl,r,k:[l,r]洗车店中最小花费为 k k k的最大收益

枚举最小花费的位置 x x x

h x , k : [ l , r ] h_{x,k}:[l,r] hx,k:[l,r]区间内含 x x x点的花费上限 ≥ k \ge k k的人数

d p l , r , k = max ⁡ { d p l , x − 1 , i ≥ k + d p x + 1 , r , j ≥ k + h x , k ∗ k } dp_{l,r,k}=\max\Big\{dp_{l,x-1,i\ge k}+dp_{x+1,r,j\ge k}+h_{x,k}*k\Big\} dpl,r,k=max{dpl,x1,ik+dpx+1,r,jk+hx,kk}

g l , r , k : max ⁡ { d p l , r , i ≥ k } g_{l,r,k}:\max\Big\{dp_{l,r,i\ge k}\Big\} gl,r,k:max{dpl,r,ik}

d p l , r , k = max ⁡ { g l , x − 1 , k + g x + 1 , r , k + h x , k ∗ k } dp_{l,r,k}=\max\Big\{g_{l,x-1,k}+g_{x+1,r,k}+h_{x,k}*k\Big\} dpl,r,k=max{gl,x1,k+gx+1,r,k+hx,kk}

k k k(每个人的承受能力 c i c_i ci)离散化

时间复杂度 O ( n 3 m ) O(n^3m) O(n3m)

输出方案,就在 D P DP DP转移的时候顺便记录最大值选取的 x x x即可

f i , j , k f_{i,j,k} fi,j,k :最小值 x x x的位置

l s t i , j , k lst_{i,j,k} lsti,j,k :后缀 g g g贡献 i ≥ x i\ge x ix中的收益最大值对应的 i i i

本题就是完全没有笛卡尔树的影子,只能说 d p dp dp的设置和寻找区间最小值以及最后方案店消费设置为 c i c_i ci用到了类似笛卡尔树的建立和询问

但如果没学过笛卡尔树,也很有可能想得到这个dp以及答案构造的贪心策略

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 52
#define maxm 4002
int n, m;
struct node { int l, r, c; }p[maxm];
int c[maxm], ans[maxn];
int h[maxn][maxm];
int f[maxn][maxn][maxm], g[maxn][maxn][maxm], lst[maxn][maxn][maxm];

void dfs( int l, int r, int k ) {
	if( l > r ) return;
	int x = f[l][r][k = lst[l][r][k]];
	ans[x] = c[k];
	dfs( l, x - 1, k );
	dfs( x + 1, r, k );
}

int main() {
	scanf( "%d %d", &n, &m );
	for( int i = 1;i <= m;i ++ ) {
		scanf( "%d %d %d", &p[i].l, &p[i].r, &p[i].c );
		c[i] = p[i].c;
	}
	sort( c + 1, c + m + 1 );
	int cnt = unique( c + 1, c + m + 1 ) - c - 1;
	for( int i = 1;i <= m;i ++ )
		p[i].c = lower_bound( c + 1, c + cnt + 1, p[i].c ) - c;
	for( int len = 1;len <= n;len ++ )
		for( int i = 1;i <= n;i ++ ) {
			int j = i + len - 1;
			if( j > n ) break;
			for( int x = i;x <= j;x ++ )
				for( int k = 1;k <= cnt;k ++ )
					h[x][k] = 0;
			for( int k = 1;k <= m;k ++ )
				if( i <= p[k].l and p[k].r <= j )
					for( int x = p[k].l;x <= p[k].r;x ++ )
						h[x][p[k].c] ++;
			for( int x = i;x <= j;x ++ )
				for( int k = cnt;k;k -- )
					h[x][k] += h[x][k + 1];
			for( int k = cnt;k;k -- ) {
				int Max = 0; 
				for( int x = i;x <= j;x ++ )
					if( Max <= g[i][x - 1][k] + g[x + 1][j][k] + c[k] * h[x][k] ) {
						Max = g[i][x - 1][k] + g[x + 1][j][k] + c[k] * h[x][k];
						f[i][j][k] = x;
					}
				if( Max >= g[i][j][k + 1] ) g[i][j][k] = Max, lst[i][j][k] = k;
				else g[i][j][k] = g[i][j][k + 1], lst[i][j][k] = lst[i][j][k + 1];
			}
		}
	dfs( 1, n, 1 );
	printf( "%d\n", g[1][n][1] );
	for( int i = 1;i <= n;i ++ ) printf( "%d ", ans[i] );
	return 0;
}

[AGC028B] Removing Blocks

AGC028B

所有 n ! n! n!种删除方案的权值和

可以看成权值和的期望再乘以 n ! n! n!

每次删除一个位置,然后两边就断开分别搞(相互独立),类似笛卡尔树建树过程

把删除时间搞出来,删一个位置就是提ta当根,然后左右接在下面

则下标满足二叉树性质,删除时间又满足最小堆性质,这是一个标准的笛卡尔树

恰好一个位置的贡献次数就是其在笛卡尔树上的深度(树根深度为 1 1 1

求出点的期望深度,再乘以其 A i A_i Ai,就是点的期望贡献,再所有点期望贡献求和就是权值和的期望(期望现性)

计算点的深度,等价于计算有多少点是该点的祖先

考虑两种情况

  • j < i j<i j<i

    • j j j要成为 i i i的祖先,就必须 [ j , i ] [j,i] [j,i]区间内 j j j是最小值

      不然笛卡尔树就会抽 [ j , i ] [j,i] [j,i]中最小值 k k k然后分成 [ j , k − 1 ] [ k + 1 , i ] [j,k-1][k+1,i] [j,k1][k+1,i]

    • 则又变成随机一个排列, [ l , r ] [l,r] [l,r]中最小值在 l l l处的概率

    • 随便选 r − l + 1 r-l+1 rl+1个数, C n r − l + 1 C_{n}^{r-l+1} Cnrl+1

    • 然后剩下的数无所谓 ( n − ( r − l + 1 ) ) ! \Big(n-(r-l+1)\Big)! (n(rl+1))!

    • l l l处强制是 r − l + 1 r-l+1 rl+1个中最小值,那么剩下的 r − l r-l rl个数顺序也无所谓 ( r − l ) ! (r-l)! (rl)!

    • 以上算的是所有排列的方案,最后除以 n ! n! n!就是概率了

    • C n r − l + 1 ( n − ( r − l + 1 ) ) ! ( r − l ) ! n ! = n ! ( r − l + 1 ) ! ( n − ( r − l + 1 ) ! ( n − ( r − l + 1 ) ) ! ( r − l ) ! n ! = ( r − l ) ! ( r − l + 1 ) ! = 1 r − l + 1 \frac{C_n^{r-l+1}\Big(n-(r-l+1)\Big)!(r-l)!}{n!}=\frac{\frac{n!}{(r-l+1)!\Big(n-(r-l+1\Big)!}\Big(n-(r-l+1)\Big)!(r-l)!}{n!}=\frac{(r-l)!}{(r-l+1)!}=\frac{1}{r-l+1} n!Cnrl+1(n(rl+1))!(rl)!=n!(rl+1)!(n(rl+1)!n!(n(rl+1))!(rl)!=(rl+1)!(rl)!=rl+11

    • 也就是 1 i − j + 1 \frac{1}{i-j+1} ij+11

  • j > i j>i j>i 同理

    • j j j要成为 i i i的祖先,就必须 [ i , j ] [i,j] [i,j]区间内 j j j是最小值
    • 则又变成随机一个排列, [ l , r ] [l,r] [l,r]中最小值在 r r r处的概率
    • 随便选 r − l + 1 r-l+1 rl+1个数, C n r − l + 1 C_{n}^{r-l+1} Cnrl+1
    • 然后剩下的数无所谓 ( n − ( r − l + 1 ) ) ! \Big(n-(r-l+1)\Big)! (n(rl+1))!
    • r r r处强制是 r − l + 1 r-l+1 rl+1个中最小值,那么剩下的 r − l r-l rl个数顺序也无所谓 ( r − l ) ! (r-l)! (rl)!
    • 以上算的是所有排列的方案,最后除以 n ! n! n!就是概率了
    • C n r − l + 1 ( n − ( r − l + 1 ) ) ! ( r − l ) ! n ! = n ! ( r − l + 1 ) ! ( n − ( r − l + 1 ) ! ( n − ( r − l + 1 ) ) ! ( r − l ) ! n ! = ( r − l ) ! ( r − l + 1 ) ! = 1 r − l + 1 \frac{C_n^{r-l+1}\Big(n-(r-l+1)\Big)!(r-l)!}{n!}=\frac{\frac{n!}{(r-l+1)!\Big(n-(r-l+1\Big)!}\Big(n-(r-l+1)\Big)!(r-l)!}{n!}=\frac{(r-l)!}{(r-l+1)!}=\frac{1}{r-l+1} n!Cnrl+1(n(rl+1))!(rl)!=n!(rl+1)!(n(rl+1)!n!(n(rl+1))!(rl)!=(rl+1)!(rl)!=rl+11
    • 也就是 1 j − i + 1 \frac{1}{j-i+1} ji+11

E ( h i ) = ∑ j = 1 i − 1 1 i − j + 1 + ∑ j = i + 1 n 1 j − i + 1 E(h_i)=\sum_{j=1}^{i-1}\frac{1}{i-j+1}+\sum_{j=i+1}^n\frac{1}{j-i+1} E(hi)=j=1i1ij+11+j=i+1nji+11

s i = ∑ x = 1 i 1 x s_i=\sum_{x=1}^i\frac{1}{x} si=x=1ix1

E ( h i ) = s i − 1 2 + s n − i + 1 − 1 2 = s i + s n − i + 1 − 1 E(h_i)=s_i-\frac{1}{2}+s_{n-i+1}-\frac{1}{2}=s_i+s_{n-i+1}-1 E(hi)=si21+sni+121=si+sni+11

a n s = n ! × ∑ i = 1 n ( s i + s n − i + 1 − 1 ) ⋅ a i ans=n!\times \sum_{i=1}^n(s_i+s_{n-i+1}-1)·a_i ans=n!×i=1n(si+sni+11)ai

#include <cstdio>
#define mod 1000000007
#define int long long
#define maxn 100005
int n;
int a[maxn], inv[maxn], s[maxn];

signed main() {
	scanf( "%lld", &n );
	for( int i = 1;i <= n;i ++ )
		scanf( "%lld", &a[i] );
	inv[1] = 1;
	for( int i = 2;i <= n;i ++ )
		inv[i] = ( mod - mod / i * inv[mod % i] % mod ) % mod;
	for( int i = 1;i <= n;i ++ )
		s[i] = ( s[i - 1] + inv[i] ) % mod;
	int ans = 0;
	for( int i = 1;i <= n;i ++ )
		ans = ( ans + ( s[i] + s[n - i + 1] - 1 ) * a[i] % mod ) % mod;
	for( int i = 1;i <= n;i ++ ) ans = ans * i % mod;
	printf( "%lld\n", ( ans + mod ) % mod );
	return 0;
}

SPOJ PERIODNI

BZOJ2616

同样的直方图考虑笛卡尔树

以高度为权值,下标为键值,建立笛卡尔树

每个点表示区间内的最小值(最低高度)——实则代表了一个完整不缺的矩阵

则每个点的左右儿子肯定是相互独立的(中间有至少一列的空白)

d p i , j : i dp_{i,j}:i dpi,j:i子树内选了 j j j辆车的方案数

则先卷一下点左右儿子一共选了 x x x辆车的方案数, t i = ∑ j = 0 i d p l s o n , j ∗ d p r s o n , i − j t_i=\sum_{j=0}^idp_{lson,j}*dp_{rson,i-j} ti=j=0idplson,jdprson,ij

在处理当前点表示的矩阵的转移, d p n o w , i = ∑ j = 0 i t i − j × j ! × ( H j ) × ( W − ( i − j ) j ) dp_{now,i}=\sum_{j=0}^it_{i-j}\times j!\times \binom{H}{j}\times \binom{W-(i-j)}{j} dpnow,i=j=0itij×j!×(jH)×(jW(ij))

  • 枚举 n o w now now点子树内安排了 i i i辆车
  • 枚举 n o w now now这一个矩阵内安排了 j j j辆车,则左右儿子及其子树总共安排了 i − j i-j ij辆车
  • H H H表示 n o w now now点代表的矩阵的高度, W W W表示 n o w now now点代表的矩阵的宽度
  • j j j辆车所在行不能相等,相当于在 H H H行中选 j j j行的方案数,列同理
  • 保证了 i i i辆车的行不会冲突,还要解决列不会冲突
  • 左右儿子及其子树的列是与 n o w now now点矩阵的列联通的,所以真正可以选的列要减去儿孙使用的 i − j i-j ij

不用 F F T \rm FFT FFT,直接卷就行

#include <stack>
#include <cstdio>
#include <cstring>
using namespace std;
#define mod 1000000007
#define int long long
#define maxn 505
#define maxm 1000005
int n, k, rt;
int h[maxn], fac[maxm], inv[maxm], lson[maxn], rson[maxn], t[maxn];
int dp[maxn][maxn];

int qkpow( int x, int y ) {
	int ans = 1;
	while( y ) {
		if( y & 1 ) ans = ans * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return ans;
}

void init( int n ) {
	fac[0] = inv[0] = 1;
	for( int i = 1;i <= n;i ++ ) fac[i] = fac[i - 1] * i % mod;
	inv[n] = qkpow( fac[n], mod - 2 );
	for( int i = n - 1;i;i -- ) inv[i] = inv[i + 1] * ( i + 1 ) % mod;
}

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

void build() {
	stack < int > s;
	for( int i = 1;i <= n;i ++ ) {
		while( ! s.empty() and h[s.top()] > h[i] ) lson[i] = s.top(), s.pop();
		if( ! s.empty() ) rson[s.top()] = i;
		s.push( i );
	}
	while( ! s.empty() ) rt = s.top(), s.pop();
}

int dfs( int now, int lst ) {
	int d = h[now] - lst, w = 1;
	if( lson[now] ) w += dfs( lson[now], h[now] );
	if( rson[now] ) w += dfs( rson[now], h[now] );
	memset( t, 0, sizeof( t ) );
	for( int i = 0;i <= w;i ++ )
		for( int j = 0;j <= i;j ++ )
			t[i] = ( t[i] + dp[lson[now]][j] * dp[rson[now]][i - j] ) % mod;
	for( int i = 0;i <= w;i ++ )
		for( int j = 0;j <= i;j ++ )
			dp[now][i] = ( dp[now][i] + t[i - j] * fac[j] % mod * C( d, j ) % mod * C( w - ( i - j ), j ) ) % mod;
	return w;
}

signed main() {
	scanf( "%lld %lld", &n, &k );
	for( int i = 1;i <= n;i ++ ) scanf( "%lld", &h[i] );
	init( maxm - 5 );
	build();
	dp[0][0] = 1;
	dfs( rt, 0 );
	printf( "%lld\n", dp[rt][k] );
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值