LeetCode 3583: 统计特殊三元组 - 从 O(n³) 到 O(n) 的顿悟之旅

题目回顾

给定整数数组 nums,找出满足以下条件的三元组 (i, j, k) 的数量:

  • 0 <= i < j < k < n
  • nums[i] == nums[j] * 2
  • nums[k] == nums[j] * 2

第一反应:暴力三重循环

看到这道题,你的第一反应可能是:

go 复制代码
func specialTriplets(nums []int) int {
    count := 0
    n := len(nums)
    for i := 0; i < n; i++ {
        for j := i + 1; j < n; j++ {
            for k := j + 1; k < n; k++ {
                if nums[i] == nums[j]*2 && nums[k] == nums[j]*2 {
                    count++
                }
            }
        }
    }
    return count
}

时间复杂度:O(n³)

这显然不够优雅。当 n = 10⁵ 时,这种做法会超时。

关键洞察:固定中间,向两边看

💡 顿悟时刻 #1:三元组问题的本质

仔细观察条件:

  • nums[i] == nums[j] * 2(i 在 j 左边)
  • nums[k] == nums[j] * 2(k 在 j 右边)

关键发现 :两个条件都依赖于 nums[j]!

如果我们固定 j 作为中间点,问题就变成了:

  • 在 j 左边有多少个值等于 nums[j] * 2?
  • 在 j 右边有多少个值等于 nums[j] * 2?

答案就是:左边数量 × 右边数量(乘法原理)

💡 顿悟时刻 #2:用空间换时间

如何快速知道左边和右边有多少个目标值?

答案:频率表(哈希表)!

维护两个频率表:

  • freqPrev:记录 j 左边各个值出现的次数
  • freqNext:记录 j 右边各个值出现的次数

查询 freqPrev[nums[j] * 2] 和 freqNext[nums[j] * 2] 都是 O(1) 操作!

💡 顿悟时刻 #3:动态维护是关键

这里是最精妙的地方!

你可能会想:每个 j 都要重新计算左右的频率表?那不还是 O(n²) 吗?

不!我们可以动态维护!

复制代码
初始状态:
freqPrev = {}(空,因为 j=0 左边没有元素)
freqNext = 整个数组的频率统计

当 j 向右移动一位时:
1. nums[j] 从"右边"变成了"当前"
2. 下一轮,nums[j] 就在"左边"了

所以只需要:
freqNext[nums[j]]--  // 从右边移除
freqPrev[nums[j]]++  // 加入左边

每次更新只需 O(1)!

完整实现

go 复制代码
func specialTriplets(nums []int) int {
    const MOD = 1_000_000_007
    tripletCount := 0
    n := len(nums)
    
    // 初始化频率表
    freqPrev := make(map[int]int)
    freqNext := make(map[int]int)
    
    // freqPrev 初始只包含 nums[0]
    freqPrev[nums[0]]++
    
    // freqNext 包含 nums[1:] 的所有元素
    for i := 1; i < n; i++ {
        freqNext[nums[i]]++
    }
    
    // 遍历中间点 j(从 1 到 n-2)
    for j := 1; j < n-1; j++ {
        // 先从 freqNext 中移除 nums[j](j 变成当前点了)
        freqNext[nums[j]]--
        if freqNext[nums[j]] == 0 {
            delete(freqNext, nums[j])
        }
        
        // 计算贡献(注意取模)
        target := nums[j] * 2
        if freqPrev[target] > 0 && freqNext[target] > 0 {
            contribution := (freqPrev[target] * freqNext[target]) % MOD
            tripletCount = (tripletCount + contribution) % MOD
        }
        
        // 将 nums[j] 加入 freqPrev(为下一轮做准备)
        freqPrev[nums[j]]++
    }
    
    return tripletCount
}

复杂度分析

时间复杂度:O(n)

  • 初始化 freqNext:O(n)
  • 主循环:n 次迭代,每次 O(1) 操作
  • 总计:O(n)

空间复杂度:O(n)

  • 两个哈希表,最坏情况存储所有不同的值

⚠️ 注意事项:取模运算

题目要求结果对 10⁹ + 7 取模,因为:

  • n 最大可达 10⁵
  • 最坏情况下,所有元素都相同,三元组数量可能达到 ( n 3 ) ≈ n 3 6 \binom{n}{3} \approx \frac{n^3}{6} (3n)≈6n3
  • 当 n = 10⁵ 时,结果可能超过 int 范围

关键点:每次累加贡献时都要取模

go 复制代码
contribution := (freqPrev[target] * freqNext[target]) % MOD
tripletCount = (tripletCount + contribution) % MOD

这样可以防止中间计算溢出!

核心思想总结

  1. 固定中间:将三元组问题转化为"固定中间点,向两边统计"
  2. 频率表:用哈希表 O(1) 查询左右区间的目标值数量
  3. 动态维护:滑动窗口思想,边遍历边更新频率表,避免重复计算

从 O(n³) 到 O(n),关键在于找到问题的对称性和可复用性!

举例验证

对于 nums = [8, 4, 2, 8, 4]:

复制代码
j=1, nums[j]=4, target=8:
  freqPrev: {8: 1}
  freqNext: {2: 1, 8: 1, 4: 1}
  贡献: 1 × 1 = 1  ✓ (i=0, j=1, k=3)

j=2, nums[j]=2, target=4:
  freqPrev: {8: 1, 4: 1}
  freqNext: {8: 1, 4: 1}
  贡献: 1 × 1 = 1  ✓ (i=1, j=2, k=4)

总计: 2 ✓

完美!🎯

相关推荐
u0111026756 分钟前
工具页案例图如何编写 alt让图片说明与处理场景对应
java·前端·javascript·图像处理·人工智能·算法·ai作画
阿狗童鞋11 分钟前
数据结构与算法实战指南
数据结构·算法
Navigator_Z33 分钟前
LeetCode //C - 1275. Find Winner on a Tic Tac Toe Game
c语言·算法·leetcode
未来之窗软件服务36 分钟前
递归时间复杂度演算T (n)=aT (n/b)+f (n)-东方仙盟
算法·仙盟创梦ide·东方仙盟
垆边人似月.1 小时前
华为机考题:明明的随机数
数据结构·算法·华为
Doubbbbbbble云1 小时前
多维空间索引结构R树与KD树性能对比研究4
算法
海上小飞龙1 小时前
【KMP算法-中篇】next 数组的递推:往前填一位,靠的是往回退几步
算法
keyipatience2 小时前
B树详解:从零构建平衡多路搜索树
数据结构·b树·算法·图搜索
FluxArt2 小时前
GPT Image 2.5 多图参考怎么用:Flux Art 生成与编辑教程
数据结构·gpt·算法
All for pursuit.3 小时前
【分治-2】215.数组中的第K个最大元素
数据结构·c++·算法·leetcode