[NOIP 2022] T4 比赛 题解

· · 题解

本题解为线段树维护区间历史版本和做法,讲解自认为较详细。

题意

给定 a_{1,\dots,n}, b_{1,\dots,n},以及 m 个询问, 每次询问给出 l, r 求:

\sum_{p=l}^{r}\sum_{q=p}^{r}(\max_{i=p}^{q}a_i) \times (\max_{i=p}^{q}b_i)

数据范围:n,m \leq 2.5 \times 10^5

题解

52 pts 做法

发现 n \times m10^6 级别,考虑接近 O(nm) 的做法。

对于区间最大值求和问题,一种常见的方法是考虑每个数的贡献。
所以,令 apre_i 表示 1 \sim i 中最大的 j 满足 a_j \gt a_ibpre_i 同理。

依次考虑子区间右边界从 $l$ 到 $r$ 递增。 右边界 $q$ 确定时,为表示方便,令 $A_p = \max_{i=p}^{q}a_i, B_p = \max_{i=p}^{q}b_i$。 可以发现,当右边界 $q$ 扩展到 $k$ 时,$A_p$ 在 $1 \sim apre_k$ 处的值不变,在 $apre_k + 1 \sim k$ 处的值变为 $a_k$。$B_p$ 同理。 因此,我们用线段树维护序列 $A_p, B_p$ 和 $\sum_{p=l}^{q}A_p \times B_p$,支持区间赋值 $A_p, B_p$。 答案为 $\sum_{q=l}^{r}(\sum_{p=l}^{q}A_p \times B_p)$。 总时间复杂度为 $O(m n \log n)$,预期 52 pts。 ### 100 pts 做法 可以发现,当右边界 $q$ 扩展到 $k$ 时,序列的修改与询问的 $l$ 无关,且不会修改 $A_{k+1,\dots,n}, B_{k+1,\dots,n}$。 所以,右边界 $q$ 从 $1$ 开始还是从 $l$ 开始不会影响询问的答案。 由此可知,以下做法也是正确的: $A_p, B_p$ 初始值为 $0$。 令 $q$ 依次扩展到 $1 \sim n$,维护 $A_p, B_p, \sum A_p \times B_p$。 然后将所有 $r \ge q$ 的询问的答案增加 $\sum_{p=l}^{r}A_p \times B_p$。 因此,每一个询问的答案是 $r = q$ 时 $\sum_{p=l}^{r}A_p \times B_p$ 的历史版本和。 --- 以下讲解如何维护区间历史版本和。 对于普通的区间历史版本和,2016 年集训队论文中有介绍。 ![](https://cdn.luogu.com.cn/upload/image_hosting/khxm4021.png) 类比着做,令 $S_i = A_i \times B_i$,设 $Ans_i$ 为历史版本和,同时我们让 $gt$ 为当前结束的时间(二者均不包含当前操作)。同样,定义 $C_i$,每一时刻结束都让 $C_i = Ans_i - gt \cdot S_i$。 令 $x'$ 表示修改前的 $x$ 信息,$\Delta x$ 表示修改前后 $x$ 的变化量。 $$\begin{aligned}\Delta C_i &= \Delta Ans_i - \Delta(gt \cdot S_i) \\ &= \Delta gt \cdot S_i' - (gt' + \Delta gt)\cdot(S_i' + \Delta S_i) + gt' \cdot S_i' \\ &= -\Delta gt \cdot \Delta S_i -gt' \cdot \Delta S_i \\ &= -(gt' + \Delta gt) \cdot \Delta S_i \\ &= -gt \cdot \Delta S_i \end{aligned}$$ 维护线段树标记逻辑很麻烦,我们考虑借助矩阵理解。 先确定状态矩阵(行向量)如下: $$\begin{bmatrix} \sum S_i & \sum A_i & \sum B_i & len & \sum C_i\end{bmatrix}$$ 其中 $len$ 为当前区间的长度。 可得转移矩阵: $$\begin{bmatrix} [a=b=0] & 0 & 0 & 0 & c \\ [a=0]b & [a=0] & 0 & 0 & ca \\ [b=0]a & 0 & [b=0] & 0 & cb \\ a \cdot b & a & b & 1 & cl \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix}$$ 其中 $a$ 非零时表示将区间中所有 $A_i$ 赋值为 $a$,$b$ 同理,$c, ca, cb, cl$ 仅供计算。 该矩阵中左上 $4 \times 4$ 的部分和 $52$ pts 的标记是等价的,结合实际意义维护即可,而 $\sum C_i$ 我们选择用矩阵维护。 这里是我对该标记的实现: ```cpp struct Tag { ull a, b; ull c, ca, cb, cl; inline bool empty() const { return !(a || b || c || ca || cb || cl); } inline void clear() { a = b = 0; c = ca = cb = cl = 0; } inline void add(const Tag &t) { if (this->empty()) return (void) (*this = t); // matrix 1 const ull a1s = a ? 0 : b, b1s = b ? 0 : a; const ull a1a = a ? 0 : 1, b1b = b ? 0 : 1; const ull l1s = a*b, l1a = a, l1b = b; const ull s1c = c, a1c = ca, b1c = cb, l1c = cl; // matrix 2 const ull s2c = t.c, a2c = t.ca, b2c = t.cb, l2c = t.cl; // matrix result const ull sc = s1c*1; const ull ac = a1s*s2c + a1a*a2c + a1c*1; const ull bc = b1s*s2c + b1b*b2c + b1c*1; const ull lc = l1s*s2c + l1a*a2c + l1b*b2c + 1*l2c + l1c*1; // assign c = sc, ca = ac, cb = bc, cl = lc; if (t.a) a = t.a; if (t.b) b = t.b; } }; ``` 其中 `add` 方法为合并两个标记(转移矩阵)。 P.S. `add` 方法的代码复杂程度较高,建议以可读性为重点,相信现代编译器是完全能够优化这简单的逻辑的。 将标记应用于线段树上的节点: ```cpp struct Node { ull s, sa, sb, c; Tag tag; } tree[N*4+10]; inline void set_val(int p, int l, int r, const Tag &tag) { Node &self = tree[p]; const ull len = r - l + 1; const ull s = self.s, sa = self.sa, sb = self.sb; if (tag.a && tag.b) { self.s = tag.a * tag.b * len; self.sa = tag.a * len; self.sb = tag.b * len; } else if (tag.a) { self.s = tag.a * sb; self.sa = tag.a * len; } else if (tag.b) { self.s = tag.b * sa; self.sb = tag.b * len; } self.c += tag.c * s + tag.ca * sa + tag.cb * sb + tag.cl * len; self.tag.add(tag); // if (l < r) push_down(p, l, r); // tree[p].tag = tag; } ``` P.S. 这里有个调试的小技巧,因为合并两个标记很难调试,所以调试时通过再次下传标记避免合并,从而方便其它部分的调试。不过这会导致线段树时间复杂度 $O(n^2)$,但调试时可以接受。 当我们把一段区间的 $A_i$ 赋值为 $v$ 时,以上矩阵 $a = v, b = 0$。 $\sum C_i$ 可由 $\Delta C_i = -gt \cdot \Delta S_i$ 推导,得到 $c = gt, ca = 0, cb = -v \cdot gt, cl = 0$。 同理得把 $B_i$ 赋值为 $v$ 时,$a = 0, b = v, c = gt, ca = -v \cdot gt, cb = 0, cl = 0$。 所以扩展右边界时的代码如下: ```cpp ++cntR; // 扩展右边界 ull a = arr[cntR].val, b = brr[cntR].val, gt = cntR; update(1, 1, n, arr[cntR].pre + 1, cntR, Tag { a, 0, gt, 0, -gt*a, 0 }); update(1, 1, n, brr[cntR].pre + 1, cntR, Tag { 0, b, gt, -gt*b, 0, 0 }); ``` 对于查询,注意 $Ans_i$ 是不包含当前操作的,所以查询的答案为 $\sum_{i=l}^{r} C_i + (gt + 1) \cdot S_i$。 总时间复杂度 $O(n \log n)$,预期 100 pts。 [完整代码](https://www.luogu.com.cn/paste/fm6c7wd4)。