个人对线段树的一些理解
以下均为本人对线段树的一些粗鄙理解,不保证正确。
我认为,带懒标记的线段树优化效率的关键是对于标记的高效合并,在一棵线段树上,如果我们每次遍历到一个节点的时候都下放标记,并且不妨将单位操作放入一个队列,那么,最终对于底层的一个组合对象,它调用的函数就是根节点到它路径上每个节点依次经过的操作,其中每个节点都要按照顺序遍历。
而这样的复杂度显然是劣,我们考虑加速合并操作队列的过程,考虑提取出若干个足以刻画信息的量,每次合并的时候维护这个“关键信息”,这样,我们就能实现区间修改单点查询。
好,所以线段树的 tag 和 val(叶子节点的值)是分开维护的,为了解决区间查询的问题,我们需要对于每个点记录子节点的 val 和(这个和运算可以是不满足交换律的,下面也记为 val),假设需要查询一个信息,我们只要扩大维护的信息量使得这个信息能够在区间修改的时候也能被快速更新,比如区间加区间 sin 和,就是对于维护信息的扩张。
在一类常见双序列问题中,我们常常不将“从动”信息列入 tag 中,列入 tag 中必然要是所求信息或是在信息传递的 DAG 中能够间接地导向所求信息,当然这个可能的导向信息是不唯一的,比如 sin 可以扩到 cos,但是也不排除别的导出 sin 的路径,只要能维护就好。
比如这个题:\(a\) 数列区间加(保证 \(a_i\) 恒非负),区间将 \(a_i=0\) 的位置的 \(b_i\) 加上 \(1\),求 \(b_i\) 和(梦熊炼石计划一个题最后一步)。我们可以将这个单位操作就只有线段树节点子树加,子树修改 \(b_i\),你发现,\(\sum b\) 只要在 val 中维护,同时 val 中要维护 min 和 cntmin,这都是十分自然的。
而修改的标记我们肯定要维护 \(\sum \Delta a\),考虑对于 a 贡献到 b 的形式比较丑陋,但是由于 \(a_i\ge0\),我们只要维护标记队列的 \(\Delta a\) 的前缀和最小值和此时对应的操作 \(2\) 的次数,这样这个题就做完了。
所以,我们往往在“主动”信息进行维护 val 和 tag,就是因为它们在 DAG 上有出边,而 tag 信息的维护则更侧重于 val 维护量之间的关系,也就是 val 描述了序列 \(a\) 的信息,而 tag 则侧重于刻画序列 \(a\) 到序列 \(a\) 或者 \(a\) 到 \(b\) 的贡献,从而用序列 \(a\) 的信息和 tag 得到新的 \(a\) 或者用序列 \(a\) 的信息和 tag 得到新的 \(b\)。相比之下,“从动”信息的维护就简单的多了。
参考代码(截取):
struct Tag {
int sum, mn, sc;
Tag(int _sum = 0, int _mn = 0, int _sc = 0) {
sum = _sum, mn = _mn, sc = _sc;
}
friend Tag operator + (Tag x, Tag y) {
int tmn = min(x.mn, x.sum + y.mn);
return Tag(x.sum + y.sum, min(x.mn, x.sum + y.mn), (x.mn == tmn ? x.sc : 0) + (x.sum + y.mn == tmn ? y.sc : 0));
}
};
struct Info {
int mn, cnt;
long long sb;
Info(int _mn = 0, int _cnt = 1, long long _sb = 0) {
mn = _mn, cnt = _cnt, sb = _sb;
}
friend Info operator + (Info x, Info y) {
int tmn = min(x.mn, y.mn);
return Info(tmn, (x.mn == tmn ? x.cnt : 0) + (y.mn == tmn ? y.cnt : 0), x.sb + y.sb);
}
void apply(Tag y) {
assert(mn + y.mn >= 0);
if (mn + y.mn == 0) sb += 1ll * cnt * y.sc;
mn += y.sum;
}
};
vector< tuple<int, int, Tag> > mdf[N];
vector< tuple<int, int, int> > qry[N];
long long res[N];
struct SegmentTree {
Info a[N], o[1 << 20];
Tag z[1 << 20];
void build(int k, int l, int r) {
z[k] = Tag();
if (l == r) {
o[k] = a[l];
return;
}
int mid = (l + r) >> 1;
build(k << 1, l, mid);
build(k << 1 | 1, mid + 1, r);
o[k] = o[k << 1] + o[k << 1 | 1];
}
void pushdown(int k) {
o[k << 1].apply(z[k]), z[k << 1] = z[k << 1] + z[k];
o[k << 1 | 1].apply(z[k]), z[k << 1 | 1] = z[k << 1 | 1] + z[k];
z[k] = Tag();
}
void modify(int k, int l, int r, int x, int y, Tag w) {
if (l > y || r < x) return;
if (l >= x && r <= y) {
o[k].apply(w);
z[k] = z[k] + w;
return;
}
pushdown(k);
int mid = (l + r) >> 1;
modify(k << 1, l, mid, x, y, w);
modify(k << 1 | 1, mid + 1, r, x, y, w);
o[k] = o[k << 1] + o[k << 1 | 1];
}
Info query(int k, int l, int r, int x, int y) {
if (l >= x && r <= y) return o[k];
pushdown(k);
int mid = (l + r) >> 1;
if (y <= mid) return query(k << 1, l, mid, x, y);
if (x > mid) return query(k << 1 | 1, mid + 1, r, x, y);
return query(k << 1, l, mid, x, y) + query(k << 1 | 1, mid + 1, r, x, y);
}
} Seg;
signed main() {
/* do something */
for (int i = 1; i <= n; ++i) {
Seg.a[i] = Info();
}
Seg.build(1, 1, n);
for (int i = 1; i <= n; ++i) {
for (auto [x, y, z] : mdf[i]) Seg.modify(1, 1, n, x, y, z);
for (auto [x, y, z] : qry[i]) res[z] = Seg.query(1, 1, n, x, y).sb;
}
/* do something*/
return 0;
}