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

思路

// todo

代码

性能

3512.使数组和能被K整除的最少操作次数

目标

给你一个整数数组 nums 和一个整数 k。你可以执行以下操作任意次:

  • 选择一个下标 i,并将 nums[i] 替换为 nums[i] - 1。

返回使数组元素之和能被 k 整除所需的最小操作次数。

示例 1:

输入: nums = [3,9,7], k = 5
输出: 4
解释:
对 nums[1] = 9 执行 4 次操作。现在 nums = [3, 5, 7]。
数组之和为 15,可以被 5 整除。

示例 2:

输入: nums = [4,1,3], k = 4
输出: 0
解释:
数组之和为 8,已经可以被 4 整除。因此不需要操作。

示例 3:

输入: nums = [3,2], k = 6
输出: 5
解释:
对 nums[0] = 3 执行 3 次操作,对 nums[1] = 2 执行 2 次操作。现在 nums = [0, 0]。
数组之和为 0,可以被 6 整除。

说明:

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

思路

返回数组元素和模 k 的余数。

代码


/**
 * @date 2025-11-29 21:53
 */
public class MinOperations3512 {

    public int minOperations(int[] nums, int k) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        return sum % k;
    }
}

性能

2872.可以被K整除连通块的最大数目

目标

给你一棵 n 个节点的无向树,节点编号为 0 到 n - 1 。给你整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 有一条边。

同时给你一个下标从 0 开始长度为 n 的整数数组 values ,其中 values[i] 是第 i 个节点的 值 。再给你一个整数 k 。

你可以从树中删除一些边,也可以一条边也不删,得到若干连通块。一个 连通块的值 定义为连通块中所有节点值之和。如果所有连通块的值都可以被 k 整除,那么我们说这是一个 合法分割 。

请你返回所有合法分割中,连通块数目的最大值 。

示例 1:

输入:n = 5, edges = [[0,2],[1,2],[1,3],[2,4]], values = [1,8,1,4,4], k = 6
输出:2
解释:我们删除节点 1 和 2 之间的边。这是一个合法分割,因为:
- 节点 1 和 3 所在连通块的值为 values[1] + values[3] = 12 。
- 节点 0 ,2 和 4 所在连通块的值为 values[0] + values[2] + values[4] = 6 。
最多可以得到 2 个连通块的合法分割。

示例 2:

输入:n = 7, edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]], values = [3,0,6,1,5,2,1], k = 3
输出:3
解释:我们删除节点 0 和 2 ,以及节点 0 和 1 之间的边。这是一个合法分割,因为:
- 节点 0 的连通块的值为 values[0] = 3 。
- 节点 2 ,5 和 6 所在连通块的值为 values[2] + values[5] + values[6] = 9 。
- 节点 1 ,3 和 4 的连通块的值为 values[1] + values[3] + values[4] = 6 。
最多可以得到 3 个连通块的合法分割。

说明:

  • 1 <= n <= 3 * 10^4
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • values.length == n
  • 0 <= values[i] <= 10^9
  • 1 <= k <= 10^9
  • values 之和可以被 k 整除。
  • 输入保证 edges 是一棵无向树。

思路

有一个节点数量为 n 的无向树,树中节点之和可以被 k 整除,现在需要对树进行划分,要求每一个连通块中的节点和也能够被 k 整除,求最大的连通块个数。

dfs 遍历树,如果子树节点和能够整除 k 则可以与父节点断开,删除边数加 1,最终连通分量个数是删除的边数加 1

代码


/**
 * @date 2025-11-28 9:31
 */
public class MaxKDivisibleComponents2872 {

    int res = 0;

    public int maxKDivisibleComponents(int n, int[][] edges, int[] values, int k) {
        List<Integer>[] g = new ArrayList[n];
        Arrays.setAll(g, x -> new ArrayList<>());
        for (int[] edge : edges) {
            g[edge[0]].add(edge[1]);
            g[edge[1]].add(edge[0]);
        }
        dfs(0, -1, g, values, k);
        return res + 1;
    }

    public int dfs(int i, int fa, List<Integer>[] g, int[] values, int k) {
        int sum = values[i];
        for (Integer next : g[i]) {
            if (next == fa) {
                continue;
            }
            int subSum = dfs(next, i, g, values, k);
            if (subSum % k == 0) {
                res++;
            } else {
                sum = (sum + subSum) % k;
            }
        }
        return sum % k;
    }

}

性能

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

}

性能

2435.矩阵中和能被K整除的路径

目标

给你一个下标从 0 开始的 m x n 整数矩阵 grid 和一个整数 k 。你从起点 (0, 0) 出发,每一步只能往 下 或者往 右 ,你想要到达终点 (m - 1, n - 1) 。

请你返回路径和能被 k 整除的路径数目,由于答案可能很大,返回答案对 10^9 + 7 取余 的结果。

示例 1:

输入:grid = [[5,2,4],[3,0,5],[0,7,2]], k = 3
输出:2
解释:有两条路径满足路径上元素的和能被 k 整除。
第一条路径为上图中用红色标注的路径,和为 5 + 2 + 4 + 5 + 2 = 18 ,能被 3 整除。
第二条路径为上图中用蓝色标注的路径,和为 5 + 3 + 0 + 5 + 2 = 15 ,能被 3 整除。

示例 2:

输入:grid = [[0,0]], k = 5
输出:1
解释:红色标注的路径和为 0 + 0 = 0 ,能被 5 整除。

示例 3:

输入:grid = [[7,3,4,9],[2,3,6,2],[2,3,7,0]], k = 1
输出:10
解释:每个数字都能被 1 整除,所以每一条路径的和都能被 k 整除。

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 5 * 10^4
  • 1 <= m n <= 5 10^4
  • 0 <= grid[i][j] <= 100
  • 1 <= k <= 50

思路

有一个矩阵 grid,从左上角出发,只能向下或向右走,求到达右下角的路径中,路径和能被 k 整除的路径数目。

定义 dp[i][j][r] 表示从 (0, 0) 到达 (i, j) 的路径和 模 kr 的路径总数,状态转移方程为 dp[i][j][r] = (dp[i - 1][j][(r + k - rem) % k] + dp[i][j - 1][(r + k - rem) % k]) % mod,其中 rem = grid[i][j] % k

代码


/**
 * @date 2025-11-26 8:49
 */
public class NumberOfPaths2435 {

    public int numberOfPaths(int[][] grid, int k) {
        int m = grid.length;
        int n = grid[0].length;
        int mod = 1000000007;
        int[][][] dp = new int[m][n][k];
        int sum = 0;
        for (int i = 0; i < m; i++) {
            sum += grid[i][0];
            dp[i][0][sum % k] = 1;
        }
        sum = 0;
        for (int j = 0; j < n; j++) {
            sum += grid[0][j];
            dp[0][j][sum % k] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                int rem = grid[i][j] % k;
                for (int r = 0; r < k; r++) {
                    dp[i][j][r] = (dp[i - 1][j][(r + k - rem) % k] + dp[i][j - 1][(r + k - rem) % k]) % mod;
                }
            }
        }
        return dp[m - 1][n - 1][0];
    }
}

性能

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

性能

1018.可被5整除的二进制前缀

目标

给定一个二进制数组 nums ( 索引从0开始 )。

我们将 xi 定义为其二进制表示形式为子数组 nums[0..i] (从最高有效位到最低有效位)。

  • 例如,如果 nums =[1,0,1] ,那么 x0 = 1, x1 = 2, 和 x2 = 5。

返回布尔值列表 answer,只有当 xi 可以被 5 整除时,答案 answer[i] 为 true,否则为 false。

示例 1:

输入:nums = [0,1,1]
输出:[true,false,false]
解释:
输入数字为 0, 01, 011;也就是十进制中的 0, 1, 3 。只有第一个数可以被 5 整除,因此 answer[0] 为 true 。

示例 2:

输入:nums = [1,1,1]
输出:[false,false,false]

说明:

  • 1 <= nums.length <= 10^5
  • nums[i] 仅为 0 或 1

思路

有一个二进制数组 nums,定义 xi 为二进制表示为 子数组 [0, i] 的数字,判断 x0 ~ x_(n-1) 能否被 5 整除。

模拟存在的问题是左移多次会溢出。假设 num = k * 5 + mod,左移 1 位相当于乘以 22 * num = k * 10 + 2 * mod,能否被 5 整除只需考虑 2 * mod | nums[i]

代码


/**
 * @date 2025-11-24 8:52
 */
public class PrefixesDivBy5_1018 {

    public List<Boolean> prefixesDivBy5_v1(int[] nums) {
        List<Boolean> res = new ArrayList<>();
        int mod = 0;
        for (int num : nums) {
            mod = (mod << 1) % 5 + num;
            res.add(mod % 5 == 0);
        }
        return res;
    }

}

性能

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

性能

3190.使所有元素都可以被3整除的最少操作数

目标

给你一个整数数组 nums 。一次操作中,你可以将 nums 中的 任意 一个元素增加或者减少 1 。

请你返回将 nums 中所有元素都可以被 3 整除的 最少 操作次数。

示例 1:

输入:nums = [1,2,3,4]
输出:3
解释:
通过以下 3 个操作,数组中的所有元素都可以被 3 整除:
将 1 减少 1 。
将 2 增加 1 。
将 4 减少 1 。

示例 2:

输入:nums = [3,6,9]
输出:0

说明:

1 <= nums.length <= 50
1 <= nums[i] <= 50

思路

统计数组中不能被 3 整除的元素个数。

代码


/**
 * @date 2025-11-22 2:02
 */
public class MinimumOperations3190 {

    public int minimumOperations(int[] nums) {
        int res = 0;
        for (int num : nums) {
            if (num % 3 != 0){
                res++;
            }
        }
        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;
    }

}

性能