Same Sum

Same Sum

(线段树 + 双哈希)

1. 问题重述

给定一个长度为 \(n\) 的整数序列,支持两种操作:

  • 区间加:将 \([L, R]\) 内所有元素加上 \(v\)。
  • 区间查询:判断 \([L, R]\)(长度为偶数)能否将内部所有数两两配对,使得每对的和相等。

2. 配对条件的数学转化

设区间长度为 \(m\)(偶数),若能够配对,令每对的和为公共和 \(S\)。

2.1 总和条件

因为共有 \(m/2\) 对,所以区间总和满足:

\[sum = \frac{m}{2} \cdot S \]

等价变形为:

\[2 \cdot sum = m \cdot S \]

2.2 最小最大值条件(构造基础)

在任意合法配对方案中,区间最小值(记作 \(\min\))必须与最大值(记作 \(\max\))配对。

反证思路:若 \(\min\) 未与 \(\max\) 配对,则 \(\min\) 会与一个小于 \(\max\) 的数配对,导致该对和小于 \((\min + \max)\);而 \(\max\) 会与一个大于 \(\min\) 的数配对,导致该对和大于 \((\min + \max)\),矛盾。

因此,公共和 \(S\) 必定满足:

\[S = \min + \max \]

2.3 对称性条件(充要条件)

在满足上述总和条件后,配对存在当且仅当多重集关于 \(S/2\) 对称。
即对于任意数值 \(x\),它在区间中出现的次数,必须等于 \(S - x\) 出现的次数。

原因:每个 \(x\) 只能与唯一确定的 \(S - x\) 配对,如果两者出现次数不等,必然有数落单,无法全配对。

3. 哈希验证对称性(多项式哈希)

由于区间长度和值域都很大,不能逐一统计每个数的出现频次。这里采用多项式哈希来快速比较多重集是否对称。

3.1 定义哈希函数

选取大质数 \(MOD\) 和底数 \(BASE\)(\(BASE\) 与 \(MOD\) 互质)。定义:

  • 正向哈希:

    \[H_+ = \sum_{i} BASE^{a_i} \pmod{MOD} \]

  • 负向哈希:

    \[H_- = \sum_{i} BASE^{-a_i} \pmod{MOD} \]

其中 \(BASE^{-a_i}\) 表示 \(BASE^{a_i}\) 在模 \(MOD\) 下的乘法逆元。

3.2 对称性的哈希等价条件

如果多重集关于 \(S/2\) 对称,那么将每个 \(a_i\) 替换为 \(S - a_i\) 后,原集合不变。替换后的 \(H_+\) 变为:

\[H_+' = \sum_{i} BASE^{S - a_i} = BASE^{S} \cdot \sum_{i} BASE^{-a_i} = BASE^{S} \cdot H_- \]

因为原集合对称,所以 \(H_+\) 必须等于 \(H_+'\),即必须满足:

\[H_+ \equiv BASE^{S} \cdot H_- \pmod{MOD} \]

反过来,在模意义下如果等式成立,就可以认为多重集对称(哈希碰撞概率极低)。

3.3 双哈希(Double Hashing)

为了彻底避免被构造数据卡掉(哈希碰撞),使用两组独立的模数和底数(比如 \(10^9+7\) 与 \(10^9+9\))。
只有两个模数下都满足上述等式,才判定为 YES。碰撞概率降至约 \(10^{-18}\) 级别,工程上视为绝对安全。

4. 算法完整流程

4.1 预处理

  • 预处理两个模数下的幂表 \(BASE_k^i\) 和 \(BASE_k^{-i}\),范围覆盖值域上限 \(2 \times 10^5\)。
  • 读入初始数组,建树。

4.2 处理类型 1 查询(区间加)

调用 update(1, 1, n, L, R, v)。

4.3 处理类型 2 查询(配对判定)

  1. 调用 query 获取区间信息 res。
  2. 计算公共和 \(S = \text{res.mn} + \text{res.mx}\)。
  3. 总和校验:若 \(\text{res.sum} \times 2 \ne (R - L + 1) \times S\),直接输出 NO。
  4. 哈希校验(双模):
    对于 \(k = 0, 1\),检查 \(\text{res.sp}[k] \equiv BASE_k^{S} \cdot \text{res.sn}[k] \pmod{MOD_k}\)。

    注意:\(S\) 可能超过预处理范围(因为 \(v\) 累加可能超过 \(2\times10^5\)),此时直接用快速幂计算 \(BASE_k^S\)。

  5. 两组校验均通过输出 YES,否则输出 NO。

5. 为什么双哈希是必要的

单模数哈希存在理论上的碰撞风险。
虽然概率约为 \(1 / MOD\)(约 \(10^{-9}\)),但若出题人刻意构造数据,可能利用多项式同余制造碰撞,导致程序输出错误。
采用两个不同的大质数作为模数,碰撞概率降至 \(10^{-18}\) 级别,足以应对各种恶意数据,保证答案正确性。

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

// -------------------------- 双哈希参数配置 --------------------------
// 选用两个大质数作为模数,碰撞概率降至 1/(1e18),几乎不可能被卡
const int MOD1 = 1000000007;
const int MOD2 = 1000000009;
// 底数(基数),必须与模数互质(这样才能求逆元),这里选取两个大奇数
const int BASE1 = 1000003;
const int BASE2 = 1000033;
// 值域上限(初始值 ai 和单次加数 v 均不超过 200000)
const int MAXV = 200000;
// 线段树节点数上限(4 * n)
const int MAXN = 200005;

// -------------------------- 预计算幂表 --------------------------
// pow1[i]     = BASE1^i % MOD1
// pow1_inv[i] = BASE1^(-i) % MOD1  (即 BASE1^i 的逆元)
ll pow1[MAXV + 2], pow1_inv[MAXV + 2];
ll pow2[MAXV + 2], pow2_inv[MAXV + 2];

// 快速幂:计算 a^b % mod
ll mod_pow(ll a, ll b, ll mod) {
    ll res = 1;
    while (b) {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}

// 预计算两个模数下的正幂和负幂(逆元幂)表
void init_pow() {
    // 费马小定理求逆元:inv = base^(mod-2) % mod
    ll inv1 = mod_pow(BASE1, MOD1 - 2, MOD1);
    ll inv2 = mod_pow(BASE2, MOD2 - 2, MOD2);
    
    pow1[0] = pow1_inv[0] = 1;
    pow2[0] = pow2_inv[0] = 1;
    for (int i = 1; i <= MAXV; ++i) {
        pow1[i] = pow1[i - 1] * BASE1 % MOD1;
        pow1_inv[i] = pow1_inv[i - 1] * inv1 % MOD1;
        pow2[i] = pow2[i - 1] * BASE2 % MOD2;
        pow2_inv[i] = pow2_inv[i - 1] * inv2 % MOD2;
    }
}

// -------------------------- 线段树节点结构体 --------------------------
/*
  每个节点维护 7 个统计量 + 5 个懒标记:
  统计量:
    minv, maxv : 区间最小值、最大值(用于计算配对公共和 S = min + max)
    sum        : 区间和(用于快速剪枝:2*sum == len*S)
    sum_pos1/2 : 正哈希 sum(BASE^a_i)   (两个模数分别存)
    sum_neg1/2 : 负哈希 sum(BASE^(-a_i))(两个模数分别存)
    len        : 区间长度
  懒标记:
    lazy_add        : 区间加的偏移量(影响 min/max/sum)
    lazy_mul_pos1/2 : 正哈希乘数(影响 sum_pos1/2)
    lazy_mul_neg1/2 : 负哈希乘数(影响 sum_neg1/2)
*/
struct Node {
    ll minv, maxv, sum;
    ll sum_pos1, sum_neg1;
    ll sum_pos2, sum_neg2;
    ll lazy_add;
    ll lazy_mul_pos1, lazy_mul_neg1;
    ll lazy_mul_pos2, lazy_mul_neg2;
    int len;
    
    // 构造函数:初始化所有数值为 0,乘法懒标记初始化为 1(单位元)
    Node() : minv(0), maxv(0), sum(0),
             sum_pos1(0), sum_neg1(0), sum_pos2(0), sum_neg2(0),
             lazy_add(0), lazy_mul_pos1(1), lazy_mul_neg1(1),
             lazy_mul_pos2(1), lazy_mul_neg2(1), len(0) {}
} tree[4 * MAXN];

int a[MAXN]; // 初始数组

// -------------------------- 建树 --------------------------
void build(int node, int l, int r) {
    if (l == r) {
        // 叶子节点:直接根据 a[l] 初始化所有值
        tree[node].minv = tree[node].maxv = a[l];
        tree[node].sum = a[l];
        tree[node].sum_pos1 = pow1[a[l]];        // BASE1^a[l]
        tree[node].sum_neg1 = pow1_inv[a[l]];    // BASE1^(-a[l])
        tree[node].sum_pos2 = pow2[a[l]];        // BASE2^a[l]
        tree[node].sum_neg2 = pow2_inv[a[l]];    // BASE2^(-a[l])
        tree[node].len = 1;
        return;
    }
    int mid = (l + r) / 2;
    build(node * 2, l, mid);
    build(node * 2 + 1, mid + 1, r);
    
    // 合并(pull up):用左右孩子更新当前节点
    tree[node].minv = min(tree[node * 2].minv, tree[node * 2 + 1].minv);
    tree[node].maxv = max(tree[node * 2].maxv, tree[node * 2 + 1].maxv);
    tree[node].sum = tree[node * 2].sum + tree[node * 2 + 1].sum;
    // 哈希值合并:直接相加再取模(因为哈希是线性求和)
    tree[node].sum_pos1 = (tree[node * 2].sum_pos1 + tree[node * 2 + 1].sum_pos1) % MOD1;
    tree[node].sum_neg1 = (tree[node * 2].sum_neg1 + tree[node * 2 + 1].sum_neg1) % MOD1;
    tree[node].sum_pos2 = (tree[node * 2].sum_pos2 + tree[node * 2 + 1].sum_pos2) % MOD2;
    tree[node].sum_neg2 = (tree[node * 2].sum_neg2 + tree[node * 2 + 1].sum_neg2) % MOD2;
    tree[node].len = tree[node * 2].len + tree[node * 2 + 1].len;
}

// -------------------------- 应用更新(核心懒操作) --------------------------
/*
  作用:将节点 node 代表的整段区间**每个数都加上 add**。
  参数:
    add        : 区间加的值 v
    mul_pos1   : 即 pow1[v]  = BASE1^v   (正哈希乘数,模 MOD1)
    mul_neg1   : 即 pow1_inv[v] = BASE1^(-v)(负哈希乘数,模 MOD1)
    mul_pos2   : 即 pow2[v]  = BASE2^v   (正哈希乘数,模 MOD2)
    mul_neg2   : 即 pow2_inv[v] = BASE2^(-v)(负哈希乘数,模 MOD2)
*/
void apply(int node, ll add, ll mul_pos1, ll mul_neg1, ll mul_pos2, ll mul_neg2) {
    // 1. 更新常规数值(纯加法)
    tree[node].minv += add;
    tree[node].maxv += add;
    tree[node].sum += add * tree[node].len;
    
    // 2. 更新哈希值(纯乘法)
    // 原理:a_i -> a_i + v  =>  BASE^(a_i+v) = BASE^v * BASE^(a_i)
    //      所以整体哈希值只需要乘以 BASE^v
    tree[node].sum_pos1 = tree[node].sum_pos1 * mul_pos1 % MOD1;
    tree[node].sum_neg1 = tree[node].sum_neg1 * mul_neg1 % MOD1;
    tree[node].sum_pos2 = tree[node].sum_pos2 * mul_pos2 % MOD2;
    tree[node].sum_neg2 = tree[node].sum_neg2 * mul_neg2 % MOD2;
    
    // 3. 累加懒标记(注意乘法标记是“累乘”)
    //    因为两次区间加 v1, v2 后,总乘数是 BASE^(v1+v2)
    tree[node].lazy_add += add;
    tree[node].lazy_mul_pos1 = tree[node].lazy_mul_pos1 * mul_pos1 % MOD1;
    tree[node].lazy_mul_neg1 = tree[node].lazy_mul_neg1 * mul_neg1 % MOD1;
    tree[node].lazy_mul_pos2 = tree[node].lazy_mul_pos2 * mul_pos2 % MOD2;
    tree[node].lazy_mul_neg2 = tree[node].lazy_mul_neg2 * mul_neg2 % MOD2;
}

// -------------------------- 下传懒标记 --------------------------
// 在递归访问子节点前,必须将当前节点的懒标记下发给左右孩子
void push(int node) {
    // 如果存在任何标记(加法不为0 或 乘法不为1)
    if (tree[node].lazy_add != 0 || 
        tree[node].lazy_mul_pos1 != 1 || tree[node].lazy_mul_neg1 != 1 ||
        tree[node].lazy_mul_pos2 != 1 || tree[node].lazy_mul_neg2 != 1) {
        
        // 将当前节点的整套标记下发给左孩子和右孩子
        apply(node * 2, tree[node].lazy_add, 
              tree[node].lazy_mul_pos1, tree[node].lazy_mul_neg1,
              tree[node].lazy_mul_pos2, tree[node].lazy_mul_neg2);
        apply(node * 2 + 1, tree[node].lazy_add,
              tree[node].lazy_mul_pos1, tree[node].lazy_mul_neg1,
              tree[node].lazy_mul_pos2, tree[node].lazy_mul_neg2);
        
        // 清空当前节点的懒标记(恢复为单位元)
        tree[node].lazy_add = 0;
        tree[node].lazy_mul_pos1 = tree[node].lazy_mul_neg1 = 1;
        tree[node].lazy_mul_pos2 = tree[node].lazy_mul_neg2 = 1;
    }
}

// -------------------------- 区间加更新 --------------------------
void update(int node, int l, int r, int ql, int qr, int v) {
    // 完全覆盖:直接打懒标记,无需递归
    if (ql <= l && r <= qr) {
        apply(node, v, pow1[v], pow1_inv[v], pow2[v], pow2_inv[v]);
        return;
    }
    // 部分覆盖:先下传已有标记,确保孩子数据正确
    push(node);
    int mid = (l + r) / 2;
    if (ql <= mid) update(node * 2, l, mid, ql, qr, v);
    if (qr > mid) update(node * 2 + 1, mid + 1, r, ql, qr, v);
    
    // 回溯时重新合并(pull up)
    tree[node].minv = min(tree[node * 2].minv, tree[node * 2 + 1].minv);
    tree[node].maxv = max(tree[node * 2].maxv, tree[node * 2 + 1].maxv);
    tree[node].sum = tree[node * 2].sum + tree[node * 2 + 1].sum;
    tree[node].sum_pos1 = (tree[node * 2].sum_pos1 + tree[node * 2 + 1].sum_pos1) % MOD1;
    tree[node].sum_neg1 = (tree[node * 2].sum_neg1 + tree[node * 2 + 1].sum_neg1) % MOD1;
    tree[node].sum_pos2 = (tree[node * 2].sum_pos2 + tree[node * 2 + 1].sum_pos2) % MOD2;
    tree[node].sum_neg2 = (tree[node * 2].sum_neg2 + tree[node * 2 + 1].sum_neg2) % MOD2;
}

// -------------------------- 区间查询 --------------------------
// 返回一个完整的 Node 结构体,包含区间所有的统计信息
Node query(int node, int l, int r, int ql, int qr) {
    // 完全覆盖:直接返回当前节点
    if (ql <= l && r <= qr) {
        return tree[node];
    }
    // 查询前必须下传懒标记,确保子树数据是最新的
    push(node);
    int mid = (l + r) / 2;
    
    // 只在左子树或只在右子树
    if (qr <= mid) return query(node * 2, l, mid, ql, qr);
    if (ql > mid) return query(node * 2 + 1, mid + 1, r, ql, qr);
    
    // 跨越左右区间:分别查询并合并
    Node left = query(node * 2, l, mid, ql, qr);
    Node right = query(node * 2 + 1, mid + 1, r, ql, qr);
    
    Node res;
    res.minv = min(left.minv, right.minv);
    res.maxv = max(left.maxv, right.maxv);
    res.sum = left.sum + right.sum;
    res.sum_pos1 = (left.sum_pos1 + right.sum_pos1) % MOD1;
    res.sum_neg1 = (left.sum_neg1 + right.sum_neg1) % MOD1;
    res.sum_pos2 = (left.sum_pos2 + right.sum_pos2) % MOD2;
    res.sum_neg2 = (left.sum_neg2 + right.sum_neg2) % MOD2;
    res.len = left.len + right.len;   // 合并后的长度
    return res;
}

// -------------------------- 主程序入口 --------------------------
int main() {
    ios::sync_with_stdio(false);cin.tie(nullptr);
    init_pow();

    int n, q; cin >> n >> q;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    build(1, 1, n);

    while (q--) {
        int op, L, R, v; cin >> op;

        if (op == 1) {
            // 类型1:区间加
            cin >> L >> R >> v;
            update(1, 1, n, L, R, v);
        } else {
            // 类型2:判断区间能否配对
            cin >> L >> R;
            Node res = query(1, 1, n, L, R);

            // 计算配对的公共和 S = 最小值 + 最大值
            ll S = res.minv + res.maxv;
            int m = R - L + 1; // 区间长度(偶数)

            // 【必要条件 1】总和条件:2 * sum == m * S
            // 如果不等,绝对无法配对,直接输出 NO
            if (res.sum * 2 != (ll)res.len * S) {
                cout << "NO\n";
                continue;
            }

            // 【必要条件 2】双哈希对称性验证
            // 原理:若区间关于 S/2 对称,则 sum(BASE^a_i) == BASE^S * sum(BASE^(-a_i))
            // 使用两个模数同时验证,避免哈希碰撞
            ll lhs1 = res.sum_pos1;
            ll rhs1 = mod_pow(BASE1, S, MOD1) * res.sum_neg1 % MOD1;

            ll lhs2 = res.sum_pos2;
            ll rhs2 = mod_pow(BASE2, S, MOD2) * res.sum_neg2 % MOD2;

            // 注意:这里必须用 mod_pow 计算 BASE^S,
            // 因为 S 可能非常大(超过预计算的 MAXV=200000),不能用 pow1[S] 直接访问

            if (lhs1 == rhs1 && lhs2 == rhs2)
                cout << "YES\n";
            else
                cout << "NO\n";
        }
    }
    return 0;
}
posted @ 2026-09-08 19:34  thenrify  阅读(18)  评论(0)    收藏  举报