3623.统计梯形的数目I

目标

给你一个二维整数数组 points,其中 points[i] = [xi, yi] 表示第 i 个点在笛卡尔平面上的坐标。

水平梯形 是一种凸四边形,具有 至少一对 水平边(即平行于 x 轴的边)。两条直线平行当且仅当它们的斜率相同。

返回可以从 points 中任意选择四个不同点组成的 水平梯形 数量。

由于答案可能非常大,请返回结果对 10^9 + 7 取余数后的值。

示例 1:

输入: points = [[1,0],[2,0],[3,0],[2,2],[3,2]]
输出: 3
解释:
有三种不同方式选择四个点组成一个水平梯形:
使用点 [1,0]、[2,0]、[3,2] 和 [2,2]。
使用点 [2,0]、[3,0]、[3,2] 和 [2,2]。
使用点 [1,0]、[3,0]、[3,2] 和 [2,2]。

示例 2:

输入: points = [[0,0],[1,0],[0,1],[2,1]]
输出: 1
解释:
只有一种方式可以组成一个水平梯形。

说明:

  • 4 <= points.length <= 10^5
  • –10^8 <= xi, yi <= 10^8
  • 所有点两两不同。

思路

有一些二维平面中的点 points,从中选取四个点组成水平梯形,返回水平梯形的数目。

水平梯形是有一对边平行于 x 轴的梯形。直接的想法是根据纵坐标分组,选两组,每组中选两个点。

可以直接计算每组的组合数 C(n, 2) = n * (n - 1) / 2,计算分组组合数的后缀和,根据乘法原理计算即可。

代码


/**
 * @date 2025-12-02 0:14
 */
public class CountTrapezoids2623 {

    /**
     * 执行通过
     */
    public int countTrapezoids(int[][] points) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int[] point : points) {
            cnt.merge(point[1], 1, Integer::sum);
        }
        int mod = 1000000007;
        for (Map.Entry<Integer, Integer> entry : cnt.entrySet()) {
            Integer c = entry.getValue();
            cnt.put(entry.getKey(), (int) ((c * (c - 1L) / 2) % mod));
        }
        int[] comb = cnt.values().stream().mapToInt(x -> x).toArray();
        int n = comb.length;
        int[] suffix = new int[n + 1];
        for (int i = n - 1; i >= 0; i--) {
            suffix[i] = (suffix[i + 1] + comb[i]) % mod;
        }
        long res = 0L;
        for (int i = 0; i < n; i++) {
            res = (res + ((long) comb[i] * suffix[i + 1])) % mod;
        }
        return (int) res;
    }
}

性能

1590.使数组和能被P整除

目标

给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。

请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。

子数组 定义为原数组中连续的一组元素。

示例 1:

输入:nums = [3,1,4,2], p = 6
输出:1
解释:nums 中元素和为 10,不能被 p 整除。我们可以移除子数组 [4] ,剩余元素的和为 6 。

示例 2:

输入:nums = [6,3,5,2], p = 9
输出:2
解释:我们无法移除任何一个元素使得和被 9 整除,最优方案是移除子数组 [5,2] ,剩余元素为 [6,3],和为 9 。

示例 3:

输入:nums = [1,2,3], p = 3
输出:0
解释:和恰好为 6 ,已经能被 3 整除了。所以我们不需要移除任何元素。

示例 4:

输入:nums = [1,2,3], p = 7
输出:-1
解释:没有任何方案使得移除子数组后剩余元素的和被 7 整除。

示例 5:

输入:nums = [1000000000,1000000000,1000000000], p = 3
输出:0

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= p <= 10^9

思路

有一个正整数数组 nums,需要移除一个最短子数组(可以为空),使得剩余元素的和能被 p 整除,返回移除的最短子数组长度,如果无法满足返回 -1

计算数组和,求出余数 rem,使用哈希表 lastIndex 记录前缀和余数的最大下标,根据当前前缀和的余数,计算需要保留的前缀数组 [0, lastIndex.get((curRem - rem + p) % p)],计算得到需要减去的子数组 [lastIndex.get((curRem - rem + p) % p) + 1, i] 它的和模 prem,取长度的最小值即可。

代码


/**
 * @date 2025-11-30 10:41
 */
public class MinSubarray1590 {

    public int minSubarray(int[] nums, int p) {
        int n = nums.length;
        long sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int rem = (int) (sum % p);
        if (rem == 0) {
            return 0;
        }
        Map<Integer, Integer> lastIndex = new HashMap<>();
        lastIndex.putIfAbsent(0, -1);
        int curRem = 0;
        int res = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            curRem = (curRem + nums[i]) % p;
            int prev = (p + curRem - rem) % p;
            res = Math.min(res, i - lastIndex.getOrDefault(prev, -n));
            lastIndex.put(curRem, i);
        }
        return res == n ? -1 : res;
    }

}

性能

3381.长度可被K整除的子数组的最大元素和

目标

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

返回 nums 中一个 非空子数组 的 最大 和,要求该子数组的长度可以 被 k 整除。

示例 1:

输入: nums = [1,2], k = 1
输出: 3
解释:
子数组 [1, 2] 的和为 3,其长度为 2,可以被 1 整除。

示例 2:

输入: nums = [-1,-2,-3,-4,-5], k = 4
输出: -10
解释:
满足题意且和最大的子数组是 [-1, -2, -3, -4],其长度为 4,可以被 4 整除。

示例 3:

输入: nums = [-5,1,2,-3,4], k = 2
输出: 4
解释:
满足题意且和最大的子数组是 [1, 2, -3, 4],其长度为 4,可以被 2 整除。

说明:

  • 1 <= k <= nums.length <= 2 * 10^5
  • -10^9 <= nums[i] <= 10^9

思路

计算长度能被 k 整除的子数组的最大元素和。

核心点是维护同余前缀和的最小值。

也有网友使用滑窗加动态规划来做,滑窗计算 长度为 k 的子数组和,动态规划累加长度 m * k 的子数组和,这里使用了贪心策略,如果前面的子数组和小于 0,直接重置为 0

代码


/**
 * @date 2025-11-27 9:06
 */
public class MaxSubarraySum3381 {

    public long maxSubarraySum(int[] nums, int k) {
        int n = nums.length;
        long[] prefix = new long[n + 1];
        for (int i = 1; i <= n; i++) {
            prefix[i] = prefix[i - 1] + nums[i - 1];
        }
        long[] minPrefix = new long[k];
        Arrays.fill(minPrefix, Long.MAX_VALUE / 2);
        long res = Long.MIN_VALUE;
        for (int i = 0; i <= n; i++) {
            int rem = i % k;
            res = Math.max(res, prefix[i] - minPrefix[rem]);
            minPrefix[rem] = Math.min(minPrefix[rem], prefix[i]);
        }
        return res;
    }

}

性能

1015.可被K整除的最小整数

目标

给定正整数 k ,你需要找出可以被 k 整除的、仅包含数字 1 的最 小 正整数 n 的长度。

返回 n 的长度。如果不存在这样的 n ,就返回-1。

注意: n 可能不符合 64 位带符号整数。

示例 1:

输入:k = 1
输出:1
解释:最小的答案是 n = 1,其长度为 1。

示例 2:

输入:k = 2
输出:-1
解释:不存在可被 2 整除的正整数 n 。

示例 3:

输入:k = 3
输出:3
解释:最小的答案是 n = 111,其长度为 3。

说明:

  • 1 <= k <= 10^5

思路

求仅包含数字 1 且能够整除正整数 k 的数字的最小长度,如不存在返回 -1

通俗讲就是多长的 11111…… 能够整除 k。为了防止溢出可以只考虑余数,关键是停止条件是什么?如何确定是否存在?

如果余数出现重复,由于计算的式子不变,后续会陷入循环。可以使用哈希表记录出现过的余数。或者循环 k 次,由于余数的范围只可能是 0 ~ k - 1k 个,如果循环 k 次还没有使余数为 0,那么根据鸽巢原理一定存在重复的余数。

代码


/**
 * @date 2025-11-25 9:26
 */
public class SmallestRepunitDivByK1015 {

    public int smallestRepunitDivByK(int k) {
        if (k == 1) {
            return 1;
        }
        int res = 1;
        int num = 1;
        while (res < k) {
            num = (num * 10 + 1) % k;
            res++;
            if (num == 0) {
                return res;
            }
        }
        return -1;
    }
}

性能

1262.可被三整除的最大和

目标

给你一个整数数组 nums,请你找出并返回能被三整除的元素 最大和。

示例 1:

输入:nums = [3,6,5,1,8]
输出:18
解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

示例 2:

输入:nums = [4]
输出:0
解释:4 不能被 3 整除,所以无法选出数字,返回 0。

示例 3:

输入:nums = [1,2,3,4,4]
输出:12
解释:选出数字 1, 3, 4 以及 4,它们的和是 12(可被 3 整除的最大和)。

说明:

  • 1 <= nums.length <= 4 * 10^4
  • 1 <= nums[i] <= 10^4

思路

求整数数组中元素的最大和,要求和能被 3 整除。

  • 如果和 sum % 3 == 1,可以去掉一个模 3 余 1 的最小元素,或者 两个模 3 余 2 的最小元素
  • 如果和 sum % 3 == 2,可以去掉一个模 3 余 2 的最小元素,或者 两个模 3 余 1 的最小元素

代码


/**
 * @date 2025-11-23 20:46
 */
public class MaxSumDivThree1262 {

    public int maxSumDivThree(int[] nums) {
        int res = 0;
        PriorityQueue<Integer> q1 = new PriorityQueue<>();
        PriorityQueue<Integer> q2 = new PriorityQueue<>();
        for (int num : nums) {
            res += num;
            if (num % 3 == 1) {
                q1.offer(num);
            } else if (num % 3 == 2) {
                q2.offer(num);
            }
        }
        int mod = res % 3;
        if (mod == 1) {
            int sub = q1.size() == 0 ? 100000 : q1.peek();
            if (q2.size() > 1) {
                sub = Math.min(sub, q2.poll() + q2.poll());
            }
            res -= sub;
        } else if (mod == 2) {
            int sub = q2.size() == 0 ? 100000 : q2.peek();
            if (q1.size() > 1) {
                sub = Math.min(sub, q1.poll() + q1.poll());
            }
            res -= sub;
        }
        return res;
    }
}

性能

1930.长度为3的不同回文子序列

目标

给你一个字符串 s ,返回 s 中 长度为 3 的不同回文子序列 的个数。

即便存在多种方法来构建相同的子序列,但相同的子序列只计数一次。

回文 是正着读和反着读一样的字符串。

子序列 是由原字符串删除其中部分字符(也可以不删除)且不改变剩余字符之间相对顺序形成的一个新字符串。

例如,"ace" 是 "abcde" 的一个子序列。

示例 1:

输入:s = "aabca"
输出:3
解释:长度为 3 的 3 个回文子序列分别是:
- "aba" ("aabca" 的子序列)
- "aaa" ("aabca" 的子序列)
- "aca" ("aabca" 的子序列)

示例 2:

输入:s = "adc"
输出:0
解释:"adc" 不存在长度为 3 的回文子序列。

示例 3:

输入:s = "bbcbaba"
输出:4
解释:长度为 3 的 4 个回文子序列分别是:
- "bbb" ("bbcbaba" 的子序列)
- "bcb" ("bbcbaba" 的子序列)
- "bab" ("bbcbaba" 的子序列)
- "aba" ("bbcbaba" 的子序列)

说明:

  • 3 <= s.length <= 10^5
  • s 仅由小写英文字母组成

思路

求长度为 3 的不同回文子序列个数。

枚举中间元素,同时枚举 a - z 作为左右两侧的字母,判断是否前后都存在。

对字母计数,在枚举过程中记录前面出现字母的次数,结合总次数可以快速判断后面是否存在相同的字母。注意,如果前面出现的字母与枚举的中间字母相同需要将次数减 1

代码


/**
 * @date 2025-11-21 8:44
 */
public class CountPalindromicSubsequence1930 {

    public int countPalindromicSubsequence_v1(String s) {
        int[] total = new int[26];
        char[] chars = s.toCharArray();
        for (char c : chars) {
            total[c - 'a']++;
        }
        boolean[][] visited = new boolean[26][26];
        int[] prefix = new int[26];
        int res = 0;
        for (char c : chars) {
            prefix[c - 'a']++;
            for (int j = 0; j < 26; j++) {
                if (!visited[j][c - 'a'] && total[j] > prefix[j] && ((j == c - 'a' && prefix[j] > 1) || j != c - 'a' && prefix[j] > 0)) {
                    visited[j][c - 'a'] = true;
                    res++;
                }
            }
        }
        return res;
    }

}

性能

1513.仅含1的子串数

目标

给你一个二进制字符串 s(仅由 '0' 和 '1' 组成的字符串)。

返回所有字符都为 1 的子字符串的数目。

由于答案可能很大,请你将它对 10^9 + 7 取模后返回。

示例 1:

输入:s = "0110111"
输出:9
解释:共有 9 个子字符串仅由 '1' 组成
"1" -> 5 次
"11" -> 3 次
"111" -> 1 次

示例 2:

输入:s = "101"
输出:2
解释:子字符串 "1" 在 s 中共出现 2 次

示例 3:

输入:s = "111111"
输出:21
解释:每个子字符串都仅由 '1' 组成

示例 4:

输入:s = "000"
输出:0

说明:

  • s[i] == '0' 或 s[i] == '1'
  • 1 <= s.length <= 10^5

思路

统计全一子串的个数。

根据等差数列求和公式计算即可。

代码


/**
 * @date 2025-11-16 22:17
 */
public class NumSub1513 {

    public int numSub(String s) {
        int mod = 1000000007;
        int n = s.length();
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == '0') {
                continue;
            }
            int start = i;
            while (i < n && s.charAt(i) == '1'){
                i++;
            }
            int cnt = i - start;
            res += ((cnt + 1L) * cnt / 2) % mod;

        }
        return res;
    }

}

性能

2536.子矩阵元素加1

目标

给你一个正整数 n ,表示最初有一个 n x n 、下标从 0 开始的整数矩阵 mat ,矩阵中填满了 0 。

另给你一个二维整数数组 queries 。针对每个查询 queries[i] = [row1i, col1i, row2i, col2i] ,请你执行下述操作:

  • 找出 左上角 为 (row1i, col1i) 且 右下角 为 (row2i, col2i) 的子矩阵,将子矩阵中的 每个元素 加 1 。也就是给所有满足 row1i <= x <= row2i 和 col1i <= y <= col2i 的 mat[x][y] 加 1 。

返回执行完所有操作后得到的矩阵 mat 。

示例 1:

输入:n = 3, queries = [[1,1,2,2],[0,0,1,1]]
输出:[[1,1,0],[1,2,1],[0,1,1]]
解释:上图所展示的分别是:初始矩阵、执行完第一个操作后的矩阵、执行完第二个操作后的矩阵。
- 第一个操作:将左上角为 (1, 1) 且右下角为 (2, 2) 的子矩阵中的每个元素加 1 。 
- 第二个操作:将左上角为 (0, 0) 且右下角为 (1, 1) 的子矩阵中的每个元素加 1 。 

示例 2:

输入:n = 2, queries = [[0,0,1,1]]
输出:[[1,1],[1,1]]
解释:上图所展示的分别是:初始矩阵、执行完第一个操作后的矩阵。 
- 第一个操作:将矩阵中的每个元素加 1 。

说明:

  • 1 <= n <= 500
  • 1 <= queries.length <= 10^4
  • 0 <= row1i <= row2i < n
  • 0 <= col1i <= col2i < n

思路

有一个元素值为 0n x n 矩阵 mat 和一系列由左上 (r1, c1) 右下 (r2, c2) 两个顶点表示的子矩阵,依次操作这些子矩阵,使其中的所有元素值加 1,返回最终得到的矩阵。

考察二维差分数组 以及 二维前缀和,这里直接当作多个一维差分处理了。

代码


/**
 * @date 2025-11-14 8:59
 */
public class RangeAddQueries2536 {

    public int[][] rangeAddQueries(int n, int[][] queries) {
        int[][] diff = new int[n][n + 1];
        for (int[] query : queries) {
            int row1 = query[0];
            int row2 = query[2];
            int col1 = query[1];
            int col2 = query[3];
            for (int j = row1; j <= row2; j++) {
                diff[j][col1]++;
                diff[j][col2 + 1]--;
            }
        }
        int[][] res = new int[n][n];
        for (int i = 0; i < n; i++) {
            int[] row = diff[i];
            res[i][0] = row[0];
            for (int j = 1; j < n; j++) {
                res[i][j] = res[i][j - 1] + row[j];
            }
        }
        return res;
    }

}

性能

3228.将1移动到末尾的最大操作次数

目标

给你一个 二进制字符串 s。

你可以对这个字符串执行 任意次 下述操作:

  • 选择字符串中的任一下标 i( i + 1 < s.length ),该下标满足 s[i] == '1' 且 s[i + 1] == '0'。
  • 将字符 s[i] 向 右移 直到它到达字符串的末端或另一个 '1'。例如,对于 s = "010010",如果我们选择 i = 1,结果字符串将会是 s = "000110"。

返回你能执行的 最大 操作次数。

示例 1:

输入: s = "1001101"
输出: 4
解释:
可以执行以下操作:
选择下标 i = 0。结果字符串为 s = "0011101"。
选择下标 i = 4。结果字符串为 s = "0011011"。
选择下标 i = 3。结果字符串为 s = "0010111"。
选择下标 i = 2。结果字符串为 s = "0001111"。

示例 2:

输入: s = "00111"
输出: 0

说明:

  • 1 <= s.length <= 10^5
  • s[i] 为 '0' 或 '1'。

思路

有一个二进制字符串 s,每次操作可以选择一个下标 i,满足 s[i] == '1's[i + 1] == '0',将 s[i] 向右移直到遇到另一个 1 或者到达末尾,返回可以执行的最大操作次数。

要使操作次数最大,每组相邻的 0 都要为其前面的 1 提供一次操作。换句话说操作尽量不要让右侧的 0 过早的合并,累加每组相邻的 0 前面 1 的个数即可。

计算前缀 1 个数,正序遍历,第一次遇到 0 时,就加上前面 1 的个数,跳过相邻的 0(因为每次操作都要移到最右侧,相邻的 0 对每个 1 只能贡献一次操作)。

代码


/**
 * @date 2025-11-13 8:44
 */
public class MaxOperations3228 {

    public int maxOperations(String s) {
        int n = s.length();
        char[] chars = s.toCharArray();
        int[] prefix = new int[n + 1];
        int res = 0;
        for (int i = 1; i <= n; i++) {
            prefix[i] = prefix[i - 1] + (chars[i - 1] - '0');
        }
        int i = 0;
        while (i < n) {
            if (chars[i] == '0') {
                res += prefix[i];
            }
            while (i < n && chars[i++] == '0') {
            }
        }
        return res;
    }

}

性能

2654.使数组所有元素变成1的最少操作次数

目标

给你一个下标从 0 开始的 正 整数数组 nums 。你可以对数组执行以下操作 任意 次:

  • 选择一个满足 0 <= i < n - 1 的下标 i ,将 nums[i] 或者 nums[i+1] 两者之一替换成它们的最大公约数。

请你返回使数组 nums 中所有元素都等于 1 的 最少 操作次数。如果无法让数组全部变成 1 ,请你返回 -1 。

两个正整数的最大公约数指的是能整除这两个数的最大正整数。

示例 1:

输入:nums = [2,6,3,4]
输出:4
解释:我们可以执行以下操作:
- 选择下标 i = 2 ,将 nums[2] 替换为 gcd(3,4) = 1 ,得到 nums = [2,6,1,4] 。
- 选择下标 i = 1 ,将 nums[1] 替换为 gcd(6,1) = 1 ,得到 nums = [2,1,1,4] 。
- 选择下标 i = 0 ,将 nums[0] 替换为 gcd(2,1) = 1 ,得到 nums = [1,1,1,4] 。
- 选择下标 i = 2 ,将 nums[3] 替换为 gcd(1,4) = 1 ,得到 nums = [1,1,1,1] 。

示例 2:

输入:nums = [2,10,6,14]
输出:-1
解释:无法将所有元素都变成 1 。

说明:

  • 2 <= nums.length <= 50
  • 1 <= nums[i] <= 10^6

思路

有一个正整数数组 nums,每次操作可以将 nums[i] 或者 nums[i + 1] 替换成它们的最大公约数。求将 nums 所有元素变为 1 的最小操作次数,如果无法完成,返回 -1

显然只要存在相邻的互质元素,就可以将其中的一个设置为 1,然后就可以将左右的元素按顺序替换为 1,总共需要操作 n 次。目标是如何用最小的操作次数使得存在一对相邻元素互质。暴力枚举 子数组 的起点与终点,计算所有元素的最大公约数,记录最小的子数组长度。

代码


/**
 * @date 2025-11-12 9:07
 */
public class MinOperations2654 {

    public int minOperations(int[] nums) {
        int n = nums.length;
        int res = Integer.MAX_VALUE;
        int oneCnt = 0;
        int gcdAll = nums[0];
        for (int num : nums) {
            if (num == 1) {
                oneCnt++;
            }
            gcdAll = gcd(gcdAll, num);
        }
        if (gcdAll > 1) {
            return -1;
        }
        if (oneCnt > 0) {
            return n - oneCnt;
        }
        for (int i = 0; i < n; i++) {
            int g = nums[i];
            for (int j = i; j < n; j++) {
                g = gcd(g, nums[j]);
                if (g == 1) {
                    res = Math.min(res, j - i + 1);
                    break;
                }
            }
        }
        return res - 2 + n;
    }

    public int gcd(int a, int b) {
        if (b == 0) {
            return a;
        }
        return gcd(b, a % b);
    }

}

性能