普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月18日LeetCode 每日一题题解

每日一题-统计坏数对的数目🟡

2025年4月18日 00:00

给你一个下标从 0 开始的整数数组 nums 。如果 i < j 且 j - i != nums[j] - nums[i] ,那么我们称 (i, j) 是一个 数对 。

请你返回 nums 中 坏数对 的总数目。

 

示例 1:

输入:nums = [4,1,3,3]
输出:5
解释:数对 (0, 1) 是坏数对,因为 1 - 0 != 1 - 4 。
数对 (0, 2) 是坏数对,因为 2 - 0 != 3 - 4, 2 != -1 。
数对 (0, 3) 是坏数对,因为 3 - 0 != 3 - 4, 3 != -1 。
数对 (1, 2) 是坏数对,因为 2 - 1 != 3 - 1, 1 != 2 。
数对 (2, 3) 是坏数对,因为 3 - 2 != 3 - 3, 1 != 0 。
总共有 5 个坏数对,所以我们返回 5 。

示例 2:

输入:nums = [1,2,3,4,5]
输出:0
解释:没有坏数对。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109

式子变形 + 枚举右维护左(Python/Java/C++/Go/JS/Rust)

作者 endlesscheng
2022年8月7日 07:57

前置题目1512. 好数对的数目

题目要求

$$
j - i \ne \textit{nums}[j] - \textit{nums}[i]
$$

移项得

$$
\textit{nums}[i]-i \ne \textit{nums}[j]-j
$$

正难则反,用总数对个数 $\dfrac{n(n-1)}{2}$ 减去满足

$$
\textit{nums}[i]-i = \textit{nums}[j]-j
$$

的数对个数,即为答案。

设 $a[i] = \textit{nums}[i]-i$,就和 1512 题完全一样了。

为什么要先更新 $\textit{ans}$,再更新 $\textit{cnt}$?理由见 1512 题 我的题解

class Solution:
    def countBadPairs(self, nums: List[int]) -> int:
        ans = comb(len(nums), 2)
        cnt = defaultdict(int)
        for i, x in enumerate(nums):
            ans -= cnt[x - i]
            cnt[x - i] += 1
        return ans
class Solution {
    public long countBadPairs(int[] nums) {
        int n = nums.length;
        long ans = (long) n * (n - 1) / 2;
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int i = 0; i < n; i++) {
            int x = nums[i] - i;
            int c = cnt.getOrDefault(x, 0);
            ans -= c;
            cnt.put(x, c + 1);
        }
        return ans;
    }
}
class Solution {
    public long countBadPairs(int[] nums) {
        int n = nums.length;
        long ans = (long) n * (n - 1) / 2;
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int i = 0; i < n; i++) {
            ans -= cnt.merge(nums[i] - i, 1, Integer::sum) - 1;
        }
        return ans;
    }
}
class Solution {
public:
    long long countBadPairs(vector<int>& nums) {
        int n = nums.size();
        long long ans = 1LL * n * (n - 1) / 2;
        unordered_map<int, int> cnt;
        for (int i = 0; i < n; i++) {
            ans -= cnt[nums[i] - i]++;
        }
        return ans;
    }
};
func countBadPairs(nums []int) int64 {
    n := len(nums)
    ans := n * (n - 1) / 2
    cnt := map[int]int{}
    for i, x := range nums {
        ans -= cnt[x-i]
        cnt[x-i]++
    }
    return int64(ans)
}
var countBadPairs = function(nums) {
    const n = nums.length;
    let ans = n * (n - 1) / 2;
    const cnt = new Map();
    for (let i = 0; i < n; i++) {
        const x = nums[i] - i;
        const c = cnt.get(x) ?? 0;
        ans -= c;
        cnt.set(x, c + 1);
    }
    return ans;
};
use std::collections::HashMap;

impl Solution {
    pub fn count_bad_pairs(nums: Vec<i32>) -> i64 {
        let n = nums.len() as i64;
        let mut ans = n * (n - 1) / 2;
        let mut cnt = HashMap::new();
        for (i, x) in nums.into_iter().enumerate() {
            let e = cnt.entry(x - i as i32).or_insert(0);
            ans -= *e as i64;
            *e += 1;
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

更多相似题目,见下面数据结构题单中的「§0.1 枚举右,维护左」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Java36毫秒 计数+求和

作者 zhy-
2022年8月7日 00:31

解题思路

1.差值(数值-下标)相同的数可以组成好数对,按相同差值聚合计数。
2.设数组长度为n,数组总共数对个数为(n-1)+(n-2)+...+1累加之和,形成等差数列,公式求和(1+n-1)*(n-1)/2,得到总数对个数。相同差值的个数也可以视为数组长度,同理得到好数对个数。
3.坏数对=总数对-好数对。

统计坏数对的数目.png

代码

###java

class Solution {
    public long countBadPairs(int[] nums) {
        int n=nums.length;
        long cnt=(long)(1.0d*(1+n-1)*(n-1)/2);
        Map<Integer,Integer> map=new HashMap<>();
        for(int i=0;i<n;i++){
            int d=nums[i]-i;
            map.put(d,map.getOrDefault(d,0)+1);
        }
        for(int count:map.values()){
            cnt-=(long)(1.0d*(1+count-1)*(count-1)/2);
        }
        return cnt;
    }
}

枚举

作者 tsreaper
2022年8月7日 00:19

解法:枚举

坏数对的数目,就是所有数对的数目,减去满足 i < jj - i == nums[j] - nums[i] 的数对数目。

把等式两边移项,变为 nums[i] - i == nums[j] - j。因此我们只需要维护一个 unordered_map<int, int> mpmp[x] 表示目前为止满足 nums[i] - i == xi 有几个。我们枚举 j,并从答案中减去 mp[nums[j] - j] 的值即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    long long countBadPairs(vector<int>& nums) {
        int n = nums.size();
        long long ans = 1LL * n * (n - 1) / 2;
        unordered_map<int, int> mp;
        for (int i = 0; i < n; i++) {
            int t = nums[i] - i;
            ans -= mp[t];
            mp[t]++;
        }
        return ans;
    }
};
昨天 — 2025年4月17日LeetCode 每日一题题解

[Python3/Java/C++/Go/TypeScript] 一题一解:枚举(清晰题解)

作者 lcbin
2025年4月17日 06:17

方法一:枚举

我们先在 $[0, n)$ 的范围内枚举下标 $j$,然后在 $[0, j)$ 的范围内枚举下标 $i$,统计满足 $\textit{nums}[i] = \textit{nums}[j]$ 且 $(i \times j) \bmod k = 0$ 的数对个数。

###python

class Solution:
    def countPairs(self, nums: List[int], k: int) -> int:
        ans = 0
        for j, y in enumerate(nums):
            for i, x in enumerate(nums[:j]):
                ans += int(x == y and i * j % k == 0)
        return ans

###java

class Solution {
    public int countPairs(int[] nums, int k) {
        int ans = 0;
        for (int j = 1; j < nums.length; ++j) {
            for (int i = 0; i < j; ++i) {
                ans += nums[i] == nums[j] && (i * j % k) == 0 ? 1 : 0;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int countPairs(vector<int>& nums, int k) {
        int ans = 0;
        for (int j = 1; j < nums.size(); ++j) {
            for (int i = 0; i < j; ++i) {
                ans += nums[i] == nums[j] && (i * j % k) == 0;
            }
        }
        return ans;
    }
};

###go

func countPairs(nums []int, k int) (ans int) {
for j, y := range nums {
for i, x := range nums[:j] {
if x == y && (i*j%k) == 0 {
ans++
}
}
}
return
}

###ts

function countPairs(nums: number[], k: number): number {
    let ans = 0;
    for (let j = 1; j < nums.length; ++j) {
        for (let i = 0; i < j; ++i) {
            if (nums[i] === nums[j] && (i * j) % k === 0) {
                ++ans;
            }
        }
    }
    return ans;
}

###rust

impl Solution {
    pub fn count_pairs(nums: Vec<i32>, k: i32) -> i32 {
        let mut ans = 0;
        for j in 1..nums.len() {
            for (i, &x) in nums[..j].iter().enumerate() {
                if x == nums[j] && (i * j) as i32 % k == 0 {
                    ans += 1;
                }
            }
        }
        ans
    }
}

###c

int countPairs(int* nums, int numsSize, int k) {
    int ans = 0;
    for (int j = 1; j < numsSize; ++j) {
        for (int i = 0; i < j; ++i) {
            ans += (nums[i] == nums[j] && (i * j % k) == 0);
        }
    }
    return ans;
}

时间复杂度 $O(n^2)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。空间复杂度 $O(1)$。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-统计数组中相等且可以被整除的数对🟢

2025年4月17日 00:00

给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 k ,请你返回满足 0 <= i < j < n ,nums[i] == nums[j] 且 (i * j) 能被 k 整除的数对 (i, j) 的 数目 。

 

示例 1:

输入:nums = [3,1,2,2,2,1,3], k = 2
输出:4
解释:
总共有 4 对数符合所有要求:
- nums[0] == nums[6] 且 0 * 6 == 0 ,能被 2 整除。
- nums[2] == nums[3] 且 2 * 3 == 6 ,能被 2 整除。
- nums[2] == nums[4] 且 2 * 4 == 8 ,能被 2 整除。
- nums[3] == nums[4] 且 3 * 4 == 12 ,能被 2 整除。

示例 2:

输入:nums = [1,2,3,4], k = 1
输出:0
解释:由于数组中没有重复数值,所以没有数对 (i,j) 符合所有要求。

 

提示:

  • 1 <= nums.length <= 100
  • 1 <= nums[i], k <= 100

双周赛t1 基于筛法的O(nlogn)解法

作者 981377660LMT
2022年2月20日 16:42

解题思路

类似于周赛t4 6015. 统计可以被 K 整除的下标对数目

  1. 筛法预处理每个因子的倍数有哪些,记录倍数时也要记录他对应的数组里的值
  2. 遍历每个数,分index=0与index>0的情况
  3. index=0直接特殊处理即可
  4. index>0时加上符合题意的配对数multiCounter[need][value],注意要减去自身重复取的情况,最后结果除以二即可

代码

###python3

from collections import defaultdict
from math import gcd
from typing import Counter, List

class Solution:
    def countPairs(self, nums: List[int], k: int) -> int:
        n = len(nums)
        multiCounter = defaultdict(lambda: defaultdict(int))
        for factor in range(1, n):
            for multi in range(factor, n, factor):
                multiCounter[factor][nums[multi]] += 1

        counter = Counter(nums)
        res1, res2 = 0, 0
        for index, value in enumerate(nums):
            if index == 0:
                res1 += counter[value] - 1
            else:
                need = k // gcd(index, k)
                res2 += multiCounter[need][value]
                if index ** 2 % k == 0:
                    res2 -= 1

        return res1 + res2 // 2

O(nlogn) 数学做法,枚举右维护左(Python/Java/C++/Go)

作者 endlesscheng
2022年2月20日 00:11

如果数组长度 $n=10^5$,$\mathcal{O}(n^2)$ 的暴力枚举就超时了,怎么优化?

从特殊到一般,先考虑所有 $\textit{nums}[i]$ 都相同的情况。此时问题简化成:

  • 统计 $0 \le i < j < n$ 且 $ij$ 能被 $k$ 整除的下标对 $(i, j)$ 的数目。

由于 $0$ 不是因子,单独统计($i=0$ 时,$ij=0$,一定能被 $k$ 整除),所以下面讨论 $1 \le i < j < n$ 的情况。

如果 $k=12$,$j=9$,哪些 $i$ 是满足要求的?

$i$ 如果是 $12$ 的倍数,肯定可以满足要求,因为 $12$ 的倍数一定能被 $12$ 整除。还有其他的 $i$ 吗?

注意到 $j=9$ 是 $3$ 的倍数,而 $12=4\times 3$,那么 $i$ 只需要是 $4$ 的倍数,便可以满足 $ij$ 是 $k$ 的倍数,因为这种情况下 $ij=(4i')(3j')=12i'j'$ 一定是 $12$ 的倍数。

换句话说,由于 $j$ 和 $k$ 的最大公因子(GCD)是 $3$,所以 $i$ 只需要是 $\dfrac{12}{3}=4$ 的倍数就可以满足要求,即 $i=4,8,12,\cdots$ 都是满足要求的。

一般地,枚举 $j$,那么 $i$ 必须是 $k'=\dfrac{k}{\text{GCD}(k,j)}$ 的倍数。

回到本题,多了一个元素值必须相同的约束。

遍历数组,设当前元素 $x=\textit{nums}[j]$,我们需要知道左边值为 $x$ 且下标是 $k'$ 的倍数的数的个数。怎么维护?例如 $\textit{nums}[4]=\textit{nums}[6]=50$,对于 $i=4$,把 $50$ 以及 $4$ 的所有因子 $1,2,4$,组成三个二元组 $(50,1),(50,2),(50,4)$ 加到哈希表中(统计二元组的个数);对于 $i=6$,把 $50$ 以及 $6$ 的所有因子 $1,2,3,6$,组成四个二元组 $(50,1),(50,2),(50,3),(50,6)$ 加到哈希表中。这样如果后面遍历到一个值为 $50$ 的数,且 $k'=2$,通过查询哈希表中的 $(50,2)$ 的个数,就能知道,左边有两个数,值为 $50$ 且下标是 $2$ 的倍数。

# 预处理每个数的因子
MX = 101
divisors = [[] for _ in range(MX)]
for i in range(1, MX):
    for j in range(i, MX, i):
        divisors[j].append(i)

class Solution:
    def countPairs(self, nums: list[int], k: int) -> int:
        ans = 0
        cnt = defaultdict(int)
        for j, x in enumerate(nums):  # 枚举 j,计算左边有多少个符合要求的 i
            if j and x == nums[0]:
                ans += 1  # 单独统计 i=0 的情况
            k2 = k // gcd(k, j)  # i 必须是 k2 的倍数
            ans += cnt[(x, k2)]
            for d in divisors[j]:  # j 是 d 的倍数
                cnt[(x, d)] += 1
        return ans
class Solution {
    private static final int MX = 101;
    private static final List<Integer>[] divisors = new ArrayList[MX];

    static {
        Arrays.setAll(divisors, i -> new ArrayList<>());
        // 预处理每个数的因子
        for (int i = 1; i < MX; i++) {
            for (int j = i; j < MX; j += i) {
                divisors[j].add(i);
            }
        }
    }

    public int countPairs(int[] nums, int k) {
        int ans = 0;
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int j = 0; j < nums.length; j++) { // 枚举 j,计算左边有多少个符合要求的 i
            int x = nums[j];
            if (j > 0 && x == nums[0]) {
                ans++; // 单独统计 i=0 的情况
            }
            int k2 = k / gcd(k, j); // i 必须是 k2 的倍数
            // 用位运算把二元组 (x, k2) 合并成一个整数
            ans += cnt.getOrDefault(k2 << 10 | x, 0);
            for (int d : divisors[j]) { // j 是 d 的倍数
                cnt.merge(d << 10 | x, 1, Integer::sum); // cnt[d<<10|x]++
            }
        }
        return ans;
    }

    private int gcd(int a, int b) {
        while (a != 0) {
            int tmp = b % a;
            b = a;
            a = tmp;
        }
        return b;
    }
}
const int MX = 101;
vector<int> divisors[MX];

auto init = [] {
    for (int i = 1; i < MX; i++) {
        for (int j = i; j < MX; j += i) {
            divisors[j].push_back(i);
        }
    }
    return 0;
}();

class Solution {
public:
    int countPairs(vector<int>& nums, int k) {
        int ans = 0;
        unordered_map<int, int> cnt;
        for (int j = 0; j < nums.size(); j++) { // 枚举 j,计算左边有多少个符合要求的 i
            int x = nums[j];
            if (j && x == nums[0]) {
                ans++; // 单独统计 i=0 的情况
            }
            int k2 = k / gcd(k, j); // i 必须是 k2 的倍数
            ans += cnt[k2 << 10 | x]; // 用位运算把二元组 (x, k2) 合并成一个整数
            for (int d : divisors[j]) { // j 是 d 的倍数
                cnt[d << 10 | x]++;
            }
        }
        return ans;
    }
};
const mx = 101
var divisors [mx][]int

func init() {
    // 预处理每个数的因子
    for i := 1; i < mx; i++ {
        for j := i; j < mx; j += i {
            divisors[j] = append(divisors[j], i)
        }
    }
}

func countPairs(nums []int, k int) (ans int) {
    type pair struct{ v, d int }
    cnt := map[pair]int{}
    for j, x := range nums { // 枚举 j,计算左边有多少个符合要求的 i
        if j > 0 && x == nums[0] {
            ans++ // 单独统计 i=0 的情况
        }
        k2 := k / gcd(k, j)
        ans += cnt[pair{x, k2}] // 统计左边有多少个数,值为 x 且下标是 k2 的倍数
        for _, d := range divisors[j] { // j 是 d 的倍数
            cnt[pair{x, d}]++
        }
    }
    return
}

func gcd(a, b int) int { for a != 0 { a, b = b%a, a }; return b }

复杂度分析

调和级数可得,预处理的时间是 $\mathcal{O}(M\log M)$,其中 $M=100$。

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。每次计算 GCD 需要 $\mathcal{O}(\log \min(k,n))$ 的时间。遍历 $1$ 到 $n-1$ 的所有因子跟预处理是一样的,由调和级数可得,需要 $\mathcal{O}(n\log n)$ 的时间。
  • 空间复杂度:$\mathcal{O}(n\log n)$。

相似题目

2183. 统计可以被 K 整除的下标对数目

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

昨天以前LeetCode 每日一题题解

每日一题-统计好子数组的数目🟡

2025年4月16日 00:00

给你一个整数数组 nums 和一个整数 k ,请你返回 nums 中  子数组的数目。

一个子数组 arr 如果有 至少 k 对下标 (i, j) 满足 i < j 且 arr[i] == arr[j] ,那么称它是一个  子数组。

子数组 是原数组中一段连续 非空 的元素序列。

 

示例 1:

输入:nums = [1,1,1,1,1], k = 10
输出:1
解释:唯一的好子数组是这个数组本身。

示例 2:

输入:nums = [3,1,4,3,2,2,4], k = 2
输出:4
解释:总共有 4 个不同的好子数组:
- [3,1,4,3,2,2] 有 2 对。
- [3,1,4,3,2,2,4] 有 3 对。
- [1,4,3,2,2,4] 有 2 对。
- [4,3,2,2,4] 有 2 对。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i], k <= 109

【普通人包看懂的简单代码】滑动窗口

作者 demouo
2023年1月15日 16:02

思路

  1. 下标[left,right]区间内如果满足条件了,那么总长度n-right个都满足条件
  2. 此时left++,以新的起点试图重新判断是否满足条件 再重复步骤1.
class Solution {
public:
    long long countGood(vector<int>& nums, int k) {
        long long res=0;
        int sum=0;//成对的总数
        unordered_map<int,int>cnt;
         for(int left=0,right=0;right<nums.size();right++){
             cnt[nums[right]]++;
            if(cnt[nums[right]]>=2){//可以成对就能为对的总数做贡献
              sum+=cnt[nums[right]]-1;//两个数多一对;三个数多两对;四个数多三对 
            }
            if(sum>=k){//对数不少于k
                while(sum>=k){
                res+=nums.size()-right;//right及其往后的都是好数组
                if(cnt[nums[left]]>=2){//滑出left时 如果left曾经对sum有过贡献 就得把它的贡献减掉
                    sum-=cnt[nums[left]]-1;
                }
                cnt[nums[left]]--;
                left++;//滑出left
                }
            }
         }
         return res;
    }
};

c++滑动窗口

作者 thdlrt
2023年1月15日 12:17
  • 思路
    • 直接枚举区间[i,j]会超时,可以使用滑动窗口来实现
    • 如果[i,j]是好子数组,那么[i,j+1],[i,j+2]...都是好子数组
    • 因此我们只需要枚举左边界i,找到最小的j使得[i,j]是好子数组,也就得到了nums.size()-j个以i开始的好子数组
    • 计算区间内相等元素对的数目:
      • 如果一个数字有k个(k>1)那么相等对有k*(k-1)/2个
      • 因此k加一时数对数目增加k*(k-1)/2-(k-1)*(k-2)/2=k-1个
      • 减少时同理
class Solution {
public:
    long long countGood(vector<int>& nums, int k) {
        long long ans=0;
        unordered_map<int,int>check;//维护滑动窗口内数据数目
        int sum=0;
        for(int i=0,j=0;j<nums.size();j++)
        {
            check[nums[j]]++;
            if(check[nums[j]]>1)//更新数对数目
                sum+=check[nums[j]]-1;
            if(sum>=k)
            {
                while(sum>=k)
                {
                    ans+=nums.size()-j;//更新i开始的好子数组数目
                    if(check[nums[i]]>1)
                        sum-=check[nums[i]]-1;
                    check[nums[i]]--;
                    i++;
                }
            }
        }
        return ans;
    }
};

「越长越合法」型滑动窗口(Python/Java/C++/Go/JS/Rust)

作者 endlesscheng
2023年1月15日 12:08

前置知识滑动窗口【基础算法精讲 03】

核心思路

  1. 如果窗口中有 $c$ 个元素 $x$,再进来一个 $x$,会新增 $c$ 个相等数对。
  2. 如果窗口中有 $c$ 个元素 $x$,再去掉一个 $x$,会减少 $c-1$ 个相等数对。

用一个哈希表 $\textit{cnt}$ 维护子数组(窗口)中的每个元素的出现次数,以及相同数对的个数 $\textit{pairs}$。

外层循环:从小到大枚举子数组右端点 $\textit{right}$。现在准备把 $x=\textit{nums}[\textit{right}]$ 移入窗口,那么窗口中有 $\textit{cnt}[x]$ 个数和 $x$ 相同,所以 $\textit{pairs}$ 会增加 $\textit{cnt}[x]$。然后把 $\textit{cnt}[x]$ 加一。

内层循环:如果发现 $\textit{pairs}\ge k$,说明子数组符合要求,右移左端点 $\textit{left}$,先把 $\textit{cnt}[\textit{nums}[\textit{left}]]$ 减少一,然后把 $\textit{pairs}$ 减少 $\textit{cnt}[\textit{nums}[\textit{left}]]$。

内层循环结束后,$[\textit{left},\textit{right}]$ 这个子数组是不满足题目要求的,但在退出循环之前的最后一轮循环,$[\textit{left}-1,\textit{right}]$ 是满足题目要求的。由于子数组越长,越能满足题目要求,所以除了 $[\textit{left}-1,\textit{right}]$,还有 $[\textit{left}-2,\textit{right}],[\textit{left}-3,\textit{right}],\ldots,[0,\textit{right}]$ 都是满足要求的。也就是说,当右端点固定在 $\textit{right}$ 时,左端点在 $0,1,2,\ldots,\textit{left}-1$ 的所有子数组都是满足要求的,这一共有 $\textit{left}$ 个。

class Solution:
    def countGood(self, nums: List[int], k: int) -> int:
        cnt = defaultdict(int)  # 比 Counter() 快
        ans = left = pairs = 0
        for x in nums:
            pairs += cnt[x]
            cnt[x] += 1
            while pairs >= k:
                cnt[nums[left]] -= 1
                pairs -= cnt[nums[left]]
                left += 1
            ans += left
        return ans
class Solution {
    public long countGood(int[] nums, int k) {
        long ans = 0;
        Map<Integer, Integer> cnt = new HashMap<>();
        int pairs = 0;
        int left = 0;
        for (int x : nums) {
            int c = cnt.getOrDefault(x, 0);
            pairs += c; // 进
            cnt.put(x, c + 1);
            while (pairs >= k) {
                x = nums[left];
                c = cnt.get(x);
                pairs -= c - 1; // 出
                cnt.put(x, c - 1);
                left++;
            }
            ans += left;
        }
        return ans;
    }
}
class Solution {
    public long countGood(int[] nums, int k) {
        long ans = 0;
        Map<Integer, Integer> cnt = new HashMap<>();
        int pairs = 0;
        int left = 0;
        for (int x : nums) {
            pairs += cnt.merge(x, 1, Integer::sum) - 1; // pairs += cnt[x]++
            while (pairs >= k) {
                pairs -= cnt.merge(nums[left], -1, Integer::sum); // pairs -= --cnt[nums[left]]
                left++;
            }
            ans += left;
        }
        return ans;
    }
}
class Solution {
public:
    long long countGood(vector<int>& nums, int k) {
        long long ans = 0;
        unordered_map<int, int> cnt;
        int pairs = 0, left = 0;
        for (int x : nums) {
            pairs += cnt[x]++;
            while (pairs >= k) {
                pairs -= --cnt[nums[left]];
                left++;
            }
            ans += left;
        }
        return ans;
    }
};
func countGood(nums []int, k int) (ans int64) {
    cnt := map[int]int{}
    pairs, left := 0, 0
    for _, x := range nums {
        pairs += cnt[x]
        cnt[x]++
        for pairs >= k {
            cnt[nums[left]]--
            pairs -= cnt[nums[left]]
            left++
        }
        ans += int64(left)
    }
    return
}
var countGood = function(nums, k) {
    const cnt = new Map();
    let ans = 0, pairs = 0, left = 0;
    for (const x of nums) {
        const c = cnt.get(x) ?? 0;
        pairs += c; // 进
        cnt.set(x, c + 1);
        while (pairs >= k) {
            const x = nums[left];
            const c = cnt.get(x);
            pairs -= c - 1; // 出
            cnt.set(x, c - 1);
            left++;
        }
        ans += left;
    }
    return ans;
};
use std::collections::HashMap;

impl Solution {
    pub fn count_good(nums: Vec<i32>, k: i32) -> i64 {
        let mut ans = 0;
        let mut cnt = HashMap::new();
        let mut pairs = 0;
        let mut left = 0;
        for &x in &nums {
            let e = cnt.entry(x).or_insert(0);
            pairs += *e;
            *e += 1;
            while pairs >= k {
                let e = cnt.get_mut(&nums[left]).unwrap();
                *e -= 1;
                pairs -= *e;
                left += 1;
            }
            ans += left;
        }
        ans as _
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{nums}$ 的长度。虽然写了个二重循环,但是内层循环中对 $\textit{left}$ 加一的执行次数不会超过 $n$ 次,所以总的时间复杂度为 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

更多相似题目,见下面滑动窗口题单中的「§2.3.1 越长越合法」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-统计数组中好三元组数目🔴

2025年4月15日 00:00

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

好三元组 指的是 3 个 互不相同 的值,且它们在数组 nums1 和 nums2 中出现顺序保持一致。换句话说,如果我们将 pos1v 记为值 v 在 nums1 中出现的位置,pos2v 为值 v 在 nums2 中的位置,那么一个好三元组定义为 0 <= x, y, z <= n - 1 ,且 pos1x < pos1y < pos1z 和 pos2x < pos2y < pos2z 都成立的 (x, y, z) 。

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

 

示例 1:

输入:nums1 = [2,0,1,3], nums2 = [0,1,2,3]
输出:1
解释:
总共有 4 个三元组 (x,y,z) 满足 pos1x < pos1y < pos1,分别是 (2,0,1) ,(2,0,3) ,(2,1,3) 和 (0,1,3) 。
这些三元组中,只有 (0,1,3) 满足 pos2x < pos2y < pos2z 。所以只有 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 <= 105
  • 0 <= nums1[i], nums2[i] <= n - 1
  • nums1 和 nums2 是 [0, 1, ..., n - 1] 的排列。

树状数组/线段树/平衡树

作者 lucifer1004
2022年2月20日 00:11

方法一:树状数组/线段树/平衡树

首先用哈希表记录每个数在数组二中的位置,然后按照数组一的顺序依次处理。

我们考虑以当前数字作为三元组中间数字的好三元组的数目。第一个数字需要是之前已经遍历过的,并且在数组二中的位置比当前数字更靠前的;第三个数字需要是当前还没有遍历过的,并且在数组二中的位置比当前数字更靠后的。这里只对数字的位置有要求,而对数字具体的值没有要求。

如何快速求出满足条件的第一个数字和第三个数字的个数呢?

以 $[4,0,1,3,2]\quad[4,1,0,2,3]$为例,考虑我们的遍历过程:

首先处理的是 4,此时数组二中的出现情况为:

$[4,X,X,X,X]$

我们需要统计的是 4 之前的有值的个数(0 个),以及 4 之后的没有值的个数(4 个)。因此以 4 为中间数字能形成 0 个好三元组。

接下来是 0,此时数组二中的出现情况为:

$[4,X,0,X,X]$

0 之前有值的个数(1 个),0 之后没有值的个数(2 个)。因此以 0 为中间数字能形成 2 个好三元组。

接下来是 1,此时数组二中的出现情况为:

$[4,1,0,X,X]$

1 之前有值的个数(1 个),1 之后没有值的个数(2 个)。因此以 1 为中间数字能形成 2 个好三元组。

接下来是 3,此时数组二中的出现情况为:

$[4,1,0,X,3]$

3 之前有值的个数(3 个),3 之后没有值的个数(0 个)。因此以 3 为中间数字能形成 0 个好三元组。

最后是 2,此时数组二中的出现情况为:

$[4,1,0,2,3]$

2 之前有值的个数(3 个),2 之后没有值的个数(0 个)。因此以 2 为中间数字能形成 0 个好三元组。

最后的答案是 4。

因为我们并不关心数字具体的值,而只关心是否出现过,所以我们实际上可以把数组二的出现情况用一个 0–1 数组来表示:

$[1,0,0,0,0]\rightarrow[1,0,1,0,0]\rightarrow[1,1,1,0,0]\rightarrow[1,1,1,0,1]\rightarrow[1,1,1,1,1]$

这时可以看出,我们用树状数组(或者线段树、平衡树)就能快速更新状态,并求出我们需要的两个数值(左边的 1 的个数和右边的 0 的个数)。

理清思路之后,代码的实现是比较容易的。

  • 时间复杂度$\mathcal{O}(N\log N)$。
  • 空间复杂度$\mathcal{O}(N)$。

参考代码(C++)

###cpp

template <class T> class FenwickTree {
  int limit;
  vector<T> arr;

  int lowbit(int x) { return x & (-x); }

public:
  FenwickTree(int limit) {
    this->limit = limit;
    arr = vector<T>(limit + 1);
  }

  void update(int idx, T delta) {
    for (; idx <= limit; idx += lowbit(idx))
      arr[idx] += delta;
  }

  T query(int idx) {
    T ans = 0;
    for (; idx > 0; idx -= lowbit(idx))
      ans += arr[idx];
    return ans;
  }
};

class Solution {
public:
    long long goodTriplets(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        FenwickTree<int> occur(n);
        unordered_map<int, int> pos;
        for (int i = 0; i < n; ++i)
            pos[nums2[i]] = i + 1;
        
        long long ans = 0;
        for (int i = 0; i < n; ++i) {
            int idx = pos[nums1[i]];
            
            int left = occur.query(idx);
            int right = n - idx - (occur.query(n) - occur.query(idx));
            ans += 1LL * left * right;
            
            occur.update(idx, 1);
        }
        
        return ans;
    }
};

转化为经典问题 (求左侧小于当前元素的个数)

作者 newhar
2022年2月20日 00:11

解法:转化为经典问题(求左侧小于当前元素的个数)+ 树状数组

设 $i < j < k$,如果 $pos2[nums1[i]] < pos2[nums1[j]] < pos2[nums1[k]]$,那么就找到了一个三元组 $(x,y,z),x = nums1[i], y = nums1[j], z = nums1[k]$。令 $f[i] = pos2[nums1[i]]$,问题由此归结为:求满足 $f[i] < f[j] < f[k] (i < j < k)$ 的三元组 $(i, j, k)$ 的数量(因为 $nums1$ 和 $nums2$ 中不包含重复元素,所以三元组 $i,j,k$ 和三元组 $x,y,z$ 是一一对应的)。

我们可以枚举三元组的中间元素 $j$,令 $left[j] =$ 在 $j$ 的左侧小于 $f[j]$ 的元素数量,$right[j] =$ 在 $j$ 右侧大于 $f[j]$ 的数量,那么以 $j$ 为中间的三元组的数量 $=left[j] \times right[j]$。

这类问题是非常典型的 LC 原题 315. 计算右侧小于当前元素的个数,可以采用 官方题解 的树状数组解法,或者归并的解法。这里我采用的是树状数组解法,代码量小,写起来出错几率小。

class Solution:
    def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        
        pos2 = dict((nums2[i], i) for i in range(n))
        f = [pos2[nums1[i]] for i in range(n)]
        
        def add(x):
            while x <= n:
                t[x] += 1
                x += (x & (-x))
        
        def query(x):
            res = 0
            while x:
                res += t[x]
                x -= (x & (-x))
            return res
        
        left, right = [0] * n, [0] * n
        
        # 计算左侧小于 f[i] 的元素个数
        t = [0] * (n + 1)
        for i in range(n):
            left[i] = query(f[i])
            add(f[i] + 1)
        
        # 计算右侧大于 f[i] 的元素个数
        t = [0] * (n + 1)
        for i in range(n-1, -1, -1):
            right[i] = n - 1 - i - query(f[i] + 1)
            add(f[i] + 1)
            
        res = 0
        for i in range(n):
            res += left[i] * right[i]
            
        return res
class Solution {
public:
    void add(vector<int>& A, int pos, int val) {
        for(; pos < A.size(); pos += (pos & (-pos))) A[pos] += val;
    }
    int query(vector<int>& A, int pos) {
        int res = 0;
        for(; pos; pos -= (pos & (-pos))) res += A[pos];
        return res;
    }
    long long goodTriplets(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        vector<int> pos2(n);
        for(int i = 0; i < nums2.size(); ++i) {
            pos2[nums2[i]] = i;
        }
        
        // 计算左侧小于该元素的个数
        vector<int> tr(n + 1, 0), left(n), right(n);
        for(int i = 0; i < n; ++i) {
            left[i] = query(tr, pos2[nums1[i]]);
            add(tr, pos2[nums1[i]] + 1, 1);
        }
        
        fill(tr.begin(), tr.end(), 0);
        
        // 计算右侧大于该元素的个数
        for(int i = n - 1; i >= 0; --i) {
            right[i] = n - 1 - i - query(tr, pos2[nums1[i]] + 1);
            add(tr, pos2[nums1[i]] + 1, 1);
        }
        
        long long res = 0;
        for(int i = 1; i < n - 1; ++i) {
            res += 1ll * left[i] * right[i];
        }
        
        return res;
    }
};

等价转换 + 树状数组(Java/Python/C++/Go)

作者 endlesscheng
2022年2月20日 00:08

提示 1

$\textit{nums}_1$ 要是变成 $[0,1,2,\dots, n-1]$ 就会简单不少。

提示 2

枚举 $y$。


前置知识:置换

置换是一个排列到另一个排列的双射。

以示例 2 为例,定义下列置换 $P(x)$:

$$
\left(\begin{array}{cccc}
x & 0 & 1 & 2 & 3 & 4\
P(x) & 1 & 2 & 4 & 3 & 0
\end{array}\right)
$$

我们可以把 $[4,0,1,3,2]$ 中的每个元素 $x$ 替换为 $P(x)$,这样可以得到一个新的排列 $[0,1,2,3,4]$。同理可以将 $[4,1,0,2,3]$ 通过置换得到新的排列 $[0,2,1,4,3]$。


将 $\textit{nums}_1$ 置换成 $[0,1,2,\dots, n-1]$,设这一置换为 $P(x)$,将 $P(x)$ 也应用到 $\textit{nums}_2$ 上。对于 $\textit{nums}_1$ 和 $\textit{nums}_2$ 中的相同元素,在置换后仍然是相同的,且元素的位置仍然是不变的,因此置换操作不会影响答案个数。

由于 $\textit{nums}_1$ 置换成了 $[0,1,2,\dots, n-1]$,因此置换后的好三元组 $(x,y,z)$ 需满足 $x<y<z$。枚举置换后的 $\textit{nums}_2$ 中的 $y$,问题就变成计算元素 $y$ 的左侧有多少个比 $y$ 小的数,以及右侧有多少个比 $y$ 大的数。这可以用树状数组/线段树/名次树来完成(Python 可以直接用 SortedList),下面代码用的是树状数组。

设 $y$ 的下标为 $i$,且其左侧有 $\textit{less}$ 个数比 $y$ 小,由于比 $y$ 大的数有 $n-1-y$ 个(注意 $y$ 的范围为 $[0,n-1]$),减去左侧比 $y$ 大的 $i-\textit{less}$ 个数,因此 $y$ 右侧有 $n-1-y-(i-\textit{less})$ 个数比它大。所以 $y$ 会有

$$
\textit{less}\cdot(n-1-y-(i-\textit{less}))
$$

个好三元组。

累加所有 $y$ 的好三元组个数,即为答案。

注意下面代码使用的是值域在 $[1,n]$ 的树状数组,需要对插入和查询的数额外加一。

  • 时间复杂度:$O(n\log n)$。
  • 空间复杂度:$O(n)$。
class Solution {
    public long goodTriplets(int[] nums1, int[] nums2) {
        var n = nums1.length;
        var p = new int[n];
        for (var i = 0; i < n; ++i)
            p[nums1[i]] = i;
        var ans = 0L;
        var tree = new int[n + 1];
        for (var i = 1; i < n - 1; ++i) {
            for (var j = p[nums2[i - 1]] + 1; j <= n; j += j & -j) // 将 p[nums2[i-1]]+1 加入树状数组
                ++tree[j];
            var y = p[nums2[i]];
            var less = 0;
            for (var j = y; j > 0; j &= j - 1) // 计算 less
                less += tree[j];
            ans += (long) less * (n - 1 - y - (i - less));
        }
        return ans;
    }
}
class Solution:
    def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        p = [0] * n
        for i, x in enumerate(nums1):
            p[x] = i
        ans = 0
        tree = [0] * (n + 1)
        for i in range(1, n - 1):
            # 将 p[nums2[i - 1]] + 1 加入树状数组
            j = p[nums2[i - 1]] + 1
            while j <= n:
                tree[j] += 1
                j += j & -j
            # 计算 less
            y, less = p[nums2[i]], 0
            j = y
            while j:
                less += tree[j]
                j &= j - 1
            ans += less * (n - 1 - y - (i - less))
        return ans
class Solution {
public:
    long long goodTriplets(vector<int> &nums1, vector<int> &nums2) {
        int n = nums1.size();
        vector<int> p(n);
        for (int i = 0; i < n; ++i)
            p[nums1[i]] = i;
        long long ans = 0;
        vector<int> tree(n + 1);
        for (int i = 1; i < n - 1; ++i) {
            for (int j = p[nums2[i - 1]] + 1; j <= n; j += j & -j) // 将 p[nums2[i-1]]+1 加入树状数组
                ++tree[j];
            int y = p[nums2[i]], less = 0;
            for (int j = y; j; j &= j - 1) // 计算 less
                less += tree[j];
            ans += (long) less * (n - 1 - y - (i - less));
        }
        return ans;
    }
};
func goodTriplets(nums1, nums2 []int) (ans int64) {
n := len(nums1)
p := make([]int, n)
for i, v := range nums1 {
p[v] = i
}
tree := make([]int, n+1)
for i := 1; i < n-1; i++ {
for j := p[nums2[i-1]] + 1; j <= n; j += j & -j { // 将 p[nums2[i-1]]+1 加入树状数组
tree[j]++
}
y, less := p[nums2[i]], 0
for j := y; j > 0; j &= j - 1 { // 计算 less
less += tree[j]
}
ans += int64(less) * int64(n-1-y-(i-less))
}
return
}

附 Python SortedList 做法:

###Python

from sortedcontainers import SortedList

class Solution:
    def goodTriplets(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        p = [0] * n
        for i, x in enumerate(nums1):
            p[x] = i
        ans = 0
        s = SortedList()
        for i in range(1, n - 1):
            s.add(p[nums2[i - 1]])
            y = p[nums2[i]]
            less = s.bisect_left(y)
            ans += less * (n - 1 - y - (i - less))
        return ans

有一道题也用到了这种置换思想:

每日一题-统计好三元组🟢

2025年4月14日 00:00

给你一个整数数组 arr ,以及 abc 三个整数。请你统计其中好三元组的数量。

如果三元组 (arr[i], arr[j], arr[k]) 满足下列全部条件,则认为它是一个 好三元组

  • 0 <= i < j < k < arr.length
  • |arr[i] - arr[j]| <= a
  • |arr[j] - arr[k]| <= b
  • |arr[i] - arr[k]| <= c

其中 |x| 表示 x 的绝对值。

返回 好三元组的数量

 

示例 1:

输入:arr = [3,0,1,1,9,7], a = 7, b = 2, c = 3
输出:4
解释:一共有 4 个好三元组:[(3,0,1), (3,0,1), (3,1,1), (0,1,1)] 。

示例 2:

输入:arr = [1,1,2,2,3], a = 0, b = 0, c = 1
输出:0
解释:不存在满足所有条件的三元组。

 

提示:

  • 3 <= arr.length <= 100
  • 0 <= arr[i] <= 1000
  • 0 <= a, b, c <= 1000

三种方法:暴力枚举 / 前缀和 / 排序+三指针(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2025年3月22日 10:25

方法一:暴力枚举

把 $\textit{arr}$ 简记为 $A$。

暴力枚举 $i,j,k$,如果同时满足

$$
\begin{aligned}
|A_i-A_j|\le a \
|A_j-A_k|\le b \
|A_i-A_k|\le c \
\end{aligned}
$$

则把答案加一。

###py

class Solution:
    def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int:
        n = len(arr)
        ans = 0
        for i in range(n):
            for j in range(i + 1, n):
                for k in range(j + 1, n):
                    if abs(arr[i] - arr[j]) <= a and abs(arr[j] - arr[k]) <= b and abs(arr[i] - arr[k]) <= c:
                        ans += 1
        return ans

###java

class Solution {
    public int countGoodTriplets(int[] arr, int a, int b, int c) {
        int n = arr.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    if (Math.abs(arr[i] - arr[j]) <= a && Math.abs(arr[j] - arr[k]) <= b && Math.abs(arr[i] - arr[k]) <= c) {
                        ans++;
                    }
                }
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        int n = arr.size(), ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    if (abs(arr[i] - arr[j]) <= a && abs(arr[j] - arr[k]) <= b && abs(arr[i] - arr[k]) <= c) {
                        ans++;
                    }
                }
            }
        }
        return ans;
    }
};

###c

int countGoodTriplets(int* arr, int arrSize, int a, int b, int c) {
    int ans = 0;
    for (int i = 0; i < arrSize; i++) {
        for (int j = i + 1; j < arrSize; j++) {
            for (int k = j + 1; k < arrSize; k++) {
                if (abs(arr[i] - arr[j]) <= a && abs(arr[j] - arr[k]) <= b && abs(arr[i] - arr[k]) <= c) {
                    ans++;
                }
            }
        }
    }
    return ans;
}

###go

func countGoodTriplets(arr []int, a, b, c int) (ans int) {
    n := len(arr)
    for i, x := range arr {
        for j := i + 1; j < n; j++ {
            for k := j + 1; k < n; k++ {
                if abs(x-arr[j]) <= a && abs(arr[j]-arr[k]) <= b && abs(x-arr[k]) <= c {
                    ans++
                }
            }
        }
    }
    return
}

func abs(x int) int { if x < 0 { return -x }; return x }

###js

var countGoodTriplets = function(arr, a, b, c) {
    const n = arr.length;
    let ans = 0;
    for (let i = 0; i < n; i++) {
        for (let j = i + 1; j < n; j++) {
            for (let k = j + 1; k < n; k++) {
                if (Math.abs(arr[i] - arr[j]) <= a && Math.abs(arr[j] - arr[k]) <= b && Math.abs(arr[i] - arr[k]) <= c) {
                    ans++;
                }
            }
        }
    }
    return ans;
};

###rust

impl Solution {
    pub fn count_good_triplets(arr: Vec<i32>, a: i32, b: i32, c: i32) -> i32 {
        let n = arr.len();
        let mut ans = 0;
        for i in 0..n {
            for j in i + 1..n {
                for k in j + 1..n {
                    if (arr[i] - arr[j]).abs() <= a && (arr[j] - arr[k]).abs() <= b && (arr[i] - arr[k]).abs() <= c {
                        ans += 1;
                    }
                }
            }
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^3)$,其中 $n$ 是 $\textit{arr}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

方法二:前缀和

枚举 $j$ 和 $k$,可以确定 $A_i$ 的范围。

  • $|A_i-A_j|\le a$ 等价于 $A_j-a\le A_i\le A_j + a$。
  • $|A_i-A_k|\le c$ 等价于 $A_k-c\le A_i\le A_k + c$。
  • 此外还有 $0\le A_i \le \max(A)$。

计算这三个范围(区间)的交集,得到 $A_i$ 的范围为

$$
[\max(A_j-a,A_k-c,0), \min(A_j + a, A_k + c, \max(A))]
$$

如果交集为空,则没有符合要求的 $A_i$。

如何计算交集中的 $A_i$ 的个数?

我们需要知道每个 $A_i$ 的出现次数,记到一个 $\textit{cnt}$ 数组中。比如 $0$ 出现 $3$ 次,$1$ 出现 $2$ 次,$3$ 出现 $4$ 次,那么有

$$
\textit{cnt} = [3,2,0,4,\cdots]
$$

统计范围中有多少个数,等价于求 $\textit{cnt}$ 的子数组的元素和。比如 $[1,3]$ 范围中的元素个数,就是 $\textit{cnt}[1]+\textit{cnt}[2]+\textit{cnt}[3]$。通过维护 $\textit{cnt}$ 数组的 前缀和 数组 $s$,就可以 $\mathcal{O}(1)$ 计算 $\textit{cnt}$ 的子数组和了。

代码实现时,无需维护 $\textit{cnt}$,而是直接维护 $s$:根据前缀和的递推式 $s[i+1] = s[i] + \textit{cnt}[i]$,如果 $\textit{cnt}[i]$ 加一了,那么下标 $\ge i+1$ 的前缀和都要加一。

###py

class Solution:
    def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int:
        ans = 0
        mx = max(arr)
        s = [0] * (mx + 2)  # cnt 数组的前缀和
        for j, y in enumerate(arr):
            for z in arr[j + 1:]:
                if abs(y - z) > b:
                    continue
                l = max(y - a, z - c, 0)
                r = min(y + a, z + c, mx)
                ans += max(s[r + 1] - s[l], 0)  # 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
            for v in range(y + 1, mx + 2):
                s[v] += 1  # 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
        return ans

###java

class Solution {
    public int countGoodTriplets(int[] arr, int a, int b, int c) {
        int mx = 0;
        for (int x : arr) {
            mx = Math.max(mx, x);
        }
        int[] s = new int[mx + 2]; // cnt 数组的前缀和

        int ans = 0;
        for (int j = 0; j < arr.length; j++) {
            int y = arr[j];
            for (int k = j + 1; k < arr.length; k++) {
                int z = arr[k];
                if (Math.abs(y - z) > b) {
                    continue;
                }
                int l = Math.max(Math.max(y - a, z - c), 0);
                int r = Math.min(Math.min(y + a, z + c), mx);
                ans += Math.max(s[r + 1] - s[l], 0); // 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
            }
            for (int v = y + 1; v < s.length; v++) {
                s[v]++; // 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        int n = arr.size(), mx = ranges::max(arr), ans = 0;
        vector<int> s(mx + 2); // cnt 数组的前缀和
        for (int j = 0; j < arr.size(); j++) {
            int y = arr[j];
            for (int k = j + 1; k < arr.size(); k++) {
                int z = arr[k];
                if (abs(y - z) > b) {
                    continue;
                }
                int l = max({y - a, z - c, 0});
                int r = min({y + a, z + c, mx});
                ans += max(s[r + 1] - s[l], 0); // 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
            }
            for (int v = y + 1; v < mx + 2; v++) {
                s[v]++; // 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
            }
        }
        return ans;
    }
};

###c

#define MIN(a, b) ((b) < (a) ? (b) : (a))
#define MAX(a, b) ((b) > (a) ? (b) : (a))

int countGoodTriplets(int* arr, int arrSize, int a, int b, int c) {
    int ans = 0, mx = 0;
    for (int i = 0; i < arrSize; i++) {
        mx = MAX(mx, arr[i]);
    }
    int* s = calloc(mx + 2, sizeof(int)); // cnt 数组的前缀和
    for (int j = 0; j < arrSize; j++) {
        int y = arr[j];
        for (int k = j + 1; k < arrSize; k++) {
            int z = arr[k];
            if (abs(y - z) > b) {
                continue;
            }
            int l = MAX(MAX(y - a, z - c), 0);
            int r = MIN(MIN(y + a, z + c), mx);
            ans += MAX(s[r + 1] - s[l], 0); // 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
        }
        for (int v = y + 1; v < mx + 2; v++) {
            s[v]++; // 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
        }
    }
    free(s);
    return ans;
}

###go

func countGoodTriplets(arr []int, a, b, c int) (ans int) {
    mx := slices.Max(arr)
    s := make([]int, mx+2) // cnt 数组的前缀和
    for j, y := range arr {
        for _, z := range arr[j+1:] {
            if abs(y-z) > b {
                continue
            }
            l := max(y-a, z-c, 0)
            r := min(y+a, z+c, mx)
            ans += max(s[r+1]-s[l], 0) // 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
        }
        for v := y + 1; v < mx+2; v++ {
            s[v]++ // 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
        }
    }
    return
}

func abs(x int) int { if x < 0 { return -x }; return x }

###js

var countGoodTriplets = function(arr, a, b, c) {
    const mx = Math.max(...arr);
    const s = Array(mx + 2).fill(0); // cnt 数组的前缀和
    let ans = 0;
    for (let j = 0; j < arr.length; j++) {
        const y = arr[j];
        for (let k = j + 1; k < arr.length; k++) {
            const z = arr[k];
            if (Math.abs(y - z) > b) {
                continue;
            }
            const l = Math.max(y - a, z - c, 0);
            const r = Math.min(y + a, z + c, mx);
            ans += Math.max(s[r + 1] - s[l], 0); // 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
        }
        for (let v = y + 1; v < mx + 2; v++) {
            s[v]++; // 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
        }
    }
    return ans;
};

###rust

impl Solution {
    pub fn count_good_triplets(arr: Vec<i32>, a: i32, b: i32, c: i32) -> i32 {
        let mut ans = 0;
        let mx = *arr.iter().max().unwrap();
        let mut s = vec![0; (mx + 2) as usize]; // cnt 数组的前缀和
        for (j, &y) in arr.iter().enumerate() {
            for &z in &arr[j + 1..] {
                if (y - z).abs() > b {
                    continue;
                }
                let l = (y - a).max(z - c).max(0);
                let r = (y + a).min(z + c).min(mx);
                // 如果 l > r + 1,s[r + 1] - s[l] 可能是负数
                ans += 0.max(s[(r + 1) as usize] - s[l as usize]);
            }
            for v in y + 1..mx + 2 {
                s[v as usize] += 1; // 把 y 加到 cnt 数组中,更新所有受到影响的前缀和
            }
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n(n+U))$,其中 $n$ 是 $\textit{arr}$ 的长度,$U=\max(\textit{arr})$。
  • 空间复杂度:$\mathcal{O}(U)$。Python 忽略切片空间。

方法三:排序 + 枚举中间 + 三指针

如果 $A_i\le 10^9$,方法二会超时,怎么办?能否做到时间复杂度和 $U$ 无关?
如果 $A$ 是有序的就好了,这样有更好的性质方便我们计算。

直接排序?不行,题目要求 $i<j<k$,不能打乱顺序。

改成创建一个 $[0,n-1]$ 的下标数组 $\textit{idx}$,对下标数组按照 $A_i$ 的值从小到大排序。

然后遍历 $\textit{idx}$ 的元素,作为下标 $j$。(枚举中间)

  • 把满足 $i<j$ 且 $|A_i-A_j|\le a$ 的 $A_i$ 保存到数组 $\textit{left}$ 中。
  • 把满足 $k>j$ 且 $|A_k-A_j|\le b$ 的 $A_k$ 保存到数组 $\textit{right}$ 中。

由于排序了,我们得到的是两个有序数组。现在问题变成:

  • 给定两个有序数组,从两个数组中各选一个数,计算绝对差 $\le c$ 的数对个数。

遍历 $\textit{left}$ 中的元素 $x$,计算 $\textit{right}$ 中有多少个元素 $z$ 满足 $|x-z|\le c$,也就是在 $[x-c,x+c]$ 中的 $z$ 的个数。

怎么求?类似前缀和的思想,这等价于:

  • $\textit{right}$ 中的 $\le x+c$ 的元素个数,减去 $\textit{right}$ 中的 $< x-c$ 的元素个数。

这可以用三指针解决:

  1. 初始化 $k_1=k_2= 0$。
  2. 遍历 $\textit{left}$ 中的元素 $x$。
  3. 如果 $\textit{right}[k_2]\le x + c$,那么增大 $k_2$,直到不满足。此时 $k_2$ 就是 $\textit{right}$ 中的 $\le x+c$ 的元素个数。
  4. 如果 $\textit{right}[k_1]< x - c$,那么增大 $k_1$,直到不满足。此时 $k_1$ 就是 $\textit{right}$ 中的 $< x-c$ 的元素个数。
  5. 把 $k_2-k_1$ 加入答案。

###py

class Solution:
    def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int:
        idx = sorted(range(len(arr)), key=lambda i: arr[i])

        ans = 0
        for j in idx:
            y = arr[j]
            left = [arr[i] for i in idx if i < j and abs(arr[i] - y) <= a]
            right = [arr[k] for k in idx if k > j and abs(arr[k] - y) <= b]

            k1 = k2 = 0
            for x in left:
                while k2 < len(right) and right[k2] <= x + c:
                    k2 += 1
                while k1 < len(right) and right[k1] < x - c:
                    k1 += 1
                ans += k2 - k1
        return ans

###java

class Solution {
    public int countGoodTriplets(int[] arr, int a, int b, int c) {
        Integer[] idx = new Integer[arr.length];
        Arrays.setAll(idx, i -> i);
        Arrays.sort(idx, (i, j) -> arr[i] - arr[j]);

        int ans = 0;
        for (int j : idx) {
            int y = arr[j];
            List<Integer> left = new ArrayList<>();
            for (int i : idx) {
                if (i < j && Math.abs(arr[i] - y) <= a) {
                    left.add(arr[i]);
                }
            }

            List<Integer> right = new ArrayList<>();
            for (int k : idx) {
                if (k > j && Math.abs(arr[k] - y) <= b) {
                    right.add(arr[k]);
                }
            }

            int k1 = 0;
            int k2 = 0;
            for (int x : left) {
                while (k2 < right.size() && right.get(k2) <= x + c) {
                    k2++;
                }
                while (k1 < right.size() && right.get(k1) < x - c) {
                    k1++;
                }
                ans += k2 - k1;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        vector<int> idx(arr.size());
        ranges::iota(idx, 0);
        ranges::sort(idx, {}, [&](int i) { return arr[i]; });

        int ans = 0;
        for (int j : idx) {
            int y = arr[j];
            vector<int> left, right;
            for (int i : idx) {
                if (i < j && abs(arr[i] - y) <= a) {
                    left.push_back(arr[i]);
                }
            }
            for (int k : idx) {
                if (k > j && abs(arr[k] - y) <= b) {
                    right.push_back(arr[k]);
                }
            }

            int k1 = 0, k2 = 0;
            for (int x : left) {
                while (k2 < right.size() && right[k2] <= x + c) {
                    k2++;
                }
                while (k1 < right.size() && right[k1] < x - c) {
                    k1++;
                }
                ans += k2 - k1;
            }
        }
        return ans;
    }
};

###c

int* _arr;
int cmp(const void* i, const void* j) {
    return _arr[*(int*)i] - _arr[*(int*)j];
}

int countGoodTriplets(int* arr, int arrSize, int a, int b, int c) {
    int* idx = malloc(arrSize * sizeof(int));
    for (int i = 0; i < arrSize; i++) {
        idx[i] = i;
    }

    _arr = arr;
    qsort(idx, arrSize, sizeof(int), cmp);

    int ans = 0;
    int* left = malloc(arrSize * sizeof(int));
    int* right = malloc(arrSize * sizeof(int));

    for (int p = 0; p < arrSize; p++) {
        int j = idx[p];
        int y = arr[j];

        int left_len = 0;
        for (int q = 0; q < arrSize; q++) {
            int i = idx[q];
            if (i < j && abs(arr[i] - y) <= a) {
                left[left_len++] = arr[i];
            }
        }

        int right_len = 0;
        for (int q = 0; q < arrSize; q++) {
            int k = idx[q];
            if (k > j && abs(arr[k] - y) <= b) {
                right[right_len++] = arr[k];
            }
        }

        int k1 = 0, k2 = 0;
        for (int i = 0; i < left_len; i++) {
            int x = left[i];
            while (k2 < right_len && right[k2] <= x + c) {
                k2++;
            }
            while (k1 < right_len && right[k1] < x - c) {
                k1++;
            }
            ans += k2 - k1;
        }
    }

    free(idx);
    free(left);
    free(right);

    return ans;
}

###go

func countGoodTriplets(arr []int, a, b, c int) (ans int) {
    idx := make([]int, len(arr))
    for i := range idx {
        idx[i] = i
    }
    slices.SortFunc(idx, func(i, j int) int { return arr[i] - arr[j] })

    for _, j := range idx {
        y := arr[j]
        var left, right []int
        for _, i := range idx {
            if i < j && abs(arr[i]-y) <= a {
                left = append(left, arr[i])
            }
        }
        for _, k := range idx {
            if k > j && abs(arr[k]-y) <= b {
                right = append(right, arr[k])
            }
        }

        k1, k2 := 0, 0
        for _, x := range left {
            for k2 < len(right) && right[k2] <= x+c {
                k2++
            }
            for k1 < len(right) && right[k1] < x-c {
                k1++
            }
            ans += k2 - k1
        }
    }
    return
}

func abs(x int) int { if x < 0 { return -x }; return x }

###js

var countGoodTriplets = function(arr, a, b, c) {
    const idx = _.range(arr.length).sort((i, j) => arr[i] - arr[j]);

    let ans = 0;
    for (const j of idx) {
        const y = arr[j];
        const left = [];
        for (const i of idx) {
            if (i < j && Math.abs(arr[i] - y) <= a) {
                left.push(arr[i]);
            }
        }

        const right = [];
        for (const k of idx) {
            if (k > j && Math.abs(arr[k] - y) <= b) {
                right.push(arr[k]);
            }
        }

        let k1 = 0, k2 = 0;
        for (const x of left) {
            while (k2 < right.length && right[k2] <= x + c) {
                k2++;
            }
            while (k1 < right.length && right[k1] < x - c) {
                k1++;
            }
            ans += k2 - k1;
        }
    }
    return ans;
};

###rust

impl Solution {
    pub fn count_good_triplets(arr: Vec<i32>, a: i32, b: i32, c: i32) -> i32 {
        let mut idx = (0..arr.len()).collect::<Vec<_>>();
        idx.sort_unstable_by_key(|&i| arr[i]);

        let mut ans = 0;
        for &j in &idx {
            let y = arr[j];
            let mut left = vec![];
            for &i in &idx {
                if i < j && (arr[i] - y).abs() <= a {
                    left.push(arr[i]);
                }
            }

            let mut right = vec![];
            for &k in &idx {
                if k > j && (arr[k] - y).abs() <= b {
                    right.push(arr[k]);
                }
            }

            let mut k1 = 0;
            let mut k2 = 0;
            for x in left {
                while k2 < right.len() && right[k2] <= x + c {
                    k2 += 1;
                }
                while k1 < right.len() && right[k1] < x - c {
                    k1 += 1;
                }
                ans += k2 - k1;
            }
        }
        ans as _
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 是 $\textit{arr}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

官方题解的理解

  • 阿巴阿巴的我终于看懂了

一、暴力解法

没啥说的,直接仨循环嵌套就是。52 ms

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        int res = 0;
        int s = arr.size();
        for(int i=s-1;i>-1;i--){
            for(int j=s-1;j>i;j--){
                for(int k=s-1;k>j;k--){
                    if(abs(arr[i]-arr[j])>a || abs(arr[j]-arr[k])>b || abs(arr[i]-arr[k])>c)
                        continue;
                    res++;
                }
            }
        }
        return res;
    }
};

不过可以在第二个循环的时候提前结束。24 ms

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        int res = 0;
        int s = arr.size();
        for(int i=s-1;i>-1;i--){
            for(int j=s-1;j>i;j--){
                if (abs(arr[i] - arr[j]) <= a)
                for(int k=s-1;k>j;k--){
                    if(abs(arr[j]-arr[k])>b || abs(arr[i]-arr[k])>c)
                        continue;
                    res++;
                }
            }
        }
        return res;
    }
};

二、官方的枚举优化

这个有点打脑壳。不过有图的话就容易理解一些。

考虑在 |arr[j] - arr[k]| ≤ b 情况下,我们需要找到一个arr[i]满足:

  1. i < j
  2. ∣arr[i]−arr[j]∣≤a ==> arr[j] - a <= arr[i] <= arr[j] + a
  3. ∣arr[i]−arr[k]∣≤c ==> arr[k] - c <= arr[i] <= arr[k] + c
  4. 0 <= arr[i] <= 1000

image.png
根据图可得:

  1. l = max(0, lj, lk)
  2. r = min(rj, rk, 1000)

注意:
这里的l, r不是索引,而是数组里的值

抛开这个,我们先看另一段代码:

for (int j = 0; j < n; ++j) {
    .......
    for (int k = arr[j]; k <= 1000; ++k) {
        ++sum[k];
    }
}

这里是把在数组[0, 1, 2,..., 1000]中所有索引为arr[j]后面的数据全部加一。如下图所示:
image.png
因为j是从头开始遍历,i必定存在j之前,所以计算l, r时可以正常得出。
好,结合之前得出的l, r:
image.png
a[i]个数是不是刚好等于sum[r] - sum[l] ?
image.png
这种呢?莫慌,取sum[l-1]就行了,因为sum[arr[i] - 1]必定小于sum[l]
所以:

ans += sum[r] - sum[l - 1];

官方代码:12ms

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        int ans = 0, n = arr.size();
        vector<int> sum(1001, 0);
        for (int j = 0; j < n; ++j) {
            for (int k = j + 1; k < n; ++k) {
                if (abs(arr[j] - arr[k]) <= b) {
                    int lj = arr[j] - a, rj = arr[j] + a;
                    int lk = arr[k] - c, rk = arr[k] + c;
                    int l = max(0, max(lj, lk)), r = min(1000, min(rj, rk));
                    if (l <= r) {
                        if (l == 0) {
                            ans += sum[r];
                        }
                        else {
                            ans += sum[r] - sum[l - 1];
                        }
                    }
                }
            }
            for (int k = arr[j]; k <= 1000; ++k) {
                ++sum[k];
            }
        }
        return ans;
    }
};
❌
❌