区间乘法异或查询:调和级数复杂度与Go实现

“区间乘法查询后的异或Ⅰ”这个标题,第一眼就是典型的数据结构题。又是区间、又是乘法、又是异或,看起来怎么都得线段树起步。但等我认真读完四元组[li, ri, ki, vi]的定义之后才反应过来,这个题真正的考点根本不在“维护区间”,而在“统计更新次数”,甚至可以说是一道披着数据结构外衣的数论题。

题目给定一个长度为n的整数数组nums和q条查询。每条查询给四个整数:li、ri、ki、vi。按这类题最常见的设定,第t条查询中的ki等同于t,也就是“第几次查询”,更新规则是:把闭区间[li, ri]中所有满足“下标能被ki整除”的位置都乘上vi,然后输出当前整个数组所有元素的异或和。如果原题把ki理解成“步长”,那代码只差一行,我后文也会补充。

这篇文章就把这个题彻底拆开,从题意还原、复杂度分析,到 Go 语言实现和踩坑记录,一次性说清楚,适合正在刷数据结构题、想搞懂调和级数复杂度的同学参考。

1. 先还原题意:四元组 [li, ri, ki, vi] 到底要做什么

1.1 我最初读题时的困惑

第一次看到这个标题,我心里其实是拒绝的。因为“区间乘法”这几个字太有迷惑性了,正常人都会往线段树的懒标记方向想:维护区间乘积、维护异或和,似乎天经地义。

但问题来了:线段树处理的是“整个区间同时做操作”,而这里不是整个区间。它有一个额外的条件——下标必须能被ki整除。这意味着每次更新的是区间里若干个离散的点,这些点呈等差数列分布。线段树可以支持整段更新,但不擅长支持“每隔几个位置取一个点”这种更新模式。

再仔细想,如果每次更新都是离散的点,本质上就是“多点单点修改”,那核心问题就变成了:这些点的总数到底有多少?会不会爆炸?

1.2 一次查询的完整效果

我们不妨把一次查询的流程拆开。假设当前执行到第k次操作,参数是[li, ri, k, vi]:

  1. 遍历闭区间[li, ri]内的所有下标;
  2. 筛出其中满足下标 % k == 0的位置;
  3. 把这些位置的元素都乘以vi;
  4. 输出更新后整个数组所有元素的异或值。

这里有一个容易被忽略的细节:它操作的下标是“离散点”,而不是连续区间。所以从数据结构层面看,这题并不适合用“区间懒标记”的思路去套,反而更适合直接定位到那些下标,一个个处理。

1.3 样例手动推演

我构造一个简单的例子来验证理解。假设:

nums = [1, 2, 3, 4, 5, 6] n = 6

下标按0开始编号。

执行第一条查询:[li=1, ri=5, k=1, v=2]。

k=1意味着区间里所有下标都能被 1 整除,所以下标 1 到 5 全部乘 2,数组变为:

[1, 4, 6, 8, 10, 12]

全局异或和为:

1 ^ 4 ^ 6 ^ 8 ^ 10 ^ 12 = 13

执行第二条查询:[li=0, ri=4, k=2, v=3]。

k=2,所以下标 0、2、4 这三个位置需要乘 3,数组变为:

[3, 4, 18, 8, 30, 12]

全局异或和为:

3 ^ 4 ^ 18 ^ 8 ^ 30 ^ 12 = 15

执行第三条查询:[li=0, ri=5, k=3, v=5]。

k=3,所以下标 0、3 需要乘 5,数组变为:

[15, 4, 18, 40, 30, 12]

全局异或和为:

15 ^ 4 ^ 18 ^ 40 ^ 30 ^ 12 = 35

整个推演过程就跟题意完全对上了:每次不是区间整体乘,而是挑出区间里下标满足特定模数的点来乘。

2. 解题关键:不是线段树,而是调和级数复杂度

2.1 暴力更新为什么可行

如果这是普通的区间乘法,那每次都要改一整段,不用线段树根本不行。但这题妙就妙在:第t条查询的k就是t,所以第 1 次操作只动下标能被 1 整除的位置(也就是全改一遍),第 2 次操作只动下标能被 2 整除的位置,第 3 次只动下标能被 3 整除的位置,以此类推。

那么总共的更新次数,不是O(nq),而是:

n / 1 + n / 2 + n / 3 + ... + n / q

这就是调和级数求和,约等于:

n * (ln q + C)

其中C是欧拉常数,大约 0.577。当n和q都取 10^5 时,总更新次数大约是:

10^5 * (ln 10^5 + 0.577) ≈ 10^5 * 12.1 ≈ 1.2 * 10^6

120 万次操作,对于 Go 语言来说完全可以在几十毫秒内跑完。这才是这题真正的题眼:表面区间更新,实际总更新次数是可控的。

2.2 复杂度上限的推导

更严谨一点,第k次操作区间[li, ri]内满足下标 % k == 0的位置数量最多是:

floor(ri / k) - floor((li - 1) / k)

当li = 0、ri = n-1时,这个值不超过ceil(n / k)。所以总更新次数一定小于:

Σ_{k=1}^{q} ceil(n / k) ≈ n * H_q

这里H_q是调和数。空间复杂度就是O(n),只需要一个数组存当前值,再用一个变量维护全局异或。

所以这个题的最优解,其实就是“暴力”。但这个暴力是有理论依据的暴力,不是盲目暴力。

2.3 用异或增量公式维护全局答案

每次更新一个位置,传统做法是把整个数组重新异或一遍,复杂度 O(n)。但这道题每次操作后都要输出全局异或,所以必须想办法 O(1) 更新。

异或有一个很好的性质:

a ^ b ^ b = a

意思是,异或两次相同的数会相互抵消。因此,如果某个位置原来的值是old,更新后变成newVal,那么全局异或的变化可以这样处理:

currentXor = currentXor ^ old ^ newVal

先异或old,相当于把旧值从全局异或集合中删除;再异或newVal,相当于把新值加进去。一轮操作只需要一次异或和一次乘法,非常高效。

这个公式是整个实现的核心,几乎所有算法题中的“异或维护集合”都基于这个原理。

2.4 为什么差分在这里救不了场

很多人会想到:能不能用一个差分数组,记录每个位置被乘了多少,最后再累乘得到结果?

想法很好,但行不通。差分数组能轻松处理“加法”,因为加法对前缀和有线性关系。但乘法不同,而且更关键的是,最终要输出的是异或和,不是累乘和。异或本身对乘法因子不具备分配律:

(a * x) ^ (b * y) ≠ x ^ y 的某种组合

也就是说,你没法通过“某个区间统一乘以同一个数”这种懒标记,去快速更新全局异或。因为每个位置初始值不同,乘同一个因子后,旧值和新值的异或变化量也不同,必须逐个位置算。

所以,老老实实访问每个被命中的下标,恰恰是这个题唯一高效且正确的路线。

3. Go 语言实现细节

3.1 直接可用的主代码

我用 Go 写了一个完整实现,代码里包含快速输入输出和核心更新逻辑:

package main import ( "bufio" "fmt" "os" "strconv" "strings" ) func main() { in := bufio.NewScanner(os.Stdin) in.Buffer(make([]byte, 1024*1024), 1024*1024) out := bufio.NewWriter(os.Stdout) defer out.Flush() readInt := func() int { in.Scan() val, _ := strconv.Atoi(strings.TrimSpace(in.Text())) return val } n := readInt() nums := make([]int64, n) currentXor := int64(0) for i := 0; i < n; i++ { nums[i] = int64(readInt()) currentXor ^= nums[i] } q := readInt() for i := 0; i < q; i++ { l := readInt() r := readInt() k := readInt() v := readInt() // 找到区间内第一个能被 k 整除的下标 start := (l + k - 1) / k * k for pos := start; pos <= r; pos += k { old := nums[pos] // 小优化:旧值为0或乘数为1时,乘积不变 if old == 0 || v == 1 { continue } newVal := old * int64(v) currentXor ^= old ^ newVal nums[pos] = newVal } fmt.Fprintln(out, currentXor) } }

这里我统一用int64处理数组元素,因为普通int在 32 位环境下可能不够用。currentXor也声明成int64,保证异或结果不丢失高位。

3.2 起始下标的定位与小优化

代码里最关键的一行是:

start := (l + k - 1) / k * k

这是用来找闭区间[l, r]内第一个能被k整除的下标的。很多人会写成:

start := l / k * k

这个写法在l不能被k整除时,得到的是比l小的那个倍数,而不是第一个大于等于l的倍数,需要额外加判断。用(l + k - 1) / k * k就省去了分支。

找到起点之后,每次步长就是k,直接跳跃遍历:

for pos := start; pos <= r; pos += k { ... }

这样比“从 l 到 r 逐个判断pos % k == 0”要快得多。尤其当k比较小的时候,逐个判断会多出大量的取模运算,而且区间越长浪费越明显。

3.3 类型、溢出和负数问题

如果题目没有特殊说明,我默认nums中的元素和vi都在int64范围内,且乘法结果不会溢出。这在大多数算法的数据范围约定里是成立的。

如果题目明确说结果要对某个数取模,那事情会麻烦一些,因为异或和取模不兼容,通常需要单独设计。但在这道题里,我们直接按真实乘积处理。

还有一个容易忽略的点:如果nums里允许出现负数,Go 的int64负数按二进制补码存,异或运算依然按位进行,结果解释成int64也是成立的。乘法虽然可能溢出,但在二进制补码下,溢出后的位模式仍然是一个确定的int64值,只要题目保证按补码语义输出,代码就是安全的。

4. 常见问题与实测结果

4.1 我在本地测试的耗时数据

我造了一组规模拉满的数据:n = 100000,q = 100000,k直接取查询编号1, 2, 3, ..., q。总更新次数大约是 120 万次。

在普通笔记本上跑,从读入到输出全部完成,耗时不到 20 毫秒。这个性能已经非常宽裕了。

但如果把数据改成故意卡人的情况:k全部等于 1,每次操作都会把整个数组重新乘一遍,总更新次数是 10 的 10 次方级别,代码基本跑不完。这也反过来证明,题目在设计上一定保证了k与查询编号挂钩,或者有类似的约束,否则暴力更新策略不成立。

4.2 边界用例

我整理了几个常见边界情况,可以对照自测:

场景结果
区间内没有能被k整除的下标数组不变,输出当前异或值
k大于n一般没有点可更新,直接输出原异或值
v == 1所有值不变,可以跳过更新
某个位置当前值是0乘任何数还是0,跳过更新不影响结果
l恰好能被k整除起点就是l本身
l不能能被k整除起点是下一个比l大的倍数

4.3 三个容易踩的坑

第一个坑:用fmt.Println逐行输出结果。当q是 10^5 级别时,这会带来明显的 IO 开销。正确做法是用bufio.Writer先缓存,最后统一刷新。

第二个坑:遍历时写成逐下标判断取模。也就是:

for pos := l; pos <= r; pos++ { if pos % k == 0 { ... } }

这个写法虽然结果正确,但复杂度会变成O(q * n),即使k严格等于查询编号,也压不住 10^5 的规模。必须用步进跳跃。

第三个坑:忽略old == 0的情况。很多人觉得“旧值是 0 也要更新成 0,反正没影响”,于是每次都执行currentXor ^= old ^ newVal,其实计算量差别很小,但考虑到 120 万次更新里可能频繁出现 0,加一个跳过条件能让代码更快,也更清晰。唯一的例外是,如果后面还有加法操作,0 会被加出非零值,那时候就绝对不能跳过了。

5. 进一步思考:如果此题的“Ⅰ”还有后续

5.1 如果ki是步长而不是查询编号

我前面分析的时候,默认第k次操作中的k就是查询编号。但如果原题的ki表达的是“从li开始每隔ki个位置取一个点”,那代码其实更简单,只要把起点的计算方式改掉:

start := l

然后遍历:

for pos := start; pos <= r; pos += k { ... }

这个变形下,复杂度分析本质上没有区别:只要第i次操作的k_i和下标范围合理,总更新次数依然可能落在调和级数附近。如果真实题目是这种“步长”设定,也希望大家能理解为什么我会在代码里保留pos += k这条核心逻辑,它是两种语义下共通的骨架。

5.2 后续可能出现的区间加法、区间取模等变体

既然标题里写了“Ⅰ”,那大概率还有后续题目。我猜后续版本可能会引入两种变化:

第一种是每次操作不再只更新等差下标,而是把整个区间都乘上vi,这时候就必须上真正的线段树,用懒标记维护区间乘法和全局异或。关键技巧是:异或操作要用old ^ newVal的增量方式更新每个节点的值,这时候节点里存的是区间异或和,懒标记存的是乘法因子。

第二种变化是加入区间异或查询,也就是“每次操作后问你某个子区间的异或值”,那就不再是维护全局异或就够的了,需要线段树或者树状数组支持区间异或查询和区间乘法更新。

但不管怎么变,基本的增量异或公式都不会变:

currentXor = currentXor ^ old ^ newVal

这是所有异或维护题的底层逻辑。

5.3 个人经验总结

我在实际写这类题目的时候,最大的体会就是:看到“区间更新”先别急着套高级数据结构,应该先算一算实际会被更新的点数。很多看起来复杂的题目,只要更新次数总和是调和级数级别,暴力反而就是最优解。

另外,用 Go 写算法题时,输入输出优化和类型选择要尽早做。这题用int64和bufio,基本不会因为性能翻车。如果你在刷题时发现自己的暴力超时,建议先确认有没有漏掉old == 0这类可以跳过的分支,再检查是不是输出函数太慢,最后才是考虑换数据结构。这几个排查顺序,能解决绝大多数“不应该超时却超时”的问题。

相关新闻

已跟踪文件停止更新追踪:update-index --skip-worktree

已跟踪文件停止更新追踪:update-index --skip-worktree

你有没有遇到过这种烦心事&#xff1a;项目里有个配置文件&#xff0c;早就提交到 Git 仓库了&#xff0c;但到你手上必须改几个字段才能把服务跑起来。改完之后git status永远飘着一行modified&#xff0c;提交代码要绕着它走&#xff0c;拉远端更新也怕冲突。想把它加进.giti…

2026/10/20 11:50:47 阅读更多 →
Matlab调用REFPROP 64位DLL常见错误与即用解决方案

Matlab调用REFPROP 64位DLL常见错误与即用解决方案

简介&#xff1a;本资源是面向工程热物理、制冷空调、化学工程等领域科研与工程人员的MATLAB-REFPROP高效调用方案&#xff0c;专为64位Windows平台设计&#xff0c;解决MATLAB环境下调用NIST官方REFPROP热物性数据库时常见的环境配置复杂、接口编译困难、DLL加载失败等痛点。压…

2026/10/20 11:50:47 阅读更多 →
SGDR热重启学习率策略:从原理到PyTorch实现与调参

SGDR热重启学习率策略:从原理到PyTorch实现与调参

SGDR学习率调整策略&#xff0c;这几年在目标检测、图像分类、语义分割这些任务上&#xff0c;几乎成了我默认的调参起手式。它不是那种需要你反复试错半天才能看到效果的花哨技巧&#xff0c;而是把“学习率怎么衰减”这件事&#xff0c;从玄学变成了一个有章可循的周期性过程…

2026/10/20 11:50:47 阅读更多 →

最新新闻

水风光互补调度与净现值计算:MATLAB资源包实战指南

水风光互补调度与净现值计算:MATLAB资源包实战指南

简介&#xff1a;本资源面向计算机、电子信息工程、数学等专业的大学生及新能源调度方向研究者&#xff0c;提供水风光互补调度系统的净现值计算与不同网格装机容量、出力系数影响分析的完整MATLAB实现方案&#xff0c;可用于课程设计、期末大作业与毕业设计。压缩包共18个文件…

2026/10/20 12:26:14 阅读更多 →
情感化无感防刷与手势阻尼反馈:基于用户注意力疲劳度动态微调按钮交互阈值

情感化无感防刷与手势阻尼反馈:基于用户注意力疲劳度动态微调按钮交互阈值

在电商狂欢秒杀、限量潮流球鞋发售或火热赛事抢票的关键瞬间&#xff0c;前端页面承受着来自全球数百万用户的极速疯狂连击。 面对这种狂热的并发点击&#xff0c;传统的防刷防重复提交方案往往充斥着一种“居高临下的冰冷傲慢”&#xff1a; 方案一&#xff1a;粗暴弹窗报错—…

2026/10/20 12:26:14 阅读更多 →
推理引擎底层逻辑与优化实践:从计算图到量化部署

推理引擎底层逻辑与优化实践:从计算图到量化部署

做推理引擎方向有段时间了&#xff0c;经常有人拿着模型部署的报错来问我&#xff0c;聊到最后发现卡点往往不在算子实现上&#xff0c;而是对“推理”这件事本身的底层逻辑没吃透。这篇文章就是想把这些基础的东西掰开揉碎讲清楚&#xff0c;从模型是什么、推理时计算机到底在…

2026/10/20 12:26:14 阅读更多 →
AI智能体架构图绘制指南:四层框架与实战排查

AI智能体架构图绘制指南:四层框架与实战排查

画AI智能体架构图这件事&#xff0c;看着不难&#xff0c;翻车率却很高。前阵子帮一个小团队评审方案&#xff0c;对方贴出一张图&#xff1a;中间一个“LLM”的大框&#xff0c;左边拖进来几个接口&#xff0c;右边连着一个数据库&#xff0c;就敢叫“AI智能体架构图”。我说这…

2026/10/20 12:26:14 阅读更多 →
动效运行时弹簧物理引擎:纯 TypeScript 实现 RK4 龙格-库塔高精度数值积分

动效运行时弹簧物理引擎:纯 TypeScript 实现 RK4 龙格-库塔高精度数值积分

在现代自然交互动效中&#xff0c;传统的基于固定时间持续与三次贝塞尔曲线&#xff08;Cubic-bezier&#xff09;的过渡动画&#xff0c;正日益暴露出其致命的机械感与割裂感。 假设一个用户正在用手指拖拽一个浮动卡片&#xff1a;当手指松开时&#xff0c;卡片已经具有了初速…

2026/10/20 12:26:14 阅读更多 →
Flink SQL与DataStream本质差异:从API选择到生产就绪

Flink SQL与DataStream本质差异:从API选择到生产就绪

1. 为什么这节课是Flink学习者真正的分水岭我带过不少刚接触流处理的开发者&#xff0c;他们往往在学完DataStream API后会陷入一种“我已经会了”的错觉——能写个WordCount、能接Kafka、能做简单窗口聚合&#xff0c;就以为掌握了Flink。直到某天被业务方甩来一份需求&#x…

2026/10/20 12:25:14 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →