2026.07 训练日记

7.7

开始搞线段树。

P3372

没什么好说的。线段树板子(不过也能用 BIT 做,甚至更快)。
细节别忘写了就行。

P3870

线段树板子。
注意 lazy tag 的更新方式。

P1438

也算是板子。线段树维护差分数组。

题目要求的区间修改是在原数组 \(a\) 的某个区间 \([l, r]\) 上加一个首项为 \(K\)、公差为 \(D\)、长度为 \(r - l + 1\) 的等差数列,显然直接维护过于繁琐。
由等差数列的性质不难发现,如果维护原数组的差分数组 \(b\),则区间修改时,仅需:

  • \(b[l]\) 实施 \(+K\) 的单点修改操作;
  • \(b[r + 1]\) 实施 \(-(K+(r-l)D)\) 的单点修改操作;
  • \(b[l + 1] \sim b[r]\) 实施 \(+D\) 的区间修改操作。

考虑到单点修改实质上就是长度为一的区间修改,故只需要实现区间修改函数即可。
同时,不难看出查询原数组的第 \(x\) 位等同于查询差分数组的第 \(1 \sim x\) 位区间和,故再实现一个区间查询函数即可。

注意细节。

P1253

有两种区间修改操作:区间加、区间重设。
对应地,lazy tag 也应有两个。注意在更新区间重设的 lazy tag 时,区间加的 tag 应当失效,设为 \(0\)
另外,区间重设的 tag 默认值应为一个极大值或极小值,考虑到重设数字可能为 \(0\)
下传也需注意。

区间查询最大值。注意实现。

7.8

P3373

线段树维护区间加、区间乘的板子。
注意:

  • 在更新区间乘的 lazy tag 时,区间加的 tag 也要对应乘上该数;
  • 在下传 tag 时,务必先下传区间乘再下传区间加。

P4513

线段树维护区间最大子段和(Maximum subsegment sum,下文简称 MSS)的模板。

我们拆解一下一个区间的 MSS 的组成。如下图:

显然,一段区间的 MSS 应该是其左子节点区间的 MSS,其右子节点区间的 MSS,其左、右子节点的两段相邻的和,这三者中的最大值。
重点观察 case 3。
为了方便描述,我们将一个区间必须包含左(右)端点的最大子段和称作最大左(右)段和(MLS、MRS)。
则 case 3 即为左子节点的 MRS 与右子节点的 MLS 之和。

关于 MLS 和 MRS 的维护,再看下图:

这是区间 MLS 组成的两种情况。其是左子节点的 MLS,左子节点区间和加右子节点的 MLS,这两者中的最大值。
同理,区间 MRS 也应为右子节点的 MRS,右子节点区间和加左子节点的 MRS,这两者中的最大值。

所以,我们的线段树需要维护区间总和、MLS、MRS、MSS 这四个值。

7.12

P1816

维护最小值。
建完树直接查询,甚至连修改都没有。
用线段树做属实是大炮打蚊子了。

P1471

被精度问题害惨咯。

要求区间加、查询区间平均数、查询区间方差。
而且是实数。不要忘记 \(\texttt{double}\) 以及某些地方为了防止精度问题的 \(\texttt{*1.0}\)

平均数比较简单:

\[\begin{aligned} \bar{A} &= \frac{1}{n} \sum A_i. \end{aligned} \]

维护一个区间和就行。

方差的公式当然也要展开:

\[\begin{aligned} s^2 &= \frac{1}{n} \sum (A_i - \bar{A})^2 \\ &= \frac{1}{n} \sum (A_i^2 + \bar{A}^2 - 2A_i \bar{A}) \\ &= \frac{1}{n} (\sum A_i^2 + n \bar{A}^2 - 2n \bar{A}^2) \\ &= \frac{1}{n} (\sum A_i^2 - n \bar{A}^2) \\ &= \frac{1}{n} \sum A_i^2 - \bar{A}^2. \end{aligned} \]

于是我们需要再维护一个区间平方和。

做区间加时,lazy tag 的下传公式也不难推导:

\[\begin{aligned} \sum (A_i + k) &= \sum A_i + nk, \\ \sum (A_i + k) ^ 2 &= \sum (A_i^2 + k^2 + 2A_i k) \\ &= \sum A_i^2 + 2k \sum A_i + nk^2. \end{aligned} \]

7.13

P6492

P4513 有些相似。下面直接贴上 pushup 函数的实现。

void pushup(int u) {
    int f = (w[u << 1].r != w[u << 1 | 1].l);
    int fl = (w[u << 1].px == w[u << 1].len);
    int fr = (w[u << 1 | 1].sx == w[u << 1 | 1].len);
    w[u].len = w[u << 1].len + w[u << 1 | 1].len;
    w[u].mx = std::max(std::max(w[u << 1].mx, w[u << 1 | 1].mx), (w[u << 1].sx + w[u << 1 | 1].px) * f);
    w[u].px = std::max(w[u << 1].px, (w[u << 1].len + w[u << 1 | 1].px) * f * fl);
    w[u].sx = std::max(w[u << 1 | 1].sx, (w[u << 1 | 1].len + w[u << 1].sx) * f * fr);
    w[u].l = w[u << 1].l;
    w[u].r = w[u << 1 | 1].r;
}

P1908

权值线段树。

首先对序列进行离散化。
然后遍历离散化后的序列 \(r\)

  • 将线段树对应 \(r_i\) 的位置 \(+1\)
  • 查询线段树对应 \(r_i +1 \sim n\) 的和,把返回值加入最终答案中。

最后输出答案即可。

记得开 \(\texttt{long long}\)

P1637

也是权值线段树。

依旧离散化。
然后因为是三元子序列,所以我们枚举中间的元素,对序列遍历两次,第一次记录其左边更小的,第二次记录其右边更大的(这些遍历操作与 P1908 类似),两次的结果对应相乘就是将该元素放在中间时可以形成的三元上升子序列个数。
\(n\) 个乘积相加,就是最终的答案。

P1558

观察到最多只有 \(30\) 种颜色,故考虑状态压缩

pushup 就是按位或。tag 的更新和下放也不难想。
注意建树的初始值为 \(1\)
注意区间左右端点反着给要 swap。

另外查询操作一定要把所有子区间按位或完之后再取 count,因为不同子区间中可能会有重复颜色。

当然还有一种做法就是直接建 \(30\) 棵线段树,非常 brute force。还是算了。

7.14

P5522

需要状压。
线段树的一个节点应当存储两个数 \(\texttt{c0}\)\(\texttt{c1}\)

  • \(\texttt{c0}\) 的二进制表示中第 \(i\) 位为 \(1\) 当且仅当字符串的第 \(i\) 位确定为 \(\texttt{0}\)
  • \(\texttt{c1}\) 的二进制表示中第 \(i\) 位为 \(1\) 当且仅当字符串的第 \(i\) 位确定为 \(\texttt{1}\)

pushup 直接用左右子节点两个数值的按位或即可。
单点修改操作也是朴素的。
留意区间查询。

首先是 out of range 的情况。这个返回 \(\texttt{\{0, 0\}}\) 即可。

接着是 in range 的情况。
若一个区间的 \(\texttt{c0}\)\(\texttt{c1}\) 按位与的结果不为 \(0\),则说明有至少一个位置既为 \(\texttt{0}\) 也为 \(\texttt{1}\),矛盾。此时返回一个 \(\texttt{\{-1, -1\}}\)
否则返回该区间的节点即可。

Otherwise,当然要先向下递归查询。
若左右子区间的查询结果有至少一个是 \(\texttt{\{-1, -1\}}\),则说明该区间存在矛盾,当前区间也要返回 \(\texttt{\{-1, -1\}}\)
否则把两个结果中的值分别按位或起来,别忘了再验证按位或之后的两个数是否矛盾。矛盾就返回 \(\texttt{\{-1, -1\}}\),否则返回按位或之后的两个数。

接着,关于答案的计算。
首先要计算一个区间中所有字符串都为 \(\texttt{?}\) 的位置数,记为 \(p\),显然 \(p\) 等于查询结果的 \(\texttt{c0}\)\(\texttt{c1}\) 按位或结果的二进制表示的 \(0\) 位数。
\(\texttt{len - __builtin_popcount(res.c0 | res.c1)}\) 即可计算。
于是一次查询中可能的符合条件的字符串数量即为 \(2^p\)
当然如果查询返回的是 \(\texttt{\{-1, -1\}}\) 的话答案就是 \(0\)

P4145

区间开平方根 + 区间求和?注意到 \(\sqrt{a} + \sqrt{b}\) 并不恒等于 \(\sqrt{a + b}\),所以没法用 lazy tag 延迟下放。
我们对 \(10^{12}\) 不断开方:\(10^{12} \to 10^6 \to 10^3 \to 31 \to 5 \to 2 \to 1\),也就是说序列 \(a\) 里面的数至多能做 \(6\) 次开方操作,直到值为 \(1\) 时开方不会再改变其值。
那好办了,我们给线段树的节点额外维护一个反映区间是否全为 \(1\) 的标记,如果 update 时某个区间标记为 \(\texttt{true}\) 则不再向下拆分,否则就拆分到底做单点修改。这就相当于一个剪枝了。
pushup 不难推导。

像本题所使用的这种,不带 lazy tag、依靠区间最值等做剪枝优化的传统静态线段树的线段树,被称作小清新线段树
代码相当简洁。

7.15

P2572

三种修改,两种查询。算法实现并不难。
最大的坑点就是,题目输入的下标 \(l, r\)0-based
因为这个卡了整整一个小时。

7.17

P1531

简易的线段树维护区间最大值。
单点修改的操作有一点点不同。

P2023

线段树维护区间乘、区间加、区间求和模板。

P2068

线段树维护单点修改、区间求和模板。
当然用 BIT 会更简单。

P2184

修改:标记一个区间。
查询:指定区间与多少个已标记的区间存在交集。

考虑维护标记区间的左右端点。即把标记一个区间 \([l, r]\) 转化为给下标为 \(l\)\(r\) 的两个位置 \(+1\)
查询也不难想到:对于指定区间 \([l', r']\),只需要查询在 \([1, r']\) 范围内有多少个区间左端点(数量记为 \(s\)),以及在 \([1, l')\) 范围内有多少个区间右端点(数量记为 \(t\)),此时 \(s - t\) 就是最终答案。
所以我们只需要两个维护单点修改、区间查询的线段树即可。
当然用 BIT 会更简单。

P2846

P3870 相同。

P3801

容易发现一次修改操作只影响一行、一列的状态。
考虑使用两个数据结构分别维护行和列的状态。并且状态取值只有 \(0/1\)

接下来关注查询操作的实现。
首先查询 \([x_1, x_2]\) 内有多少行被修改,以及 \([y_1. y_2]\) 内有多少列被修改。答案分别记为 \(r\)\(c\)
另外将 \((x_2 - x_1 + 1)\)\((y_2 - y_1 + 1)\) 分别记为 \(s\)\(t\)
参考下图。

粉色格子为被影响的行和列的交点。交点的数量为 \(r\cdot c\)
由容斥原理很容易得出,若这些交点也计入总数,我们最终答案的计算公式为 \(\text{ans} = r\cdot t + c\cdot s - r\cdot c\)
然而题目要求不计这些交点,故在上式的基础上还需要再减去 \(r\cdot c\)。即

\[\boxed{ \text{ans} = r\cdot t + c\cdot s - 2\cdot r\cdot c. } \]

所以我们只需要两个维护单点修改、区间查询的线段树。
还是那句话,当然用 BIT 会更简单。

7.19

P10673

蛮有意思的一道题。
显然,我们需要维护一个桶 \(a\) 记录不同种类的糖果的数量。

考虑糖果的数量怎样对最小愤怒值产生贡献。设分给的人数为 \(k\)。由抽屉原理,不难看出当糖果 \(i\) 的数量 \(a_i > k\) 时才会产生贡献,贡献为 \((a_i - k)\)
换言之,更新糖果数量(\(\pm 1\))后,对于任意 \(k \in [1, a_i - 1]\)(当对糖果数量 \(-1\) 时区间上界为 \(a_i\)),其答案都会对应地 \(\pm 1\)

这样我们就自然能想到用数据结构维护一个答案数组 \(s\),其中 \(s_i\) 表示当前分给 \(i\) 人时的最小愤怒值。
更新糖果数量就等价于对 \(s\) 进行区间修改,查询时即对 \(s\) 进行单点查询。
用线段树。

需要注意的是,操作 \(1\) 的数量可能很多,所以分给的人数的上界应当在 \((n + q)\)。即 \(\texttt{MAXN}\) 应当取 \((2\cdot 10^6 + 5)\)
所以在线段树的修改和查询都需要以 \([1, n + q]\) 为初始区间,故枚举操作时不可以套 \(\texttt{while (q--)}\),这会使 \(q\) 不断减小。
相反,用 \(\texttt{for (int i = 1; i <= q; ++i)}\) 才是正确的。

依旧是那句话,当然用 BIT 会更简单。

P13825

这题竟然要开 \(\texttt{unsigned long long}\) 才能过最后一个点。查询出题人精神状态。

看似是普通的线段树模板。但是数据范围:

  • 对于 \(100\%\) 的数据,\(1 \le m,k \le {10}^5\)\(1 \leq l \leq r \leq n\leq 10^9\)

\(4n\) 的空间会直接爆掉。

这时就需要一种新的技巧,即动态开点线段树(Dynamic segment tree)
顾名思义,其核心思想就是,节点只有在有需要的时候才被创建

这时我们就不能用传统的 \(2n\)\((2n + 1)\) 来表示左右子节点了,我们需要一个全局变量 \(\texttt{cnt}\) 标记当前创建的节点的编号,并将节点 \(u\) 的左右子节点分别记录记录在数组 \(\texttt{ls[u]}\)\(\texttt{rs[u]}\) 中。
同时,在 update 和 pushdown 中,当访问的节点还未被创建则需要当场创建出来。这也就引出一个非常重要的细节:update 函数的参数 \(\boldsymbol{u}\) 必须是传引用的。

一开始在全局定义一个 \(\texttt{int root}\) 表示根节点即可。

接下来关心一下本题的细节。
本题完全不需要实现 build 函数,观察到序列初始是 \(a_i = i\) 的等差数列。所以我们的线段树可以看作初始值全为 \(0\),维护时只关心区间和的变化量,查询完输出答案的时候再加上原等差数列的区间和即可。
原等差数列的区间和非常好计算,设区间为 \([l, r]\),则区间和为 \(\displaystyle\boxed{\frac{1}{2}(l + r)(r - l + 1)}\)

至于预分配的数组大小,理论上界是 \(m \log n\)\(n\) 为原序列长度,\(m\) 为操作数),保险可以开 \(4\) 倍。在本题就是 \(4 \cdot 10^5 \cdot \log 10^9 = \boxed{1.2 \cdot 10^7}\)

CF242E

区间异或、区间和。

异或这种东西当然是无法直接用 lazy tag 维护的了。怎么办呢?
我们先考虑只有一个 bit 的情况。\(\oplus 0\) 时自然是不需要操作,\(\oplus 1\) 时参照 P3870 翻转的维护即可。非常浅显。
不难发现其实可以直接把 int 做二进制拆分,对每一位都建一棵线段树维护上述操作,最后再整合起来即可。

提取一个数的二进制表示第 \(i\) 位的方法(设最低位为第 \(0\) 位):\(\texttt{(x >> i) & 1}\)
注意这里不是 \(\texttt{x & (1 << i)}\),这个的结果并不是我们需要的 \(0/1\),而会变成 \(0/2^i\)。不要混淆。

只要想到了拆位这一关键,具体的实现就不难推导了。

当需要维护位运算时,不妨尝试二进制拆分这条路径。

CF915E

动态开点线段树小题。
数据有点坑人。

7.20 - 7.30

吃数论。

7.31

做点大模拟折磨下自己。

P1058

题面浅显易懂。

推导最终立体图的行数和列数计算公式。
推导将某一行某一列某一高度的立方体坐标转化成立体图左上角坐标的计算公式。
考虑画立方体的顺序:(在立体图的视角下)从后往前、从左往右、从下往上画。
画立方体。
输出。

比较简单。

月底总结

做了大半个月的线段树,学了小半个月的数论。
7.20 得知自己已被严肃录取。综合分 \(95.8125/100 \text{pts}\),严肃位列第一。
八月初就要去学校集训咯。

posted @ 2026-07-08 12:28  ZephyriaNahida  阅读(24)  评论(0)    收藏  举报