Hi 你们好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。git
这里是第 170 期的第 2 题,也是题目列表中的第 1310 题 -- 『子数组异或查询』github
有一个正整数数组 arr
,现给你一个对应的查询数组 queries
,其中 queries[i] = [Li, Ri]
。shell
对于每一个查询 i
,请你计算从 Li
到 Ri
的 __XOR__ 值(即 arr[Li] xor arr[Li+1] xor ... xor arr[Ri]
)做为本次查询的结果。segmentfault
并返回一个包含给定查询 queries
全部结果的数组。数组
示例 1:优化
输入:arr = [1,3,4,8], queries = [[0,1],[1,2],[0,3],[3,3]] 输出:[2,7,14,8] 解释: 数组中元素的二进制表示形式是: 1 = 0001 3 = 0011 4 = 0100 8 = 1000 查询的 XOR 值为: [0,1] = 1 xor 3 = 2 [1,2] = 3 xor 4 = 7 [0,3] = 1 xor 3 xor 4 xor 8 = 14 [3,3] = 8
示例 2:spa
输入:arr = [4,8,2,10], queries = [[2,3],[1,3],[0,0],[0,3]] 输出:[8,0,4,4]
提示:code
1 <= arr.length <= 3 * 10^4
1 <= arr[i] <= 10^9
1 <= queries.length <= 3 * 10^4
queries[i].length == 2
0 <= queries[i][0] <= queries[i][1] < arr.length
MEDIUMblog
这又是一道很是直白的题目。数据提供了一个 queries
数组,其中每个 query 其实就是在给定的 arr
数组中划定一个范围,而后咱们须要作的计算就是把这个范围内的全部数字进行异或(xor)运算,最终获得这个 query 的结果。排序
简单粗暴,没什么奇怪的装饰和描述。那么就先上直接方案,brute force,奥利给,淦了!
其实这里没有什么须要额外分析的,就是根据题目描述淦就完事了。具体流程以下:
const xorQueries = (arr, queries) => { const ret = new Uint16Array(queries.length); for (let i = 0; i < queries.length; ++i) { let val = 0; for (let j = queries[i][0]; j <= queries[i][1]; ++j) { val ^= arr[j]; } ret[i] = val; } return ret; };
因为是 brute force,时间天然不会理想,跑到了 800ms+。
原本想借着和小伙伴出去玩开溜,不过良心是在有点看不下去。摸摸猪鼻子,咱们换个思路再来一次。
看着 queries
里的一大堆范围,小猪不禁的想到了小时候学校门口的小卖部里那些好吃的小浣熊干脆面,以及小卖部的那个小窗户。等等,小窗户...窗口...滑动窗口...妙啊,咱们能够用滑动窗口的思路来解决这个问题。小猪真是个想象力丰富的宝宝,嘤嘤嘤 >.<
先解释一下这里滑动窗口的思路吧。假设当前已经基于范围 [x1, y1]
计算出了咱们的目标值 v1
,接下来咱们想计算范围 [x2, y2]
的目标值,那么其实彻底能够不用从新计算全部内容,只须要把当前窗口的左边界从 x1
移动到 x2
,把右边界从 y1
移动到 y2
便可。具体到针对 v1
值的变化便是配合边界的移动进行值的运算,而刚好咱们须要作的异或操做是一个执行两次就至关于撤销的操做。因而能够很是方便的进行 v1
到 v2
的计算。
为了让咱们的滑动行为相比于直接计算更加有优点,这时候须要各个目标窗口最好是有必定的顺序,这样就不会出现一会儿很大幅度的滑动,以及很是浪费的来回滑动。因此咱们会先对 queries
进行一个排序。可是最后的返回结果须要是符合题目给定数据的顺序,因此咱们不能直接修改 queries
原地排序,只能新开一个空间进行排序。
那么具体流程以下:
queries
数组,并按照范围的开始点和结束点来进行排序遍历已排序过的数组,进行窗口的滑动,并记录每个窗口的计算值
queries
数组的顺序赋值计算值基于以上流程,咱们能够实现相似下面的代码:
const xorQueries = (arr, queries) => { const ret = new Uint32Array(queries.length); const map = new Map(); // 复制原始数组,并按照左边界从小到大排序,若是左边界相同,再按照右边界从小到大排序 const sorted = [...queries].sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]); let val = left = right = 0; for (let i = 0; i < sorted.length; ++i) { const [start, end] = sorted[i]; // 移动左边界 while (left < start) val ^= arr[left++]; // 移动右边界,须要区分两种状况,由于是基于左边界排序的,因此新的右边界可能比以前的右边界小 while (right <= end) val ^= arr[right++]; while (right > end + 1) val ^= arr[--right]; map.set(left + '-' + (right - 1), val); } for (let i = 0; i < queries.length; ++i) { ret[i] = map.get(queries[i][0] + '-' + queries[i][1]); } return ret; };
这个代码的时间大约能跑到 400ms+,说明咱们的优化思路确实起到了做用,不过还不够。We need more!
上面的思路已经提到了一点,即咱们需求的异或操做,针对这个操做咱们能够看看它的一些特性:
(4) === (3 ^ 4 ^ 3) (4 ^ 5) === (3 ^ 4 ^ 5 ^ 3) (4 ^ 5) === (2 ^ 3 ^ 4 ^ 5 ^ 2 ^ 3)
不知道这样写完小伙伴们有没有发现一件事情,也就是咱们的目标范围 [x, y]
的运算值其实能够转化为 [start, x) ^ [start, y]
。
而后咱们再看,若是从 0 开始遍历 arr
,咱们能够很容易的获得从 0 开始的不断累积各个数组值的异或运算值。换句话说就是咱们能够很容易的计算出 [0, n]
这个范围的值。那么结合上面的那个转化,对于 [x, y]
这个范围其实能够经过 [0, x) ^ [0, y]
来计算获得。
到此,咱们能够整理出这个思路的具体流程:
arr
获得各个从 0 开始的范围的目标运算值queries
,针对每一个具体的 query 范围,根据上面的转化方式求得运算值是否是一会儿简单了好多。而且这里还有个小优化,咱们能够直接在 arr
数组中记录从 0 开始的累积运算值,从而不须要额外的储存空间。
基于以上流程,咱们能够实现相似下面的代码:
const xorQueries = (arr, queries) => { const ret = new Uint32Array(queries.length); for (let i = 1; i < arr.length; ++i) { arr[i] ^= arr[i - 1]; } for (let i = 0; i < queries.length; ++i) { ret[i] = arr[queries[i][1]]; queries[i][0] !== 0 && (ret[i] = arr[queries[i][0] - 1] ^ ret[i]); } return ret; };
到这里,咱们的时间复杂度下降到了 O(n),额外的空间使用下降到了 O(1)。应该已经到比较极限啦。
这也是一道内容简单粗暴的题,两次的思路转换都是基于一些题目数据的特性进行的。在实际的生产环境中,其实相似的状况还有不少,即根据具体需求的一些特性,咱们每每能找到更优秀的处理方法。