点击获取AI摘要

2179. 统计数组中好三元组数目 H

给你两个下标从 0 开始且长度为 n 的整数数组 nums1nums2 ,两者都是 [0, 1, ..., n - 1]排列

好三元组 指的是 3互不相同 的值,且它们在数组 nums1nums2 中出现顺序保持一致。换句话说,如果我们将 pos1v1_v 记为值 vnums1 中出现的位置,pos2v2_v 为值 vnums2 中的位置,那么一个好三元组定义为 0 <= x, y, z <= n - 1 ,且 pos1x1_x < pos1y1_y < pos1z1_z 和 pos2x2_x < pos2y2_y < pos2z2_z 都成立的 (x, y, z)

请你返回好三元组的 总数目

示例 1:

输入:nums1 = [2,0,1,3], nums2 = [0,1,2,3]
输出:1
解释:
总共有 4 个三元组 (x,y,z) 满足 pos1x1_x < pos1y1_y < pos1z1_z ,分别是 (2,0,1) ,(2,0,3) ,(2,1,3) 和 (0,1,3) 。
这些三元组中,只有 (0,1,3) 满足 pos2x2_x < pos2y2_y < pos2z2_z 。所以只有 1 个好三元组。

示例 2:

输入:nums1 = [4,0,1,3,2], nums2 = [4,1,0,2,3]
输出:4
解释:总共有 4 个好三元组 (4,0,3) ,(4,0,2) ,(4,1,3) 和 (4,1,2) 。

提示:

  • n == nums1.length == nums2.length
  • 3 <= n <= 10510^5
  • 0 <= nums1[i], nums2[i] <= n - 1
  • nums1nums2[0, 1, ..., n - 1] 的排列。

问题分析

题目给定两个长度为n且下标从0开始的整数数组nums1nums2,这两个数组都是[0,1,...,n−1]的排列。定义了好三元组为3个互不相同的值,且它们在数组nums1nums2中出现顺序保持一致
即若pos1v为值v在nums1中出现的位置,pos2v为值v在nums2中出现的位置,
好三元组需满足0 <= x, y, z <= n−1,且pos1x < pos1y < pos1zpos2x < pos2y < pos2z都成立的(x,y,z) ,要求返回好三元组的总数目。

算法思路

由于两个数组都是 0 到 n - 1 的排列,考虑以下转换:

  1. 构造新数组
    我们先建立一个数组 p,其中

    p[i]=pos2[nums1[i]]p[i]=\text{pos2}[\text{nums1}[i]]

    这里的 pos2[v]\text{pos2}[v] 表示值 vvnums2 中的位置。
    由于 nums1 本身已经按其在原数组中的顺序排序,因此原题中要求 “pos1xx < pos1yy < pos1zz” 已自动满足。问题就转化为:在数组 p 中计数满足

    p[i]<p[j]<p[k]p[i]<p[j]<p[k]

    且满足 i<j<ki<j<k 的三元组。

  2. 双遍扫描计数
    对于数组中的每个中间元素 p[j]p[j],设:

    • LjL_j:在 jj 之前(即 i<ji<j)满足 p[i]<p[j]p[i] < p[j] 的数量;
    • RjR_j:在 jj 之后(即 k>jk>j)满足 p[k]>p[j]p[k] > p[j] 的数量;

    则以 p[j]p[j] 为中间元素的有效三元组数量为 Lj×RjL_j \times R_j。整个三元组数量为所有 jj 上的累加和。

  3. 树状数组(Fenwick Tree)的作用
    由于 nn 可能达到 10510^5 级别,直接遍历统计 LjL_j​ 和 RjR_j​ 是 O(n2)O(n^2) 的效率。借助树状数组,我们可以在 O(logn)O(\log n) 内查询某个前缀的值,从而:

    • 正向遍历计算每个位置 jjLjL_j
    • 逆向遍历计算每个位置 jjRjR_j

时间复杂度

  • 构造映射与数组转换O(n)O(n)

  • 正序与逆序遍历树状数组操作:使用两个树状数组分别进行两次扫描(每个元素需要正向逆向各一次),每个查询和更新均为 O(lognO(\log n,共计 O(nlogn)O(n \log n)

  • 总体时间复杂度O(nlogn)O(n \log n)

  • 空间复杂度O(n)O(n), 用于存储两个计数数组及树状数组的结构

代码分解

FenwickTree 类
负责单点更新和区间查询,从而在 O(logn)O(\log n) 时间内计算前缀和。

构造映射与数组转换
nums2 中每个数值映射到对应的位置,构造数组 p 后,数组 p 就是按照 nums1 顺序排列的 nums2 中的下标,即 p[i] 是 nums1 的第 i 个元素在 nums2 中的位置。

左侧计数(L[j])
使用树状数组,从左到右扫描数组 p,对于每个位置 j,查询 p[j] 之前比它小的数字个数,并更新树状数组,通过遍历 p 数组并动态查询当前值之前的前缀和实现。

右侧计数(R[j])
使用另一个树状数组,从右到左扫描数组 p,对于每个位置 j,查询已经处理的右侧元素中比 p[j] 大的数量,通过总已处理元素数减去小于等于 p[j] 的数目得到。

三元组数量累加
对每个中间位置 j,三元组数量为 L[j]×R[j]L[j] \times R[j],对所有可能的中间位置累加即为最终答案。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class FenwickTree:
def __init__(self, size: int):
self.size = size
self.tree = [0] * (size + 1)

def update(self, index: int, delta: int):
# index 从 0 开始转换到 1-based
index += 1
while index <= self.size:
self.tree[index] += delta
index += index & -index

def query(self, index: int) -> int:
# 查询前缀和 [0, index],index 为 0-based
index += 1
res = 0
while index:
res += self.tree[index]
index -= index & -index
return res

class Solution:
@staticmethod
def goodTriplets(nums1: list, nums2: list) -> int:
n = len(nums1)

# 建立 nums2 中值 -> 位置 的映射
pos2 = [0] * n
for idx, num in enumerate(nums2):
pos2[num] = idx

# 构造数组 p,其中 p[i] = pos2[nums1[i]]
p = [pos2[num] for num in nums1]

# 第一步:正序遍历,计算每个 j 左侧比 p[j] 小的数量 L[j]
left_count = [0] * n
fenwick_left = FenwickTree(n)
for j in range(n):
# p[j] 前有多少数比它小
left_count[j] = fenwick_left.query(p[j] - 1)
fenwick_left.update(p[j], 1)

# 第二步:逆序遍历,计算每个 j 右侧比 p[j] 大的数量 R[j]
right_count = [0] * n
fenwick_right = FenwickTree(n)
for j in range(n - 1, -1, -1):
# 右侧比 p[j] 大的数量 = 当前已处理的个数中 p 值大于 p[j] 的数量
# 先查询 p[j] 的前缀和,已加入的元素总数为 (n-1 - j)
right_count[j] = fenwick_right.query(n - 1) - fenwick_right.query(p[j])
fenwick_right.update(p[j], 1)

# 三元组数量:对每个 j 中间元素,累加 L[j] * R[j]
result = 0
for j in range(n):
result += left_count[j] * right_count[j]
return result