问题描述 链接到标题
给你一个整数数组 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 <= nums.length <= 1000
0 <= nums[i] < 2¹⁶
解题思路 链接到标题
可以先用哈希表ump
存储nums[i] & nums[j]
的结果,再与nums[k]
按位与,如果结果为0,res += ump[nums[i] & nums[j]]
。
优化,我们把二进制看成集合,二进制从低到高第i
位为1表示i
在集合中,为0表示i
不在集合中,例如$a = 1101_{(2)}$表示集合$A={0,2,3}$;
那么,$a & b = 0$即集合$A$和集合$B$没有交集,或者说$B$是集合$\complement_U A$,这里$U={0,1,2,…,15}$,对应的数字即$0xffff$,一个数异或$0xffff$就能得到这个数的补集;
所以,代码可以优化成枚举$m = nums[k]\oplus 0xffff$的子集;
要枚举$m$的子集$s$,可以将$s$从$m$不断减一直到0,如果$s & m = s$,说明$s$是$m$的子集;
更高效的做法是直接“跳到”下一个子集,即$s$更新为$(s - 1)& m$,这样做的正确性在于,$s-1$只把$s$最低位的$1$改成了$0$(考虑$s$对应的集合),比这个$1$更低的$0$全都变成了$1$,因此下一个子集,一定是$s-1$的子集,直接$&m$,就能得到下一个子集;
最后,当$s=0$时,由于$-1$的二进制全为$1$,所有$(s-1)&m = m$,所以我们可以判断下一个子集是否又回到$m$,来判断是否要退出循环。
注:这一技巧常用于子集状态压缩DP中
代码 链接到标题
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 ^= 0xffff;
int s = m;
do { // 枚举 m 的子集(包括空集)
ans += cnt[s];
s = (s - 1) & m;
} while (s != m);
}
return ans;
}
};