普通视图

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

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

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日LeetCode 每日一题题解

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

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;
    }
};

统计好三元组

2020年8月12日 11:04

方法一:枚举

思路与算法

用 $O(n^3)$ 的循环依次枚举所有的 $(i, j, k)$,这里 $0 \leq i < j < k < {\rm arr.length}$,对于每组 $(i, j, k)$,判断 ${\rm arr}[i]$、${\rm arr}[j]$、${\rm arr}[k]$ 是否满足条件。

最终统计出所有满足条件的三元组的数量。

代码

###cpp

class Solution {
public:
    int countGoodTriplets(vector<int>& arr, int a, int b, int c) {
        int n = arr.size(), cnt = 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) {
                        ++cnt;
                    }
                }
            }
        }
        return cnt;
    }
};

###Java

class Solution {
    public int countGoodTriplets(int[] arr, int a, int b, int c) {
        int n = arr.length, cnt = 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) {
                        ++cnt;
                    }
                }
            }
        }
        return cnt;
    }
}

###JavaScript

var countGoodTriplets = function(arr, a, b, c) {
    const n = arr.length;
    let cnt = 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) {
                    ++cnt;
                }
            }
        }
    }
    return cnt;
};

###Python

class Solution:
    def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int:
        n = len(arr)
        cnt = 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:
                        cnt += 1
        return cnt

###C#

public class Solution {
    public int CountGoodTriplets(int[] arr, int a, int b, int c) {
        int n = arr.Length, cnt = 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) {
                        ++cnt;
                    }
                }
            }
        }
        return cnt;
    }
}

###Go

func countGoodTriplets(arr []int, a int, b int, c int) int {
    n := len(arr)
cnt := 0
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
for 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 {
cnt++
}
}
}
}
return cnt
}

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

###C

int countGoodTriplets(int* arr, int arrSize, int a, int b, int c) {
    int cnt = 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) {
                    ++cnt;
                }
            }
        }
    }
    return cnt;
}

###TypeScript

function countGoodTriplets(arr: number[], a: number, b: number, c: number): number {
    let n = arr.length, cnt = 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) {
                    ++cnt;
                }
            }
        }
    }
    return cnt;
};

###Rust

impl Solution {
    pub fn count_good_triplets(arr: Vec<i32>, a: i32, b: i32, c: i32) -> i32 {
        let mut cnt = 0;
        let n = arr.len();
        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 {
                        cnt += 1;
                    }
                }
            }
        }
        cnt
    }
}

复杂度分析

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

  • 空间复杂度:$O(1)$。

方法二:枚举优化

思路与算法

我们考虑 $O(n^2)$ 枚举满足 $|\rm arr[j]-\rm arr[k]|\le b$ 的二元组 $(j,k)$,统计这个二元组下有多少 $i$ 满足条件。由题目已知 $i$ 的限制条件为 $|\rm arr[i]-\rm arr[j]|\le a \ &&\ |\rm arr[i]-\rm arr[k]|\le c$,我们可以拆开绝对值,得到符合条件的值一定是 $[\rm arr[j]-a,\rm arr[j]+a]$ 和 $[\rm arr[k]-c,\rm arr[k]+c]$ 两个区间的交集,我们记为 $[l,r]$。因此,在枚举 $(j,k)$ 这个二元组的时候,我们只需要快速统计出满足 $i<j$ 且 $\rm arr[i]$ 的值域范围在 $[l,r]$ 的 $i$ 的个数即可。

很容易想到维护一个 $\rm arr[i]$ 频次数组的前缀和 $\rm sum$,对于一个二元组 $(j,k)$,我们可以 $O(1)$ 得到答案为 $\rm sum[r]-\rm sum[l-1]$。考虑怎么维护保证当前频次数组存的数的下标符合 $i<j$ 的限制,我们只要从小到大枚举 $j$,每次 $j$ 移动指针加一的时候,将 $\rm arr[j]$ 的值更新到 $\rm sum$ 数组中即可,这样能保证枚举到 $j$ 的时候 $\rm sum$ 数组里存的值的下标满足限制。

「将 $\rm arr[j]$ 的值更新到 $\rm sum$ 数组中」这个操作在本方法中是暴力更新,因为数组的值域上限很小,有能力的读者可以考虑怎么在进一步优化这一部分的复杂度,可以从离散化或者树状数组的角度考虑,这里不再赘述。

代码

###cpp

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;
    }
};

###Java

class Solution {
    public int countGoodTriplets(int[] arr, int a, int b, int c) {
        int ans = 0, n = arr.length;
        int[] sum = new int[1001];
        for (int j = 0; j < n; ++j) {
            for (int k = j + 1 ; k < n; ++k) {
                if (Math.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 = Math.max(0, Math.max(lj, lk)), r = Math.min(1000, Math.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;
    }
}

###JavaScript

var countGoodTriplets = function(arr, a, b, c) {
    const n = arr.length, sum = new Array(1001).fill(0);
    let ans = 0;
    for (let j = 0; j < n; ++j) {
        for (let k = j + 1; k < n; ++k) {
            if (Math.abs(arr[j] - arr[k]) <= b) {
                const lj = arr[j] - a, rj = arr[j] + a;
                const lk = arr[k] - c, rk = arr[k] + c;
                const l = Math.max(0, Math.max(lj, lk)), r = Math.min(1000, Math.min(rj, rk));
                if (l <= r) {
                    if (l === 0) {
                        ans += sum[r];
                    }
                    else {
                        ans += sum[r] - sum[l - 1];
                    }
                }
            }
        }
        for (let k = arr[j]; k <= 1000; ++k) {
            sum[k] += 1;
        }
    }
    return ans;
};

###Python

class Solution:
    def countGoodTriplets(self, arr: List[int], a: int, b: int, c: int) -> int:
        ans = 0
        n = len(arr)
        total = [0] * 1001
        for j in range(n):
            for k in range(j + 1, n):
                if abs(arr[j] - arr[k]) <= b:
                    lj, rj = arr[j] - a, arr[j] + a
                    lk, rk = arr[k] - c, arr[k] + c
                    l = max(0, lj, lk)
                    r = min(1000, rj, rk)
                    if l <= r:
                        ans += total[r] if l == 0 else total[r] - total[l - 1]
            for k in range(arr[j], 1001):
                total[k] += 1
        
        return ans

###C#

public class Solution {
    public int CountGoodTriplets(int[] arr, int a, int b, int c) {
        int ans = 0, n = arr.Length;
        int[] sum = new int[1001];
        for (int j = 0; j < n; ++j) {
            for (int k = j + 1; k < n; ++k) {
                if (Math.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 = Math.Max(0, Math.Max(lj, lk)), r = Math.Min(1000, Math.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;
    }
}

###Go

func countGoodTriplets(arr []int, a int, b int, c int) int {
    ans := 0
n := len(arr)
sum := make([]int, 1001)
for j := 0; j < n; j++ {
for k := j + 1; k < n; k++ {
if abs(arr[j] - arr[k]) <= b {
lj, rj := arr[j] - a, arr[j] + a
lk, rk := arr[k] - c, arr[k] + c
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 k := arr[j]; k <= 1000; k++ {
sum[k]++
}
}
return ans
}

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

###C

int countGoodTriplets(int* arr, int arrSize, int a, int b, int c) {
    int ans = 0;
    int sum[1001] = {0};
    for (int j = 0; j < arrSize; ++j) {
        for (int k = j + 1; k < arrSize; ++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 = fmax(0, fmax(lj, lk)), r = fmin(1000, fmin(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;
}

###TypeScript

function countGoodTriplets(arr: number[], a: number, b: number, c: number): number {
    let ans = 0;
    const n = arr.length;
    const sum = new Array(1001).fill(0);
    for (let j = 0; j < n; ++j) {
        for (let k = j + 1; k < n; ++k) {
            if (Math.abs(arr[j] - arr[k]) <= b) {
                const lj = arr[j] - a, rj = arr[j] + a;
                const lk = arr[k] - c, rk = arr[k] + c;
                const l = Math.max(0, Math.max(lj, lk)), r = Math.min(1000, Math.min(rj, rk));
                if (l <= r) {
                    if (l === 0) {
                        ans += sum[r];
                    } else {
                        ans += sum[r] - sum[l - 1];
                    }
                }
            }
        }
        for (let k = arr[j]; k <= 1000; ++k) {
            sum[k]++;
        }
    }
    return ans;
};

###Rust

use std::cmp::{max, min};

impl Solution {
    pub fn count_good_triplets(arr: Vec<i32>, a: i32, b: i32, c: i32) -> i32 {
        let mut ans = 0;
        let n = arr.len();
        let mut sum = vec![0; 1001];
        for j in 0..n {
            for k in j + 1..n {
                if (arr[j] - arr[k]).abs() <= b {
                    let lj = arr[j] - a;
                    let rj = arr[j] + a;
                    let lk = arr[k] - c;
                    let rk = arr[k] + c;
                    let l = max(0, max(lj, lk));
                    let r = min(1000, min(rj, rk));
                    if l <= r {
                        if l == 0 {
                            ans += sum[r as usize];
                        } else {
                            ans += sum[r as usize] - sum[(l - 1) as usize];
                        }
                    }
                }
            }
            for k in arr[j]..=1000 {
                sum[k as usize] += 1;
            }
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(n^2+nS)$,其中 $n$ 是数组 $\textit{arr}$ 的长度,$S$ 为数组的值域上限,这里为 $1000$。

  • 空间复杂度:$O(S)$。我们需要 $O(S)$ 的空间维护 $\rm arr[i]$ 频次数组的前缀和。

昨天以前LeetCode 每日一题题解

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

2025年4月13日 00:00

我们称一个数字字符串是 好数字 当它满足(下标从 0 开始)偶数 下标处的数字为 偶数 且 奇数 下标处的数字为 质数 (235 或 7)。

  • 比方说,"2582" 是好数字,因为偶数下标处的数字(2 和 8)是偶数且奇数下标处的数字(5 和 2)为质数。但 "3245" 不是 好数字,因为 3 在偶数下标处但不是偶数。

给你一个整数 n ,请你返回长度为 n 且为好数字的数字字符串 总数 。由于答案可能会很大,请你将它对 109 + 7 取余后返回 。

一个 数字字符串 是每一位都由 0 到 9 组成的字符串,且可能包含前导 0 。

 

示例 1:

输入:n = 1
输出:5
解释:长度为 1 的好数字包括 "0","2","4","6","8" 。

示例 2:

输入:n = 4
输出:400

示例 3:

输入:n = 50
输出:564908303

 

提示:

  • 1 <= n <= 1015

统计好数字的数目

2021年7月4日 14:07

方法一:快速幂

思路与算法

对于偶数下标处的数字,它可以为 $0, 2, 4, 6, 8$ 共计 $5$ 种,而长度为 $n$ 的数字字符串有 $\lfloor \dfrac{n+1}{2} \rfloor$ 个偶数下标,其中 $\lfloor x \rfloor$ 表示对 $x$ 向下取整。

对于奇数下标处的数字,它可以为 $2, 3, 5, 7$ 共计 $4$ 种,而长度为 $n$ 的数字字符串有 $\lfloor \dfrac{n}{2} \rfloor$ 个奇数下标。

因此长度为 $n$ 的数字字符串中,好数字的总数即为:

$$
5^{\lfloor \frac{n+1}{2} \rfloor} \cdot 4^{\lfloor \frac{n}{2} \rfloor}
$$

在本题中,由于 $n$ 的取值最大可以到 $10^{15}$,如果通过普通的乘法运算直接求出上式中的幂,会超出时间限制,因此我们需要使用快速幂算法对幂的求值进行优化。

快速幂算法可以参考「50. Pow(x, n)」的官方题解

代码

###C++

class Solution {
private:
    static constexpr int mod = 1000000007;
    
public:
    int countGoodNumbers(long long n) {
        // 快速幂求出 x^y % mod
        auto quickmul = [](int x, long long y) -> int {
            int ret = 1, mul = x;
            while (y > 0) {
                if (y % 2 == 1) {
                    ret = (long long)ret * mul % mod;
                }
                mul = (long long)mul * mul % mod;
                y /= 2;
            }
            return ret;
        };
        
        return (long long)quickmul(5, (n + 1) / 2) * quickmul(4, n / 2) % mod;
    }
};

###Python

class Solution:
    def countGoodNumbers(self, n: int) -> int:
        mod = 10**9 + 7
        
        # 快速幂求出 x^y % mod
        def quickmul(x: int, y: int) -> int:
            ret, mul = 1, x
            while y > 0:
                if y % 2 == 1:
                    ret = ret * mul % mod
                mul = mul * mul % mod
                y //= 2
            return ret
            
        return quickmul(5, (n + 1) // 2) * quickmul(4, n // 2) % mod

###Java

class Solution {
    long mod = 1000000007;
    
    public int countGoodNumbers(long n) {
        return (int) (quickmul(5, (n + 1) / 2) * quickmul(4, n / 2) % mod);
    }

    // 快速幂求出 x^y % mod
    public long quickmul(int x, long y) {
        long ret = 1;
        long mul = x;
        while (y > 0) {
            if (y % 2 == 1) {
                ret = ret * mul % mod;
            }
            mul = mul * mul % mod;
            y /= 2;
        }

        return ret;
    }
}

###C#

public class Solution {
    long mod = 1000000007;

    public int CountGoodNumbers(long n) {
        return (int) (Quickmul(5, (n + 1) / 2) * Quickmul(4, n / 2) % mod);
    }

    // 快速幂求出 x^y % mod
    public long Quickmul(int x, long y) {
        long ret = 1;
        long mul = x;
        while (y > 0) {
            if (y % 2 == 1) {
                ret = ret * mul % mod;
            }
            mul = mul * mul % mod;
            y /= 2;
        }

        return ret;
    }
}

###Go

func countGoodNumbers(n int64) int {
    mod := int64(1000000007)

    // 快速幂求出 x^y % mod
    quickmul := func(x, y int64) int64 {
        ret := int64(1)
        mul := x
        for y > 0 {
            if y % 2 == 1 {
                ret = ret * mul % mod
            }
            mul = mul * mul % mod
            y /= 2
        }
        return ret
    }

    return int(quickmul(5, (n + 1) / 2) * quickmul(4, n / 2) % mod)
}

###C

#define MOD 1000000007

// 快速幂求出 x^y % mod
long long quickmul(int x, long long y) {
    long long ret = 1;
    long long mul = x;
    while (y > 0) {
        if (y % 2 == 1) {
            ret = (ret * mul) % MOD;
        }
        mul = (mul * mul) % MOD;
        y /= 2;
    }
    return ret;
}

int countGoodNumbers(long long n) {
    return (int)(quickmul(5, (n + 1) / 2) * quickmul(4, n / 2) % MOD);
}

###JavaScript

var countGoodNumbers = function(n) {
    const mod = 1000000007n;

    // 快速幂求出 x^y % mod
    function quickmul(x, y) {
        let ret = 1n;
        let mul = x;
        while (y > 0) {
            if (y % 2n === 1n) {
                ret = (ret * mul) % mod;
            }
            mul = (mul * mul) % mod;
            y = y / 2n;
        }
        return ret;
    }

    return Number(quickmul(5n, BigInt(n + 1) / 2n) * quickmul(4n, BigInt(n) / 2n) % mod);
};

###TypeScript

function countGoodNumbers(n: number): number {
    const mod: bigint = 1000000007n;

    // 快速幂求出 x^y % mod
    function quickmul(x: bigint, y: bigint): bigint {
        let ret: bigint = 1n;
        let mul: bigint = x;
        while (y > 0n) {
            if (y % 2n === 1n) {
                ret = (ret * mul) % mod;
            }
            mul = (mul * mul) % mod;
            y = y / 2n;
        }
        return ret;
    }

    return Number(quickmul(5n, BigInt(n + 1) / 2n) * quickmul(4n, BigInt(n) / 2n) % mod);
};

###Rust

const MOD: i64 = 1000000007;

impl Solution {
    pub fn count_good_numbers(n: i64) -> i32 {
        (Self::quickmul(5, (n + 1) / 2) * Self::quickmul(4, n / 2) % MOD) as i32
    }

    // 快速幂求出 x^y % mod
    fn quickmul(x: i32, mut y: i64) -> i64 {
        let mut ret = 1 as i64;
        let mut mul = x as i64;
        while y > 0 {
            if y % 2 == 1 {
                ret = ret * mul % MOD;
            }
            mul = mul * mul % MOD;
            y /= 2;
        }
        ret
    }
}

复杂度分析

  • 时间复杂度:$O(\log n)$。

  • 空间复杂度:$O(1)$。

乘法原理+快速幂(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2021年7月4日 12:09

长为 $n$ 的数字字符串:

  • 有 $a = \left\lceil\dfrac{n}{2}\right\rceil=\left\lfloor\dfrac{n+1}{2}\right\rfloor$ 个偶数下标,每个下标可以填 $0,2,4,6,8$ 一共 $5$ 种偶数,方案数为 $5^a$。
  • 有 $b = \left\lfloor\dfrac{n}{2}\right\rfloor$ 个奇数下标,每个下标可以填 $2,3,5,7$ 一共 $4$ 种质数,方案数为 $4^b$。

由于偶数下标和奇数下标互相独立,根据乘法原理,方案数相乘,答案为

$$
5^a4^b
$$

上式需要用快速幂计算,原理见【图解】一张图秒懂快速幂

注意取模。关于模运算的知识点,见 模运算的世界:当加减乘除遇上取模

class Solution:
    def countGoodNumbers(self, n: int) -> int:
        MOD = 1_000_000_007
        return pow(5, (n + 1) // 2, MOD) * pow(4, n // 2, MOD) % MOD
class Solution {
    private static final int MOD = 1_000_000_007;

    public int countGoodNumbers(long n) {
        return (int) (pow(5, (n + 1) / 2) * pow(4, n / 2) % MOD);
    }

    private long pow(long x, long n) {
        long res = 1;
        while (n > 0) {
            if ((n & 1) > 0) {
                res = res * x % MOD;
            }
            x = x * x % MOD;
            n >>= 1;
        }
        return res;
    }
}
class Solution {
    const int MOD = 1'000'000'007;

    long long qpow(long long x, long long n) {
        long long res = 1;
        while (n) {
            if (n & 1) {
                res = res * x % MOD;
            }
            x = x * x % MOD;
            n >>= 1;
        }
        return res;
    }

public:
    int countGoodNumbers(long long n) {
        return qpow(5, (n + 1) / 2) * qpow(4, n / 2) % MOD;
    }
};
const int MOD = 1000000007;

long long qpow(long long x, long long n) {
    long long res = 1;
    while (n) {
        if (n & 1) {
            res = res * x % MOD;
        }
        x = x * x % MOD;
        n >>= 1;
    }
    return res;
}

int countGoodNumbers(long long n) {
    return qpow(5, (n + 1) / 2) * qpow(4, n / 2) % MOD;
}
const mod = 1_000_000_007

func countGoodNumbers(n int64) int {
    return pow(5, (int(n)+1)/2) * pow(4, int(n)/2) % mod
}

func pow(x, n int) int {
    res := 1
    for ; n > 0; n >>= 1 {
        if n&1 > 0 {
            res = res * x % mod
        }
        x = x * x % mod
    }
    return res
}
const MOD = 1_000_000_007n;

var countGoodNumbers = function(N) {
    const n = BigInt(N);
    return Number(pow(5n, (n + 1n) / 2n) * pow(4n, n / 2n) % MOD);
};

var pow = function(x, n) {
    let res = 1n;
    while (n) {
        if (n & 1n) {
            res = res * x % MOD;
        }
        x = x * x % MOD;
        n >>= 1n;
    }
    return res;
};
impl Solution {
    const MOD: i64 = 1_000_000_007;

    pub fn count_good_numbers(n: i64) -> i32 {
        (Self::pow(5, (n + 1) / 2) * Self::pow(4, n / 2) % Self::MOD) as i32
    }

    fn pow(mut x: i64, mut n: i64) -> i64 {
        let mut res = 1;
        while n > 0 {
            if n & 1 > 0 {
                res = res * x % Self::MOD;
            }
            x = x * x % Self::MOD;
            n >>= 1;
        }
        res
    }
}

复杂度分析

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

更多相似题目,见下面数学题单中的「§2.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站@灵茶山艾府

Java + 找规律 + 快速幂

作者 QuRuijie
2021年7月4日 12:09

一看就是找规律的题

先暴力,找到规律

回溯枚举n个数字的每一个数字
在判断数字是否为好数字
static int count = 0;
    public static int countGoodNumbers(long n) {
        dfs(0,(int)n,new StringBuilder());
        return count;
    }
    public static void dfs(int i, int n, StringBuilder sb){
        if(i>=n){
            String s =sb.toString();
            for (int j = 0; j < n; j++) {
                int a = s.charAt(j)-'0';
                if(j%2==0){
                    if( a % 2 !=0){
                        break;
                    }
                }else{
                    if(a!=2 && a!=3 && a!=7 && a!=5){
                        break;
                    }
                }
                if(j==n-1){
                    count++;
                }
            }
        }else{
            int N=10;
            for (int j = 0; j < N; j++) {
                StringBuilder ss=new StringBuilder(sb);
                ss.append((char)('0'+j));
                dfs(i+1,n,ss);
            }
        }
    }

发现以下规律

5  20  100  400 2000
1   2   3    4    5
一开始为5
每次到奇数就 * 5
每次到偶数就 * 4
所以结果为 
4 ^ (n中的偶数) * 5 ^ (n中的偶数)
    public int countGoodNumbers(long n) {
        int N=(int)Math.pow(10,9)+7;
        //如果是奇数,还得乘个5
        int cheng=1;
        if(n%2==1){
            cheng=5;
            //n变为偶数
            n-=1;
        }
        //n中有多少个偶数
        long o=n/2;
        // 4*5 的 偶数 次方
        long a = myPow(20,o,N);
        long ans = a * cheng;
        return (int)(ans%N);
    }
    //快速幂 (记得要取余N,不只是结果取余,每次乘机也要取余)
    public long myPow(long x, long n,int N) {
        if(n==0){
            return 1;
        }
        long m=n;
        long sum=1;
        while(m!=0){
            if((m&1)==1){
                sum*=x;
                sum%=N;
            }
            x*=x;
            x%=N;
            m>>=1;
        }
        return sum;
    }

image.png

搜索 & 枚举

作者 tsreaper
2024年9月1日 00:08

解法:搜索 & 枚举

因为好整数是 $k$ 回文整数的排列,因此我们可以把数字频率相同的 $k$ 回文整数视为同一种。枚举每种 $k$ 回文整数,再算算它不以 $0$ 为开头的排列有几种即可。

怎样枚举每种 $k$ 回文整数呢?由于回文数要求前半和后半相同,因此我们可以通过 DFS 搜索回文数的前半部分填什么。找到一个 $k$ 回文整数之后,再统计它的数字频率即可。这一部分的复杂度是 $\mathcal{O}(b^{\frac{n}{2}} \times b)$,其中 $b = 10$ 是可选的数字种类。因为 $n$ 很小所以没问题。

最后一个问题:给定每种数字的频率,怎么求不以 $0$ 为开头的排列有几种?我们可以用全排列数扣掉以 $0$ 为开头的排列。带重复元素的全排列数公式为

$$\frac{n!}{\prod n_i!}$$

其中 $n_i$ 是第 $i$ 种元素的出现频率。那么设 $c_i$ 为数字 $i$ 出现的频率,答案就是

$$
\frac{n!}{\prod\limits_{i = 0}^9 c_i!} - \frac{(n - 1)!}{(c_0 - 1)! \times \prod\limits_{i = 1}^9 c_i!}
$$

因为对于每种 $k$ 回文整数,我们需要 $\mathcal{O}(b)$ 的复杂度计算答案,这一部分的复杂度也是 $\mathcal{O}(b^{\frac{n}{2}} \times b)$。

参考代码(c++)

###cpp

class Solution {
public:
    long long countGoodIntegers(int n, int K) {
        long long P[n];
        P[0] = 1;
        for (int i = 1; i < n; i++) P[i] = P[i - 1] * 10;

        int cnt[10] = {0};
        // 用 string 保存每种数的出现次数,因为 vector 不能放进 unordered_set 里
        unordered_set<string> st;
        auto dfs = [&](auto &&self, int l, int r, long long now) {
            if (l > r) {
                // 所有数都填完了,检查能否被 K 整除,并记录数字出现频率
                if (now % K != 0) return;
                string s;
                for (int i = 0; i < 10; i++) s.push_back(cnt[i]);
                st.insert(s);
                return;
            }

            // 枚举第 l 和 r 位放什么数,注意不能有前导 0
            for (int i = (r == n - 1 ? 1 : 0); i < 10; i++) {
                if (l == r) {
                    cnt[i]++;
                    self(self, l + 1, r - 1, now + i * P[l]);
                    cnt[i]--;
                } else {
                    cnt[i] += 2;
                    self(self, l + 1, r - 1, now + i * (P[l] + P[r]));
                    cnt[i] -= 2;
                }
            }
        };
        dfs(dfs, 0, n - 1, 0);

        // 预处理阶乘
        long long fac[n + 1];
        fac[0] = 1;
        for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i;

        long long ans = 0;
        // 枚举每种 K 回文整数
        for (auto &s : st) {
            // 全排列数
            long long a = fac[n];
            for (int i = 0; i < 10; i++) a /= fac[s[i]];
            ans += a;
            if (s[0] == 0) continue;
            // 减去以 0 为开头的排列数
            a = fac[n - 1];
            for (int i = 0; i < 10; i++) {
                int t = (i == 0 ? s[i] - 1 : s[i]);
                a /= fac[t];
            }
            ans -= a;
        }
        return ans;
    }
};

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

作者 lcbin
2025年4月12日 09:44

方法一:枚举 + 组合数学

我们可以考虑枚举所有长度为 $n$ 的回文数,判断它们是否是 $k$ 回文数。由于回文数的性质,我们只需要枚举前半部分的数字,然后将其反转拼接到后面即可。

前半部分的数字长度为 $\lfloor \frac{n - 1}{2} \rfloor$,那么前半部分的数字范围是 $[10^{\lfloor \frac{n - 1}{2} \rfloor}, 10^{\lfloor \frac{n - 1}{2} \rfloor + 1})$。我们可以将前半部分的数字拼接到后面,形成一个长度为 $n$ 的回文数。注意到,如果 $n$ 是奇数,则需要将中间的数字做特殊处理。

然后,我们判断该回文数是否是 $k$ 回文数,如果是,则统计该回文数的所有排列组合。为了避免重复,我们可以使用一个集合 $\textit{vis}$ 来存储已经出现过的回文数的每个最小排列。如果该回文数的最小排列已经出现过,则跳过该回文数。否则,我们统计该回文数有多少个不重复的排列组合,添加到答案中。

我们可以使用一个数组 $\textit{cnt}$ 来统计每个数字出现的次数,然后使用组合数学的公式计算排列组合的数量。具体来说,假设数字 $0$ 出现了 $x_0$ 次,数字 $1$ 出现了 $x_1$ 次,...,数字 $9$ 出现了 $x_9$ 次,那么该回文数的排列组合数量为:

$$
\frac{(n - x_0) \cdot (n - 1)!}{x_0! \cdot x_1! \cdots x_9!}
$$

其中 $(n - x_0)$ 表示最高位可以选择除 $0$ 以外的所有数字,而 $(n - 1)!$ 表示除最高位以外的所有数字的排列组合数量,然后我们除以每个数字出现的次数的阶乘,避免重复。

最后,我们将所有的排列组合数量相加,得到最终的答案。

###python

class Solution:
    def countGoodIntegers(self, n: int, k: int) -> int:
        fac = [factorial(i) for i in range(n + 1)]
        ans = 0
        vis = set()
        base = 10 ** ((n - 1) // 2)
        for i in range(base, base * 10):
            s = str(i)
            s += s[::-1][n % 2 :]
            if int(s) % k:
                continue
            t = "".join(sorted(s))
            if t in vis:
                continue
            vis.add(t)
            cnt = Counter(t)
            res = (n - cnt["0"]) * fac[n - 1]
            for x in cnt.values():
                res //= fac[x]
            ans += res
        return ans

###java

class Solution {
    public long countGoodIntegers(int n, int k) {
        long[] fac = new long[n + 1];
        fac[0] = 1;
        for (int i = 1; i <= n; i++) {
            fac[i] = fac[i - 1] * i;
        }

        long ans = 0;
        Set<String> vis = new HashSet<>();
        int base = (int) Math.pow(10, (n - 1) / 2);

        for (int i = base; i < base * 10; i++) {
            String s = String.valueOf(i);
            StringBuilder sb = new StringBuilder(s).reverse();
            s += sb.substring(n % 2);
            if (Long.parseLong(s) % k != 0) {
                continue;
            }

            char[] arr = s.toCharArray();
            Arrays.sort(arr);
            String t = new String(arr);
            if (vis.contains(t)) {
                continue;
            }
            vis.add(t);
            int[] cnt = new int[10];
            for (char c : arr) {
                cnt[c - '0']++;
            }

            long res = (n - cnt[0]) * fac[n - 1];
            for (int x : cnt) {
                res /= fac[x];
            }
            ans += res;
        }

        return ans;
    }
}

###cpp

class Solution {
public:
    long long countGoodIntegers(int n, int k) {
        vector<long long> fac(n + 1, 1);
        for (int i = 1; i <= n; ++i) {
            fac[i] = fac[i - 1] * i;
        }

        long long ans = 0;
        unordered_set<string> vis;
        int base = pow(10, (n - 1) / 2);

        for (int i = base; i < base * 10; ++i) {
            string s = to_string(i);
            string rev = s;
            reverse(rev.begin(), rev.end());
            s += rev.substr(n % 2);
            if (stoll(s) % k) {
                continue;
            }
            string t = s;
            sort(t.begin(), t.end());
            if (vis.count(t)) {
                continue;
            }
            vis.insert(t);
            vector<int> cnt(10);
            for (char c : t) {
                cnt[c - '0']++;
            }
            long long res = (n - cnt[0]) * fac[n - 1];
            for (int x : cnt) {
                res /= fac[x];
            }
            ans += res;
        }
        return ans;
    }
};

###go

func factorial(n int) []int64 {
fac := make([]int64, n+1)
fac[0] = 1
for i := 1; i <= n; i++ {
fac[i] = fac[i-1] * int64(i)
}
return fac
}

func countGoodIntegers(n int, k int) (ans int64) {
fac := factorial(n)
vis := make(map[string]bool)
base := int(math.Pow(10, float64((n-1)/2)))

for i := base; i < base*10; i++ {
s := strconv.Itoa(i)
rev := reverseString(s)
s += rev[n%2:]
num, _ := strconv.ParseInt(s, 10, 64)
if num%int64(k) != 0 {
continue
}
bs := []byte(s)
slices.Sort(bs)
t := string(bs)

if vis[t] {
continue
}
vis[t] = true
cnt := make([]int, 10)
for _, c := range t {
cnt[c-'0']++
}
res := (int64(n) - int64(cnt[0])) * fac[n-1]
for _, x := range cnt {
res /= fac[x]
}
ans += res
}

return
}

func reverseString(s string) string {
t := []byte(s)
for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 {
t[i], t[j] = t[j], t[i]
}
return string(t)
}

###ts

function countGoodIntegers(n: number, k: number): number {
    const fac = factorial(n);
    let ans = 0;
    const vis = new Set<string>();
    const base = Math.pow(10, Math.floor((n - 1) / 2));

    for (let i = base; i < base * 10; i++) {
        let s = `${i}`;
        const rev = reverseString(s);
        if (n % 2 === 1) {
            s += rev.substring(1);
        } else {
            s += rev;
        }

        if (+s % k !== 0) {
            continue;
        }

        const bs = Array.from(s).sort();
        const t = bs.join('');

        if (vis.has(t)) {
            continue;
        }

        vis.add(t);

        const cnt = Array(10).fill(0);
        for (const c of t) {
            cnt[+c]++;
        }

        let res = (n - cnt[0]) * fac[n - 1];
        for (const x of cnt) {
            res /= fac[x];
        }
        ans += res;
    }

    return ans;
}

function factorial(n: number): number[] {
    const fac = Array(n + 1).fill(1);
    for (let i = 1; i <= n; i++) {
        fac[i] = fac[i - 1] * i;
    }
    return fac;
}

function reverseString(s: string): string {
    return s.split('').reverse().join('');
}

###rust

impl Solution {
    pub fn count_good_integers(n: i32, k: i32) -> i64 {
        use std::collections::HashSet;
        let n = n as usize;
        let k = k as i64;
        let mut fac = vec![1_i64; n + 1];
        for i in 1..=n {
            fac[i] = fac[i - 1] * i as i64;
        }

        let mut ans = 0;
        let mut vis = HashSet::new();
        let base = 10_i64.pow(((n - 1) / 2) as u32);

        for i in base..base * 10 {
            let s = i.to_string();
            let rev: String = s.chars().rev().collect();
            let full_s = if n % 2 == 0 {
                format!("{}{}", s, rev)
            } else {
                format!("{}{}", s, &rev[1..])
            };

            let num: i64 = full_s.parse().unwrap();
            if num % k != 0 {
                continue;
            }

            let mut arr: Vec<char> = full_s.chars().collect();
            arr.sort_unstable();
            let t: String = arr.iter().collect();
            if vis.contains(&t) {
                continue;
            }
            vis.insert(t);

            let mut cnt = vec![0; 10];
            for c in arr {
                cnt[c as usize - '0' as usize] += 1;
            }

            let mut res = (n - cnt[0]) as i64 * fac[n - 1];
            for &x in &cnt {
                if x > 0 {
                    res /= fac[x];
                }
            }
            ans += res;
        }

        ans
    }
}

###js

/**
 * @param {number} n
 * @param {number} k
 * @return {number}
 */
var countGoodIntegers = function (n, k) {
    const fac = factorial(n);
    let ans = 0;
    const vis = new Set();
    const base = Math.pow(10, Math.floor((n - 1) / 2));

    for (let i = base; i < base * 10; i++) {
        let s = String(i);
        const rev = reverseString(s);
        if (n % 2 === 1) {
            s += rev.substring(1);
        } else {
            s += rev;
        }

        if (parseInt(s, 10) % k !== 0) {
            continue;
        }

        const bs = Array.from(s).sort();
        const t = bs.join('');

        if (vis.has(t)) {
            continue;
        }

        vis.add(t);

        const cnt = Array(10).fill(0);
        for (const c of t) {
            cnt[parseInt(c, 10)]++;
        }

        let res = (n - cnt[0]) * fac[n - 1];
        for (const x of cnt) {
            res /= fac[x];
        }
        ans += res;
    }

    return ans;
};

function factorial(n) {
    const fac = Array(n + 1).fill(1);
    for (let i = 1; i <= n; i++) {
        fac[i] = fac[i - 1] * i;
    }
    return fac;
}

function reverseString(s) {
    return s.split('').reverse().join('');
}

时间复杂度 $({10}^m \times n \times \log n)$,空间复杂度 $O({10}^m \times n)$,其中 $m = \lfloor \frac{n - 1}{2} \rfloor$。


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

每日一题-统计好整数的数目🔴

2025年4月12日 00:00

给你两个  整数 n 和 k 。

如果一个整数 x 满足以下条件,那么它被称为 k 回文 整数 。

  • x 是一个 回文整数 。
  • x 能被 k 整除。

如果一个整数的数位重新排列后能得到一个 k 回文整数 ,那么我们称这个整数为 整数。比方说,k = 2 ,那么 2020 可以重新排列得到 2002 ,2002 是一个 k 回文串,所以 2020 是一个好整数。而 1010 无法重新排列数位得到一个 k 回文整数。

请你返回 n 个数位的整数中,有多少个  整数。

注意 ,任何整数在重新排列数位之前或者之后 都不能 有前导 0 。比方说 1010 不能重排列得到 101 。

 

示例 1:

输入:n = 3, k = 5

输出:27

解释:

部分好整数如下:

  • 551 ,因为它可以重排列得到 515 。
  • 525 ,因为它已经是一个 k 回文整数。

示例 2:

输入:n = 1, k = 4

输出:2

解释:

两个好整数分别是 4 和 8 。

示例 3:

输入:n = 5, k = 6

输出:2468

 

提示:

  • 1 <= n <= 10
  • 1 <= k <= 9

回溯+全排列模板题

作者 sierpinski-f
2024年9月1日 09:42

Problem: 3272. 统计好整数的数目

[TOC]

思路

先回溯找到所有可能的回文串,然后统计每个回文串的排列个数,注意排列不能0开头

Code

###Python3

@cache
def fac(x):
    if x == 0:
        return 1
    return x * fac(x-1)

class Solution:
    def countGoodIntegers(self, n: int, k: int) -> int:
        pow10 = [1] * n
        for i in range(1, n):
            pow10[i] = pow10[i - 1] * 10 % k

        st = set()
        m = (n + 1) // 2
        res = []
        def dfs(i: int, j: int) -> None:
            if i == m:
                if j == 0:
                    cur = res.copy()
                    cur += cur[:n // 2]
                    cur.sort()
                    st.add("".join(cur))

                return
            
            mn = 1 if i == 0 else 0
            for d in range(mn, 10):  
                if n % 2 and i == m - 1:  # 正中间
                    j2 = (j + d * pow10[i]) % k
                else:
                    j2 = (j + d * (pow10[i] + pow10[-1 - i])) % k
                res.append(str(d))
                dfs(i + 1, j2)
                res.pop()
            
        dfs(0, 0)
        ans = 0
        for x in st:
            cnt = Counter(x)
            f = fac(n)
            for v in cnt.values():
                f //= fac(v)
            
            # 减去0开头的
            if cnt['0'] > 0:
                f1 = fac(n-1) 
                for k, v in cnt.items():
                    if k == '0':
                        f1 //= fac(v - 1)
                    else:
                        f1 //= fac(v)
                f -= f1 
            ans += f 
        
        return ans

枚举所有回文数+组合数学(Python/Java/C++/Go)

作者 endlesscheng
2024年9月1日 08:48

考虑枚举所有长为 $n$ 的回文数。

首先,知道了回文数的左半边,就知道了回文数的右半边,所以可以枚举回文数的左半边。

设 $m = \left\lfloor\dfrac{n-1}{2}\right\rfloor$,设 $\textit{base} = 10^m$。

在 $[\textit{base}, 10\cdot\textit{base})$ 范围内枚举所有长为 $n$ 的回文数的左半边。

如果回文数 $x$ 能被 $k$ 整除,那么接下来需要解决的问题有两个:

  1. 计算 $x$ 有多少个不同的排列。
  2. 不能重复统计。

为了保证不重复统计,可以像 49. 字母异位词分组 那样,把 $x$ 的十进制字符串 $s$ 排序,如果之前遇到过同样的字符串 $t$,那么 $s$ 生成的所有排列,$t$ 也能生成。用哈希表记录排序后的字符串,如果 $s$ 排序后在哈希表中,那么就跳过。

下面是组合数学时间。

本质上计算的是「有重复元素的排列个数」。

统计 $s$ 中的每个数字的出现次数 $\textit{cnt}$。

先填最高位。由于不能有前导零,最高位可以填的数有 $n-\textit{cnt}_0$ 个。其余 $n-1$ 个数随便排,有 $(n-1)!$ 种方案。

当然,这里面有重复的,例如 $x=34543$,其中两个 $3$ 和两个 $4$ 的排列就是重复的,由于这两个 $3$ 无法区分,两个 $4$ 无法区分,方案数要除以 $2!2!$。

综上,排列个数为

$$
\dfrac{(n-\textit{cnt}0)\cdot (n-1)!}{\prod\limits{i=0}^{9}\textit{cnt}_i!}
$$

加入答案。

具体请看 视频讲解 第三题,欢迎点赞关注~

###py

class Solution:
    def countGoodIntegers(self, n: int, k: int) -> int:
        fac = [factorial(i) for i in range(n + 1)]
        ans = 0
        vis = set()
        base = 10 ** ((n - 1) // 2)
        for i in range(base, base * 10):  # 枚举回文数左半边
            s = str(i)
            s += s[::-1][n % 2:]
            if int(s) % k:  # 回文数不能被 k 整除
                continue

            sorted_s = ''.join(sorted(s))
            if sorted_s in vis:  # 不能重复统计
                continue
            vis.add(sorted_s)

            cnt = Counter(sorted_s)
            res = (n - cnt['0']) * fac[n - 1]
            for c in cnt.values():
                res //= fac[c]
            ans += res
        return ans

###java

class Solution {
    public long countGoodIntegers(int n, int k) {
        int[] factorial = new int[n + 1];
        factorial[0] = 1;
        for (int i = 1; i <= n; i++) {
            factorial[i] = factorial[i - 1] * i;
        }

        long ans = 0;
        Set<String> vis = new HashSet<>();
        int base = (int) Math.pow(10, (n - 1) / 2);
        for (int i = base; i < base * 10; i++) { // 枚举回文数左半边
            String s = Integer.toString(i);
            s += new StringBuilder(s).reverse().substring(n % 2);
            if (Long.parseLong(s) % k > 0) { // 回文数不能被 k 整除
                continue;
            }

            char[] sortedS = s.toCharArray();
            Arrays.sort(sortedS);
            if (!vis.add(new String(sortedS))) { // 不能重复统计
                continue;
            }

            int[] cnt = new int[10];
            for (char c : sortedS) {
                cnt[c - '0']++;
            }
            int res = (n - cnt[0]) * factorial[n - 1];
            for (int c : cnt) {
                res /= factorial[c];
            }
            ans += res;
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long countGoodIntegers(int n, int k) {
        vector<int> factorial(n + 1);
        factorial[0] = 1;
        for (int i = 1; i <= n; i++) {
            factorial[i] = factorial[i - 1] * i;
        }

        long long ans = 0;
        unordered_set<string> vis;
        int base = pow(10, (n - 1) / 2);
        for (int i = base; i < base * 10; i++) { // 枚举回文数左半边
            string s = to_string(i);
            s += string(s.rbegin() + (n % 2), s.rend());
            if (stoll(s) % k) { // 回文数不能被 k 整除
                continue;
            }

            ranges::sort(s);
            if (!vis.insert(s).second) { // 不能重复统计
                continue;
            }

            int cnt[10]{};
            for (char c : s) {
                cnt[c - '0']++;
            }
            int res = (n - cnt[0]) * factorial[n - 1];
            for (int c : cnt) {
                res /= factorial[c];
            }
            ans += res;
        }
        return ans;
    }
};

###go

func countGoodIntegers(n, k int) (ans int64) {
factorial := make([]int, n+1)
factorial[0] = 1
for i := 1; i <= n; i++ {
factorial[i] = factorial[i-1] * i
}

vis := map[string]bool{}
base := int(math.Pow10((n - 1) / 2))
for i := base; i < base*10; i++ { // 枚举回文数左半边
x := i
t := i
if n%2 > 0 {
t /= 10
}
for ; t > 0; t /= 10 {
x = x*10 + t%10
}
if x%k > 0 { // 回文数不能被 k 整除
continue
}

bs := []byte(strconv.Itoa(x))
slices.Sort(bs)
s := string(bs)
if vis[s] { // 不能重复统计
continue
}
vis[s] = true

cnt := [10]int{}
for _, c := range bs {
cnt[c-'0']++
}
res := (n - cnt[0]) * factorial[n-1]
for _, c := range cnt {
res /= factorial[c]
}
ans += int64(res)
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(10^m\cdot n\log n)$,其中 $m = \left\lfloor\dfrac{n-1}{2}\right\rfloor$。
  • 空间复杂度:$\mathcal{O}(10^m\cdot 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站@灵茶山艾府

枚举 + 组合数学

作者 mipha-2022
2024年9月1日 00:15

Problem: 3272. 统计好整数的数目

[TOC]

思路

枚举

回文,很容易想到折半枚举,n最大10位,折半枚举最大就5位,所以:

  • 枚举回文左边
  • 镜像得到右边
  • 拼接成完整数字,再判断该数字能否被k整除
        # 枚举
        n_half = (n+1) // 2
        start = 10**(n_half-1)
        end = 10**n_half
        dt = set()
        for num in range(start,end,1):
            l = str(num)
            if n&1:
                r = l[:-1][::-1]
            else:
                r = l[::-1]
            
            s = l+r
            if int(s) % k == 0:
                # 去重
                dt.add(''.join(sorted(s)))

组合数学

将第一步得到的数字打乱,看可以得到多少个没有前缀0的整数。

  • 枚举非0头位
  • 剩下的位置塞入其它数字
        res = 0
        # 组合数学
        for s in dt:
            # 计数
            cnt = Counter(s)

            # 开头非0
            for w in '123456789':
                if cnt[w]:
                    cnt[w] -= 1
                    # 组合数学
                    t = 1
                    rest = n - 1
                    for lw in '0123456789':
                        # 剩余位中插入数字
                        t *= comb(rest,cnt[lw])
                        rest -= cnt[lw]
                    # 回溯
                    cnt[w] += 1
                    res += t

        return res

更多题目模板总结,请参考2023年度总结与题目分享

Code

###Python3

class Solution:
    def countGoodIntegers(self, n: int, k: int) -> int:
        '''
        打表
        折半 + 组合数字
        '''
        
        # 枚举
        n_half = (n+1) // 2
        start = 10**(n_half-1)
        end = 10**n_half
        dt = set()
        for num in range(start,end,1):
            l = str(num)
            if n&1:
                r = l[:-1][::-1]
            else:
                r = l[::-1]
            
            s = l+r
            if int(s) % k == 0:
                # 去重
                dt.add(''.join(sorted(s)))
        
        res = 0
        # 组合数学
        for s in dt:
            # 计数
            cnt = Counter(s)

            # 开头非0
            for w in '123456789':
                if cnt[w]:
                    cnt[w] -= 1
                    # 组合数学
                    t = 1
                    rest = n - 1
                    for lw in '0123456789':
                        if cnt[lw]:
                            t *= comb(rest,cnt[lw])
                            rest -= cnt[lw]
                    # 回溯
                    cnt[w] += 1
                    res += t

        return res
❌
❌