1. 题目
给你一个长度为 n 的整数数组 nums 和一个大小为 q 的二维整数数组 queries,其中 queries[i] = [li, ri, ki, vi]。对于每个查询,需要按以下步骤依次执行操作:
设定 idx = li。 当 idx <= ri 时: 更新:nums[idx] = (nums[idx] * vi) % (10e9 + 7)。 将 idx += ki。 在处理完所有查询后,返回数组 nums 中所有元素的 按位异或 结果。
示例 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 <= 10e5 1 <= nums[i] <= 10e9 1 <= q == queries.length <= 10e5 queries[i] = [li, ri, ki, vi] 0 <= li <= ri < n 1 <= ki <= n 1 <= vi <= 10e5
2. 分析
今天这道题是昨天的升级版,题目描述一模一样,只是提示里面的数据量级变了,支持的范围更广了,将昨天的答案拉出来一执行,毫无意外的超时。
之前的版本是纯暴力执行,简单的遍历,我的思路是将相同步长,相同起始点,相同终点的数据,做合并处理,这样能一定程度上减少循环次数,答案是有效的,能解决一定量级和指定用例的问题,但是满足不了定制的测试用例。
class Solution {
private static final int MOD = 1000000007;
public int xorAfterQueries(int[] nums, int[][] queries) {
int len = nums.length;
int from;
int to;
int step;
int ratio;
Map<String, Integer> map = new HashMap<>();
for (int[] query : queries) {
from = query[0];
to = query[1];
step = query[2];
ratio = query[3];
int count = (to - from + 1) / step;
if (count > 100 && count > len / 2) {
// 超过一半时,分成两段
int mid = (from + to) / 2;
// 前半段
this.cacheHandle(from, mid, step, ratio, map);
// 后半段
this.cacheHandle(mid + 1, to, step, ratio, map);
continue;
}
this.cacheHandle(from, to, step, ratio, map);
}
for (Map.Entry<String, Integer> next : map.entrySet()) {
String[] split = next.getKey().split(":");
from = Integer.parseInt(split[0]);
to = Integer.parseInt(split[1]);
step = Integer.parseInt(split[2]);
for (int m = from; m <= to; m += step) {
nums[m] = (int) (((long) nums[m] * next.getValue()) % MOD);
}
}
int res = 0;
for (int num : nums) {
res ^= num;
}
return res;
}
private void cacheHandle(int from, int to, int step, int ratio, Map<String, Integer> map) {
String key = from + ":" + to + ":" + step;
if (!map.containsKey(key)) {
map.put(key, ratio);
} else {
map.put(key, (int) (((long) map.get(key) * ratio) % MOD));
}
}
}
这个解法通过不了最后几个定制的测试用例,即使一分为二,基数还是太大了,我又想着能不能一分为三,将中间大部分共有的给抽出来做批量处理,感觉理论上是可行的,不过也是面向用例编程了。
看了官方的题解,和我的思路差别很大,根号分治+差分的解法,看到这两个词都想一键关机了。根号分治比较好理解,就是当遍历的数据范围不大时,采用暴力解法,大了就采用差分的方式,此题大小的判断使用的是步长与数组长度的平方根就行比较。差分大概的思路是将相同步长的数据合并在一起处理,有正向的乘,就有逆向的除,不过,这里的除不是真的用除法来实现的,而是逆元。具体关于差分的描述这里不多赘述,我不是专业的,也阐述不清楚,哈哈哈。
下面代码实现我贴了官方的解答,其中pow(v, MOD - 2) % MOD就是乘积后求余的反向逆元运算。
3. 代码实现
class Solution {
private static final int MOD = 1_000_000_007;
private int pow(long x, long y) {
long res = 1;
while (y > 0) {
if ((y & 1) == 1) {
res = res * x % MOD;
}
x = x * x % MOD;
y >>= 1;
}
return (int) res;
}
public int xorAfterQueries(int[] nums, int[][] queries) {
int n = nums.length;
int T = (int) Math.sqrt(n);
List<List<int[]>> groups = new ArrayList<>(T);
for (int i = 0; i < T; i++) {
groups.add(new ArrayList<>());
}
for (int[] q : queries) {
int l = q[0], r = q[1], k = q[2], v = q[3];
if (k < T) {
groups.get(k).add(new int[]{l, r, v});
} else {
for (int i = l; i <= r; i += k) {
nums[i] = (int) ((long) nums[i] * v % MOD);
}
}
}
long[] dif = new long[n + T];
for (int k = 1; k < T; k++) {
if (groups.get(k).isEmpty()) {
continue;
}
Arrays.fill(dif, 1);
for (int[] q : groups.get(k)) {
int l = q[0], r = q[1], v = q[2];
dif[l] = dif[l] * v % MOD;
int R = ((r - l) / k + 1) * k + l;
dif[R] = dif[R] * pow(v, MOD - 2) % MOD;
}
for (int i = k; i < n; i++) {
dif[i] = dif[i] * dif[i - k] % MOD;
}
for (int i = 0; i < n; i++) {
nums[i] = (int) ((long) nums[i] * dif[i] % MOD);
}
}
int res = 0;
for (int x : nums) {
res ^= x;
}
return res;
}
}
4. 总结
这道题个人觉得浅尝辄止吧,其中涉及的数学运算不懂确实不懂。
不过有几个涉及求余的公式是有必要知道的,在其他算法题中也遇到过几次,不然多次相乘后可能超出有效数值范围,导致最后的结果错误。
乘积求余
(a * b) % c = ((a % c) * (b % c)) % c
商求余
(a / b) % MOD = (a % MOD) * inv(b) % MOD
其中inv(b)为逆元,inv(b) = pow (b, MOD-2, MOD)