📌  相关文章
📜  来自Array的对的计数,总和等于其按位与的两倍(1)

📅  最后修改于: 2023-12-03 15:26:34.398000             🧑  作者: Mango

来自Array的对的计数,总和等于其按位与的两倍

在计算机科学中,位运算常常被用来提高程序效率。这里介绍一道经典的位运算题目——来自Array的对的计数,总和等于其按位与的两倍。

问题描述

给定一个整数数组 nums,求其中的数对 (i, j),满足 i < j 并且 nums[i] + nums[j] = nums[i] & nums[j] * 2。

解法
方法一

首先,我们来看一下题目中的等式:

nums[i] + nums[j] = nums[i] & nums[j] * 2

等号两边同时减去 nums[i],得到

nums[j] = nums[i] & nums[j] * 2 - nums[i]

我们可以发现,等式右边的部分是关于 nums[i] 和 nums[j] 的二元函数,而它的函数值只与它们的二进制表示有关。

考虑对于二进制的第 k 位(从右往左数第 k 位,从 0 开始),如果 nums[i] 和 nums[j] 在该位都为 1,那么它们的和在该位上的值就是 0,而按位与的值在该位上也是 1,按照题意得知 sum = nums[i] + nums[j] = 0。如果 nums[i] 和 nums[j] 在该位都为 0,那么它们的和在该位上的值就是 0,按位与的值在该位上也是 0,按照题意得知 sum = 0。否则,在该位上 nums[i] 和 nums[j] 必有一个为 1,一个为 0,它们的和在该位上的值就是 1,按位与的值在该位上也是 0,按照题意得知 sum = nums[i] + nums[j]。

因此,对于每一位,我们只需要分别统计数组中所有数在该位上的 1 的个数,以及在该位上 nums[i] & nums[j] * 2 的二进制表示中 1 的个数,就可以统计出所有满足条件的数对数目。注意到由于题目要求 i < j,因此我们在统计 nums[i] 的时候只需要遍历到 j-1。

举个例子,假设数组 nums 中有两个元素 nums[i] 和 nums[j],它们的二进制表示如下:

nums[i] = 101101 nums[j] = 111010

我们来统计它们在第 2 位上的 1 的个数:nums[i] 在该位上有 1,nums[j] 在该位上也有 1,由于 nums[i] + nums[j] = nums[i] & nums[j] * 2,因此 sum = 0。接着我们来统计在第 3 位上 nums[i] & nums[j] * 2 二进制表示中 1 的个数:nums[i] 在该位上有 0,nums[j] 在该位上有 1,按照题意得知 sum = nums[i] + nums[j] = 101101 + 111010 = 1011111。

下面是具体的实现代码:

class Solution:
    def countPairs(self, nums: List[int]) -> int:
        res = 0
        for k in range(30):
            # 统计所有数在第 k 位上的 1 的个数
            c1 = sum(1 for num in nums if num & (1 << k))
            for i in range(len(nums)-1):
                # 只统计 j > i 的情况
                for j in range(i+1, len(nums)):
                    # 统计在第 k 位上 nums[i] & nums[j] * 2 二进制表示中 1 的个数
                    if (nums[i] & nums[j] * 2 - nums[i]) & (1 << k):
                        res += 1
            # 统计所有满足条件的数对数目,注意要除以 2,因为每个数对会被统计两次
            res += c1 * (c1 - 1) // 2
        return res

时间复杂度为 O(n^2 log C),其中 C 表示数组中最大的数,主要由于要遍历所有的数对以及处理二进制表示,空间复杂度为 O(1)。

方法二

方法一的时间复杂度最高达到了 O(n^3),可以发现大量时间的浪费在了遍历数组上。因此,我们通过将原式稍微变形来规避遍历数组的过程。具体地,将原式左右两边同时除以 nums[j],得到:

nums[i] / nums[j] + 1 = (nums[i] & nums[j] * 2) / nums[j] + 1

由于 nums[j] ≠ 0,因此等式左右两边的商均有意义。

注意到等式右边的部分被除数为 nums[j] 的整数倍,因此如果 nums[i] 是 nums[j] 的倍数,那么 nums[i] 一定满足要求;否则,我们可以将等式右边的部分写成整除和余数的和的形式:

(nums[i] & nums[j] * 2) / nums[j] + 1 = (nums[i] & nums[j] * 2) // nums[j] + (nums[i] & nums[j] * 2) % nums[j] / nums[j] + 1

显然,如果 nums[i] & nums[j] * 2 < nums[j],那么等式右边的第一个加数就是 0,sum = nums[i] & nums[j] * 2 - nums[i];否则,等式右边的第二个加数就是 0,sum = nums[i] & nums[j] * 2 / nums[j] - 1,即 nums[i] / nums[j] - 1。

具体的实现代码如下:

class Solution:
    def countPairs(self, nums: List[int]) -> int:
        cnt = [0] * (1 << 21)
        for num in nums:
            cnt[num] += 1
        res = 0
        for j in range(len(nums)):
            for i in range(nums[j] // 2 + 1):
                if cnt[i] and i != nums[j] - i:
                    if (nums[j] - i) * 2 & nums[j]:
                        res += cnt[i] * cnt[nums[j] - i]
                        if i == 0 or nums[j] - i == 0:  # 除去重复计算的情况
                            res -= cnt[i] * cnt[nums[j] - i]
                    elif i == 0 or nums[j] - i == 0:
                        res += cnt[i] * (cnt[i] - 1) // 2
        return res

时间复杂度为 O(n log C),数组 cnt 中最多有 O(C) 个非零值,因此空间复杂度为 O(C)。

总结

本文介绍了一道经典的位运算题目——来自Array的对的计数,总和等于其按位与的两倍,提供了两种解法。第一种解法时间复杂度为 O(n^2 log C),第二种解法时间复杂度为 O(n log C),两种解法在空间复杂度上都是 O(C)。当然,我们也可以通过位运算将解法二中的除法转化为位运算,从而进一步提升效率。