[做题笔记] 浅谈笛卡尔树结构的应用

笛卡尔树内核简单,但是应用广泛,和序列规划、计数、最值类问题联系很大。

SPOJ PERIODNI

题目描述

点此看题

解法

可以考虑建出笛卡尔树,每个点的管辖范围是高为它的一个极长子矩形,为了防止不同矩形的决策互相影响我们把这个极长子矩形删掉以后再递归到儿子。

\(f[i][j]\) 表示以 \(i\) 为根的子树中选出 \(j\) 个关键点的方案数,先把子树的方案合并上来:

\[tmp[j]\leftarrow\sum_{k=0}^jf[ls][k]\times f[rs][j-k] \]

然后考虑当前这个子矩形的决策,我们考虑先让子树放了车再在当前点的子矩形里面放车,这样就可以消去棋盘每一列的影响,因为笛卡尔树帮助我们去除了列的影响所以可以直接计数,设这个矩形的宽是 \(W\) 高是 \(H\),那么有转移:

\[f[i][j]=\sum_{k=0}^jtmp[j-k]\times k!\times {H\choose k}\times {W-(j-k)\choose k} \]

暴力转移时间复杂度 \(O(nk^2)\)

总结

笛卡尔树可以帮助消去一些列的限制,考虑每个点管辖范围即可。

#include <cstdio>
const int M = 505;
const int N = 1000005;
const int MOD = 1e9+7;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,a[M],fac[N],inv[N];
int ch[M][2],s[M],f[M][M],tmp[M];
void init(int n)
{
	inv[0]=inv[1]=fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
}
int C(int n,int m)
{
	if(n<m || m<0) return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int dfs(int u,int t)
{
	if(!u)
	{
		f[u][0]=1;
		return 0;
	}
	int H=a[u]-t,W=1;
	W+=dfs(ch[u][0],a[u]);
	W+=dfs(ch[u][1],a[u]);
	for(int i=0;i<=W;i++) tmp[i]=0;
	for(int i=0;i<=W;i++)
		for(int j=0;i+j<=W;j++)
			tmp[i+j]=(tmp[i+j]
			+f[ch[u][0]][i]*f[ch[u][1]][j])%MOD;
	for(int i=0;i<=W;i++)
		for(int j=0;j<=i;j++)
			f[u][i]=(f[u][i]+tmp[i-j]*fac[j]%MOD
			*C(H,j)%MOD*C(W-(i-j),j))%MOD;
	return W;
}
signed main()
{
	n=read();k=read();init(1e6);
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		while(m && a[s[m]]>=a[i])
			ch[i][0]=s[m],m--;
		if(m) ch[s[m]][1]=i;
		s[++m]=i;
	}
	dfs(s[1],0);
	printf("%lld\n",f[s[1]][k]);
}

Histogram Coloring

题目描述

点此看题

解法

还是笛卡尔树最小值分治的方法,考虑上一层传下来的染色方案怎么接着染,如果是黑白相间的那么每次可以选择复制或者取反,但如果不是黑白相间的就只能取反了。

这启示我们要分类讨论,设 \(dp[u][0/1]\) 表示 \(u\) 的子树内,是黑白相间\(/\)任意染色(包含黑白相间)的方案数,设这一层的最小值有 \(m\) 个,矩形高是 \(x\)(广义笛卡尔树,把所有最小值取出来划分成多个区间):

\(dp[u][0]\leftarrow 2^x\prod dp[v][0]\),也就是考虑枚举每个高度初始是 R/B,那么本层就有唯一的状态,且上一层的任何状态都能合法地转移下来。

\(dp[u][1]\leftarrow 2^m\prod(dp[v][0]+dp[v][1])+(2^x-2)\prod dp[v][0]\),第一项的意思是最小值的位置初始可以任意染色,然后如果儿子黑白相间那么初始有复制和取反两种选择,否则只能取反;第二项是计算当前还是黑白相间的染色方案,由于初始 R/B 打头的黑白相间方案各被第一项计算了一次,所以要减去。

时间复杂度 \(O(n^2+n\log h)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1005;
const int MOD = 1e9+7;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,rt,cnt,a[M],f[M][2];
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
void dfs(int &x,int l,int r,int c)
{
	x=++cnt;f[x][0]=1;
	if(l>r) return ;
	int mi=1e9,m=0,p[r-l+5]={};
	for(int i=l;i<=r;i++)
		mi=min(mi,a[i]);
	p[++m]=l-1;
	for(int i=l;i<=r;i++)
		if(a[i]==mi) p[++m]=i;
	p[++m]=r+1;f[x][1]=1;
	for(int i=2;i<=m;i++)
	{
		int s=0;
		dfs(s,p[i-1]+1,p[i]-1,mi);
		f[x][0]=f[x][0]*f[s][0]%MOD;
		f[x][1]=f[x][1]*(f[s][0]+f[s][1])%MOD;
	}
	int t1=qkpow(2,m-2),t2=qkpow(2,mi-c);
	f[x][1]=(t1*f[x][1]+(t2-2)*f[x][0])%MOD;
	f[x][0]=t2*f[x][0]%MOD;
	return ;
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	dfs(rt,1,n,0);
	printf("%lld\n",(f[rt][1]+MOD)%MOD);
}

Tree Depth P

题目描述

点此看题

解法

首先可以转一下题意(过程不想写,不会问我):问有 \(k\) 个逆序对的排列构造出的笛卡尔树每个位置的深度在所有情况下的总和。

直接 \(dp\) 估摸着要 \(n^6\),而且难以优化。笛卡尔树有一些很好的性质,首先我们把点 \(u\) 的深度拆个贡献:

\[dep_u=\sum_{v} [lca(u,v)=v] \]

考虑 \(v\)\(u\) 祖先的充要条件:\([\min(u,v),\max(u,v)]\) 这段区间中 \(p_v\) 是最小值。那么我们要算的满足上述条件的排列个数,然后把这个方案记在 \(u\) 上面即可,首先我们可以写出没有限制的生成函数:

\[\prod_{i=1}^{n-1}\sum_{j=0}^i x^j \]

然后考虑这个限制的影响,我们可以先除去这个区间和 \(v\) 构成的逆序对(也就是做一个退背包),如果 \(v<u\) 那么 \(v\) 作为最小值对区间逆序对的影响是 \(0\),如果 \(u<v\) 那么 \(v\) 作为最小值对区间逆序对的影响是 \(v-u\),直接找对应项的系数即可。

具体实现过程中要做前缀和优化的背包和退背包,不用真的枚举区间,只需要枚举区间长度就可以了,时间复杂度 \(O(nk)\),记得最后要加上 \(u=v\) 的贡献。

总结

深度可以拆贡献,也就是拆成某个点是另一个点的 \(lca\)

#include <cstdio>
#include <cstring>
const int M = 90005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,nw,f[M],g[M],ans[M];
void add(int &x,int y) {x=(x+y)%m;}
void sub(int &x,int y) {x=(x-y+m)%m;}
void mul(int x)
{
	memset(g,0,sizeof g);
	for(int i=0;i<=nw;i++) g[i]=f[i];
	memset(f,0,sizeof f);
	nw+=x;
	for(int i=1;i<=nw;i++) add(g[i],g[i-1]);
	for(int i=0;i<=nw;i++)
	{
		add(f[i],g[i]);
		if(i>x) sub(f[i],g[i-x-1]);
	}
}
void div(int x)
{//f[i]=g[i]-g[i-x-1]->g[i]=f[i]+g[i-x-1]
	memset(g,0,sizeof g);
	for(int i=0;i<=nw;i++)
	{
		add(g[i],f[i]);
		if(i>x) add(g[i],g[i-x-1]);
	}
	nw-=x;
	memset(f,0,sizeof f);
	for(int i=0;i<=nw;i++)
	{
		add(f[i],g[i]);
		if(i) sub(f[i],g[i-1]);
	}
}
signed main()
{
	n=read();k=read();m=read();
	f[0]=1;
	for(int i=1;i<n;i++) mul(i);
	for(int i=1;i<n;i++)
	{
		div(i);
		for(int j=i+1;j<=n;j++)
			add(ans[j],f[k]);
		for(int j=1;j<=n-i;j++)
			if(k>=i) add(ans[j],f[k-i]);
		mul(i);
	}
	for(int i=1;i<=n;i++)
		printf("%lld ",(ans[i]+f[k])%m);
}
posted @ 2021-09-01 19:53  C202044zxy  阅读(621)  评论(0编辑  收藏  举报