[数据结构入门]分治树(猫树)
#1.0 算法实现
#1.1 预处理
考虑一个序列的分治树,对于每个节点,设当前节点维护区间 \([l,r]\) 的中点为 \(mid\),预处理出所有形如 \([i,mid](l\leq i \leq mid)\) 的信息与 \([mid+1,j](mid+1\leq j\leq r)\) 的信息,这个过程的时间复杂度为 \(O(n\log n).\)
#1.2 查询
对于任何区间查询 \([l,r]\),分治树上都存在一个对应区间包含 \([l,r]\),且中点 \(m\) 在 \([l,r]\) 内的节点,找到此节点,将预处理的 \([l,m]\) 和 \([m+1,r]\) 的信息合并,即可得到询问的答案。
#1.3 优化查询复杂度
直接查找这个节点的复杂度是 \(O(\log n)\),可以将序列长度扩充至 \(2\) 的幂(维护区间为 \([1,2^k]\)),我们考虑将这个区间建成一颗分治树,显然这是一颗满二叉树,那么,我们要找的节点必然是 \([l,l]\) 和 \([r,r]\) 的 \(\texttt{LCA}\),我们采用堆式存储,即节点 \(i\) 的左儿子编号为 \(2i\),右儿子为 \(2i+1\),观察其二进制形式,不难发现,每一个左儿子的编号相当于父结点编号左移一位,右儿子则是左移一位加一,所以代表 \([l,l]\) 和 \([r,r]\) 这两个区间的两个节点的 \(\texttt{LCA}\) 必然是两者编号在二进制下的最长相同前缀(只适用于在同一深度的节点)。
至于这两点的编号是多少,我们可以在预处理时提前存储,这样,我们单次查询的时间复杂度为 \(O(1)\)。
假设节点编号分别为 \(x,y\),那么,他们的 \(\texttt{LCA}\) 编号便是 x >> log2[x ^ y]
,这里 log2[k]
存储的是 \(k\) 在二进制表示下有多少位。
#1.4 其他
至于空间复杂度,考虑每一层保存的状态,每层有 \(n\) 种,一共有 \(\log n\) 层,故总体为 \(O(n\log n)\)。在实现时对于每个节点应当采用动态存储,如 vector
或指针,或者提前计算层数(\(\log n\)),按层储存状态,保证空间复杂度。
由猫树处理的过程不难发现,所维护的信息必须满足区间可加性。
在实际实现时,我们不必真正建立出左右儿子,只需分出区间进行预处理即可,因此,我们在存储信息时可采用二维数组,形如 VAL[MAX_D][N]
,其中 MAX_D
指的是最大层数。
如果我们按层储存,那么编号为 \(x,y\) 的点的 \(\texttt{LCA}\) 所在层数为 log2[x] - log2[x ^ y]
.
#2.0 例题
#2.1 区间最大子段和
可以发现,区间 \([l,r]\) 的最大子段和要么在 \([l,mid]\) 中,要么在 \([mid+1,r]\) 中,要么横跨 \(mid\),所以我们可以维护最大前后缀和来解决这个问题。
对于不横跨 \(mid\) 的,正常做最大子段和即可。对于横跨 \(mid\) 的,维护的前后缀要求不能有中断。
/*SPOJ GSS1 https://www.spoj.com/problems/GSS1/ */
const int N = 1000100;
const int INF = 0x3fffffff;
int loc[N],a[N],n,m,len = 1;
int s[25][N],p[25][N],lg[N];
/*s 是不能与 mid 断开的前后缀,p 是可与 mid 断开的最大子段*/
inline int Max(const int &a,const int &b){
return a > b ? a : b;
}
inline void build(int k,int l,int r,int d){
if (l == r) {loc[l] = k;return;} //叶节点,记录编号
int mid = (l + r) >> 1;
int pre,sm;
/*维护 mid 左边*/
s[d][mid] = a[mid],p[d][mid] = a[mid];
pre = sm = a[mid];sm = sm > 0 ? sm : 0;
for (int i = mid - 1;i >= l;i --){
pre += a[i],sm += a[i];
s[d][i] = Max(s[d][i + 1],pre),
p[d][i] = Max(p[d][i + 1],sm);
sm = sm > 0 ? sm : 0;
/*如果小于零了,后面的维护可以
从中断开,保证最大值*/
}
/*维护 mid 右边*/
s[d][mid + 1] = a[mid + 1],
p[d][mid + 1] = a[mid + 1];
pre = sm = a[mid + 1]; sm = sm > 0 ? sm : 0;
for (int i = mid + 2;i <= r;i ++){
pre += a[i],sm += a[i];
s[d][i] = Max(s[d][i - 1],pre);
p[d][i] = Max(p[d][i - 1],sm);
sm = sm > 0 ? sm : 0;
}
build(k << 1,l,mid,d + 1);
build(k << 1 | 1,mid + 1,r,d + 1);
}
inline int query(int l,int r){
if (l == r) return a[l];
int d = lg[loc[l]] - lg[loc[l] ^ loc[r]];
return Max(Max(p[d][l],p[d][r]),s[d][l] + s[d][r]);
}
int main(){
scanf("%d",&n);
while (len < n) len <<= 1;
for (int i = 1;i <= n;i ++)
scanf("%d",&a[i]);
for (int i = 1;i <= len << 1;i ++)
lg[i] = lg[i >> 1] + 1;
build(1,1,len,1);
scanf("%d",&m);
while (m --){
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",query(l,r));
}
return 0;
}