P6327 题解:区间加,其实是在转圈

区间加,求区间 sin⁡\sin 和。只存 sin⁡\sin 不够,还得带上 cos⁡\cos。

目录
  1. 把区间加法看成旋转
  2. 懒标记只记角度增量
  3. 代码

题目链接。每次给一段数都加上 dd,或者查询这一段的 ∑sin⁡ai\sum\sin a_i,角度按弧度计算。

只存正弦和无法更新:sin⁡(a+d)\sin(a+d) 还依赖 cos⁡a\cos a,所以节点要把两者一起存。

把区间加法看成旋转

每个角度对应向量 (cos⁡a,sin⁡a)(\cos a,\sin a)。加上 dd,由两角和公式:

sin⁡(a+d)=sin⁡acos⁡d+cos⁡asin⁡d,cos⁡(a+d)=cos⁡acos⁡d−sin⁡asin⁡d.\begin{aligned} \sin(a+d)&=\sin a\cos d+\cos a\sin d,\\ \cos(a+d)&=\cos a\cos d-\sin a\sin d. \end{aligned}

对整个区间求和,记 S=∑sin⁡aiS=\sum\sin a_i、C=∑cos⁡aiC=\sum\cos a_i,就得到合向量的旋转:

S′=Scos⁡d+Csin⁡d,C′=Ccos⁡d−Ssin⁡d.\begin{aligned} S'&=S\cos d+C\sin d,\\ C'&=C\cos d-S\sin d. \end{aligned}

合并孩子时,SS、CC 分别相加,不需要区间长度。

懒标记只记角度增量

连续旋转 d1d_1、d2d_2,等于旋转 d1+d2d_1+d_2,标记直接相加。下传给两个孩子后,清空父标记。

代码

#include <cmath>
struct Node {
double s = 0, c = 0;
long long lazy = 0;
};
void apply(Node& node, long long d) {
double sn = std::sin(d), cs = std::cos(d);
double s = node.s, c = node.c;
node.s = s * cs + c * sn;
node.c = c * cs - s * sn;
node.lazy += d;
}
Node merge(Node left, Node right) {
return {left.s + right.s, left.c + right.c, 0};
}

更新两项时都要使用旧的 SS、CC,不能先覆盖 node.s,再拿新值算 node.c。查询输出按题目要求保留一位小数;累计角度用 long long,和与三角函数使用 double。

建树 O(n)O(n),单次修改、查询 O(log⁡n)O(\log n),空间 O(n)O(n)。

本文原发表于洛谷专栏。