# RGB 三元组求和(线段树 + 分治合并)

RGB 三元组求和(线段树 + 分治合并)

一、题目描述

给定一个长度为 N 的非负整数序列 A,以及一个由 R, G, B 组成的字符串 S。

需要处理 Q 次操作:

  • 1 i x:将 A[i] 修改为 x。
  • 2 l r:查询在区间 [l, r] 中,所有满足 l <= i < j < k <= r 且 S[i]='R', S[j]='G', S[k]='B' 的 A[i] * A[j] * A[k] 之和,结果对 998244353 取模。

对应题目:AtCoder 线段树分治应用题E

二、核心思路:线段树的分治与可合并性

这道题如果暴力做,每次查询都要枚举所有三元组,显然会超时。使用线段树维护区间信息是标准做法。

线段树本质上是一种分治结构,它的精髓在于:如果我们能高效地将左右两个子区间的答案合并(Merge)得到父区间的答案,那么就能在 O(log n) 的时间内完成更新和查询。

因此,我们的核心任务是:设计线段树节点需要维护哪些信息,才能支持这种合并?

2.1 节点信息设计

对于一个区间,我们需要维护以下 6 种信息(对应代码中的结构体):

  1. R:区间内所有 S[i]='R' 的 A[i] 之和。
  2. G:区间内所有 S[i]='G' 的 A[i] 之和。
  3. B:区间内所有 S[i]='B' 的 A[i] 之和。
  4. RG:区间内所有满足 i < j、S[i]='R'、S[j]='G' 的 A[i] * A[j] 之和。
  5. GB:区间内所有满足 i < j、S[i]='G'、S[j]='B' 的 A[i] * A[j] 之和。
  6. RGB:区间内所有满足 i < j < k、S[i]='R'、S[j]='G'、S[k]='B' 的 A[i] * A[j] * A[k] 之和。

三、Merge 函数的推导(分治合并)

假设我们已经有了左半区间 L 和右半区间 R 的信息,如何推导出合并后的大区间信息?

对于单元素信息,直接相加即可:

  • res.R = L.R + R.R
  • res.G = L.G + R.G
  • res.B = L.B + R.B

关键在于跨区间的贡献:

1. 合并 RG

一个 RG 对要么都在左边,要么都在右边,要么R在左边、G在右边。

  • 跨区间的贡献为:左区间的 R 之和乘上右区间的 G 之和,即 L.R * R.G。
  • res.RG = L.RG + R.RG + L.R * R.G

2. 合并 GB

同理,跨区间的贡献为:左区间的 G 之和乘上右区间的 B 之和。

  • res.GB = L.GB + R.GB + L.G * R.B

3. 合并 RGB

现在把左儿子区间 [l, mid] 记作 L,右儿子区间 [mid+1, r] 记作 R。合并后的区间是 [l, r]。

合并后的 RG、GB、RGB 里的每一项,按下标分布只有两种可能:

全部落在左区间 → 左儿子已经统计过

全部落在右区间 → 右儿子已经统计过

跨区间 → 左边取一部分,右边取一部分,左区间的下标一定小于右区间的下标
跨区间:分为两种切法:

  • R 在左,G 和 B 在右:贡献为 L.R * R.GB
  • R 和 G 在左,B 在右:贡献为 L.RG * R.B

所以合并公式一定是:

合并后的量 = 左儿子的量 + 右儿子的量 + 跨区间的新贡献
`res.RGB = L.RGB + R.RGB + L.R * R.GB + L.RG * R.B`

四、AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N = 2e5 + 5;
const ll MOD = 998244353;
ll a[N];

struct node
{
    int l = 0, r = 0;
    ll R = 0, G = 0, B = 0;
    ll RG = 0, GB = 0;
    ll RGB = 0;
} seg[N << 2];

string s;

// 核心分治合并函数
node merge(node L, node R)
{
    node res;
    res.l = L.l, res.r = R.r;
    res.R = (L.R + R.R) % MOD;
    res.G = (L.G + R.G) % MOD; 
    res.B = (L.B + R.B) % MOD;
    
    res.RG = (L.RG + R.RG + L.R * R.G) % MOD;
    res.GB = (L.GB + R.GB + L.G * R.B) % MOD;
    
    // RGB 合并:左RGB + 右RGB + (左R * 右GB) + (左RG * 右B)
    res.RGB = (L.RGB + R.RGB + L.R * R.GB + L.RG * R.B) % MOD;
    
    return res;
}

void push_up(int p)
{
    seg[p] = merge(seg[p << 1], seg[p << 1 | 1]);
}

void build(int p, int L, int R)
{
    seg[p].l = L, seg[p].r = R;
    if (L == R) 
    {
        if (s[L] == 'R') seg[p].R = a[L] % MOD;
        else if (s[L] == 'G') seg[p].G = a[L] % MOD;
        else seg[p].B = a[L] % MOD;
        return;
    }
    int mid = (L + R) >> 1;
    build(p << 1, L, mid);
    build(p << 1 | 1, mid + 1, R);
    push_up(p);
}

node query(int p, int L, int R)
{
    int l = seg[p].l, r = seg[p].r;
    if (L <= l && r <= R) return seg[p];
    
    int mid = (l + r) >> 1;
    node resl, resr;
    // 注意:如果查询区间只包含一侧,直接返回那一侧的结果,不需要 merge 空节点
    if (L <= mid && R <= mid) return query(p << 1, L, R);
    if (L > mid && R > mid) return query(p << 1 | 1, L, R);
    
    resl = query(p << 1, L, R);
    resr = query(p << 1 | 1, L, R);
    return merge(resl, resr);
}

void update(int p, int pos, ll k)
{
    int l = seg[p].l, r = seg[p].r;
    if (l == r)
    {
        // 清空原来的值,防止多次修改叠加
        seg[p].R = seg[p].G = seg[p].B = 0;
        if (s[l] == 'R') seg[p].R = k;
        else if (s[l] == 'G') seg[p].G = k;
        else seg[p].B = k;
        return;
    }
    int mid = (l + r) >> 1;
    if (pos <= mid) update(p << 1, pos, k);
    else update(p << 1 | 1, pos, k);
    push_up(p);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, q;
    cin >> n >> q;
    for (int i = 1; i <= n; i ++) cin >> a[i];
    cin >> s;
    s = " " + s; // 1-indexed
    build(1, 1, n);
    
    int op;
    while (q --)
    {
        cin >> op;
        if (op == 1)
        {
            int i;
            ll x;
            cin >> i >> x;
            update(1, i, x % MOD);
            a[i] = x % MOD;
        }
        else 
        {
            int l, r;
            cin >> l >> r;
            node res = query(1, l, r);
            cout << res.RGB << '\n';
        }
    }
    return 0;
}

五、复杂度

  • 时间复杂度:O((N + Q) log N)。建树 O(N),每次修改和查询都是 O(log N),每次合并操作是 O(1) 常数时间。
  • 空间复杂度:O(N),线段树数组开销。

六、注意点点

  1. 查询函数query不能直接void查询:在查询的时候可能目标区间由多个子区间组成,这些区间在计算横跨左右的时候,不能直接通过加减等运算在次计算跨越左右,得用node类型存储出来,最后最后再merge合并
  2. int溢出:L.R * R.G 这种操作,即使 L.R 和 R.G 都模过 MOD,两个 10^9 级别的数相乘会爆 int,要用 long long。

七、总结

这道题是“线段树分治思想”的更加明显的体现。线段树维护的不仅是数据,更是状态之间的转移关系。通过定义 R, G, B, RG, GB, RGB 六种递进状态,我们将原本需要 O(N^3) 暴力枚举的复杂组合问题,转化为在合并区间时的常数级状态转移,极大地降低了复杂度。

posted @ 2026-11-03 11:52  leaderling  阅读(7)  评论(0)    收藏  举报