线段树 - 同时支持区间加和区间乘
同时支持区间加和区间乘需要两个懒标记
ai太笨了问不懂原理
所以说,只看实现
点击查看代码
class SegTree
{
private:
int n; ll mod;
vector<ll> tr;
vector<ll> lazy_mul;
vector<ll> lazy_add;
vector<ll> arr;
void push_up(int p)
{
tr[p] = (tr[p << 1] + tr[p << 1 | 1]) % mod;
}
void build(int p, int l, int r)
{
lazy_mul[p] = 1;
lazy_add[p] = 0;
if (l == r)
{
tr[p] = arr[l] % mod;
return;
}
int mid = l + (r - l >> 1);
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
push_up(p);
}
void apply_add(int p, int l, int r, ll k)
{
tr[p] = (tr[p] + k * (r - l + 1)) % mod;
lazy_add[p] = (lazy_add[p] + k) % mod;
}
void apply_mul(int p, int l, int r, ll k)
{
tr[p] = tr[p] * k % mod;
lazy_mul[p] = (lazy_mul[p] * k) % mod;
lazy_add[p] = (lazy_add[p] * k) % mod;
}
void push_down(int p, int l, int r)
{
int mid = l + (r - l >> 1);
if (lazy_mul[p] != 1)
{
apply_mul(p << 1, l, mid, lazy_mul[p]);
apply_mul(p << 1 | 1, mid + 1, r, lazy_mul[p]);
lazy_mul[p] = 1;
}
if (lazy_add[p] != 0)
{
apply_add(p << 1, l, mid, lazy_add[p]);
apply_add(p << 1 | 1, mid + 1, r, lazy_add[p]);
lazy_add[p] = 0;
}
}
void range_mul(int p, int l, int r, int ql, int qr, ll k)
{
if (ql <= l && r <= qr)
{
apply_mul(p, l, r, k);
return ;
}
push_down(p, l, r);
int mid = l + (r - l >> 1);
if (ql <= mid) range_mul(p << 1, l, mid, ql, qr, k);
if (qr > mid) range_mul(p << 1 | 1, mid + 1, r, ql, qr, k);
push_up(p);
}
void range_add(int p, int l, int r, int ql, int qr, ll k)
{
if (ql <= l && r <= qr)
{
apply_add(p, l, r, k);
return ;
}
push_down(p, l, r);
int mid = l + (r - l >> 1);
if (ql <= mid) range_add(p << 1, l, mid, ql, qr, k);
if (qr > mid) range_add(p << 1 | 1, mid + 1, r, ql, qr, k);
push_up(p);
}
ll range_sum(int p, int l, int r, int ql, int qr)
{
if (ql <= l && r <= qr)
{
return tr[p];
}
push_down(p, l, r);
int mid = l + (r - l >> 1);
ll res = 0;
if (ql <= mid) res = (res + range_sum(p << 1, l, mid, ql, qr)) % mod;
if (qr > mid) res = (res + range_sum(p << 1 | 1, mid + 1, r, ql, qr)) % mod;
return res;
}
public:
SegTree(const vector<ll>& original, ll mod_val) : arr(original), mod(mod_val)
{
n = arr.size() - 1;
tr.resize(4 * n + 4);
lazy_mul.resize(4 * n + 4, 1);
lazy_add.resize(4 * n + 4, 0);
build(1, 1, n);
}
void mul(int l, int r, ll k)
{
range_mul(1, 1, n, l, r, k % mod);
}
void add(int l, int r, ll k)
{
range_add(1, 1, n, l, r, k % mod);
}
ll query(int l, int r)
{
return range_sum(1, 1, n, l, r);
}
};
关键问题在于两次懒标记的实现上面。具体的原理还弄不明白,总之就记一个在push_down的时候先乘后加吧。
然后就是加的时候的懒标记不用标记乘的数组,乘的懒标记两个数组都要标记
DeepSeek给的数学解释
是完全看不懂的证明TAT
线段树同时支持区间加和区间乘的核心数学原理是:每个操作都可以视为一个仿射变换(一次函数),而仿射变换的复合结果仍然是仿射变换。通过维护一对参数 (mul, add),就能无歧义地合并任意顺序的加法和乘法操作。
1. 仿射变换形式
一个元素(如区间和、区间内的每个数)经过一次操作后,变化规律可写为:
\(f(x) = a \cdot x + b\)
- 区间加 \(k\):对应 \(a = 1, b = k\),即 \(f(x) = x + k\)。
- 区间乘 \(k\):对应 \(a = k, b = 0\),即 \(f(x) = k \cdot x\)。
对同一个元素连续施加多个操作,相当于将这些仿射变换复合。
2. 复合规则
设先执行 \(f_1(x) = a_1 x + b_1\),再执行 \(f_2(x) = a_2 x + b_2\),则复合结果为:
即复合后的变换参数为:
注意顺序:先应用 \(f_1\),后应用 \(f_2\),在标记合并时,新标记要放在前面(因为新操作在时间上后发生)。
3. 在线段树中的标记合并
节点上维护的懒标记 (mul, add) 表示“对当前区间内的每个数先乘 mul 再加 add”,即变换 \(T(x) = mul \cdot x + add\)。
- 新增一个区间加 \(k\):相当于施加 \(G(x) = x + k\)。复合规则(新操作后发生):\[T_{\text{new}}(x) = G(T(x)) = (x + k) \stackrel{?}{=} \text{不对} \]正确推导:原变换 \(T\) 先执行,新操作 \(G\) 后执行,所以复合为 \(G \circ T\):\[G(T(x)) = 1\cdot (mul\cdot x + add) + k = mul\cdot x + (add + k) \]因此 \(mul\) 不变,\(add \leftarrow add + k\)。
- 新增一个区间乘 \(k\):施加 \(H(x) = kx\)。复合 \(H \circ T\):\[H(T(x)) = k\cdot (mul\cdot x + add) = (k\cdot mul)\cdot x + (k\cdot add) \]因此 \(mul \leftarrow mul \cdot k\),\(add \leftarrow add \cdot k\)。
这正好对应了常见的标记更新代码。
4. 为什么不冲突?
因为仿射变换的复合是封闭且满足结合律的,无论接收到多少加法和乘法操作,最终结果都可以唯一地表示为一个仿射变换 \(x \mapsto Mx + A\)。因此,在线段树节点上只需要维护这一对参数,下传时按复合规则更新子节点的标记,就不会产生任何歧义或冲突。
若随意改变顺序(如先加后乘 vs 先乘后加),其复合结果不同,但只要固定一种顺序(通常选择“先乘后加”作为懒标记的语义),并让所有新操作都按这个顺序复合,就能保持一致。
5. 数学本质总结
所有加法和乘法操作构成一个仿射变换半群(实际上是群,若考虑乘数非零),参数对 \((a,b)\) 的复合运算 \((a_2,b_2) \circ (a_1,b_1) = (a_2 a_1,\ a_2 b_1 + b_2)\) 赋予了该集合一个幺半群结构。线段树的懒标记正是利用了这个代数结构,使得标记的合并与下传可以像函数复合一样安全、唯一地完成。
这一原理不仅适用于加法和乘法,也可以推广到其他满足封闭性的区间操作(如赋值、取最大值等),只需找到对应的代数结构即可。
浙公网安备 33010602011771号