猫树学习笔记

Orz,算法发明者

猫树是解决无修改区间或树上询问的高效算法.
一般而言,询问的运算满足结合律和快速合并的特点.
算法的主要思想是离线,然后类似ST表那样合并.(基于线段树实现)

首先,把线段树的值域补成二的次幂 l e n len len.(有效值域为 [ 0 , l e n ) [0,len) [0,len))
每次询问的时候,找到使 l , r l,r l,r(询问前先-1)第一次在线段树上分开的节点 x x x.
x = ( l + l e n ) > > ( 1 + ⌊ l o g ( l xor ⁡ r ) ⌋ ) x=(l+len)>>(1+\left\lfloor{log(l \operatorname{xor} r)}\right\rfloor) x=(l+len)>>(1+log(lxorr)).
以下为了方便记 l o g [ x ] = ( 1 + ⌊ l o g ( x ) ⌋ ) , 即 表 示 x 的 二 进 制 位 数 log[x]=(1+\left\lfloor{log(x)}\right\rfloor),即表示x的二进制位数 log[x]=(1+log(x)),x
其中 l + l e n l+len l+len恰好 l l l在线段树上的编号, l o g [ l  xor   r ) ] log[l ~\text{xor}~~ r)] log[l xor  r)]则恰好求出了二进制位不同的最高位(也就对应着分开时的线段树节点的高度).

然后,我们来考虑预处理… x , l , r , d x,l,r,d x,l,r,d表示线段树节点 x x x的范围为 [ l , r ) [l,r) [l,r),深度为 d d d.
m i d = ( l + r ) / 2 mid=(l+r)/2 mid=(l+r)/2.那么左区间为 [ l , m i d ) , 右 区 间 为 [ m i d , r ) [l,mid),右区间为[mid,r) [l,mid),[mid,r).

∀ i ∈ [ l , m i d ) , c a l c u l a t e    t h e    i n f o r m a t i o n    f r o m    i   t o   m i d − 1 \forall i\in[l,mid),calculate~~ the ~~information~~ from ~~i~ to~ mid-1 i[l,mid),calculate  the  information  from  i to mid1
∀ i ∈ [ m i d , r ) , c a l c u l a t e    t h e    i n f o r m a t i o n    f r o m    m i d   t o   i \forall i\in[mid,r),calculate~~ the ~~information~~ from ~~mid~ to~ i i[mid,r),calculate  the  information  from  mid to i
然后,询问的时候要么最优信息要么在 m i d mid mid一侧,要么为两侧合并.

以上就是猫树的大致思想.

例题1

SP1043 GSS1 - Can you answer these queries I
题意:求区间最大子段和.

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define lc (x<<1)
#define rc (x<<1|1)
#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++)
using namespace std;
const int N=1<<16|10,size=1<<20;

char buf[size],*p1=buf,*p2=buf;
template<class o> void qr(o &x) {
	char c=gc; x=0; int f=1;
	while(!isdigit(c)){if(c=='-')f=-1; c=gc;}
	while(isdigit(c)) x=x*10+c-'0',c=gc;
	x*=f;
}
template<class o> void qw(o x) {
	if(x/10) qw(x/10);
	putchar(x%10+'0');
}
template<class o> void pr2(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); puts("");
}

int n,m,len,t,a[N],f[20][N],g[20][N],Log[N];
//f[d][i],g[d][i]分别表示第d层i~mid区间的最大子段和、包含mid的最大子段和.(这里的mid只是中间的一个模糊的点) 
void bt(int x,int l,int r,int d) {//处理区间为[l,r)
	if(l+1==r||l>=n) return;
	int mid=(l+r)>>1,pre=0,sum=0;
	f[d][mid]=g[d][mid]=-N;
	for(int i=mid-1;i>=l;i--) {//左区间[l,mid) 
		pre+=a[i];
		sum=max(sum,0)+a[i];
		f[d][i]=max(f[d][i+1],sum);
		g[d][i]=max(g[d][i+1],pre);
	}
	f[d][mid]=g[d][mid]=a[mid];
	pre=sum=a[mid];
	for(int i=mid+1;i<=r;i++) {//右区间[mid,r) 
		pre+=a[i];
		sum=max(sum,0)+a[i];
		f[d][i]=max(f[d][i-1],sum);
		g[d][i]=max(g[d][i-1],pre);
	}
	bt(lc,l,mid,d+1);
	bt(rc,mid,r,d+1);
}

int ask(int l,int r) {
	if(l==r) return a[l];
	int d=t-Log[l^r];
	return max(max(f[d][l],f[d][r]),g[d][l]+g[d][r]);
}

int main() {
	qr(n);for(int i=0;i<n;i++) qr(a[i]);
	for(len=1;len<n;len<<=1);
	for(int i=2;i<=len;i++) Log[i]=Log[i>>1]+1;
	t=Log[len]; bt(1,0,len,1);
	qr(m); while(m--) {
		int l,r; qr(l); qr(r);
		pr2(ask(l-1,r-1));
	}
	return 0;
}

例题2

SP2916 GSS5 - Can you answer these queries V

因为sb错误调到自闭

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define lc (x<<1)
#define rc (x<<1|1)
#define gc getchar()//(p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++)
using namespace std;
const int N=1<<14|10,size=1<<20,inf=2e9;

char buf[size],*p1=buf,*p2=buf;
template<class o> void qr(o &x) {
	char c=gc; x=0; int f=1;
	while(!isdigit(c)){if(c=='-')f=-1; c=gc;}
	while(isdigit(c)) x=x*10+c-'0',c=gc;
	x*=f;
}
template<class o> void qw(o x) {
	if(x/10) qw(x/10);
	putchar(x%10+'0');
}
template<class o> void pr1(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); putchar(' ');
}
template<class o> void pr2(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); puts("");
}

int T,n,m,t,len,Log[N],f[16][N],g[16][N],h[16][N],a[N],s[N],ss[N];
//f表示到mid的前缀和最大值,g表示到mid的后缀和最大值 
void bt(int x,int l,int r,int d) {
	if(l+1==r||n<=l) return;
	int mid=(l+r)>>1,sum;
	for(int i=l;i<r;i++) f[d][i]=s[i],g[d][i]=ss[i];
	sum=0;
	h[d][mid]=-inf;
	for(int i=mid-1;i>l;i--) {
		f[d][i-1]=max(f[d][i-1],f[d][i]),
		g[d][i-1]=max(g[d][i-1],g[d][i]),
		sum+=a[i];
		h[d][i]=max(h[d][i+1],sum);
		sum=max(sum,0);
	}
	h[d][l]=max(h[d][l+1],sum+a[l]);
	h[d][mid]=a[mid];
	sum=max(a[mid],0);
	for(int i=mid;i<r-1;i++) {
		f[d][i+1]=max(f[d][i+1],f[d][i]),
		g[d][i+1]=max(g[d][i+1],g[d][i]),
		sum+=a[i+1];
		h[d][i+1]=max(h[d][i],sum);
		sum=max(sum,0);
	}
	bt(lc,l,mid,d+1);
	bt(rc,mid,r,d+1);
}

int queryl(int l,int r) {//从左起的最大子段和 
	if(l==r) return a[l];
	int d=t-Log[l^r],t=(l?s[l-1]:0);
	return max(f[d][l],f[d][r])-t;
}
int queryr(int l,int r) {//从右起的最大子段和 
	if(l==r) return a[l];
	int d=t-Log[l^r];
	return max(g[d][l],g[d][r])-ss[r+1];
}
int ask(int l,int r) {//区间最大子段和 
	if(l==r) return a[l];
	int d=t-Log[l^r];
	return max(max(h[d][l],h[d][r]),g[d][l]+f[d][r]-s[n-1]);
}
int query(int l,int r,int L,int R) {
	if(r<L) return queryr(l,r)+s[L-1]-s[r]+queryl(L,R);
	int ans=ask(L,r);
	if(l<L) ans=max(ans,queryr(l,L)+queryl(L,R)-a[L]);
	if(r<R) ans=max(ans,queryr(l,r)+queryl(r,R)-a[r]);
	return ans; 
}

int main() {
	for(int i=1;i<N;i++) Log[i]=Log[i>>1]+1;
	qr(T); while(T--) {
		qr(n); 
		qr(a[0]);s[0]=a[0];
		for(int i=1;i<n;i++) qr(a[i]),s[i]=s[i-1]+a[i];
		ss[n]=0;
		for(int i=n-1;i>=0;i--) ss[i]=ss[i+1]+a[i];
		for(len=1;len<n;len<<=1);
		t=Log[len]; bt(1,0,len,1);
		qr(m); while(m--) {
			int l,r,L,R;
			qr(l); qr(r); qr(L); qr(R);
			l--; r--; L--; R--;
			pr2(query(l,r,L,R));
		}
	}
	return 0;
}

小技巧

l , r l,r l,r得到一开始分开区间的 L , R , m i d L,R,mid L,R,mid.

int k=Log[l^r];
int d=t-k,L=l>>k<<k,R=((l>>k)+1)<<k,mid=L+R>>1;

例题3

BZOJ 4540. [Hnoi2016]序列
[题意]:求区间子段最小值之和.
方法1:莫队:
单调栈+倍增
预处理 a [ 0 ] = a [ n + 1 ] = − i n f a[0]=a[n+1]=-inf a[0]=a[n+1]=inf
L [ i ] , R [ i ] L[i],R[i] L[i],R[i]为小于 a [ i ] a[i] a[i]的左右最靠近 i i i的位置.
如果要增加/减小左边一个位置 l l l的影响,那么 i ∼ R [ i ] i\sim R[i] iR[i]的影响为 ( R [ i ] − i ) ∗ a [ i ] (R[i]-i)*a[i] (R[i]i)a[i], R [ i ] ∼ R [ R [ i ] ] 的 影 响 为 ( R [ R [ i ] ] − R [ i ] ) ∗ a [ R [ i ] ] ) R[i]\sim R[R[i]]的影响为(R[R[i]]-R[i])*a[R[i]]) R[i]R[R[i]](R[R[i]]R[i])a[R[i]]).以此类推,直到找到一个点 p p p,满足 R [ p ] − 1 ≥ r R[p]-1\ge r R[p]1r,此时的贡献为 ( r − p ) ∗ a [ p ] (r-p)*a[p] (rp)a[p].
我们可以按 R R R建一棵树, i − > R [ i ] i->R[i] i>R[i]边权为 ( R [ i ] − i ) ∗ a [ i ] (R[i]-i)*a[i] (R[i]i)a[i],这样变化时就能用倍增跳了.

同样的我们也可以按 L L L建一棵树.

这样我们得到一个 O ( n ∗ n ∗ log ⁡ n ) O(n*\sqrt n*\log n) O(nn logn)的做法.

显然,这样复杂度过高.

方法2:猫树:
这种方法的复杂度仅有 O ( n ∗ log ⁡ n ) O(n*\log n) O(nlogn).
首先,问题可分解为两个部分1. [ l , m i d ) , [ m i d , r ) [l,mid),[mid,r) [l,mid),[mid,r)的子问题贡献;2.跨 m i d mid mid的贡献.
对于第一部分,显然用同上的单调栈就能解决.
对于第二部分,我们可以考虑用归并思想求答案.

#include<map>
#include<set>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#define lc (x<<1)
#define rc (x<<1|1)
#define gc getchar()//(p1==p2&&(p2=(p1=buf)+fread(buf,1,size,stdin),p1==p2)?EOF:*p1++)
using namespace std;
typedef long long ll;
const int N=1<<17|10,inf=2e9;

//char buf[size],*p1=buf,*p2=buf;
template<class o> void qr(o &x) {
	char c=gc; x=0; int f=1;
	while(!isdigit(c)){if(c=='-')f=-1; c=gc;}
	while(isdigit(c)) x=x*10+c-'0',c=gc;
	x*=f;
}
template<class o> void qw(o x) {
	if(x/10) qw(x/10);
	putchar(x%10+'0');
}
template<class o> void pr1(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); putchar(' ');
}
template<class o> void pr2(o x) {
	if(x<0)x=-x,putchar('-');
	qw(x); puts("");
}

int n,m,len,t,Log[N],sta[N],top;
ll a[N],Min[N],f[20][N],g[20][N],h[20][N],s[20][N];

void bt(int x,int l,int r,int d) {
	if(l+1==r||n<=l) return ;
	int mid=(l+r)>>1,p=mid-1,q=mid;
	ll sum=0,pre=0,S=0;  
	*sta=mid; top=0;
	for(int i=mid-1;i>=l;i--) {
		while(top&&a[i]<=a[sta[top]]) 
			sum-=a[sta[top]]*(sta[top-1]-sta[top]),top--;
		sum+=a[i]*(sta[top]-i); sta[++top]=i;//sum为一端点为i的答案 
		s[d][i]=(S+=(Min[i]=a[sta[1]]));//i~mid的最小值 
		f[d][i]=(pre+=sum);//f[d][i]表示i~mid这段的子段贡献之和. 
	}
	sum=pre=S=0;
	*sta=mid-1; top=0;
	for(int i=mid;i<r;i++) {
		while(top&&a[i]<=a[sta[top]])
			sum-=a[sta[top]]*(sta[top]-sta[top-1]),top--;
		sum+=a[i]*(i-sta[top]); sta[++top]=i;
		s[d][i]=(S+=(Min[i]=a[sta[1]]));
		f[d][i]=(pre+=sum);
	}
	top=sum=0;//利用归并,求出合并的代价. 
	//对于i∈[l,mid),j∈[mid,r),min(a[i..j)]=min(Min[d][i],Min[d][j]).
	//对于一个i∈[l,r),Min[i]的贡献次数就为比Min[i]大的异侧的数的个数. 
	while(l<=p||q<r) //sum表示前top个数的贡献之和 
		if(q>=r||(l<=p&&Min[p]>=Min[q])) g[d][p]=++top,sum+=(top-(mid-p	 ))*Min[p],h[d][p--]=sum;
		else 					 		 g[d][q]=++top,sum+=(top-(q-mid+1))*Min[q],h[d][q++]=sum;
		//g为按Min的排名,Min往两侧都是不严格递减的哦~ 
	bt(lc,l,mid,d+1);
	bt(rc,mid,r,d+1);
}

ll query(ll l,ll r) {
	if(l==r) return a[l];
	int k,d=t-(k=Log[l^r]);
	int L=l>>k<<k,R=((l>>k)+1)<<k,mid=L+R>>1;
	int p=g[d][l],q=g[d][r];
	ll ans=f[d][l]+f[d][r];
	if(p<q) ans+=h[d][l]+(s[d][r]-(l+p-1>=mid?s[d][l+p-1]:0))*(mid-l);
	else ans+=h[d][r]+(s[d][l]-(r-q+1<mid?s[d][r-q+1]:0))*(r-mid+1);
	return ans;
}
	

int main() {
	qr(n); qr(m); 
	for(int i=0;i<n;i++) qr(a[i]);
	for(len=1;len<n;len<<=1);
	for(int i=1;i<=len;i++) Log[i]=Log[i>>1]+1;
	t=Log[len]; bt(1,0,len,1); while(m--) {
		int l,r; qr(l); qr(r);
		pr2(query(l-1,r-1));
	}
	return 0;
}


  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值