从《楼房重建》出发浅谈一类使用线段树维护前缀最大值的算法
在博客园食用更佳:https://www.cnblogs.com/PinkRabbit/p/Segment-Tree-and-Prefix-Maximums.html。
首先需要申明的是,真的是浅谈,因为我对这个算法的认识还是非常低的。
既然是从《楼房重建》出发,那么当然是先看看这道题:
[清华集训2013]楼房重建
bzoj 链接
题意简述:
有
初始时
每次询问从
能看到一栋楼
题解:
令
则一栋楼房
直接进入正题,我们使用线段树维护这个东西。
考虑线段树上的某一个节点表示的区间
- 这个区间中的
s_i 的最大值。 - 仅考虑这个区间时的上述答案,也就是不考虑
[1, l - 1] 对本区间的影响,而是看作整体的前缀最大值个数。
可以发现只有单点修改,那么我们只需考虑递归到底层节点后,一层层往上维护信息即可。
当前考虑一个节点
信息 1 是容易维护的,只要两个子树取
但是信息 2 如果直接用两个子树信息相加,是错误的,因为没有考虑左子树向右子树的贡献。
进一步分析:可以发现直接继承左子树的信息是没问题的,但是右子树信息不能直接继承。
考虑引入一个新函数:
为了方便表述,把信息 1 记做
其中蓝色的是左子树贡献,红色的是右子树贡献。
当当前节点
否则考虑左右子树的贡献分别计算,分成两种情况考虑:
-
此时对右子树来说,$pre$ 是无意义的,所以递归进左子树,右子树的贡献直接用“全部”减“左子树”计算即可。 -
此时对左子树来说,就不可能贡献任何前缀最大值了,所以贡献为 $0$,然后递归进右子树即可。
可以看出,调用一次
每次维护当前节点的答案时,只要令
可以发现有
至此可以写出本题的代码:
#include <cstdio>
typedef long long LL;
const int MN = 100005, MS = 1 << 18 | 7;
int N, Q, H[MN];
inline bool gt(int p1, int p2) { // s[p1] is greater than s[p2]
if (!p2) return H[p1];
return (LL)H[p1] * p2 > (LL)H[p2] * p1;
}
#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
int id[MS], cnt[MS];
void Build(int i, int l, int r) {
id[i] = l, cnt[i] = 1;
if (l == r) return ;
Build(ls), Build(rs);
}
int Calc(int i, int l, int r, int p) {
if (l == r) return gt(l, p);
if (gt(id[li], p)) return Calc(ls, p) + (cnt[i] - cnt[li]);
else return 0 + Calc(rs, p);
}
void Mdf(int i, int l, int r, int p) {
if (l == r) return ;
if (p <= mid) Mdf(ls, p);
else Mdf(rs, p);
id[i] = gt(id[ri], id[li]) ? id[ri] : id[li];
cnt[i] = cnt[li] + Calc(rs, id[li]);
}
int main() {
scanf("%d%d", &N, &Q);
Build(1, 1, N);
while (Q--) {
int p, x;
scanf("%d%d", &p, &x);
H[p] = x, Mdf(1, 1, N, p);
printf("%d\n", Calc(1, 1, N, 0));
}
return 0;
}
但是,我们注意到一个很关键的性质:
当
也就是说这个信息要满足一定程度上的可减性。
但是有很多信息是不满足可减性的,比如
为了能让这种线段树适应更一般的情况,我们修改维护的信息的意义:
- 仍然维护这个区间中的
s_i 的最大值。 - 此时并不是维护区间的答案,而是仅考虑该区间的影响后,却又只统计右子树的答案。
也就是说令当前节点对应的区间为[l, r] ,区间中点为mid ,则:
维护的答案是,只考虑g_l \sim g_r 时,在区间[mid + 1, r] 中的答案。
仍然把信息 1 记做
对于叶节点,信息 2 则看作是未定义的。
然后考虑维护当前节点的信息(也就是 Pushup),仍然引入一个
此时它的作用仍然是计算在
它的伪代码如下:
其实变化并不大,因为此时
每次维护当前节点的答案时,只要令
其实更好写了,代码如下:
#include <cstdio>
typedef long long LL;
const int MN = 100005, MS = 1 << 18 | 7;
int N, Q, H[MN];
inline bool gt(int p1, int p2) { // s[p1] is greater than s[p2]
if (!p2) return H[p1];
return (LL)H[p1] * p2 > (LL)H[p2] * p1;
}
#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
int id[MS], cnt[MS];
void Build(int i, int l, int r) {
id[i] = l, cnt[i] = 1;
// if i is a leaf node, then cnt[i] can be any value.
// but here, for convenience, we just let it be 1.
if (l == r) return ;
Build(ls), Build(rs);
}
int Calc(int i, int l, int r, int p) {
if (l == r) return gt(l, p);
if (gt(id[li], p)) return Calc(ls, p) + cnt[i];
else return 0 + Calc(rs, p);
}
void Mdf(int i, int l, int r, int p) {
if (l == r) return ;
if (p <= mid) Mdf(ls, p);
else Mdf(rs, p);
id[i] = gt(id[ri], id[li]) ? id[ri] : id[li];
cnt[i] = Calc(rs, id[li]);
}
int main() {
scanf("%d%d", &N, &Q);
Build(1, 1, N);
while (Q--) {
int p, x;
scanf("%d%d", &p, &x);
H[p] = x, Mdf(1, 1, N, p);
printf("%d\n", Calc(1, 1, N, 0));
}
return 0;
}
[CodeForces 671E]Organizing a Race
CF 链接
题意简述:
题意的抽象过程太复杂了,这里仅考虑抽象后的模型:
给出两个长度为
你需要动态维护整个数组中满足
而
题解:
可以发现,因为这里要维护的东西变成
考虑在线段树的每个节点维护三个信息:
- 这个区间中
a_i 的最小值,记做{a\mathrm{min}} 。 - 这个区间中
b_i 的最大值,记做{b\mathrm{max}} 。 - 仅考虑该区间时,在右子树内的答案,记做
\mathrm{ans} 。
因为是区间修改
此时需要面对两个问题,下传标记(Pushdown)和维护信息(Pushup)。
对于打标记,当一个节点被打上区间
那么最重要的问题仍然是维护信息(Pushup),仍然是写出类似函数
对于当前节点
假如
否则左子树中所有的
最后需要求整个数组中满足
考虑一个新函数
- 如果
{b\mathrm{max}}[\mathrm{leftchild}[i]] > pre ,也就是说pre 影响不到右子树:
那么,如果\mathrm{ans}[i] \le k ,就递归进右子树,否则递归进左子树。
复杂度显然是\mathcal O (\log n) 。 - 如果
{b\mathrm{max}}[\mathrm{leftchild}[i]] \le pre ,也就是说左子树完全被pre 控制了:
先递归进右子树查询,如果没查询到,则考虑左子树因为被pre 控制了,限制变为a_i + pre \le k 。
则移项得到a_i \le k - pre ,在左子树内是一个正常的线段树上二分的子问题(需要新写一个函数查询)。
因为只会进行\mathcal O (\log n) 次线段树上二分,所以时间复杂度为\mathcal O (\log^2 n) 。
至此我们在