2176.统计数组中相等且可以被整除的数对

目标

给你一个下标从 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

思路

找出数组中下标不同的数对,使数对元素值相同且下标的乘积能够被 k 整除,返回满足要求的数对个数。

根据题意模拟即可。

有网友提出了使用原数组原地保存后面一个相同元素的下标,组成一个元素值相同的链表,避免重复判断不相等的元素。

也有网友提出了 O(nlogn) 的解法,与当前元素 nums[j] 组成合法数对需要满足 i < j,并且 nums[i] == nums[j] && (i * j) % k == 0nums[i] 必须提供因子 k2 = k / gcd(k, j),问题转化为查找 j 前,包含因子 k2 且与当前元素值相同的元素个数。提前预处理每一个数的因子列表,遍历当前值的因子列表,将每一个因子与当前值组成 key 并计数。

代码

/**
 * @date 2025-04-17 0:09
 */
public class CountPairs2176 {

    public int countPairs(int[] nums, int k) {
        int n = nums.length;
        int res = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (nums[i] == nums[j] && i * j % k == 0){
                    res++;
                }
            }
        }
        return res;
    }
}

性能

2537.统计好子数组的数目

目标

给你一个整数数组 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 <= 10^5
  • 1 <= nums[i], k <= 10^9

思路

返回数组 nums 的好子数组个数,好子数组定义为 子数组内至少有 k(i, j) 满足 i < jarr[i] == arr[j]

可以使用滑动窗口,保证窗口内的子数组中至少包含 k 个合法数对。使用哈希表记录重复元素的个数,同时累加该重复元素新增加的数对个数,如果数对个数大于等于 k,收缩左边界直到数对个数小于 k,在循环中累加结果,子数组 [l, r ~ n - 1] 的个数为 n - r。当然也可以累加 子数组 [0 ~ l - 1, r] 个数为 l,结果是一样的。

代码


/**
 * @date 2025-04-16 8:39
 */
public class CountGood2537 {

    public long countGood(int[] nums, int k) {
        int n = nums.length;
        long res = 0;
        Map<Integer, Integer> cnt = new HashMap<>();
        int l = 0;
        int pairCnt = 0;
        for (int r = 0; r < n; r++) {
            pairCnt += cnt.getOrDefault(nums[r], 0);
            cnt.merge(nums[r], 1, Integer::sum);
            while (pairCnt >= k){
                res += n - r;
                cnt.merge(nums[l], -1, Integer::sum);
                pairCnt -= cnt.get(nums[l++]);
            }
        }
        return res;
    }

}

性能

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

目标

给你两个下标从 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 < pos1z ,分别是 (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 <= 10^5
  • 0 <= nums1[i], nums2[i] <= n - 1
  • nums1 和 nums2 是 [0, 1, ..., n - 1] 的排列。

提示:

  • For every value y, how can you find the number of values x (0 ≤ x, y ≤ n - 1) such that x appears before y in both of the arrays?
  • Similarly, for every value y, try finding the number of values z (0 ≤ y, z ≤ n - 1) such that z appears after y in both of the arrays.
  • Now, for every value y, count the number of good triplets that can be formed if y is considered as the middle element.

思路

有两个 0 ~ n - 1 的排列,好三元组指这两个排列的公共子序列,求好三元组的总数目。

// todo

代码

性能

1534.统计好三元组

目标

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

如果三元组 (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

思路

返回数组中的好三元组的数量,所谓好三元组就是前两个元素值差的绝对值小于 a,后两个元素元素值差的绝对值小于 b,首尾元素差的绝对值小于 c。

数据量不大可以直接暴力解。

网友提出了另一种解法,枚举 j、k,确定 i 的范围,然后使用前缀和快速获取范围内的元素个数。

代码


/**
 * @date 2025-04-14 0:06
 */
public class CountGoodTriplets1534 {

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

性能

1922.统计好数字的数目

目标

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

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

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

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

示例 1:

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

示例 2:

输入:n = 4
输出:400

示例 3:

输入:n = 50
输出:564908303

说明:

1 <= n <= 10^15

思路

定义好数字是奇数下标为质数,偶数下标为偶数的数字,返回长度为 n 的好数字字符串的个数,结果对 10^9 + 7 取余。注意允许包含前导零。

偶数下标可选 0 2 4 6 8,奇数下标可选 2 3 5 7。实际上是考查快速幂的计算,如果 n 是奇数,那么最高位下标为偶数,5^(n/2+1) * 4^(n/2)。如果 n 是偶数,最高位下标为奇数,5^(n/2) * 4^(n/2)。合在一起就是 5^(n/2 + n%2) * 4^(n/2)

代码


/**
 * @date 2025-04-13 0:43
 */
public class CountGoodNumbers1922 {

    private static final int MOD = 1000000007;

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

    public long pow(int base, long exponent) {
        long res = 1L;
        while (exponent > 0) {
            if ((exponent & 1) == 1) {
                res = (int) (base * res % MOD);
            }
            base = (int) (((long) base * base) % MOD);
            exponent >>= 1;
        }
        return res;
    }

}

性能

3272.统计好整数的数目

目标

给你两个 正 整数 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

思路

求长度为 n 的正整数中有多少个可以重新排列成回文整数并且能够整除 k

// todo

代码

性能

2843.统计对称整数的数目

目标

给你两个正整数 low 和 high 。

对于一个由 2 * n 位数字组成的整数 x ,如果其前 n 位数字之和与后 n 位数字之和相等,则认为这个数字是一个对称整数。

返回在 [low, high] 范围内的 对称整数的数目 。

示例 1:

输入:low = 1, high = 100
输出:9
解释:在 1 到 100 范围内共有 9 个对称整数:11、22、33、44、55、66、77、88 和 99 。

示例 2:

输入:low = 1200, high = 1230
输出:4
解释:在 1200 到 1230 范围内共有 4 个对称整数:1203、1212、1221 和 1230 。

说明:

  • 1 <= low <= high <= 10^4

思路

计算给定区间内的对称整数数目,对称整数的长度为偶数,且左边数字之和等于右边数字之和。

数据范围小可以直接暴力枚举。

代码

class Solution {
    public int countSymmetricIntegers(int low, int high) {
        int res = 0;
        for (int i = low; i <= high; i++) {
            String num = String.valueOf(i);
            int r = num.length();
            if (r % 2 == 1) {
                continue;
            }
            r--;
            int l = 0;
            int diff = 0;
            while (l < r) {
                diff += num.charAt(l++) - num.charAt(r--);
            }
            res += diff != 0 ? 0 : 1;
        }
        return res;
    }
}

性能

2999.统计强大整数的数目

目标

给你三个整数 start ,finish 和 limit 。同时给你一个下标从 0 开始的字符串 s ,表示一个 正 整数。

如果一个 正 整数 x 末尾部分是 s (换句话说,s 是 x 的 后缀),且 x 中的每个数位至多是 limit ,那么我们称 x 是 强大的 。

请你返回区间 [start..finish] 内强大整数的 总数目 。

如果一个字符串 x 是 y 中某个下标开始(包括 0 ),到下标为 y.length - 1 结束的子字符串,那么我们称 x 是 y 的一个后缀。比方说,25 是 5125 的一个后缀,但不是 512 的后缀。

示例 1:

输入:start = 1, finish = 6000, limit = 4, s = "124"
输出:5
解释:区间 [1..6000] 内的强大数字为 124 ,1124 ,2124 ,3124 和 4124 。这些整数的各个数位都 <= 4 且 "124" 是它们的后缀。注意 5124 不是强大整数,因为第一个数位 5 大于 4 。
这个区间内总共只有这 5 个强大整数。

示例 2:

输入:start = 15, finish = 215, limit = 6, s = "10"
输出:2
解释:区间 [15..215] 内的强大整数为 110 和 210 。这些整数的各个数位都 <= 6 且 "10" 是它们的后缀。
这个区间总共只有这 2 个强大整数。

示例 3:

输入:start = 1000, finish = 2000, limit = 4, s = "3000"
输出:0
解释:区间 [1000..2000] 内的整数都小于 3000 ,所以 "3000" 不可能是这个区间内任何整数的后缀。

说明:

  • 1 <= start <= finish <= 10^15
  • 1 <= limit <= 9
  • 1 <= s.length <= floor(log10(finish)) + 1
  • s 数位中每个数字都小于等于 limit 。
  • s 不包含任何前导 0 。

思路

返回指定区间 [start, finish] 内,后缀为 s 且每个数字不超过 limit 的数字个数。

数位dp,需要特殊处理后缀,比如 s = 10,start = 101, finish = 521 还剩两位时,01 < 10, 21 > 10 都不能计数。

代码


/**
 * @date 2025-04-10 20:19
 */
public class NumberOfPowerfulInt2999 {

    public long numberOfPowerfulInt(long start, long finish, int limit, String s) {
        long suffix = Long.parseLong(s);
        if (finish < suffix) {
            return 0L;
        }
        int[] high = Long.toString(finish).chars().map(x -> x - '0').toArray();
        int hl = high.length;
        long[] mem = new long[hl];
        int[] low = new int[hl--];
        long tmp = start;
        while (tmp > 0) {
            low[hl--] = (int) (tmp % 10);
            tmp /= 10;
        }
        Arrays.fill(mem, -1L);
        return dfs(0, low, high, true, true, mem, limit, s);
    }

    public long dfs(int index, int[] low, int[] high, boolean isLowLimit, boolean isHighLimit, long[] mem, int limit, String s) {
        if (index == high.length - s.length()) {
            boolean unaviable = false;
            if (isHighLimit) {
                StringBuilder hr = new StringBuilder();
                int tmp = index;
                while (tmp < high.length) {
                    hr.append(high[tmp++]);
                }
                unaviable = Long.parseLong(hr.toString()) < Long.parseLong(s);
            }
            if (isLowLimit) {
                StringBuilder lr = new StringBuilder();
                while (index < high.length) {
                    lr.append(low[index++]);
                }
                unaviable = unaviable || Long.parseLong(lr.toString()) > Long.parseLong(s);
            }
            return unaviable ? 0 : 1;
        }
        if (!isLowLimit && !isHighLimit && mem[index] != -1) {
            return mem[index];
        }
        long res = 0;
        int up = isHighLimit ? Math.min(high[index], limit) : limit;
        int down = isLowLimit ? low[index] : 0;

        for (int i = down; i <= up; i++) {
            res += dfs(index + 1, low, high, isLowLimit && i == low[index], isHighLimit && i == high[index], mem, limit, s);
        }

        if (!isHighLimit && !isLowLimit) {
            mem[index] = res;
        }
        return res;
    }

}

性能

3375.使数组的值全部为K的最少操作次数

目标

给你一个整数数组 nums 和一个整数 k 。

如果一个数组中所有 严格大于 h 的整数值都 相等 ,那么我们称整数 h 是 合法的 。

比方说,如果 nums = [10, 8, 10, 8] ,那么 h = 9 是一个 合法 整数,因为所有满足 nums[i] > 9 的数都等于 10 ,但是 5 不是 合法 整数。

你可以对 nums 执行以下操作:

  • 选择一个整数 h ,它对于 当前 nums 中的值是合法的。
  • 对于每个下标 i ,如果它满足 nums[i] > h ,那么将 nums[i] 变为 h 。

你的目标是将 nums 中的所有元素都变为 k ,请你返回 最少 操作次数。如果无法将所有元素都变 k ,那么返回 -1 。

示例 1:

输入:nums = [5,2,5,4,5], k = 2
输出:2
解释:
依次选择合法整数 4 和 2 ,将数组全部变为 2 。

示例 2:

输入:nums = [2,1,2], k = 2
输出:-1
解释:
没法将所有值变为 2 。

示例 3:

输入:nums = [9,7,5,3], k = 1
输出:4
解释:
依次选择合法整数 7 ,5 ,3 和 1 ,将数组全部变为 1 。

说明:

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

思路

每次操作可以将数组中所有大于 h 的数变为 h,求将数组中所有元素变为 k 所需的最小操作数,如果无法实现返回 -1

如果数组中存在小于 k 的元素,无论如何也无法实现。否则,每次操作可以将最大元素全部变为次最大元素,问题变为统计数组中严格大于 k 的元素个数。

代码


/**
 * @date 2025-04-09 8:48
 */
public class MinOperations3375 {

    public int minOperations(int[] nums, int k) {
        Set<Integer> set = new HashSet<>();
        boolean lt = false;
        for (int num : nums) {
            if (num > k) {
                set.add(num);
            } else if (num < k) {
                lt = true;
            }
        }
        return lt ? -1 : Math.max(set.size(), 0);
    }

}

性能

3396.使数组元素互不相同所需的最少操作次数

目标

给你一个整数数组 nums,你需要确保数组中的元素 互不相同 。为此,你可以执行以下操作任意次:

  • 从数组的开头移除 3 个元素。如果数组中元素少于 3 个,则移除所有剩余元素。

注意:空数组也视作为数组元素互不相同。返回使数组元素互不相同所需的 最少操作次数 。

示例 1:

输入: nums = [1,2,3,4,2,3,3,5,7]
输出: 2
解释:
第一次操作:移除前 3 个元素,数组变为 [4, 2, 3, 3, 5, 7]。
第二次操作:再次移除前 3 个元素,数组变为 [3, 5, 7],此时数组中的元素互不相同。
因此,答案是 2。

示例 2:

输入: nums = [4,5,6,4,4]
输出: 2
解释:
第一次操作:移除前 3 个元素,数组变为 [4, 4]。
第二次操作:移除所有剩余元素,数组变为空。
因此,答案是 2。

示例 3:

输入: nums = [6,7,8,9]
输出: 0
解释:
数组中的元素已经互不相同,因此不需要进行任何操作,答案是 0。

说明:

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

思路

每次操作可以删除数组前三个元素,求使数组元素互不相同所需要的最小操作次数。

最直接的想法是记录数组中每个元素的出现次数,同时记录重复元素的个数,然后模拟删除操作,如果将某个元素的出现次数减为 1,那么将重复元素个数减 1,直到重复元素个数为 0,返回操作次数。

网友题解使用逆向思维,倒序遍历数组,直到遇到第一个重复的元素,由于操作是从头开始的,因此一定要删除该重复元素。假如下标是 i,那么需要操作 ⌈(i + 1)/3⌉ = i/3 + 1 次。

对于整数 a >= 0, b > 0,有 ⌈a/b⌉ = ⌊(a + b - 1)/b⌋,将 a = qb + r 带入分析即可。或者直接写 ⌊a/b⌋ + a % b > 0 ? 1 : 0

正向考虑稍微有点复杂,需要找到重复数字前一个下标中的最大下标。

代码


/**
 * @date 2025-04-08 8:43
 */
public class MinimumOperations3396 {

    public int minimumOperations_v1(int[] nums) {
        int[] index = new int[101];
        Set<Integer> set = new HashSet<>();
        Arrays.fill(index, -1);
        int maxIndex = -1;
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            if (set.contains(num)) {
                maxIndex = Math.max(maxIndex, index[num]);
            }
            set.add(num);
            index[num] = i;
        }
        if (maxIndex == -1) {
            return 0;
        } else {
            return maxIndex / 3 + 1;
        }
    }

    public int minimumOperations(int[] nums) {
        int[] cnt = new int[101];
        Queue<Integer> q = new ArrayDeque<>();
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            q.offer(num);
            if (++cnt[num] > 1) {
                set.add(num);
            }
        }
        if (set.size() == 0) {
            return 0;
        }
        int sameCnt = set.size();
        int res = 0;
        while (!q.isEmpty()) {
            res++;
            for (int i = 0; !q.isEmpty() && i < 3; i++) {
                if (--cnt[q.poll()] == 1) {
                    sameCnt--;
                }
            }
            if (sameCnt == 0) {
                break;
            }
        }
        return res;
    }

}

性能