今日广义笛卡尔树大学习
广义笛卡尔树大学习
网络上没有找到相关的直接讲解的 blog 啊,但是有相关题目和题解,有没有会的大佬写篇单独的讲解和总结啊 QAQ
——by 写这篇文章前的作者菌
自己动手,丰衣足食!
不过本文作者自己编写内容不多,尤其是例题部分,大多是转述题解,只是想学算法的话看一看开头的构造部分得了……
定义和使用
寻常笛卡尔树难以处理非排列,因为这会使其难以判断取谁为根。因此,我们考虑隔壁线段树的方法(怎么考虑到的),将点拆成圆点和方点——前者代表单点,后者代表区间,注意区间方点可以存在单点区间,但其仍不是圆点。
我们建多叉笛卡尔树,考虑如下递归定义(小根堆):
- 第一层是整个区间;
- 对于每个方点,我们将所有最小值作为圆点挂在儿子上,此时原序列会被拆成多个区间(可能有单点区间),将其作为方点挂在儿子上,按照中序遍历的顺序将这些儿子排序;
- 继续递归方点儿子;
- 所有叶子都是圆点时,递归结束。
(其实递归过程就是扫一遍区间,把恰好等于区间最值的点拉出来当圆点)
参考资料 [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 二分……
不过留给会做的人吧。
参考资料 & 声明
后话
by U群:有人说这玩意也叫“直方图剖分”,并给出了简要构造方案,不得不说确实很形象,但我依然没搜到相关讲解……这么冷门吗,还是太简单了大家懒得写?

浙公网安备 33010602011771号