异或运算的基本性质:
- 相同数值异或,结果为 0
- 任意数值与 0 进行异或,结果为数值本身
- 异或本身满足交换律
应用1: 找出只出现一次的1个数字
先来一个简单的例子, 例如在一个数组中除了一个数字出现了一次, 数组中的其他数字都出现了两次,这里我们可以直接想到用哈希表的做法,这样的空间复杂度为 $O(n)$ , 思考常量空间的做法, 这时就可以用到异或运算了, 异或运算中两个相同的数进行异或结果为0, 故我们的思路直接将整个数组异或一遍最后的结果即为那个只出现一次的那个数字, 这里进行扩展的话, 那就是在一个数组中, 找到一个只出现奇数次的数字, 其他数字出现的次数均为偶次数的情况.
int soulution(vector<int> &nums)
{
int res = 0;
for(auto val : nums) res ^= val; // 整个数组直接异或一遍
return res;
}
应用2: 找出数组中只出现一次的两个数字
题目链接: 73. 数组中只出现一次的两个数字
题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了两次。
请写程序找出这两个只出现一次的数字。
你可以假设这两个数字一定存在。
样例
输入:[1,2,3,3,4,4]
输出:[1,2]
思路分析
这种情况下的只出现一次的数字有两个,如果直接将整个数组异或一遍, 最后得到的结果是我们要找的两个数异或的结果. 那么如何将问题转化成上面应用1的情况, 那么我们就要把这两个数分开, 分到不同的两组去, 那么划分的标准是什么呢, 目前有的数据是这两个数异或后的结果, 而异或后结果为1的位上,说明原本这两个数在这个位上的数值就不一样, 故我们可以以它们二进制表示中的第k位为1或者为0将它们分开. 具体思路就是从低位开始判断这两个数异或的结果的位数那一位为1,记此时第k位为1, 由于这是不同的两个数,其异或结果必然存在某位为1, 那么我们将整个数组中的数划分为两个类, 第k位为1和第k位为0的, 那么就将这两个数分开来,其他被分到这两类中的数,由于必然是都出现了两次的,故其中一个类所有数的异或结果就是我们要找的其中一个数, 对于另外一个数我们也不必再求一遍,此时将找到的数异或上原本两个数异或的结果就得到了第二个我们要找的数。
C++ 代码
class Solution
{
public:
vector<int> findNumsAppearOnce(vector<int>& nums)
{
int sum = 0;
for (auto x: nums) sum ^= x;
int k = 0;
while (!(sum >> k & 1)) k ++ ; // 找到此时第k位为1
int X = 0;
for (auto x: nums)
if (x >> k & 1) X ^= x; // 将此时第k位为1的数异或一遍,最后的x即为其中一个数
return {X, sum ^ X};
}
};
应用3: LC137. 只出现一次的数字 II
题目链接 Lc 137. 只出现一次的数字 II
题目描述
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
样例
输入:nums = [2,2,3,2]
输出:3
输入:nums = [0,1,0,1,0,1,99]
输出:99
提示
- 1 <= nums.length <= 3 * 104
- $-2^{31}$ <= nums[i] <= $2^{31}$ - 1
- nums 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次
思路分析(转载)
这道题力扣的官方题解的最优做法就是逻辑电路的一个做法,其中就用到了异或运算,这里并不说明那个方法,因为确实要自己推导,故这里说一个比较简单的方法,且空间复杂度也为$O(1)$的一个做法。 那就是利用 int类型固定为 32 位,使用一个长度为 32的数组 cnt[] 记录下所有数值的每一位共出现了多少次,再对 cnt[]数组的每一位进行 mod3 操作,重新拼凑出只出现一次的数值。
举个 🌰,考虑样例 [1,1,1,3],11 和 33 对应的二进制表示分别是 00..001 和 00..011,存入 cnt[]cnt[] 数组后得到 [0,0,…,0,1,4]。进行 modmod 33 操作后得到 [0,0,…,0,1,1],再转为十进制数字即可得「只出现一次」的答案 33。
参考自三叶大佬: 【宫水三叶】一题三解:「哈希表」&「位数统计」&「DFA」
宫水三叶
C++代码
// 位数统计的方法
class Solution
{
public:
int singleNumber(vector<int>& nums)
{
int cnt[32] = {0};
for(auto val : nums)
for(int i = 0; i < 32; ++i)
if(val >> i & 1) cnt[i]++; // 记录数组每一个数组在每一位上出现的次数
int res = 0;
for(int i = 0; i < 32; ++i)
if(cnt[i] % 3) res |= 1 << i; // 若那一位 mod 3之后不为0,说明我们要找的数字这一位必然为1
return res;
}
};
应用4: 解码异或后的排列
题目链接: 1734. 解码异或后的排列
题目描述
给你一个整数数组 perm ,它是前 n 个正整数的排列,且 n 是个 奇数 。
它被加密成另一个长度为 n - 1 的整数数组 encoded ,满足 encoded[i] = perm[i] XOR perm[i + 1] 。比方说,如果 perm = [1,3,2] ,那么 encoded = [2,1] 。
给你 encoded 数组,请你返回原始数组 perm 。题目保证答案存在且唯一。
样例
输入:encoded = [1,2,3], first = 1
输出:[1,0,2,1]
解释:若 arr = [1,0,2,1] ,那么 first = 1 且 encoded = [1 XOR 0, 0 XOR 2, 2 XOR 1] = [1,2,3]
思路分析
C++代码
class Solution
{
public:
vector<int> decode(vector<int>& encoded)
{
vector<int> res;
int n = encoded.size();
int t1 = 0, t2 = 0;
for(int i = 1; i <= n + 1; ++i) t1 ^= i; // 得到perm数组的异或和
for(int i = 1; i < n; i += 2) t2 ^= encoded[i]; // 得到除了perm第一个元素外的异或和
res.push_back(t1 ^ t2); // 得到perm数组的第一个元素
for(int i = 0; i < n; ++i) res.push_back(encoded[i] ^ res[i]); // 递推得到原数组
return res;
}
};
应用5: 最大异或和
题目链接: Acwing 3485. 最大异或和
题目描述
给定一个非负整数数列 a,初始长度为 N。
请在所有长度不超过 M 的连续子数组中,找出子数组异或和的最大值。
子数组的异或和即为子数组中所有元素按位异或得到的结果。
注意:子数组可以为空。
提示
输入格式
第一行包含两个整数 N,M。
第二行包含 N 个整数,其中第 i 个为 ai。
输出格式
输出可以得到的子数组异或和的最大值。
数据范围
对于 20% 的数据, 1≤ M ≤ N ≤ 100
对于 50% 的数据, 1≤ M ≤ N ≤ 1000
对于 100% 的数据,1≤ M ≤ N ≤ $10^5$,0 ≤ai ≤ $2^{31}$−1
样例
输入样例:
3 2
1 2 4
输出样例:
6
思路分析
- 首先思考朴素做法, 由于是考虑所有长度不超过 M 的连续子数组中,找出子数组异或和的最大值,故这里我们外层循环i 表示起点, 内存循环 j 表示距离起点i长度 ≤ M 的坐标, 这样算法时间复杂度为 $O(n^2)$, 而这里1≤ M ≤ N ≤ $10^5$, 显然朴素做法应该会TLE, 故这里我们需要进行优化.
C++朴素算法代码
#include<iostream>
using namespace std;
constexpr int N = 1e+6;
int q[N], f[N];
auto main() -> int
{
ios::sync_with_stdio(false);
int n, m; cin >> n >> m;
for(int i = 1; i <= n; ++i) cin >> f[i];
for(int i = 1; i <= n; ++i)
{
int pre = 0;
for(int j = i; j <= i + m - 1; ++j) pre ^= f[j], res = max(res, pre);
}
cout << res << endl;
return 0;
}
- 这里由于是要求一段区间的异或和, 故我们可以考虑用前缀和的方式, 在$O(1)$的时间内求出一区间的异或和, 目前朴素算法的复杂度为$O(n^2)$, 需要优化掉一维,考虑优化成$O(nlogn)$, 这里我们可以用这道题 Acwing 143.最大异或对 的做法来进行优化, 此时我们已经通过前缀和将一段区间的异或和处理成两个数异或的结果,故我们可以用Tire树在$O(logn)$的时间内找到一个最大的异或对, 故这里我们只需要加入区间长度M的限制即可, 即长度不在M区间内的我们将其从树上删除。
C++ Tire 树 + 前缀和优化算法
#include<iostream>
using namespace std;
constexpr int N = 31 * 1e+5;
int s[N], son[N][2], cnt[N], idx;
auto insert(int val, int v)
{
int p = 0;
for(int i = 30; i >= 0; --i)
{
int x = val >> i & 1;
if(!son[p][x]) son[p][x] = ++idx;
p = son[p][x];
cnt[p] += v; // 标记该结点是否被使用
}
}
auto query(int val) -> int
{
int p = 0, res = 0;
for(int i = 30; i >= 0; --i)
{
int x = val >> i & 1;
if(cnt[son[p][!x]]) p = son[p][!x], res = res * 2 + 1;
else p = son[p][x], res = res * 2;
}
return res;
}
auto main() -> int
{
ios::sync_with_stdio(false);
int n, m; cin >> n >> m;
for(int i = 1; i <= n; ++i) cin >> s[i], s[i] ^= s[i - 1];
int res = 0;
insert(s[0], 1);
for(int i = 1; i <= n; ++i)
{
if(i > m) insert(s[i - m - 1], -1); // 删除
res = max(res, query(s[i]));
insert(s[i], 1);
}
cout << res << endl;
return 0;
}
写的好棒佬!