最近,在 leetcode 上做了两道题都是关于前缀异或的:1310. 子数组异或查询、1442. 形成两个异或相等数组的三元组数目
前缀异或简介
给定一个数组arr,当需要频繁进行计算 arr 中 给定区间 [i, j] 内所有元素的异或时,直接使用暴力求解往往效率不高,此时可以考虑使用前缀异或。
1、前缀异或数组
首先,需要构造一个前缀异或数组 xors,其特点如下:
-
xors.length = arr.length + 1
为了统一写法(即,不用单独i = 0 的情况),这里把 xors 长度初始化为 arr.length + 1。如果 xors.lenght == arr.lenght ,计算 [i, j] 内异或时,需要单独考虑 i == 0 情况。
-
xors[i] = 0
-
x o r s [ i ] = a r r [ 0 ] ⊕ a r r [ 1 ] ⊕ . . . ⊕ a r r [ i − 1 ] xors[i] = arr[0] \oplus arr[1] \oplus ... \oplus arr[i - 1] xors[i]=arr[0]⊕arr[1]⊕...⊕arr[i−1] // arr数组中索引在 [0, i) 区间内所有元素的异或 ⊕ \oplus ⊕
具体构造过程,见如下代码:
/* 1、构造前缀异或数组 */
// xors[i] = arr中索引在[0, i) 内所有元素的异或
int[] xors = new int[arr.length + 1]; // 统一写法,避免讨论i=0;
xors[0] = 0; // 0 ^ i = i
for (int i = 1; i < xors.length; i++) {
xors[i] = xors[i - 1] ^ arr[i - 1];
}
2、求 [i, j] 内元素的异或
令 数组 arr 中 [i, j] 内所有元素的异或为
S
i
j
S_{ij}
Sij。
即,
S
i
j
=
a
r
r
[
i
]
⊕
a
r
r
[
i
+
1
]
⊕
.
.
.
⊕
a
r
r
[
j
]
S_{ij} = arr[i] \oplus arr[i + 1] \oplus ... \oplus arr[j]
Sij=arr[i]⊕arr[i+1]⊕...⊕arr[j]
由于
x
o
r
s
[
i
]
=
a
r
r
[
0
]
⊕
a
r
r
[
1
]
⊕
.
.
.
⊕
a
r
r
[
i
−
1
]
xors[i] = arr[0] \oplus arr[1] \oplus ... \oplus arr[i - 1]
xors[i]=arr[0]⊕arr[1]⊕...⊕arr[i−1],
以及异或的两个性质:
- i ⊕ i = 0 i \oplus i = 0 i⊕i=0;
- 0 ⊕ i = i 0 \oplus i = i 0⊕i=i
所以 S i j = a r r [ i ] ⊕ a r r [ i + 1 ] ⊕ . . . ⊕ a r r [ j ] S_{ij} = arr[i] \oplus arr[i + 1] \oplus ... \oplus arr[j] Sij=arr[i]⊕arr[i+1]⊕...⊕arr[j]
即, S i j = ( a r r [ 0 ] ⊕ a r r [ 1 ] ⊕ . . . ⊕ a r r [ i − 1 ] ) ⊕ ( a r r [ 0 ] ⊕ a r r [ 1 ] ⊕ . . . ⊕ a r r [ i − 1 ] ) ⊕ ( a r r [ i ] ⊕ a r r [ i + 1 ] ⊕ . . . ⊕ a r r [ j ] ) S_{ij} = (arr[0] \oplus arr[1] \oplus ... \oplus arr[i - 1] ) \oplus (arr[0] \oplus arr[1] \oplus ... \oplus arr[i - 1] ) \oplus (arr[i] \oplus arr[i + 1] \oplus ... \oplus arr[j]) Sij=(arr[0]⊕arr[1]⊕...⊕arr[i−1])⊕(arr[0]⊕arr[1]⊕...⊕arr[i−1])⊕(arr[i]⊕arr[i+1]⊕...⊕arr[j])
即, S i j = ( a r r [ 0 ] ⊕ a r r [ 1 ] ⊕ . . . ⊕ a r r [ i − 1 ] ) ⊕ ( a r r [ 0 ] ⊕ a r r [ 1 ] ⊕ . . . ⊕ a r r [ i − 1 ] ⊕ ( a r r [ i ] ⊕ a r r [ i + 1 ] ⊕ . . . ⊕ a r r [ j ] ) ) S_{ij} = (arr[0] \oplus arr[1] \oplus ... \oplus arr[i - 1] ) \oplus (arr[0] \oplus arr[1] \oplus ... \oplus arr[i - 1] \oplus (arr[i] \oplus arr[i + 1] \oplus ... \oplus arr[j])) Sij=(arr[0]⊕arr[1]⊕...⊕arr[i−1])⊕(arr[0]⊕arr[1]⊕...⊕arr[i−1]⊕(arr[i]⊕arr[i+1]⊕...⊕arr[j])) 【异或结合律】
即,$S_{ij} = x o r s [ i ] ⊕ x o r s [ j + 1 ] xors[i] \oplus xors[j + 1] xors[i]⊕xors[j+1]
xors[i] = arr[0, i) 内所有元素的异或,这里是左闭右开区间。 S i j S_{ij} Sij中是左闭右闭区间。
例子
有一个正整数数组 arr,现给你一个对应的查询数组 queries,其中 queries[i] = [Li, Ri]。
对于每个查询 i,请你计算从 Li 到 Ri 的 XOR 值(即 arr[Li] xor arr[Li+1] xor … xor arr[Ri])作为本次查询的结果。
并返回一个包含给定查询 queries 所有结果的数组。
public int[] xorQueries(int[] arr, int[][] queries) {
// xors[i]表示arr中索引在[0, i) 内所有元素的异或
int[] xors = new int[arr.length + 1]; /** !!! 注意和solution的区别 */
xors[0] = arr[0];
for (int i = 1; i < xors.length; i++) { // 构造前缀异或数组 xors
xors[i] = xors[i - 1] ^ arr[i - 1];
}
int[] ans = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int l = queries[i][0]; // 左闭右闭区间
int r = queries[i][1];
/** !!! 注意和solution的区别 */
ans[i] = xors[l] ^ xors[r + 1]; // 统一写法
}
return ans;
}
如果 xors.length = arr.lenght,则此时 xors[i] 表示arr中索引在 [0, i] 内所有元素的异或,此时计算 S i j S_{ij} Sij 时需要单独考虑 i == 0情况。【不推荐这样写,还是初始化 xors.length = arr.lenght + 1,统一写法】
代码如下:
public int[] xorQueries(int[] arr, int[][] queries) {
// xors[i]表示arr中索引在 [0, i] 内所有元素的异或
int[] xors = new int[arr.length];
xors[0] = arr[0];
for (int i = 1; i < xors.length; i++) { // 构造前缀异或数组 xors
xors[i] = xors[i - 1] ^ arr[i]; // 区别1
}
int[] ans = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int l = queries[i][0]; // 左闭右闭区间
int r = queries[i][1];
if (l == 0) { // 区别2:单独处理l==0
ans[i] = xors[r];
} else { // l > 0
// 区别3
ans[i] = xors[l - 1] ^ xors[r]; // 用到了异或的结合律、x ^ x == 0
}
}
return ans;
}
解法一:三层 for
由于arr.length范围最大是 300,可以三重暴力枚举i、j、k
- 构造前缀异或数组xors
- 三层 for 暴力枚举i、j、k
- 利用前缀异或数组 计算a、b。注意:[左开右闭区间)
- int a = xors[i] ^ xors[j]; // [i, j) 内所有元素的异或
- int b = xors[j] ^ xors[k + 1]; // [j, k],即[j, k + 1) 内所有元素的异或
- 判断条件优化
- 原始条件:a == b
- 即,xors[i] ^ xors[j] == xors[j] ^ xors[k + 1]
- 也即, xors[i] == xors[k + 1]
public int countTriplets(int[] arr) {
/* 1、构造前缀异或数组 */
// xors[i] = [0, i) 内所有元素的异或
int[] xors = new int[arr.length + 1]; // 统一写法,避免讨论i=0
xors[0] = 0;
for (int i = 1; i < xors.length; i++) {
xors[i] = xors[i - 1] ^ arr[i - 1];
}
/* 2、由于arr.length范围最大是 300,可以三重暴力枚举i、j、k */
// 1)暴力枚举i、j、k
int ans = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
for (int k = j; k < arr.length; k++) {
// 2)利用前缀异或数组 计算a、b。注意:[左开右闭区间)
// int a = xors[i] ^ xors[j]; // [i, j) 内所有元素的异或
// int b = xors[j] ^ xors[k + 1]; // [j, k],即[j, k + 1) 内所有元素的异或
// if (a == b) {
// ans++;
// }
/**
* <优化>
* a == b
* 即,xors[i] ^ xors[j] == xors[j] ^ xors[k + 1]
* 也即, xors[i] == xors[k + 1]
* */
if (xors[i] == xors[k + 1]) {
ans++;
}
}
}
}
return ans;
}
解法二: 两层 for
- 由于解法一中<优化>后的判断条件为:if (xors[i] == xors[k + 1]) ans++;
- 因此,当满足 xors[i] == xors[k + 1] 时,j 只要在 [i+1, k] 内都是满足要求的
- 所以,只用两层枚举 i 和 k即可。即,三重for -> 两层for <两层for>
public int countTriplets(int[] arr) {
/* 1、构造前缀异或数组 */
// xors[i] = [0, i) 内所有元素的异或
int[] xors = new int[arr.length + 1]; // 统一写法,避免讨论i=0
xors[0] = 0;
for (int i = 1; i < xors.length; i++) {
xors[i] = xors[i - 1] ^ arr[i - 1];
}
/* 2、e重暴力枚举i、k */
// 1)暴力枚举i、k
int ans = 0;
for (int i = 0; i < arr.length; i++) {
for (int k = i + 1; k < arr.length; k++) {
// 2)利用前缀异或数组 计算a、b。注意:[左开右闭区间)
// int a = xors[i] ^ xors[j]; // [i, j) 内所有元素的异或
// int b = xors[j] ^ xors[k + 1]; // [j, k],即[j, k + 1) 内所有元素的异或
// if (a == b) {
// ans++;
// }
/**
* <优化> a == b 即,xors[i] ^ xors[j] == xors[j] ^ xors[k + 1] 也即, xors[i] ==
* xors[k + 1]
*/
if (xors[i] == xors[k + 1]) {
// ans++;
// !!! 在此条件下,任意 j ∈ [i + 1, k] 都满足条件
ans += (k - i);
}
}
}
return ans;
}