2026-01-21:区间乘法查询后的异或Ⅰ。用go语言,给定一个长度为 n 的整数数组 nums 和 q 条查询,每条查询用四个整数表示:[li, ri, ki, vi]。对每一条查询,从位置 li 开始,每次向右移动 ki 个索引并处理当前位置,直到索引超过 ri 为止;处理方式是将当前 nums[idx] 乘以 vi,然后对 1000000007 取模并写回数组。把所有查询按顺序执行完毕后,返回数组中所有元素按位异或的结果。
1 <= n == nums.length <= 1000。
1 <= nums[i] <= 1000000000。
1 <= q == queries.length <= 1000。
queries[i] = [li, ri, ki, vi]。
0 <= li <= ri < n。
1 <= ki <= n。
1 <= vi <= 100000。
输入: nums = [2,3,1,5,4], queries = [[1,4,2,3],[0,2,1,2]]。
输出: 31。
解释:
第一个查询 [1, 4, 2, 3] 将下标 1 和 3 的元素乘以 3,数组变为 [2, 9, 1, 15, 4]。
第二个查询 [0, 2, 1, 2] 将下标 0、1 和 2 的元素乘以 2,数组变为 [4, 18, 2, 15, 4]。
所有元素的异或为 4 ^ 18 ^ 2 ^ 15 ^ 4 = 31。
题目来自力扣3653。
🧠 解决过程分析
1. 问题理解与直接模拟的局限性
题目要求处理多条查询,每条查询指定一个起始位置 li、结束位置 ri、步长 ki 和乘数 vi。我们需要从 li 开始,每次前进 ki 步,将对应位置的元素乘以 vi 并对 1,000,000,007 取模。所有查询处理完毕后,计算整个数组的异或值。
最直接的方法是模拟每个查询的每一步操作。但最坏情况下(例如 ki=1,区间覆盖整个数组),单条查询需要操作 O(n) 次,总操作次数可能达到 O(n * q)。对于 n 和 q 最大为1000的情况,最坏情况操作次数可能达到百万级别,虽然Go语言可能能够处理,但存在优化空间。
2. 核心优化策略:根号分治
代码采用了根号分治策略,根据步长 ki 的大小将查询分为两类,分别处理以平衡效率。
- 大步长查询(ki ≥ B):当步长较大时,受影响的元素位置较少。例如,
ki接近n时,受影响的元素个数很少。这类查询采用直接模拟的方式处理,因为操作次数本身就不多。 - 小步长查询(ki < B):当步长较小时,受影响的元素可能很多,直接模拟代价高。因此采用分组批量处理的方法。
这里的关键是选取分界点 B,代码中将其设为查询数量的平方根 (B := int(math.Sqrt(float64(len(queries)))))。这是一个经验值,旨在平衡两类查询的处理代价。
3. 处理大步长查询(ki ≥ B)
对于大步长查询,代码直接遍历每个受影响的位置 (i = li; i <= ri; i += ki),执行乘法取模操作。这一步非常直观。
4. 处理小步长查询(ki < B)
这是算法的核心优化部分,目的是避免对每个小步长查询都进行多次遍历。其处理流程如下:
-
按步长和余数分组:首先,将所有小步长查询按照它们的步长
ki分组。对于同一个步长ki,由于操作的位置序列是li, li+ki, li+2ki, ...,这些位置在模ki意义下具有相同的余数 (li % ki)。因此,对于每个步长ki,再根据起始位置的余数将查询细分到不同的“桶”中。这样,每个桶内的查询操作的是数组中完全独立的一组位置序列。 -
独立处理每个序列:对于每个桶(即步长为
k,起始余数为m的序列),代码需要处理所有作用在这个序列上的乘法操作。为了高效计算每个位置最终的累积乘数,使用了差分技巧。- 差分数组初始化:为这个序列创建一个差分数组
diff。这个序列在数组中的实际位置是m, m+k, m+2k, ...。 - 记录操作影响:对于每个查询
(l, r, v),它会影响序列中从索引startIndex = (l - m) / k到endIndex = (r - m) / k的位置。为了表示这次区间乘法操作,在差分数组的startIndex位置乘以v,并在endIndex + 1的位置乘以v的模逆元(相当于除法,用于抵消超出区间的影响)。 - 前缀积还原结果:在所有查询记录到差分数组后,对差分数组求前缀积。此时,前缀积数组每个位置的值,就是对应序列位置最终需要乘上的总系数。最后,将这个系数应用到数组对应的实际位置上即可。
当某个余数序列对应的查询数量很少时(例如代码中判断为1),会退化为直接模拟,以避免差分数组操作的开销。
- 差分数组初始化:为这个序列创建一个差分数组
5. 计算最终结果
在所有查询处理完毕后,无论是大步长直接修改的,还是小步长通过差分数组批量修改的,结果都已经更新到 nums 数组中。最后一步是遍历整个数组,计算所有元素的按位异或值并返回。
⏱️ 复杂度分析
时间复杂度
- 大步长查询:每个查询最多操作 O(n / B) 个元素。总共有 q 条查询,所以这部分时间复杂度为 O(q * (n / B))。
- 小步长查询:
- 分组阶段是 O(q)。
- 处理每个序列时,差分操作的总次数与查询次数成正比。
- 应用前缀积到数组时,需要遍历每个序列的所有元素。由于所有序列覆盖了整个数组且不重叠,所以遍历的元素总数是 O(n)。
- 最终异或计算是 O(n)。
综合来看,总的时间复杂度主要由 O(q * (n / B) + n) 主导。由于 B 取 √q,时间复杂度约为 O(n √q)。在 n 和 q 最大为1000时,√q 最大约为31,操作次数在可接受范围内。
空间复杂度
- 主要的额外空间是用于分组的
groups数组和每个序列的差分数组diff。 groups数组大小为 B,即 O(√q)。- 差分数组的总长度不会超过 n。
- 因此,总的空间复杂度为 O(n)。
💎 总结
这个解决方案通过巧妙的根号分治策略,将问题根据操作步长划分为两种情况处理,并对最耗时的密集区间操作采用了差分和前缀积的技巧进行批量更新,避免了简单的暴力模拟可能带来的性能瓶颈,从而在给定的约束条件下高效地解决了问题。
Go完整代码如下:
package main
import (
"fmt"
"math"
)
const mod = 1_000_000_007
func xorAfterQueries(nums []int, queries [][]int) (ans int) {
n := len(nums)
B := int(math.Sqrt(float64(len(queries))))
type tuple struct{ l, r, v int }
groups := make([][]tuple, B)
for _, q := range queries {
l, r, k, v := q[0], q[1], q[2], q[3]
if k < B {
groups[k] = append(groups[k], tuple{l, r, v})
} else {
for i := l; i <= r; i += k {
nums[i] = nums[i] * v % mod
}
}
}
diff := make([]int, n+1)
for k, g := range groups {
if g == nil {
continue
}
buckets := make([][]tuple, k)
for _, t := range g {
buckets[t.l%k] = append(buckets[t.l%k], t)
}
for start, bucket := range buckets {
if bucket == nil {
continue
}
if len(bucket) == 1 {
// 只有一个询问,直接暴力
t := bucket[0]
for i := t.l; i <= t.r; i += k {
nums[i] = nums[i] * t.v % mod
}
continue
}
for i := range (n-start-1)/k + 1 {
diff[i] = 1
}
for _, t := range bucket {
diff[t.l/k] = diff[t.l/k] * t.v % mod
r := (t.r-start)/k + 1
diff[r] = diff[r] * pow(t.v, mod-2) % mod
}
mulD := 1
for i := range (n-start-1)/k + 1 {
mulD = mulD * diff[i] % mod
j := start + i*k
nums[j] = nums[j] * mulD % mod
}
}
}
for _, x := range nums {
ans ^= x
}
return
}
func pow(x, n int) int {
res := 1
for ; n > 0; n /= 2 {
if n%2 > 0 {
res = res * x % mod
}
x = x * x % mod
}
return res
}
func main() {
nums := []int{2, 3, 1, 5, 4}
queries := [][]int{{1, 4, 2, 3}, {0, 2, 1, 2}}
result := xorAfterQueries(nums, queries)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
import math
MOD = 1_000_000_007
def pow_mod(x: int, n: int) -> int:
"""快速幂取模"""
res = 1
while n > 0:
if n & 1:
res = res * x % MOD
x = x * x % MOD
n >>= 1
return res
def xor_after_queries(nums: list[int], queries: list[list[int]]) -> int:
n = len(nums)
B = int(math.sqrt(len(queries)))
# 处理大k的直接修改
for query in queries:
l, r, k, v = query
if k >= B:
i = l
while i <= r:
nums[i] = nums[i] * v % MOD
i += k
# 按k分组处理小k的情况
groups = [[] for _ in range(B)]
for query in queries:
l, r, k, v = query
if k < B:
groups[k].append((l, r, v))
# 处理每个k的组
for k, group in enumerate(groups):
if not group:
continue
# 按起始位置模k分组
buckets = [[] for _ in range(k)]
for l, r, v in group:
start = l % k
buckets[start].append((l, r, v))
# 处理每个桶
for start in range(k):
bucket = buckets[start]
if not bucket:
continue
if len(bucket) == 1:
# 只有一个询问,直接暴力
l, r, v = bucket[0]
i = l
while i <= r:
nums[i] = nums[i] * v % MOD
i += k
continue
# 计算最大索引
max_idx = (n - 1 - start) // k + 1 if start < n else 0
diff = [1] * (max_idx + 1)
# 应用差分
for l, r, v in bucket:
idx_l = l // k
idx_r = (r - start) // k + 1
diff[idx_l] = diff[idx_l] * v % MOD
if idx_r <= max_idx:
inv_v = pow_mod(v, MOD - 2) # 使用费马小定理求逆元
diff[idx_r] = diff[idx_r] * inv_v % MOD
# 前缀积恢复实际值并应用
mul_d = 1
for i in range(max_idx):
mul_d = mul_d * diff[i] % MOD
idx = start + i * k
if idx < n:
nums[idx] = nums[idx] * mul_d % MOD
# 计算异或结果
ans = 0
for x in nums:
ans ^= x
return ans
# 测试代码
if __name__ == "__main__":
nums = [2, 3, 1, 5, 4]
queries = [[1, 4, 2, 3], [0, 2, 1, 2]]
result = xor_after_queries(nums, queries)
print(result)
C++完整代码如下:
#include <iostream>
#include <vector>
#include <cmath>
#include <cstdint>
using namespace std;
const int64_t MOD = 1'000'000'007;
// 快速幂取模
int64_t pow_mod(int64_t x, int64_t n) {
int64_t res = 1;
while (n > 0) {
if (n & 1) {
res = res * x % MOD;
}
x = x * x % MOD;
n >>= 1;
}
return res;
}
struct Tuple {
int l, r, v;
Tuple(int l, int r, int v) : l(l), r(r), v(v) {}
};
int64_t xorAfterQueries(vector<int64_t>& nums, vector<vector<int>>& queries) {
int n = nums.size();
int B = static_cast<int>(sqrt(queries.size()));
// 处理大k的直接修改
for (const auto& q : queries) {
int l = q[0], r = q[1], k = q[2];
int64_t v = q[3];
if (k >= B) {
for (int i = l; i <= r; i += k) {
nums[i] = nums[i] * v % MOD;
}
}
}
// 按k分组处理小k的情况
vector<vector<Tuple>> groups(B);
for (const auto& q : queries) {
int l = q[0], r = q[1], k = q[2];
int64_t v = q[3];
if (k < B) {
groups[k].emplace_back(l, r, v);
}
}
// 处理每个k的组
vector<int64_t> diff(n + 1);
for (int k = 0; k < B; ++k) {
if (groups[k].empty()) continue;
// 按起始位置模k分组
vector<vector<Tuple>> buckets(k);
for (const auto& t : groups[k]) {
int start = t.l % k;
buckets[start].push_back(t);
}
// 处理每个桶
for (int start = 0; start < k; ++start) {
const auto& bucket = buckets[start];
if (bucket.empty()) continue;
if (bucket.size() == 1) {
// 只有一个询问,直接暴力
const auto& t = bucket[0];
for (int i = t.l; i <= t.r; i += k) {
nums[i] = nums[i] * t.v % MOD;
}
continue;
}
// 初始化diff数组
int max_idx = (n - start - 1) / k + 1;
if (start >= n) max_idx = 0;
for (int i = 0; i <= max_idx; ++i) {
diff[i] = 1;
}
// 应用差分
for (const auto& t : bucket) {
int idx_l = t.l / k;
int idx_r = (t.r - start) / k + 1;
diff[idx_l] = diff[idx_l] * t.v % MOD;
if (idx_r <= max_idx) {
int64_t inv_v = pow_mod(t.v, MOD - 2); // 费马小定理求逆元
diff[idx_r] = diff[idx_r] * inv_v % MOD;
}
}
// 前缀积恢复实际值并应用
int64_t mul_d = 1;
for (int i = 0; i < max_idx; ++i) {
mul_d = mul_d * diff[i] % MOD;
int idx = start + i * k;
if (idx < n) {
nums[idx] = nums[idx] * mul_d % MOD;
}
}
}
}
// 计算异或结果
int64_t ans = 0;
for (int64_t x : nums) {
ans ^= x;
}
return ans;
}
int main() {
vector<int64_t> nums = {2, 3, 1, 5, 4};
vector<vector<int>> queries = {{1, 4, 2, 3}, {0, 2, 1, 2}};
int64_t result = xorAfterQueries(nums, queries);
cout << result << endl;
return 0;
}