文章目录

题目描述
示例 1:
输入: nums = [1,1,1], queries = [[0,2,1,4]]
输出: 4
解释:
唯一的查询 [0, 2, 1, 4] 将下标 0 到下标 2 的每个元素乘以 4。
数组从 [1, 1, 1] 变为 [4, 4, 4]。
所有元素的异或为 4 ^ 4 ^ 4 = 4。
示例 2:输入: 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。
提示:1 <= n == nums.length <= 105
1 <= nums[i] <= 109
1 <= q == queries.length <= 105
queries[i] = [li, ri, ki, vi]
0 <= li <= ri < n
1 <= ki <= n
1 <= vi <= 105
思路简述
这道题的核心操作模式虽然和昨天的每日一题相同容易理解,但数据范围提升到了 ( 105) 级别。如果直接使用暴力法模拟每次查询,时间复杂度会达到 ( O(nq) )。在最坏情况下(例如步长 ( k=1 ) 时每次查询遍历整个数组),运算量会高达 ( 1010 ) 次,这显然会导致超时。因此,我们需要采用一种更高效的优化策略------根号分治(Sqrt Decomposition)。
1. 根号分治的核心思想
既然是分治,那么根号分治的关键也在于选择一个基准值本题也就是原始数组长度n,根据题目我们可以用查询中步长k(步长就是指的每次进行的 idx += ki 这个公式, 步长就是ki) 与该基准值的大小关系,将查询分为"大k"和"小k"两类,分别采用不同的策略处理,以此在整体上平衡时间复杂度。
我们设定基准值是 T (也就是根号n),然后分情况讨论:
-
大k( k > T ):直接暴力模拟 :
这种情况我们可以直接进行暴力,至于原因是因为k越大,一次查询计算改动的数字越少,如:n = 1e5 , q = 1e5, T = √1e5 ≈ 316 , 那个大k 即 >= 317 ,而总操作数也就是从 0 开始,每次 +317,直到 ≤99999总个数 ≈ 100000 ÷ 317 ≈ 315 次,10 万次查询总操作次数
总操作数 = 100000 × 315 = 31,500,000 次≈ 3150 万次
-
小k( k < T ):模意义下的乘法差分 :
当步长 k 很小时,如果还使用暴力,在最坏情况下(如 k=1)每次查询都要遍历整个数组,( 105 ) 次查询就会有 ( 1010) 次操作,必然超时。因此我们需要引入乘法差分的思想,来批量处理这些"小k"的查询。
什么是乘法差分?
我们先回忆一下常规的加法差分 :对于原数组 a,我们构造差分数组 b ,使得 b[i] = a[i] - a[i-1] (i >= 2),且 b[1] = a[1]。当我们想要给区间 ( [l, r] ) 内的所有数都加 ( v ) 时,只需简单地执行 b[l] += v 和 b[r+1] -= v,最后对 ( b ) 求一遍前缀和就能得到更新后的 ( a ) 数组。
但在这道题中,操作是乘法 ,并且是在模运算 的意义下进行的。模运算中没有直接的"除法"概念,因此我们需要用乘法逆元来模拟"除法"的效果。
题目中给定的模数 ( MOD = 109 + 7 ) 是一个质数,根据费马小定理 ,对于任意一个数 ( v ),它在模 ( MOD ) 下的逆元 ( inv(v) = v{MOD-2} mod MOD )。你可以简单理解为:在模 ( MOD ) 下,"除以 ( v )"的效果完全等价于"乘以 ( v ) 的逆元"。
基于此,我们可以构造出乘法差分数组,具体步骤如下:
- 初始化一个全为 1 的数组
dif(因为乘法的单位元是 1)。 - 对于一个查询
[l, r, k, v](即给下标l, l+k, l+2k, ..., r都乘以 ( v )):- 我们执行
dif[l] = dif[l] * v % MOD(这相当于加法差分中的"加 ( v )")。 - 计算该序列结束后的下一个位置
R = ((r - l) / k + 1) * k + l,并执行dif[R] = dif[R] * inv(v) % MOD(这相当于加法差分中的"减 ( v )")。
- 我们执行
- 处理完所有该 ( k ) 值的查询后,对
dif数组在"步长为 ( k ) 的维度"下求前缀积 。此时,每个位置 ( i ) 的dif[i]就代表了nums[i]最终需要乘的总倍数。 - 最后将
nums[i]乘以dif[i]并取模,完成对原数组的更新。
2. 快速幂
因为逆元的计算需要算 ( v{MOD-2} mod MOD ),而 ( MOD-2 ) 是一个 ( 109 ) 级别的超大指数。如果直接循环乘 ( 109) 次,显然会严重超时。这时候就需要快速幂出马,它能将计算幂次的时间复杂度降到 O(log y) 。
快速幂的核心思想是二进制拆分指数 。举个例子,比如我们要算 ( x13):
13 的二进制是 1101,即 13 = 8 + 4 + 1。
因此 x13 = x8 * x4 * x1
我们不需要傻傻地乘 13 次,只需要通过不断平方 ( x ),并在对应二进制位为 1 时将当前的 ( x ) 乘到结果中,只需 log 级别的次数即可完成计算。
代码实现
cpp
class Solution {
public:
const int MOD = 1e9 + 7; // 题目规定的模数
// 快速幂函数:计算 (x^y) % MOD
// 利用二进制拆分指数,将时间复杂度降为 O(log y)
int pow(long long x, long long y)
{
long long res = 1; // 初始化结果为乘法单位元 1
for (; y; y >>= 1) { // 循环处理指数 y 的每一位二进制位,右移一位相当于除以 2
if (y & 1) { // 如果当前二进制位是 1(即 y 为奇数)
res = res * x % MOD; // 将当前的 x 乘到结果中
}
x = x * x % MOD; // x 自乘,对应 x^(2^0) -> x^(2^1) -> x^(2^2)... 的递进
}
return res;
}
int xorAfterQueries(vector<int>& nums, vector<vector<int>>& queries) {
int n = nums.size();
int T = sqrt(n); // 根号分治的基准值 T = sqrt(n)
// groups[k] 用于存储所有步长为 k 的小k查询(k < T)
// 每个查询在组内存储为 {l, r, v}
vector<vector<vector<int>>> groups(T);
// 第一步:遍历所有查询,进行分类处理
for(auto& q : queries)
{
int l = q[0], r = q[1], k = q[2], v = q[3];
if(k < T)
{
// 小k:加入对应的分组,稍后统一用乘法差分批量处理
groups[k].push_back({l, r, v});
}
else
{
// 大k:直接暴力模拟
// 因为 k 很大,所以单次查询修改的元素数量很少,不会超时
for(int i = l; i <= r; i += k)
{
// 注意转为 long long 防止乘法过程中 int 溢出
nums[i] = 1ll * nums[i] * v % MOD;
}
}
}
// 第二步:处理所有小k的查询,使用乘法差分优化
vector<long long> dif(n + T); // 差分数组,稍微开大点防止后续计算越界
for(int k = 1; k < T; k++) // 遍历每一个可能的小k值
{
if(groups[k].empty())
continue; // 如果该 k 值没有查询,直接跳过
// 初始化差分数组为全 1(乘法的单位元)
fill(dif.begin(), dif.end(), 1);
// 对每个该 k 值的查询,更新乘法差分数组
for(auto &q : groups[k])
{
int l = q[0], r = q[1], v = q[2];
// 乘法差分的"加 v"操作:在起始位置 l 乘 v
dif[l] = dif[l] * v % MOD;
// 计算该等差数列结束后的下一个位置 R
// ((r - l)/k + 1) 是该序列中元素的总个数
// 乘以 k 再加 l,就是序列最后一个元素的下一个位置
int R = ((r - l) / k + 1) * k + l;
// 乘法差分的"减 v"操作:在 R 位置乘 v 的逆元
dif[R] = dif[R] * pow(v, MOD - 2) % MOD;
}
// 对差分数组按步长 k 求前缀积,得到每个位置的总乘数
// 注意:这里是按步长 k 跳跃进行的,即处理 i, i+k, i+2k...
for (int i = k; i < n; i++) {
dif[i] = dif[i] * dif[i - k] % MOD;
}
// 将计算出的总乘数应用到原数组 nums 上
for (int i = 0; i < n; i++) {
nums[i] = 1ll * nums[i] * dif[i] % MOD;
}
}
// 第三步:计算最终数组中所有元素的异或和
int res = 0;
for (int i = 0; i < n; i++){
res = res ^ nums[i];
}
return res;
}
};
复杂度解析
- 时间复杂度 : O((n + q) √n )。
- 大k查询:每次查询的操作数不超过√n ,总共有 q 次查询,故为 O(q√n)。
- 小k查询:k 的取值最多有 √n 种。对于每种 k ,处理差分数组和应用到原数组的时间都是 O(n),故总为 ( O(n√n)。
- 两者相加,总时间复杂度为 O((n + q) √n )。在 n 和 q 为 105 时,运算量约为 105 * 300 = 3 * 107 ,符合时间限制要求。
- 空间复杂度 :O(n + q)
- 主要开销为分组存储查询的
groups数组和差分数组dif。
- 主要开销为分组存储查询的
踩坑记录
- 费马小定理与逆元的理解:一开始对模运算下的乘法逆元概念比较陌生,后来通过看题解才了解到费马小定理(因为模数是质数),之后理解了可以用快速幂计算 ( v^{MOD-2} ) 来得到逆元,以此模拟"除法"的效果。
- 根号分治思路的构建:这道题的优化思路比较巧妙,一开始拿到题完全没想到要分情况处理。后来仔细研究了题解和详细解释,才理解了通过"根号"这个基准值来平衡时间复杂度的精髓------大k时暴力很快,小k时用差分高效,两者结合才能通过极限数据。这部分思想还需要多找类似的题来巩固消化。
如果这篇博客对你有帮助,别忘了点赞支持一下~也可以收藏起来,方便后续刷题复习时随时翻看。要是能顺手点个关注,爱弥斯还能得到漂泊者批准的游戏时间哦!

