这个题目我也是绕了很久才想明白其核心的思想,以下给出我对于该题目解法的理解。
关键字:位运算、枚举+子集优化
题目:按位与为0的三元组
给你一个整数数组 nums ,返回其中 按位与三元组 的数目。
按位与三元组 是由下标 (i, j, k) 组成的三元组,并满足下述全部条件:
0 <= i < nums.length
0 <= j < nums.length
0 <= k < nums.length
nums[i] & nums[j] & nums[k] == 0 ,其中 & 表示按位与运算符。
示例 1:
输入:nums = [2,1,3]
输出:12
解释:可以选出如下 i, j, k 三元组:
(i=0, j=0, k=1) : 2 & 2 & 1
(i=0, j=1, k=0) : 2 & 1 & 2
(i=0, j=1, k=1) : 2 & 1 & 1
(i=0, j=1, k=2) : 2 & 1 & 3
(i=0, j=2, k=1) : 2 & 3 & 1
(i=1, j=0, k=0) : 1 & 2 & 2
(i=1, j=0, k=1) : 1 & 2 & 1
(i=1, j=0, k=2) : 1 & 2 & 3
(i=1, j=1, k=0) : 1 & 1 & 2
(i=1, j=2, k=0) : 1 & 3 & 2
(i=2, j=0, k=1) : 3 & 2 & 1
(i=2, j=1, k=0) : 3 & 1 & 2
示例 2:
输入:nums = [0,0,0]
输出:27
思维流程:
观察示例一,我们可以很自然的想到三重循环的暴力解法,但是这种方式显然是不行的。参考了官方的解法和排行第二的解法,以下给出一种更为直观和通俗的理解方法。
(1) 二进制子集的概念
把二进制数看成集合,二进制从低到高第 i 位为 1 表示 i 在集合中,为 0 表示 i 不在集合中,例如 a = 1101 --> { 0, 2, 3 }。
(2)补集的概念
A & B = 0,相当于用二进制子集表示的A集合与B集合没有交集,也可以理解为B是A的补集的子集,或者A是B补集的子集。本题中 U = { 0,1,2,…,15 } ,全集对应的数字就是 0xffff 。一个数字异或 0xffff 就可以得到它的补集。
懂了这两个概念再顺一遍整体的思维过程:
先写一个O(n^2)的枚举,计算所有的nums[ i ] & nums[ j ] 。它们相与的结果,也就是两个集合的交,一定是在全集U中的,所以需要一个 2^16 大小的数组,用来保存每种相与集合出现的次数。
然后再枚举全集中每种集合的补集,如果在之前的计算结果中出现了当前位置的补集,那么反向思考之前计算过的集合和当前枚举的集合互为补集,就构成了一个三元组,这三个集合相与就是0。
注意: 考虑空集 当 s = 0 时,再减一,又会回到全1的情况,补码的性质。
示例代码:
class Solution {
public:
int countTriplets(vector<int> &nums) {
int cnt[1 << 16]{};
//先枚举两个集合相与的结果
for (int x : nums)
for (int y : nums)
//相与后的结果对应数组的下标,数组中保存该集合出现的次数
++cnt[x & y];
int ans = 0;
for (int m : nums) {
//m表示全集
m ^= 0xffff;
//利用s变量,枚举所有可能的子集,s从全集开始取
int s = m;
do { // 枚举 m 的子集(包括空集)
ans += cnt[s];
//(s-1)&m 表示数组中从右往左,下一个集合的补集
//如果该补集在第一个循环中计算中出现过,那么就形成了按位与为0的三元组
s = (s - 1) & m;
} while (s != m);
}
return ans;
}
};