2026-01-17:完美对的数目。用go语言,给定一个整数数组 nums。我们把满足下面条件的一对索引 (i, j)(且 i < j)称为“完美对”:
- 记 a = nums[i],b = nums[j];
- 在两者差的绝对值 |a - b| 和和的绝对值 |a + b| 中,较小的那个不大于 a、b 的绝对值中的较小值;
- 在两者差的绝对值 |a - b| 和和的绝对值 |a + b| 中,较大的那个不小于 a、b 的绝对值中的较大值。 任务是统计数组中不同的这样的“完美对”的数量。说明:|x| 表示 x 的绝对值(非负数)。 2 <= nums.length <= 100000。 -1000000000 <= nums[i] <= 1000000000。 输入: nums = [-3,2,-1,4]。 输出: 4。 解释: 有 4 个完美对:
| (i, j) | (a, b) | min(|a - b|, |a + b|) | min(|a|, |b|) | max(|a - b|, |a + b|) | max(|a|, |b|) |
|---|---|---|---|---|---|
| (0, 1) | (-3, 2) | min(|-3 - 2|, |-3 + 2|) = min(5, 1) = 1 | min(|-3|, |2|) = min(3, 2) = 2 | max(|-3 - 2|, |-3 + 2|) = max(5, 1) = 5 | max(|-3|, |2|) = max(3, 2) = 3 |
| (0, 3) | (-3, 4) | min(|-3 - 4|, |-3 + 4|) = min(7, 1) = 1 | min(|-3|, |4|) = min(3, 4) = 3 | max(|-3 - 4|, |-3 + 4|) = max(7, 1) = 7 | max(|-3|, |4|) = max(3, 4) = 4 |
| (1, 2) | (2, -1) | min(|2 - (-1)|, |2 + (-1)|) = min(3, 1) = 1 | min(|2|, |-1|) = min(2, 1) = 1 | max(|2 - (-1)|, |2 + (-1)|) = max(3, 1) = 3 | max(|2|, |-1|) = max(2, 1) = 2 |
| (1, 3) | (2, 4) | min(|2 - 4|, |2 + 4|) = min(2, 6) = 2 | min(|2|, |4|) = min(2, 4) = 2 | max(|2 - 4|, |2 + 4|) = max(2, 6) = 6 | max(|2|, |4|) = max(2, 4) = 4 |
题目来自力扣3649。
🧠 算法步骤分解
整个算法的核心思路是,在将数组中的负数转换为正数并排序后,利用双指针技巧高效地统计满足特定数值条件的数对。
-
处理负数
- 操作:遍历整数数组
nums,如果某个元素x是负数,则将其替换为其绝对值(即x *= -1)。 - 目的:由于题目条件涉及绝对值,且完美对的条件对于
(a, b)和(-a, -b)等情形是等价的,将所有数转为非负数可以简化后续的比较和计算,避免符号带来的复杂性。这一步确保了数组中所有元素都是非负的。
- 操作:遍历整数数组
-
数组排序
- 操作:使用排序算法(代码中为
slices.Sort(nums))对处理后的非负数组进行升序排序。 - 目的:排序是为了让数值相近的元素聚集在一起,为后续使用双指针法创造条件。在一个有序的数组中,可以更高效地找到满足特定大小关系的数对。
- 操作:使用排序算法(代码中为
-
双指针统计完美对
- 操作:初始化一个指针
left指向数组起始位置(0)。然后遍历数组,将当前遍历到的元素nums[j]视为数对中的b。在遍历过程中,调整left指针的位置,使得从left到j-1的所有元素nums[i]都满足nums[i] * 2 >= b(或者说,跳过那些nums[i] * 2 < b的元素)。对于每个j,满足条件的i的范围就是left到j-1,因此完美对的数量增加(j - left)。 - 关键点:在排序后的非负数组中,完美对的条件可以简化为判断
a和b的大小关系。这里的逻辑是,对于固定的b,需要找到所有满足条件的a(a <= b),使得a和b的数值关系符合原始题目中关于绝对值和与差的条件。通过检查a * 2与b的关系,可以高效地筛选这些数对。指针left随着j的增大而单向向右移动,不会回退,这保证了高效性。
- 操作:初始化一个指针
⏱️ 复杂度分析
-
总的时间复杂度:O(n log n)。 这主要由排序步骤决定,因为排序的时间复杂度通常是 O(n log n)。后续的双指针遍历过程,
left指针和j指针各遍历数组一次,是线性的 O(n) 操作。因此,整个算法的总时间复杂度由排序步骤主导,为 O(n log n)。 -
总的额外空间复杂度:O(1)。 算法在执行过程中只使用了固定数量的额外变量(如
ans,left,j等),这些空间不随输入数组的大小n而变化。排序操作如果是在原数组上进行(如代码中的slices.Sort),通常也不需要额外的空间(即原地排序)。因此,额外的空间复杂度是常数级别的 O(1)。
Go完整代码如下:
package main
import (
"fmt"
"slices"
)
func perfectPairs(nums []int) (ans int64) {
for i, x := range nums {
if x < 0 {
nums[i] *= -1
}
}
slices.Sort(nums)
left := 0
for j, b := range nums {
for nums[left]*2 < b {
left++
}
// a=nums[i],其中 i 最小是 left,最大是 j-1,一共有 j-left 个
ans += int64(j - left)
}
return
}
func main() {
nums := []int{-3, 2, -1, 4}
result := perfectPairs(nums)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
from typing import List
def perfectPairs(nums: List[int]) -> int:
# 将所有数字转为非负
nums = [abs(x) for x in nums]
# 排序
nums.sort()
ans = 0
left = 0
for j, b in enumerate(nums):
# 移动左指针,找到满足 a*2 >= b 的最小索引
while left < j and nums[left] * 2 < b:
left += 1
# a = nums[i],其中 i 从 left 到 j-1,共 j-left 个
ans += j - left
return ans
def main():
nums = [-3, 2, -1, 4]
result = perfectPairs(nums)
print(result)
if __name__ == "__main__":
main()
C++完整代码如下:
#include <vector>
#include <algorithm>
#include <cstdlib> // for abs
#include <iostream>
long long perfectPairs(std::vector<int>& nums) {
// 将所有数字转为非负
for (int& num : nums) {
if (num < 0) {
num = -num;
}
}
// 排序
std::sort(nums.begin(), nums.end());
long long ans = 0;
int left = 0;
int n = nums.size();
for (int j = 0; j < n; ++j) {
int b = nums[j];
// 移动左指针,找到满足 a*2 >= b 的最小索引
while (left < j && nums[left] * 2 < b) {
++left;
}
// a = nums[i],其中 i 从 left 到 j-1,共 j-left 个
ans += j - left;
}
return ans;
}
int main() {
std::vector<int> nums = {-3, 2, -1, 4};
long long result = perfectPairs(nums);
std::cout << result << std::endl;
return 0;
}