还是你们太牛了。
猫树
概述:类似于 ST表,可以支持快速查询大部分静态区间问题。
实现:考虑将询问分治,对于每次分治到的区间 \([l,r]\),处理出 \([l,mid]\) 中每个后缀的信息与 \((mid,r]\) 中每个前缀的信息。那么对于一个询问 \([L,R] (L \le mid < R)\),就可以通过合并前后缀信息来得到答案了。
举例:如果我们需要查询区间 \(\max\),那么由于 \(\max\limits_{i=L}^{R} a_{i} =\max(\max\limits_{i=L}^{mid}a_i ,\max\limits_{i=mid+1}^{R} a_i)\),所以答案就直接是 \(\max(rmax_L,lmax_R)\) 了。这样做的时间复杂度,由于一个点 \(i\) 只会在 \(O(\log)\) 个分治区间中被用到,所以是 \(O(T n\log n +qT)\) 的,其中 \(T\) 是合并信息的时间复杂度。
例题
SP1043 GSS1 - Can you answer these queries I
最大子段和。那么我们记录后缀最大子段和和前缀最大子段和,则答案就是 \(\max(rmax_L,lmax_R,rmax_L+lmax_R)\) 了。时间复杂度 \(O(n\log n+q)\)。
怎么做到 \(O(1)\) 询问的?那么我们就需要快速找到一个区间 \([L,R]\) 在哪一次分治会被算到了。我们将每次的 \(mid\) 看作一个点,那么就相当于是 \(L,R\) 这两个叶子的 \(\operatorname{LCA}\) 对应的深度了。基于二叉树,\(L,R\) 的 \(\operatorname{LCA}\) 是它们最长公共前缀。所以这个公共前缀的长度就是具体在哪次分治被算到了。
代码:
il void build(int u,int l,int r,int dep){
if(l==r) return pos[l]=u,void(0);
int mid=l+r>>1,s1=a[mid+1],s2=a[mid];
lmax[dep][mid+1]=lmax_[dep][mid+1]=a[mid+1],
rmax[dep][mid]=rmax_[dep][mid]=a[mid];
for(re int i=mid+2;i<=r;++i){
s1+=a[i];
lmax[dep][i]=max(lmax[dep][i-1]+a[i],a[i]);
lmax_[dep][i]=s1;
}
for(re int i=mid-1;i>=l;--i){
s2+=a[i];
rmax[dep][i]=max(rmax[dep][i+1]+a[i],a[i]);
rmax_[dep][i]=s2;
}
for(re int i=mid+2;i<=r;++i){
lmax[dep][i]=max(lmax[dep][i],lmax[dep][i-1]);
lmax_[dep][i]=max(lmax_[dep][i],lmax_[dep][i-1]);
}
for(re int i=mid-1;i>=l;--i){
rmax[dep][i]=max(rmax[dep][i],rmax[dep][i+1]);
rmax_[dep][i]=max(rmax_[dep][i],rmax_[dep][i+1]);
}
build(ls(u),l,mid,dep+1),
build(rs(u),mid+1,r,dep+1);
return ;
}
il int query(int l,int r){
if(l==r) return a[l];
int dep=__[pos[l]]-__[pos[l]^pos[r]];
return max({lmax[dep][r],rmax[dep][l],lmax_[dep][r]+rmax_[dep][l]});
}
il void solve(){
n=rd,len=2;
while(len<n) len<<=1;
for(re int i=2;i<=(len<<1);++i) __[i]=__[i>>1]+1;
for(re int i=1;i<=n;++i) a[i]=rd;
build(1,1,len,1),m=rd;
while(m--){
int l=rd,r=rd;
printf("%lld\n",query(l,r));
}
return ;
}
这里给一些注意事项:
- 需要将序列长度拓展为 \(2^k\),不然无法保证两点的 \(\operatorname{LCA}\) 求的是对的。
- 分治点的数量是 \(2n\) 级别的,所以可能用到的 \(\log_2 x\),\(x < 2n\)。
P6240 好吃的题目
是一个区间背包问题,对于一次询问合并前缀后缀的背包即可。时间复杂度 \(O(nV\log n +qV)\)。
P3246 [HNOI2016] 序列
这就不得不搬出 BZOJ4262 Sum 了,甚至是加强版。模拟赛的时候花费 2h 搓出类猫树,8kb 豪夺最长代码。然后 cf 场上直接套用,以 10kb 代码冲爆 *2700。你们还是太牛了。
题目是让求:\(\sum\limits_{l=L}^{R}\sum\limits_{r=l}^{R}\min\limits_{i=l}^{r} a_i\)。
那么考虑区间中一个点 \(i\) 对答案的贡献。它应该是:\(a_i \times (\sum\limits_{l=L}^{i}\sum\limits_{r=i}^{R}[\min\limits_{j=l}^{r} a_j=a_i])\)。那么我们在分治的时候,对于当前分治区间中的每个 \(i\),就可以找到它前面第一个比它大的和后面第一个不比它小的,记为 \(x,y\)。这里的 \((x,y,i)\) 应该在 \(mid\) 的同一侧。然后分类讨论:
- \(L \le l \le r \le mid\),且 \(L \le i \le mid\)。那么 \(x < l \le i \le r < y\) 的区间 \([l,r]\) 都会是 \(i\) 来贡献。总的贡献为 \(a_i(i-x)(y-i)\)。
- \(mid < l \le r \le R\),且 \(mid< i \le R\)。那么 \(x < l \le i \le r < y\) 的区间 \([l,r]\) 都会是 \(i\) 来贡献。总的贡献为 \(a_i(i-x)(y-i)\)。
- \(L \le l \le mid < r \le R\) 且 \(L \le i \le mid\)。如果 \(i\) 后面第一个不比它小的数在 \([L,mid]\),那么没有贡献。那么记 \(y'\) 为这个点的下标,则有 \(a_i (i-x)(y'-mid)\)。
- \(L \le l \le mid < r \le R\) 且 \(mid< i \le R\)。同上。
那么怎么快速处理出后缀和前缀信息呢。对于第 \(1\) 中情况,当 \(L > x\) 时,\(L \to L-1\) 都会产生 \(a_i(y-i)\) 的贡献。那么我们使用变量记录这些增量,并且在 \(L=x\) 时减去 \(a_i(y-i)\) 的增量即可;第 \(2\) 种情况同理。对于第 \(3\) 种情况,它的限制是两个区间了,我们将式子拆成:\(a_i(iy'-imid-xy'+xmid)=(a_ii)y'-(a_iimid)-(a_i)xy'+(a_imid)x\)。那么除了 \((a_i)xy'\) 以外都是可以仿照第 \(1\) 种情况处理的。\((a_i)xy'\) 的实际贡献是 \(a_i \max(x,L)\min(R,y')\)。

浙公网安备 33010602011771号