线段树 - 同时支持区间加和区间乘

同时支持区间加和区间乘需要两个懒标记
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_2(f_1(x)) = a_2 (a_1 x + b_1) + b_2 = (a_2 a_1) x + (a_2 b_1 + b_2) \]

即复合后的变换参数为:

\[a = a_2 a_1,\qquad b = a_2 b_1 + 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)\) 赋予了该集合一个幺半群结构。线段树的懒标记正是利用了这个代数结构,使得标记的合并与下传可以像函数复合一样安全、唯一地完成。

这一原理不仅适用于加法和乘法,也可以推广到其他满足封闭性的区间操作(如赋值、取最大值等),只需找到对应的代数结构即可。

posted @ 2026-05-19 20:25  RonF02  阅读(3)  评论(0)    收藏  举报