今日广义笛卡尔树大学习

广义笛卡尔树大学习

网络上没有找到相关的直接讲解的 blog 啊,但是有相关题目和题解,有没有会的大佬写篇单独的讲解和总结啊 QAQ
——by 写这篇文章前的作者菌

自己动手,丰衣足食!

不过本文作者自己编写内容不多,尤其是例题部分,大多是转述题解,只是想学算法的话看一看开头的构造部分得了……

定义和使用

寻常笛卡尔树难以处理非排列,因为这会使其难以判断取谁为根。因此,我们考虑隔壁线段树的方法(怎么考虑到的),将点拆成圆点和方点——前者代表单点,后者代表区间,注意区间方点可以存在单点区间,但其仍不是圆点。

我们建多叉笛卡尔树,考虑如下递归定义(小根堆):

  1. 第一层是整个区间;
  2. 对于每个方点,我们将所有最小值作为圆点挂在儿子上,此时原序列会被拆成多个区间(可能有单点区间),将其作为方点挂在儿子上,按照中序遍历的顺序将这些儿子排序;
  3. 继续递归方点儿子;
  4. 所有叶子都是圆点时,递归结束。

(其实递归过程就是扫一遍区间,把恰好等于区间最值的点拉出来当圆点)

参考资料 [1] 给出了序列 \(1,1,4,5,1,4\) 的构建方式,是我找到的唯一一个直接带抽象图形式的,我的递归过程反推自这里。另外,这里给出 \(9,9,8,2,4,4,3,5,3\) 的图(这里用大根堆,与上面小根堆建图互相理解)

对于一个点,若其结构为 [x],表示这是一个圆点,后面的括号为这个点的权值;若其结构为 [l,r],表示这是一个方点,后面的括号为这个点所代表的区间的每个点的权值。一般而言,实现时方点只用维护区间内最值,但这里为了方便观察,将其全部列出:

(画图手搓,质量问题见谅)

注意到,这样定义的树,其每一个方点所代表的区间均为某个最值的极长序列,在做与之相关的问题时会很方便。比如,直方图的最大子矩阵之类的问题。显而易见,其构造为 \(O(N^2)\)

例题一、AT_agc041_f

题目描述:

给定一个 \(N\) 列的直方图,每一列高 \(h_i\) ,我们可以在直方图上放置任意数量的車,一个車可以占领整列和对应行一直延伸到边界的所有格子。

求有多少种放置方式使每个点都能被至少一个車覆盖。

原题数据范围:\(1 \le h_i \le n \le 400\)

加强版:\(1 \le n \le 3000\)\(1\le h_i \le 10^9\)

part 1

(注意,由于代码和题解都是作者菌缝的拼好饭,所以代码的变量名不等于题解的变量名)

不难注意到,原题等价于对于每个格子,必须要被占领。

直接对每个格子容斥,钦定 \(i\) 个违反,容斥系数 \((-1)^i\)

复杂度大概是 \(O(2^{\sum h_i})\),绝对会 T 飞。

part 2

注意,下文开始所有选法均自带part 1 的 \((-1)^i\) 的容斥系数,最后是直接进行求和,也就是说容斥系数在算法里面!作者菌太菜了,看题解时没看出来这点,寻思了好久……

注意到,列是一整列。

考虑枚举集合 \(S\),表示哪几列存在钦定违反规则的格子,对于不属于 \(S\) 的列保证不存在违反规则的格子。这样,我们完成了行列分离。

由于每一个“极长连续行”之间独立,不妨考虑单独一行,设其长度 \(len\),这一行中有 \(p\) 列存在违反规则的格子(即不能放車),存在:

  • 第一类,没有这一行格子被选:显然方案为 \(2^{len-p}\)
  • 第二类,有格子被选:枚举选择格子的个数,贡献为 \(\sum_{i=1}^p {p\choose i} (-1)^i =-[p>0]\),注意这里我们带着选择格子自身的容斥系数。

然而这是错的,这个方法无法保证 \(x \in S\) 列中均存在未被占领的格子。但是,注意到,我们所求的问题是任意 \(x\in S\) 列均存在未被占领的格子,似乎长得好像原问题?

再容斥一遍!

我们钦定不能选格子的集合 $T \subseteq S $,容斥系数 \((-1)^{|T|}\)。注意,虽然没有选格子,但是仍然不能放車!

问题转化为,若连续 \(q\) 个格子在 \(T\) 中,则还能选的格子数量只剩下 \(p-q\),其第二类贡献为 \(-[p-q>0]=-[p>q]\)

到此为止,枚举 \(S\)\(T\),我们获得了约 \(O(3^n)\) 做法。

part 3

此时,我们引入直方图剖分(即广义笛卡尔树),将每一行的连续段压在一个方点里。

注意到,对于一个方点 \([l,r]\),根据定义 $p= |S\cap[l,r]| $ 和 \(q=|T \setminus S\cap[l,r]|\),我们发现我们只关心 \(p\) 的值和 \([p>q] = 1-[p=q]\)

考虑在广义笛卡尔树上 dp,特别的,我们先只考虑在方点 \(x\) 区间内部的 \(|S|\) 的大小,而不考虑整体,记为 \(s\),以及当前是否存在 \(op=[p=q]\)。将其记为 \(f_{x,s,op}\)

考虑子树合并,合并两个儿子时直接对 \(s\) 做加法,对 \(op\) 做与,即:

$f_{x,i,op_1}\times f_{v,i,op_2} \rightarrow f_{xu,i+j,op_1 \And op_2} $

然后考虑单个连续行的贡献,应该为 \((2^{len-i}-(1-op))\)。假设存在 \(b_x\) 厚的连续行,则:

\(f_{x,i,op} \times (2^{len-i}-(1-op))^{b_x} \rightarrow f_{x,i,op}\)

这一段 dp 的核心在于,不同子树的区间不交。

(记笔记干嘛,愣着啊)

然后,就有人问了:“老大,叽里呱啦说了那么多,都是方点,圆点有啥用啊?”

当然是初始化啊(由于内容相同,下用 \(0\) 代替所有圆点的 \(id\)):

$f_{0,0,1}=f_{0,1,0}=1 $

$f_{0,1,1}=-1 $

另外对于所有 \(f_{x,0,1}\) 初始值都为 \(1\),即啥限制都没有(在最后一步计算行贡献时会计算其贡献,可怜的作者菌因为这句调了好久 QAQ)

代码

//I often reminisce about the past
#include<bits/stdc++.h>
using namespace std;
bool Mst;
#define LL long long
#define For(a,b,c) for(int (a)=(b);(a)<=(c);(a)++)
#define Rep(a,b,c) for(int (a)=(b);(a)>=(c);(a)--)
const int N=410+10,M=50+10,K=20+3,Mod=998244353;
const int INF=0x3f3f3f3f;
//const LL INF=0x3f3f3f3f3f3f3f3f,all=1ll;
int n,m,k;

template <typename T> void read(T &a){
	char c=getchar();T w=1,f=0;while(c<'0'||c>'9'){if(c=='-') w=-w;c=getchar();}
	while(c>='0'&&c<='9') f=f*10+c-'0',c=getchar();a=f*w;
}
template <typename T> void tomin(T &a,const T &b){if(b<a) a=b;}
template <typename T> void tomax(T &a,const T &b){if(a<b) a=b;}
int& modadd(int &a,const int &b,const int &mod=Mod){a+=b;a<0?a+=Mod:a>=Mod?a-=Mod:0;return a;}
int& modsub(int &a,const int &b,const int &mod=Mod){a-=b;a<0?a+=Mod:a>=Mod?a-=Mod:0;return a;}
int& modmul(int &a,const int &b,const int &mod=Mod){a=1ll*a*b%mod;return a;}
int ksm(int a,int b,int mod=Mod){int res=1;while(b){if(b&1) modmul(res,a,mod);modmul(a,a,mod);b>>=1;}return res;}
int inv(int x){return ksm(x,Mod-2,Mod);}

int h[N];

int c0[N][N],c1[N][N];
void init()
{
	int pow=1;
	For(i,0,n)
	{
		c0[i][0]=c1[i][0]=1;
		For(j,1,n)
		{
			c1[i][j]=1ll*c1[i][j-1]*pow%Mod;
			c0[i][j]=1ll*c0[i][j-1]*(pow-1)%Mod;
		}
		modmul(pow,2);
	}
}

vector <int> g[N];
int len[N],idx;
int build(int l,int r,int preh)
{
	int num=h[l],x=++idx,p=l;
	For(i,l+1,r) tomin(num,h[i]);
	len[x]=num-preh;
	For(i,l,r)
		if(h[i]==num)
		{
			if(p<=i-1) g[x].push_back(build(p,i-1,num));
			g[x].push_back(0);
			p=i+1;
		}
	if(p<=r) g[x].push_back(build(p,r,num));
	return x;
}


int f[N][N][2];
int sz[N];

void dfs(int x)
{
	if(!x) return ;
	f[x][0][1]=1;
	for(int v:g[x])
	{
		dfs(v);
		sz[x]+=sz[v];
		Rep(i,sz[x],0)
		{
			int res0=0,res1=0;
			For(j,0,min(sz[v],i))
			{
				int z=1ll*(f[v][j][0]+f[v][j][1])*(f[x][i-j][0]+f[x][i-j][1])%Mod;
				int y=1ll*f[v][j][1]*f[x][i-j][1]%Mod;
				modadd(res0,(z-y)%Mod);modadd(res1,y);
			}
			f[x][i][0]=res0;
			f[x][i][1]=res1;
		}
	}
	For(i,0,sz[x])
	{
		modmul(f[x][i][0],c0[sz[x]-i][len[x]]);
		modmul(f[x][i][1],c1[sz[x]-i][len[x]]);
	}
}

int Test=1;
void mian()
{
	read(n);
	For(i,1,n) read(h[i]);
	init();
	int root=build(1,n,0);
	sz[0]=1;f[0][1][1]=Mod-1;f[0][0][1]=f[0][1][0]=1;
	dfs(root);
	int res=0;
	For(i,0,n)
	{
		modadd(res,(f[root][i][0]+f[root][i][1])%Mod);
	}
	printf("%d\n",res);
}
bool Med;
signed main()
{
	cerr<<(&Mst-&Med)/1024.0/1024.0<<" MB\n";
//	freopen("debug.in","r",stdin);
//	freopen("debug.out","w",stdout);
//	cin>>Test;
	while(Test--) mian();
	return 0;
}

例题二、P9266

作者不会 wqs 二分……

不过留给会做的人吧。

参考资料 & 声明

[1] AT_agc041_f 题解

[2] 另一篇 AT_agc041_f 题解

后话

by U群:有人说这玩意也叫“直方图剖分”,并给出了简要构造方案,不得不说确实很形象,但我依然没搜到相关讲解……这么冷门吗,还是太简单了大家懒得写?

posted @ 2026-03-02 19:54  FarrisL  阅读(74)  评论(2)    收藏  举报