文章

3655. 区间乘法查询后的异或 ii

3655. 区间乘法查询后的异或 ii

LeetCode 每日一题复盘:3655. 区间乘法查询后的异或 II

标签: 数组 数学 根号分治 差分数组 快速幂 难度: 困难 耗时: 约 3 小时

题目描述

给你一个长度为 n 的整数数组 nums 和一个大小为 q 的二维整数数组 queries,其中 queries[i] = [li, ri, ki, vi]。 对于每个查询,需要按以下步骤依次执行操作:

  1. 设定 idx = li。
  2. 当 idx <= ri 时:更新 nums[idx] = (nums[idx] * vi) % (1000000007)。将 idx += ki。

在处理完所有查询后,返回数组 nums 中所有元素的按位异或结果。 (提示:数据范围极大,n 和 q 均可达 10^5)


阶段一:直觉暴力解法 (TLE)

最开始顺着题目逻辑写的模拟。测试用例全过,但在提交时遇到了“超出时间限制 (TLE)”。 失败原因分析: 遇到极端情况(如步长 ki = 1,且区间极长),双重循环的时间复杂度会飙升,无法通过 $10^5$ 级别的大数据测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
    public int xorAfterQueries(int[] nums, int[][] queries) {
        int MOD = 1000000007;
        int[] bravexuneth;
        for(int i = 0; i < queries.length; i++) {
            bravexuneth = queries[i];
            int li = bravexuneth[0], ri = bravexuneth[1], ki = bravexuneth[2], vi = bravexuneth[3];
            int idx = li;
            while(idx <= ri && idx < nums.length) {
                long temp = (long)nums[idx] * vi;
                nums[idx] = (int)(temp % MOD);
                idx += ki;
            }
        }
        // ... 扫尾异或计算 ...
    }
}

阶段二:优化和破局思路

为了解决大数量级下的区间跳跃修改问题,引入了以下核心架构:

1.根号分治 (核心调度): 以 $\sqrt{n}$ (即 B) 为界限。步长大的(大跳),直接用 while 暴力修改,因为跳不了几次;步长小的(小跳),走差分数组记账。
2.带步长的二维差分数组 (小跳记账):开辟 diff[B+1][n] 数组,每一行代表一条特定步长的独立路线。在起点乘上 vi,在越界后的“取消点”进行消除。
3.乘法逆元与快速幂 (解决取模除法):在取模规则下无法直接除以 vi 来取消标记,需要借助费马小定理求出 vi 的乘法逆元(即 $vi^{MOD-2}$),这里手写了 fastpow 快速幂算法将 $O(N)$ 优化到 $O(\log N)$。

阶段三:最终AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Solution {
    int MOD = 1000000007;
    public int xorAfterQueries(int[] nums, int[][] queries) {
        int n = nums.length;
        int B = (int)Math.sqrt(n);
        
        // 构建二维差分账本,行代表步长 k,列代表原数组下标
        int[][] diff = new int[B + 1][n];
        for(int k = 1; k <= B; k++) {
            Arrays.fill(diff[k], 1); // 乘法初始化必须为 1
        }
        
        int[] bravexuneth;
        for(int i = 0; i < queries.length; i++) {
            bravexuneth = queries[i];
            int li = bravexuneth[0], ri = bravexuneth[1], ki = bravexuneth[2], vi = bravexuneth[3];
            
            if(ki > B){
                // 大跳:暴力模拟
                int idx = li;
                while(idx <= ri && idx < nums.length) {
                    long temp = (long)nums[idx] * vi;
                    nums[idx] = (int)(temp % MOD);
                    idx += ki;
                }
            } else {
                // 小跳:差分记账
                diff[ki][li] = (int)((long)diff[ki][li] * vi % MOD);
                int steps = (ri - li) / ki;
                int cancelidx = li + (steps + 1) * ki; // 跨出边界的取消点
                
                // 终点取消:乘上逆元
                if(cancelidx < n) {
                    long invV = fastpow(vi, MOD - 2);
                    diff[ki][cancelidx] = (int)((long)diff[ki][cancelidx] * invV % MOD);
                }
            }
        }
        
        // 结算差分数组并映射回原数组 nums
        for(int k = 1; k <= B; k++) {
            for(int i = 0; i < n; i++) {
                if(i >= k) {
                    diff[k][i] = (int)((long)diff[k][i] * diff[k][i-k] % MOD);
                }
                if(diff[k][i] != 1) { // 性能优化:过滤无效的 1
                    nums[i] = (int)((long)nums[i] * diff[k][i] % MOD);
                }
            }
        }
        
        // 最终异或求和
        int result = 0;
        for(int i = 0; i < n; i++) {
            result ^= nums[i];
        }
        return result;
    }
    
    // 快速幂工具方法
    private long fastpow(long a, long b) {
        long res = 1;
        a = a % MOD;
        while(b > 0){
            if(b % 2 == 1) {
                res = (res * a) % MOD;
            }
            a = (a * a) % MOD;
            b >>= 1;
        }
        return res;
    }
}

踩坑与复盘心得

1.数组越界血泪史: 初始化二维差分数组时,循环条件错写成了 k <= B + 1,导致在处理短数组(B较小)时直接抛出 Index 2 out of bounds for length 2。教训: 二维数组开辟了 B+1 行,合法的最大行下标就是 B,千万不能越界。
2.大数相乘必转 long: 运算 nums[idx] * vi 前,如果不强转 long,遇到两个极大 int 相乘会直接溢出成负数或乱码,取模前必须升维。
3.百川归海的顺序: 结算映射时,内部的 i >= k 前缀积滚动计算,必须放在向 nums 映射赋值的前面,顺序绝对不能乱。

参考资料

根号分治

乘法逆元

快速幂


本文由作者按照 CC BY 4.0 进行授权

热门标签