析合树

析合树是一种解决连续段问题的数据结构。
比如这样的一个问题:给你一个 1 1 1 n n n的排列,然后有一堆询问,每次询问区间 [ l , r ] [l,r] [l,r],问包含 [ l , r ] [l,r] [l,r]的最小连续段是什么。
也就是这题
所谓的连续段,就是满足 m a x l . . r − m i n l . . r + 1 = r − l + 1 max_{l..r}-min_{l..r}+1=r-l+1 maxl..rminl..r+1=rl+1的连续段。


推荐这篇:https://oi-wiki.org/ds/divide-combine/
里面有比较系统的介绍。
虽然没有线性的构造方法


关于析合树,有各种各样的定义和性质,那些东西就是叫人看不懂的……
所以我在这里就简单地说一说。
先说个性质:对于两个连续段 [ l 1 , r 1 ] [l_1,r_1] [l1,r1] [ l 2 , r 2 ] [l_2,r_2] [l2,r2]
如果它们有交集(即 [ l 2 , r 1 ] [l_2,r_1] [l2,r1]),则交集也是连续段。
并且它们的并集也是连续段。
这个不用解释吧……


有一个叫本源连续段的概念,不过具体指什么,我也不太懂……所以就不说了……
估计学到差不多的时候看字面意思就知道是什么东西了……
在这里插入图片描述
盗张图来瞅一瞅。
析合树的每个点都表示一个连续段,确切地说,是本源连续段。
析合树有两种点:析点和合点。
析点满足所有相邻儿子都不能合并成个连续段,合点满足所有相邻儿子都能合并成个连续段(可以看成儿子都是顺序或倒序的)。
有个很显然的性质是,析点至少有四个节点,合点至少有两个节点。
至于最下面的叶子结点就不要管了……程序实现时为了方便我会将其设为析点。
如果建出了析合树,前面的那个问题就有的答案:求出两个点的 L C A LCA LCA,如果是析点,则就是析点代表的区间。如果是合点,则就是 l l l r r r的祖先在 L C A LCA LCA下的儿子之间组成的区间。


接下来就是最重要的问题:如何建析合树。
建析合树用的是增量法,也就是一个一个点加进去。
维护一个栈,表示目前形成的森林的根节点。
假设加进去的点为 x x x,栈顶为 t o p top top

  1. t o p top top为合点,并且 t o p top top的最后一个儿子可以和 x x x合并。这个时候就直接将 x x x作为 t o p top top的儿子,然后将 t o p top top递归下去继续干。
  2. t o p top top x x x可以合并。新建一个合点,作为 t o p top top x x x的父亲,然后将它们的父亲递归下去。
  3. 往前面找若干个节点,满足这若干个节点和 x x x可以合并。然后新建一个析点,将这些点都作为它的儿子,然后将它递归下去。
  4. 若栈为空,或者第三个操作没有找到, x x x节点就加入栈顶。

然而直接这样建树是 O ( n 2 ) O(n^2) O(n2)的。上面第三个操作的时候可能一直往前找都找不到,那这样一次就是 O ( n ) O(n) O(n)的了。
考虑直接在这方面优化。一个简单的思路是求出 L i L_i Li,表示 [ L i , i ] [L_i,i] [Li,i] i i i为右端点的最长的连续段。
这样在第三个操作的时候就能够明确最多找到哪里,并且,最后面这些节点全部都会合并起来,栈顶的节点就是 [ L i , i ] [L_i,i] [Li,i]
问题转化成了如何求 L i L_i Li
看看这题。用那个线段树的做法就行了。
大体思路就是维护两个单调栈,然后在线段树上维护。
求出了 L i L_i Li,那么建树的时间复杂度就是 O ( n ) O(n) O(n)的了。
可惜预处理需要 O ( n lg ⁡ n ) O(n \lg n) O(nlgn)的时间来预处理。

放个代码:

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#define N 100010
int n,a[N];
int smn[N],smx[N],tn,tx;
int mn[N*4],tag[N*4];
int L[N];
inline void pushdown(int k){
	if (tag[k]){
		mn[k<<1]+=tag[k],mn[k<<1|1]+=tag[k];
		tag[k<<1]+=tag[k],tag[k<<1|1]+=tag[k];
		tag[k]=0;
	}
}
void add(int k,int l,int r,int st,int en,int c){
	if (st<=l && r<=en){
		mn[k]+=c;
		tag[k]+=c;
		return;
	}
	pushdown(k);
	int mid=l+r>>1;
	if (st<=mid)
		add(k<<1,l,mid,st,en,c);
	if (mid<en)
		add(k<<1|1,mid+1,r,st,en,c);
	mn[k]=min(mn[k<<1],mn[k<<1|1]);
}
int find(int k,int l,int r,int st,int en,int v){
	if (mn[k]>v)
		return 0;
	if (l==r)
		return l;
	pushdown(k);
	int mid=l+r>>1,res=0;
	if (st<=mid)
		res=find(k<<1,l,mid,st,en,v);
	if (!res && mid<en)
		res=find(k<<1|1,mid+1,r,st,en,v);
	return res;
}
int cnt;
struct Node{
	Node *fa,*lst;
	int mn,mx;
	bool xihe;
	int l,r;
} d[N*2],*null=d;
int num[N];
Node *st[N*2];
int top;
inline int ok(Node *x,Node *y){
	if (x->mx+1==y->mn)
		return 1;
	if (x->mn-1==y->mx)
		return -1;
	return 0;
}
void insert(Node *x){
	int tmp;
	if (top && st[top]->xihe==1 && (tmp=ok(st[top]->lst,x))){
		if (tmp==1)
			st[top]->mx=x->mx;
		else
			st[top]->mn=x->mn;
		st[top]->lst=x;
		st[top]->r=x->r;
		x->fa=st[top--];
		insert(x->fa);
		return;
	}
	if (top && (tmp=ok(st[top],x))){
		if (tmp==1)
			d[++cnt]={null,x,st[top]->mn,x->mx,1,st[top]->l,x->r};
		else
			d[++cnt]={null,x,x->mn,st[top]->mx,1,st[top]->l,x->r};
		st[top--]->fa=x->fa=&d[cnt];
		insert(x->fa);
		return;
	} 
	for (int i=top,mn=x->mn,mx=x->mx;i && L[x->r]<=st[i]->r;--i){
		mn=min(mn,st[i]->mn);
		mx=max(mx,st[i]->mx);
		if (mx-mn+1==x->r-st[i]->l+1){
			d[++cnt]={null,x,mn,mx,0,st[i]->l,x->r};
			x->fa=&d[cnt]; 
			for (;top>=i;--top)
				st[top]->fa=&d[cnt];
			insert(x->fa);
			return;
		}
	}
	st[++top]=x;
}
int f[N*2][18],dep[N*2];
void getdep(int x){
	if (dep[x] || x==0)
		return;
	getdep(f[x][0]);
	dep[x]=dep[f[x][0]]+1;
}
inline void getans(int u,int v){
	if (dep[u]>dep[v]){
		for (int k=dep[u]-dep[v],i=0;k;k>>=1,++i)
			if (k&1)
				u=f[u][i];
	}
	else
		for (int k=dep[v]-dep[u],i=0;k;k>>=1,++i)
			if (k&1)
				v=f[v][i];
	for (int i=17;i>=0;--i)
		if (f[u][i]!=f[v][i])
			u=f[u][i],v=f[v][i];
	if (d[f[u][0]].xihe==0)
		printf("%d %d\n",d[f[u][0]].l,d[f[v][0]].r);
	else
		printf("%d %d\n",d[u].l,d[v].r);
}
int main(){
	freopen("c.in","r",stdin);
	freopen("c.out","w",stdout);
	scanf("%d",&n);
	for (int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	for (int i=1;i<=n;++i){
		for (;tn && a[smn[tn]]>a[i];--tn)
			add(1,1,n,smn[tn-1]+1,smn[tn],a[smn[tn]]-a[i]);
		for (;tx && a[smx[tx]]<a[i];--tx)
			add(1,1,n,smx[tx-1]+1,smx[tx],a[i]-a[smx[tx]]);
		smn[++tn]=i,smx[++tx]=i;
		add(1,1,n,i,i,i);
		L[i]=find(1,1,n,1,i,i);
	}
	st[0]=null;
	*null={null,null,0,0,0,0,0};
	for (int i=1;i<=n;++i){
		d[num[i]=++cnt]={null,null,a[i],a[i],0,i,i};
		insert(&d[cnt]);
	}
	assert(top==1 && st[top]->l==1 && st[top]->r==n);
	for (int i=1;i<=cnt;++i)
		f[i][0]=d[i].fa-d;
	for (int i=1;i<=cnt;++i)
		getdep(i);
	for (int i=1;i<=17;++i)
		for (int j=1;j<=cnt;++j)
			f[j][i]=f[f[j][i-1]][i-1];
	int Q;
	scanf("%d",&Q);
	while (Q--){
		int l,r;
		scanf("%d%d",&l,&r);
		getans(num[l],num[r]);
	}
	return 0;
}

到此为止了么?
不,实际上有真正的 O ( n ) O(n) O(n)解法。
对于一个区间 [ l , r ] [l,r] [l,r],找到 [ l , r ] [l,r] [l,r]中的最大值 m x mx mx和最小值 m n mn mn
然后找到值为 [ m n , m x ] [mn,mx] [mn,mx]的最右位置 R R R。显然,如果 r &lt; R r&lt;R r<R,那么以 r r r为右端点的最大区间左端点小于 l l l。也就是说,不存在 L &lt; l L&lt;l L<l的区间 [ L , r ] [L,r] [L,r]包含 [ l , r ] [l,r] [l,r]
在做第三个操作的时候,沿路记下 m n mn mn m x mx mx,求出 R R R。如果 r &lt; R r&lt;R r<R,就不用做下去了,因为 R R R只会越来越大。
求的时候直接用 R M Q RMQ RMQ O ( 1 ) O(1) O(1)处理询问。
这是个非常好的优化,尽管它看起来仍然是 O ( n 2 ) O(n^2) O(n2)的,但是数据似乎不怎么卡。
为了满足强迫症的想法,我们使劲将它优化到 O ( n ) O(n) O(n)
对于栈中的每个点,可以记录一个 f a i l fail fail指针表示它之前找到的第一个 r &lt; R r&lt;R r<R的地方。
那么以后就不需要暴力找,只需要跳 f a i l fail fail来找。
每条 f a i l fail fail边只会跳 O ( 1 ) O(1) O(1)次(跳了意味着这次 R ≤ r R\leq r Rr,如果在后来还没有成功, f a i l r fail_r failr一定会指向一个更前的点。这样在后来跳的时候,再也不会经过这条边。)
所以这样做的时间复杂度是 O ( n ) O(n) O(n)的!
然而,我们愕然发现预处理的时间复杂度依然是 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)……

先放个代码:

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
//#include <cassert>
#define N 100010
int lg[N];
int n,a[N];
int mx[N][17];
inline int query(int l,int r){
	int m=lg[r-l+1];
	return max(mx[l][m],mx[r-(1<<m)+1][m]);
}
int cnt;
struct Node{
	Node *fa,*lst;
	int mn,mx;
	bool xihe;
	int l,r;
} d[N*2],*null=d;
int num[N];
int top;
Node *st[N*2];
int fail[N*2],mnf[N*2],mxf[N*2];
inline int ok(Node *x,Node *y){return x->mx+1==y->mn?1:(x->mn-1==y->mx?-1:0);}
void insert(Node *x){
	if (!top){
		st[++top]=x;
		fail[top]=0;
		mnf[top]=x->mn;
		mxf[top]=x->mx;
		return;
	}
	int tmp;
	if (st[top]->xihe==1 && (tmp=ok(st[top]->lst,x))){
		if (tmp==1)
			st[top]->mx=x->mx;
		else
			st[top]->mn=x->mn;
		st[top]->lst=x;
		st[top]->r=x->r;
		x->fa=st[top];
		top--;
		insert(x->fa);
		return;
	}
	if (tmp=ok(st[top],x)){
		if (tmp==1)
			d[++cnt]={null,x,st[top]->mn,x->mx,1,st[top]->l,x->r};
		else
			d[++cnt]={null,x,x->mn,st[top]->mx,1,st[top]->l,x->r};
		st[top]->fa=x->fa=&d[cnt];
		top--;
		insert(x->fa);
		return;
	}
	for (int i=top,mn=x->mn,mx=x->mx;i;mn=min(mn,mnf[i]),mx=max(mx,mxf[i]),i=fail[i]){
		mn=min(mn,st[i]->mn);
		mx=max(mx,st[i]->mx);
		if (query(mn,mx)>x->r){
			st[++top]=x;
			fail[top]=i;
			mnf[top]=mn;
			mxf[top]=mx;
			return;
		}
		if (mx-mn+1==x->r-st[i]->l+1){
			d[++cnt]={null,x,mn,mx,0,st[i]->l,x->r};
			x->fa=&d[cnt];
			for (;top>=i;--top)
				st[top]->fa=&d[cnt];
			insert(x->fa);
			return;
		}
	}
}
int f[N*2][18],dep[N*2];
void getdep(int x){
	if (dep[x] || x==0)
		return;
	getdep(f[x][0]);
	dep[x]=dep[f[x][0]]+1;
}
inline void getans(int u,int v){
	if (dep[u]>dep[v]){
		for (int k=dep[u]-dep[v],i=0;k;k>>=1,++i)
			if (k&1)
				u=f[u][i];
	}
	else
		for (int k=dep[v]-dep[u],i=0;k;k>>=1,++i)
			if (k&1)
				v=f[v][i];
	for (int i=17;i>=0;--i)
		if (f[u][i]!=f[v][i])
			u=f[u][i],v=f[v][i];
	if (d[f[u][0]].xihe==0)
		printf("%d %d\n",d[f[u][0]].l,d[f[v][0]].r);
	else
		printf("%d %d\n",d[u].l,d[v].r);
}
int main(){ 
	freopen("c.in","r",stdin);
	freopen("c.out","w",stdout);
	scanf("%d",&n);
	lg[0]=0,lg[1]=0;
	for (int i=2;i<=n;++i)
		lg[i]=lg[i>>1]+1;
	for (int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	for (int i=1;i<=n;++i)
		mx[a[i]][0]=i;
	for (int i=1;1<<i<=n;++i)
		for (int j=1;j+(1<<i)-1<=n;++j)
			mx[j][i]=max(mx[j][i-1],mx[j+(1<<i-1)][i-1]);
	st[0]=null;
	*null={null,null,0,0,0,0,0};
	for (int i=1;i<=n;++i){
		d[num[i]=++cnt]={null,null,a[i],a[i],0,i,i};
		insert(&d[cnt]);
	}
	for (int i=1;i<=cnt;++i)
		f[i][0]=d[i].fa-d;
	for (int i=1;i<=cnt;++i)
		getdep(i);
	for (int i=1;i<=17;++i)
		for (int j=1;j<=cnt;++j)
			f[j][i]=f[f[j][i-1]][i-1];
	int Q;
	scanf("%d",&Q);
	while (Q--){
		int l,r;
		scanf("%d%d",&l,&r);
		getans(num[l],num[r]);
	}
	return 0;
}

接着我们考虑如何处理 [ l , r ] [l,r] [l,r]的询问。
其实只要处理出所有 i ∈ [ l , r ) i \in [l,r) i[l,r) [ i , i + 1 ] [i,i+1] [i,i+1]的并,就是 [ l , r ] [l,r] [l,r]询问的答案。
对于栈中的每个节点,处理出 [ l , r ] [l,r] [l,r]的答案(好像是 [ l , r ) [l,r) [l,r)?)。对于每条 f a i l fail fail边,处理出中间答案的并。
所以现在的问题是求所有 [ i , i + 1 ] [i,i+1] [i,i+1]的答案。
实际上这可以看成许多个区间询问,然而我们要 O ( n ) O(n) O(n)来预处理出这些东西。
区间询问怎么预处理?有个很牛的方法是 O ( n ) O(n) O(n)建出笛卡尔树,然后变成 L C A LCA LCA问题。用 T a r j a n − L C A Tarjan-LCA TarjanLCA来求。这样就可以达到 O ( n ) O(n) O(n)了。
或者也可以在建树之后用 + 1 − 1 R M Q +1-1RMQ +11RMQ来解决,然而我没有打过
所以实际上 O ( n ) O(n) O(n)好像没有什么用啊……毕竟如果出题,肯定不只是析合树,然而其它操作总是要 lg ⁡ \lg lg级别的……
而且这样代码复杂度还很恐怖……
我没打过,别找我拿标程……


如果真的要打析合树,用线段树的 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)方法似乎是一种不错的选择,因为思路比较简单。
或者用 O ( n lg ⁡ n ) O(n\lg n) O(nlgn)预处理 S T ST ST表来搞,还有记录 f a i l fail fail边,这样代码长度会短一些。
至于纯正的 O ( n ) O(n) O(n)……就在嘴巴上说说算了吧……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值