1 条题解
-
0
NWWSUMA 问题详细题解
问题分析
计算序列所有连续子段的最小公倍数之和:
NWWSUMA(b₁,...,bₖ) = Σᵢ₌₁ᵏ Σⱼ₌ᵢᵏ NWW(bᵢ,...,bⱼ)核心挑战
- 数据规模:n ≤ 1e5, q ≤ 3e5,暴力O(n²)不可行
- 查询处理:需要高效回答区间查询
- 模运算:结果对M取模
算法核心思想
1. 关键洞察:质因数分解 + 单调性
- LCM性质:LCM由各质因子的最大指数决定
- 单调性:固定右端点r,向左移动l时,LCM单调不减
- 跳跃变化:LCM只在某些关键点发生变化(当某个质因子的最大指数变化时)
2. 整体架构:离线处理 + 扫描线
// 算法框架 1. 预处理质因数分解 2. 按右端点分组查询(离线) 3. 从左到右扫描,维护每个质因子的控制区间 4. 用特殊线段树维护LCM值和历史信息 5. 回答查询关键技术详解
1. 历史版本线段树
设计目标:同时维护当前值和历史累计和
class sgt_t { vector<int> sum, hissum, tag1, tag2; // sum: 当前LCM值 // hissum: 历史LCM和 // tag1: 乘法标记(更新当前值) // tag2: 历史累加标记(更新历史值) };核心操作原理:
cover(x, t1, t2)执行:hissum[x] += sum[x] × t2(累加历史)sum[x] ×= t1(更新当前)- 合并标记:
tag2 = tag1×t2 + tag2,tag1 ×= t1
为什么这样设计?
- 需要同时支持:区间乘法(更新LCM)和区间历史累加
- 传统线段树难以同时处理这两种操作
- 通过巧妙的标记合并,实现高效更新
2. 质因数控制区间维护
问题:对于质因子p,如何快速找到需要更新的区间?
解决方案:单调栈
for (auto &[p, k] : prime_factors) { int last_pos = i, last_val = 0; // 弹出栈中指数≤k的元素 while (!prime_map[p].empty() && prime_map[p].back().second <= k) { auto [pos, exp] = prime_map[p].back(); // 更新区间 [pos+1, last_pos] tree.modify(pos + 1, last_pos, fastpow(p, k - last_val), 0); last_pos = pos; last_val = exp; prime_map[p].pop_back(); } // 处理剩余区间 if (last_val < k) { int L = prime_map[p].empty() ? 0 : prime_map[p].back().first + 1; tree.modify(L, last_pos, fastpow(p, k - last_val), 0); } prime_map[p].emplace_back(i, k); }单调栈的作用:
- 维护每个质因子的"控制区间"
- 栈中元素按位置递减,指数递增
- 新元素入栈时,弹出被"统治"的旧区间
3. 子段和的高效计算
关键技巧:扫描线 + 历史累加
// 处理到位置i时 tree.modify(0, i, 1, 1); // 关键操作! // 这相当于: for (int l = 0; l <= i; l++) { hissum[l] += sum[l]; // 累加以l开头,i结尾的子段LCM }为什么有效?
sum[l]存储的是NWW(a[l], ..., a[i])- 每次执行
modify(0, i, 1, 1)就累加了所有以i结尾的子段 - 最终
hissum[l]存储了所有以l开头的子段LCM和
算法流程逐步分析
步骤1:预处理
sieve_t sieve_worker(max_a + 5); // 线性筛 vector<vector<pair<int, int>>> queries(n); // 按右端点分组步骤2:扫描处理
对于每个位置 i = 0 to n-1:
2.1 处理质因数
auto factors = sieve_worker.factorize(a[i]); for (auto &[p, k] : factors) { // 更新质因子p的控制区间 update_prime_interval(p, k, i); }2.2 累加子段和
tree.modify(0, i, 1, 1); // 累加以i结尾的所有子段2.3 回答查询
for (auto &[l, id] : queries[i]) { answer[id] = tree.query(l, i); // 查询[l,i]的历史和 }步骤3:输出结果
for (int ans : answer) printf("%d\n", ans);正确性证明
定理1:LCM维护正确性
对于固定右端点i,经过质因数更新后:
sum[l] = NWW(a[l], a[l+1], ..., a[i]) 对所有l ≤ i证明:
- 每个质因子p独立处理
- 单调栈确保每个区间获得正确的最大指数
- 线段树乘法更新正确应用质因子幂次
定理2:历史和正确性
处理完位置i后:
hissum[l] = Σ_{j=l}^{i} NWW(a[l], ..., a[j]) 对所有l ≤ i证明:
- 每次
modify(0, i, 1, 1)累加当前所有sum值 - 由数学归纳法可得正确性
定理3:查询正确性
查询[l, r]的答案 =
tree.query(l, r)证明:- 由定理2,
hissum[l]包含以l开头的所有子段到r - 线段树区间查询正确求和
复杂度分析
时间复杂度
- 质因数分解:O(n × log(max_a))
- 单调栈操作:每个质因子每个位置入栈出栈一次,O(n × log(max_a))
- 线段树操作:每次更新O(log n),总O(n × log n × log(max_a))
- 查询处理:O(q × log n)
总计:O((n × log n × log(max_a)) + q × log n)
空间复杂度
- 线段树:O(n)
- 质因数存储:O(n × log(max_a))
- 查询存储:O(n + q)
样例详细验证
输入
n=4, a=[6,4,1,3] 查询: (3,4), (1,2), (1,4), (2,2)处理过程
i=0 (a₀=6)
- 质因数:2¹, 3¹
- 更新区间:[0,0] × 2¹, [0,0] × 3¹
- 累加:hissum[0] += 6
- sum = [6,1,1,1], hissum = [6,0,0,0]
i=1 (a₁=4)
- 质因数:2²
- 弹出栈:无(2² > 2¹)
- 更新区间:[0,1] × 2¹ (2²÷2¹)
- 累加:hissum[0] += 12, hissum[1] += 4
- sum = [12,4,1,1], hissum = [18,4,0,0]
i=2 (a₂=1)
- 无质因数
- 累加:hissum[0] += 12, hissum[1] += 4, hissum[2] += 1
- sum = [12,4,1,1], hissum = [30,8,1,0]
i=3 (a₃=3)
- 质因数:3¹
- 更新区间:[0,3] × 3¹(相对之前)
- 累加:hissum[0] += 36, hissum[1] += 12, hissum[2] += 3, hissum[3] += 3
- 最终hissum = [66,20,4,3]
查询验证:
- (3,4): hissum[2] = 4 ✓ (子段:1, 1+3, 3)
- (1,2): hissum[0]-hissum[1]? 实际查询[0,1] = 20 ✓
实现细节与技巧
1. 模运算处理
// 使用int64_t防止溢出 hissum[x] = ((int64_t)sum[x] * t2 + hissum[x]) % mod;2. 线性筛优化
// 记录最小质因子,实现O(log n)分解 smallest_prime[i * p] = p;3. 边界处理
int L = prime_map[p].empty() ? 0 : prime_map[p].back().first + 1;学习价值
算法设计思想
- 问题分解:将复杂问题分解为独立质因子的处理
- 单调性利用:利用LCM的单调性减少更新次数
- 离线处理:通过重新组织查询优化效率
- 数据结构创新:设计支持复杂操作的特殊线段树
实际应用
这种"数论+数据结构"的思路在以下场景有广泛应用:
- 区间GCD/LCM查询
- 区间乘积问题
- 带修改的区间统计问题
- 1
信息
- ID
- 4592
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者