📅  最后修改于: 2023-12-03 15:26:34.398000             🧑  作者: Mango
在计算机科学中,位运算常常被用来提高程序效率。这里介绍一道经典的位运算题目——来自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)。当然,我们也可以通过位运算将解法二中的除法转化为位运算,从而进一步提升效率。