Same Sum
Same Sum
(线段树 + 双哈希)
1. 问题重述
给定一个长度为 \(n\) 的整数序列,支持两种操作:
- 区间加:将 \([L, R]\) 内所有元素加上 \(v\)。
- 区间查询:判断 \([L, R]\)(长度为偶数)能否将内部所有数两两配对,使得每对的和相等。
2. 配对条件的数学转化
设区间长度为 \(m\)(偶数),若能够配对,令每对的和为公共和 \(S\)。
2.1 总和条件
因为共有 \(m/2\) 对,所以区间总和满足:
等价变形为:
2.2 最小最大值条件(构造基础)
在任意合法配对方案中,区间最小值(记作 \(\min\))必须与最大值(记作 \(\max\))配对。
反证思路:若 \(\min\) 未与 \(\max\) 配对,则 \(\min\) 会与一个小于 \(\max\) 的数配对,导致该对和小于 \((\min + \max)\);而 \(\max\) 会与一个大于 \(\min\) 的数配对,导致该对和大于 \((\min + \max)\),矛盾。
因此,公共和 \(S\) 必定满足:
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_+\) 必须等于 \(H_+'\),即必须满足:
反过来,在模意义下如果等式成立,就可以认为多重集对称(哈希碰撞概率极低)。
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 查询(配对判定)
- 调用
query获取区间信息res。 - 计算公共和 \(S = \text{res.mn} + \text{res.mx}\)。
- 总和校验:若 \(\text{res.sum} \times 2 \ne (R - L + 1) \times S\),直接输出 NO。
- 哈希校验(双模):
对于 \(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\)。
- 两组校验均通过输出 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;
}

浙公网安备 33010602011771号