题目描述

✅ 982. 按位与为零的三元组

image-20260929105459114

image-20260929105459392

题意分析

统计满足 nums[i] & nums[j] & nums[k] == 0 的下标三元组数量。三个下标分别独立选择,可以相同;交换下标位置后,只要三元组不同,就应单独计数。因此不能去重数组,也不能只枚举递增下标。

直接枚举三个下标需要 $O(n^3)$ 时间。按位与满足结合律,后一个数只关心前两个数与出来的结果,可以先把有相同中间结果的数对合并计数。

解法:有序数对计数 + 第三个数匹配

核心思路

[!blue]

定义 cnt[mask] 为满足 nums[i] & nums[j] == mask 的有序下标对数量。双层循环都遍历整个数组,包含 i == j,也会分别统计 (i, j) 和 (j, i)。虽然代码循环变量保存的是数值,但每个数组位置都会被访问,所以重复值对应的多个位置仍会增加各自的频次。

固定第三个位置的值 c 后,原条件变成 mask & c == 0,即二者没有共同为 1 的二进制位。某个 mask 满足条件时,产生它的所有有序数对都能与当前第三个位置组成合法三元组,因此应增加整个 cnt[mask],而不是只增加一次。

每个三元组都有唯一的第三个位置和唯一的前两数与结果,会被归入恰好一个这样的计数中;反过来,每次加入的数对与当前第三个位置都满足原条件。因此这种合并既不漏计,也不重复计算同一个下标三元组。

题目中的数都小于 $2^{16}$,两个数的与结果也只有低 16 位,所以中间状态最多有 $2^{16}$ 种。哈希表只保存实际出现的结果,第三层搜索规模由不同 mask 的数量决定。

解题步骤

  1. 双层枚举全部有序对,累加按位与结果频次。
  2. 逐个枚举第三个数。
  3. 遍历已出现的 mask,按位与为零则加入对应频次。
  4. 返回总计数。

mask == 0 可以与任意第三个数匹配;第三个数为零时,所有数对都能匹配。数组长度最多为 1000,三元组总数最多是 $n^3 = 10^9$,答案及数对频次都能用 int 保存。

代码实现

class Solution {
    public int countTriplets(int[] nums) {
        // cnt[mask] = 有多少个有序对 (i, j) 满足 nums[i] & nums[j] == mask。
        Map<Integer, Integer> cnt = new HashMap<>();

        for (int a : nums) {
            for (int b : nums) {
                int mask = a & b;

                cnt.put(mask, cnt.getOrDefault(mask, 0) + 1);
            }
        }

        int answer = 0;

        for (int c : nums) {
            for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
                // c 的每个 1 位都必须落在 mask 的 0 位上。
                if ((e.getKey() & c) == 0) {
                    answer += e.getValue();
                }
            }
        }

        return answer;
    }
}
func countTriplets(nums []int) int {
    // cnt[mask] = 有多少个有序对 (i, j) 满足 nums[i] & nums[j] == mask。
    cnt := make(map[int]int)
    for _, a := range nums {
        for _, b := range nums {
            cnt[a&b]++
        }
    }

    answer := 0
    for _, c := range nums {
        for mask, times := range cnt {
            // c 的每个 1 位都必须落在 mask 的 0 位上。
            if mask&c == 0 {
                answer += times
            }
        }
    }
    return answer
}

复杂度分析

  • 时间复杂度:期望 $O(n^2+nU)$,U 为实际出现的与结果数,$U≤min(n^2,2^{16})$。
  • 空间复杂度:$O(U)$。

关键点总结

[!green]

  • 归并的是相同中间结果,频次不能丢失。
  • 有序对包含 i=j,不使用三数之和的去重方式。
  • Java 判断位运算结果时需要将按位与表达式加括号。

易错点总结

[!yellow]

  • 只枚举 i<j:漏掉反向顺序和相同下标。
  • 匹配一个 mask 只加一:没有计入该结果对应的多个数对。
  • 先将输入去重:重复位置仍应分别计数。
  • 要求三个数中必须有零:例如 2、1、3 都非零,但三者按位与为零。

相似题目

题目 难度 关联与区别
454. 四数相加 II 中等 同样先累计两项组合的结果频次,再匹配后半部分,本题组合运算是按位与而非加法。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/63091914
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!