Question 923.
Given an integer array A, and an integer target, return the number of tuples i, j, k such that i < j < k and A[i] + A[j] + A[k] == target.
As the answer can be very large, return it modulo 10^9 + 7.
Example 1:
Input: A = [1,1,2,2,3,3,4,4,5,5], target = 8
Output: 20
Explanation:
Enumerating by the values (A[i], A[j], A[k]):
(1, 2, 5) occurs 8 times;
(1, 3, 4) occurs 8 times;
(2, 2, 4) occurs 2 times;
(2, 3, 3) occurs 2 times.
Example 2:
Input: A = [1,1,2,2,2,2], target = 5
Output: 12
Explanation:
A[i] = 1, A[j] = A[k] = 2 occurs 12 times:
We choose one 1 from [1,1] in 2 ways,
and two 2s from [2,2,2,2] in 6 ways.
Note:
- 3 <= A.length <= 3000
- 0 <= A[i] <= 100
- 0 <= target <= 300
题目分析
tag
类别是 arrays and strings,特点是连续存储空间中 members 的调度。
题目
本题要在连续存储空间中寻找 3 个元素,要求 3 个元素的和等于目标值的个数,元素可能重复。
思路
示例:
A = [1,1,2,2,3,3,4,4,5,5]
target = 8
- 相比于之前的三数和不同条件的题目,本题叠加了相同元素组合数量的求解。总体思路是先排序,找到目标组合,获得组合中元素的数量计算组合由不同元素组成的数量,再加总所有可能性。
首先排序,A = [1,1,2,2,3,3,4,4,5,5];
接着取第一个元素 1,1 * 3 = 3,小于目标值 8。最小指针指向 1,最大指针指向 5,当前和 1 + 1 + 5 = 7,小于目标值 8,最小指针右移指向 2。当前和 1 + 2 + 5 = 8,等于目标值 8,可能组合记录 (1, 2, 5)。接着最大指针和最小指针分别向左、右移动跳过重复的元素,最小指针指向 3, 最大指针指向 4,当前和 1 + 3 + 4 = 8, 等于目标值 8,可能组合记录 (1, 3, 4)。接着最大指针和最小指针分别向左、右移动跳过重复的元素,最小指针指向 4, 最大指针指向 3,最小指针大于了最大指针,当前结束;
然后取第二个元素 1,等于前面的元素,跳过;
继续取第三个元素 2,2 * 3 = 6,小于目标值 8。最小指针指向 2,最大指针指向 5,当前和 2 + 2 + 5 = 9,大于目标值 8,最大指针左移跳过重复元素指向 4。当前和 2 + 2 + 4 = 8,等于目标值 8,可能组合记录 (2, 2, 4)。接着最大指针和最小指针分别向左、右移动跳过重复的元素,最小指针指向第一个 3, 最大指针指向第二个 3,当前和 2 + 3 + 3 = 8,等于目标值 8,可能组合记录 (2, 3, 3);
接着取第四个元素 2,等于前面的元素,跳过;
再取第五个元素 3,3 * 3 = 9,大于目标值 8,终止,当前可能组合记录是 (1, 2, 5), (1, 3, 4), (2, 2, 4), (2, 3, 3);
统计各个元素的个数 { 1: 2, 2: 2, 3: 2, 4: 2, 5: 2};
计算所有可能性:(1, 2, 5) 的可能性是 2 * 2 * 2 = 8,(1, 3, 4) 的可能性是 2 * 2 * 2 = 8,(2, 2, 4) 的可能性是 1 * 1 * 2 = 2,(2, 3, 3)的可能性是 1 * 1 * 2 = 2,总计 8 + 8 + 2 + 2 = 20。
算法复杂度是 O(n^2)
import sys
from typing import List
from collection import defaultdict
def three_sum_with_multiplicity(nums: List[int], target: int) -> int:
def _skip_left(left: int, right: int) -> int:
"""
略过重复的最小指针
"""
while left < right and nums[left] == nums[left + 1]:
left += 1
return left
def _skip_right(left: int, right: int) -> int:
"""
略过重复的最大指针
"""
while left < right and nums[right] == nums[right - 1]:
right -= 1
return right
# 初始化符合条件记录个数为 0
count = 0
# 根据题中条件返回 bound 的余数
bound = 10 ** 9 + 7
# 记录符合目标值的三个数的组合情况
record = []
# 输入 array 排序
nums.sort()
length = len(nums)
# 三个数之和,所以遍历到倒数第三个数
for i in range(length)[:-2]:
# 如果不是第一个数且等于 array 前一个数,不会产生新的组合,跳过
if i == 0 or nums[i] != nums[i - 1]:
# 如果第一个数的三倍已经大于目标值,则跳出循环,因为后面的数至少都等于第一个数
if nums[i] * 3 > target:
break
# 当前值下一个是最小指针, array 最后一个是最大指针
left, right = i + 1, length - 1
# 最小最大指针相交前不断循环
while left < right:
# 当前三个数的和
current_sum = nums[i] + nums[left] + nums[right]
# 当前和等于目标值,则记录当前组合,移动最小/大指针,跳过重复
if current_sum == target:
record.append((nums[i], nums[left], nums[right]))
left = _skip_left(left, right)
right = _skip_right(left, right)
left += 1
right -= 1
# 如果当前和小于目标值,则最小指针右移,跳过重复
elif current_sum < target:
left = _skip_left(left, right)
left += 1
# 如果当前和大于等于目标值,则最大值指针左移
else:
right = _skip_right(left, right)
right -= 1
# 统计元素个数
element = defaultdict(int)
for e in nums:
element[e] += 1
# 计算符合条件组合的个数
for a, b, c in record:
if a == b and b == c:
# 如果三个数相等,则可能性是组合的计算 n * (n-1) * (n-2) / (3 * 2)
count = (count + element[a] * (element[a] - 1) * (element[a] - 2) // 6) % bound
elif a == b:
# 如果两个数相等,则相等两个数的可能性是组合的计算 n * (n - 1) / 2
count = (count + element[a] * (element[a] - 1) // 2 * element[c]) % bound
elif b == c:
count = (count + element[c] * (element[c] - 1) // 2 * element[a]) % bound
else:
# 如果都不相等,连乘即可
count = (count + element[a] * element[b] * element[c]) % bound
return count
- 发现上面的 python 代码非常繁琐,还有优化空间。主要冗余的部分是指针检索的时候跳过重复的部分。调整上面解法的逻辑顺序,先获得每个元素个数的统计,接着去重后再排序,这样的列表更容易处理。
Python
def three_sum_with_multiplicity_optimize(A: List[int], target: int) -> int:
from collections import Counter
bound = 10 ** 9 + 7
element = Counter(A)
A = sorted(element.items(), key=lambda x: x[0])
res = 0
for i in range(len(A)):
j = i
k = len(A) - 1
new_target = target - A[i][0]
while j <= k:
if A[j][0] + A[k][0] < new_target:
j += 1
elif A[j][0] + A[k][0] > new_target:
k -= 1
else:
if A[i][0] == A[k][0]:
res = (res + A[i][1] * (A[i][1] - 1) * (A[i][1] - 2) // 6) % bound
elif A[i][0] == A[j][0]:
res = (res + A[k][1] * A[i][1] * (A[i][1] - 1) // 2) % bound
elif A[j][0] == A[k][0]:
res = (res + A[i][1] * A[j][1] * (A[j][1] - 1) // 2) % bound
else:
res = (res + A[i][1] * A[j][1] * A[k][1]) % bound
j += 1
k -= 1
return res
Java
// TODO
相关题目
LeetCode 1. Two Sum
LeetCode 167. Two Sum II
LeetCode 170. Two Sum III
LeetCode 15. Three Sum
LeetCode 16. Three Sum Closest
LeetCode 259. Three Sum Smaller
网友评论