2685.统计完全连通分量的数量

目标

给你一个整数 n 。现有一个包含 n 个顶点的 无向 图,顶点按从 0 到 n - 1 编号。给你一个二维整数数组 edges 其中 edges[i] = [ai, bi] 表示顶点 ai 和 bi 之间存在一条 无向 边。

返回图中 完全连通分量 的数量。

如果在子图中任意两个顶点之间都存在路径,并且子图中没有任何一个顶点与子图外部的顶点共享边,则称其为 连通分量 。

如果连通分量中每对节点之间都存在一条边,则称其为 完全连通分量 。

示例 1:

输入:n = 6, edges = [[0,1],[0,2],[1,2],[3,4]]
输出:3
解释:如上图所示,可以看到此图所有分量都是完全连通分量。

示例 2:

输入:n = 6, edges = [[0,1],[0,2],[1,2],[3,4],[3,5]]
输出:1
解释:包含节点 0、1 和 2 的分量是完全连通分量,因为每对节点之间都存在一条边。
包含节点 3 、4 和 5 的分量不是完全连通分量,因为节点 4 和 5 之间不存在边。
因此,在图中完全连接分量的数量是 1 。

说明:

  • 1 <= n <= 50
  • 0 <= edges.length <= n * (n - 1) / 2
  • edges[i].length == 2
  • 0 <= ai, bi <= n - 1
  • ai != bi
  • 不存在重复的边

思路

求无向图中完全连通分量的个数。完全连通分量指连通分量中任意两个节点之间都有一条边。

暴力解法是使用并查集维护连通分量,找出同一连通分量内的节点,判断两两之间是否有边。

优化点:可以利用节点与边的关系来判断是否是完全连通分量,节点 v 与 边 e 的关系为:e = C(v, 2) = v * (v - 1) / 2。

代码


/**
 * @date 2026-07-14 11:27
 */
public class CountCompleteComponents2685 {

    private class UnionFind {

        private final int[] fa;

        public UnionFind(int n) {
            fa = new int[n];
            Arrays.setAll(fa, i -> i);
        }

        public int find(int e) {
            if (e != fa[e]) {
                fa[e] = find(fa[e]);
            }
            return fa[e];
        }

        public void union(int a, int b) {
            int x = find(a);
            int y = find(b);
            if (x > y) {
                fa[x] = y;
            } else {
                fa[y] = x;
            }
        }

        public int getCompleteComponents(Set<Integer>[] g) {
            int n = fa.length;
            int res = 0;
            Set<Integer> visited = new HashSet<>();
            for (int i = 0; i < n; i++) {
                if (visited.contains(i)) {
                    continue;
                }
                visited.add(i);
                List<Integer> list = new ArrayList<>();
                for (int j = 0; j < n; j++) {
                    if (find(j) == find(i)) {
                        visited.add(j);
                        list.add(j);
                    }
                }
                int size = list.size();
                boolean flag = true;
                here:
                for (int p = 0; p < size; p++) {
                    for (int q = p + 1; q < size; q++) {
                        if (!g[list.get(p)].contains(list.get(q))) {
                            flag = false;
                            break here;
                        }
                    }
                }
                if (flag) {
                    res++;
                }
            }
            return res;
        }
    }

    public int countCompleteComponents(int n, int[][] edges) {
        UnionFind uf = new UnionFind(n);
        Set<Integer>[] g = new HashSet[n];
        Arrays.setAll(g, x -> new HashSet<>());
        for (int[] edge : edges) {
            int a = edge[0];
            int b = edge[1];
            uf.union(a, b);
            g[a].add(b);
            g[b].add(a);
        }
        return uf.getCompleteComponents(g);
    }

}

性能

3756.连接非零数字并乘以其数字和II

目标

给你一个长度为 m 的字符串 s,其中仅包含数字。另给你一个二维整数数组 queries,其中 queries[i] = [li, ri]。

对于每个 queries[i],提取 子串 s[li..ri],然后执行以下操作:

  • 将子串中所有 非零数字 按照原始顺序连接起来,形成一个新的整数 x。如果没有非零数字,则 x = 0。
  • 令 sum 为 x 中所有数字的 数字和 。答案为 x * sum。

返回一个整数数组 answer,其中 answer[i] 是第 i 个查询的答案。

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

子串 是字符串中的一个连续、非空 字符序列。

示例 1:

输入: s = "10203004", queries = [[0,7],[1,3],[4,6]]
输出: [12340, 4, 9]
解释:
s[0..7] = "10203004"
    x = 1234
    sum = 1 + 2 + 3 + 4 = 10
    因此,答案是 1234 * 10 = 12340。
s[1..3] = "020"
    x = 2
    sum = 2
    因此,答案是 2 * 2 = 4。
s[4..6] = "300"
    x = 3
    sum = 3
    因此,答案是 3 * 3 = 9。

示例 2:

输入: s = "1000", queries = [[0,3],[1,1]]
输出: [1, 0]
解释:
s[0..3] = "1000"
    x = 1
    sum = 1
    因此,答案是 1 * 1 = 1。
s[1..1] = "0"
    x = 0
    sum = 0
    因此,答案是 0 * 0 = 0。

示例 3:

输入: s = "9876543210", queries = [[0,9]]
输出: [444444137]
解释:
s[0..9] = "9876543210"
    x = 987654321
    sum = 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 45
    因此,答案是 987654321 * 45 = 44444444445。
    返回结果为 44444444445 mod (10^9 + 7) = 444444137。

说明:

  • 1 <= m == s.length <= 10^5
  • s 仅由数字组成。
  • 1 <= queries.length <= 10^5
  • queries[i] = [li, ri]
  • 0 <= li <= ri < m

思路

有一个数字字符串 s,针对每一个子串 s[queries[i][0], queries[i][1]],返回其非零数字所表示的数字 乘以 每位数字之和 对 1000000007 取余的结果。

与 3754.连接非零数字并乘以其数字和I 相比,本题的数字是由 queries 给出的子串,需要返回每一个子串的结果。

数位和可以使用前缀和快速计算。子串非零数字所表示的数字也可以通过前缀计算。

区间 [l, r] 所表示的数字对 MOD 取模的值为 (prefixNum[r + 1] + MOD - prefixNum[l] * base[k] % MOD) % MOD,例如,1230456,[2, 4] 中的非零数字所表示的数字是 34,它等于 prefixNum[5]:1234 - prefixNum[2]:12 * 100,其中 100 = 10^k,k 表示 [l, r] 中非零数字的个数。

代码


/**
 * @date 2026-07-08 9:50
 */
public class SumAndMultiply3756 {

    public int[] sumAndMultiply(String s, int[][] queries) {
        int n = s.length();
        int[] prefix = new int[n + 1];
        int[] prefixLength = new int[n + 1];
        long[] prefixNum = new long[n + 1];
        for (int i = 0; i < n; i++) {
            int d = s.charAt(i) - '0';
            prefix[i + 1] = prefix[i] + d;
            prefixLength[i + 1] = prefixLength[i] + (d != 0 ? 1 : 0);
            prefixNum[i + 1] = (prefixNum[i] * (d != 0 ? 10 : 1) + d) % MOD;
        }
        int ql = queries.length;
        int[] res = new int[ql];
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            int sum = prefix[r + 1] - prefix[l];
            long x = (prefixNum[r + 1] + MOD - prefixNum[l] * base[prefixLength[r + 1] - prefixLength[l]] % MOD) % MOD;
            res[i] = (int) (x * sum % MOD);
        }
        return res;
    }

}

性能

3754.连接非零数字并乘以其数字和I

目标

给你一个整数 n。

将 n 中所有的 非零数字 按照它们的原始顺序连接起来,形成一个新的整数 x。如果不存在 非零数字 ,则 x = 0。

sum 为 x 中所有数字的 数字和 。

返回一个整数,表示 x * sum 的值。

示例 1:

输入: n = 10203004
输出: 12340
解释:
非零数字是 1、2、3 和 4。因此,x = 1234。
数字和为 sum = 1 + 2 + 3 + 4 = 10。
因此,答案是 x * sum = 1234 * 10 = 12340。

示例 2:

输入: n = 1000
输出: 1
解释:
非零数字是 1,因此 x = 1 且 sum = 1。
因此,答案是 x * sum = 1 * 1 = 1。

说明:

  • 0 <= n <= 10^9

思路

计算整数 n 的所有非零数字之和记为 sum,去掉 n 中的 0 形成的新整数记为 x,返回 x * sum 的值。

依题意模拟即可。

代码


/**
 * @date 2026-07-07 9:13
 */
public class SumAndMultiply3754 {

    public long sumAndMultiply(int n) {
        long sum = 0;
        long x = 0;
        int base = 1;
        while (n > 0) {
            int rem = n % 10;
            sum += rem;
            x += rem * base;
            base *= rem == 0 ? 1 : 10;
            n /= 10;
        }
        return x * sum;
    }
}

性能

1344.时钟指针的夹角

目标

给你两个数 hour 和 minutes 。请你返回在时钟上,由给定时间的时针和分针组成的较小角的角度(60 单位制)。

示例 1:

输入:hour = 12, minutes = 30
输出:165

示例 2:

输入:hour = 3, minutes = 30
输出;75

示例 3:

输入:hour = 3, minutes = 15
输出:7.5

示例 4:

输入:hour = 4, minutes = 50
输出:155

示例 5:

输入:hour = 12, minutes = 0
输出:0

说明:

  • 1 <= hour <= 12
  • 0 <= minutes <= 59
  • 与标准答案误差在 10^-5 以内的结果都被视为正确结果。

思路

计算时钟时针与分针的较小夹角。

问题的关键是时针转动的角度不能直接按 hour 来算,根据分针的指向有一定的偏移,偏移的角度是 minutes / 60 * 30,其中 30 是一个小时格子的角度。hour / 12 * 360 + minutes / 60 * 30 - minutes / 60 * 360 = 30 * hour - minutes * 5.5

代码


/**
 * @date 2026-06-18 9:17
 */
public class AngleClock1344 {

    public double angleClock(int hour, int minutes) {
        double res = Math.abs(30 * hour - minutes * 5.5);
        return Math.min(res, 360 - res);
    }
}

性能

3558.给边赋权值的方案数I

目标

给你一棵 n 个节点的无向树,节点从 1 到 n 编号,树以节点 1 为根。树由一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示在节点 ui 和 vi 之间有一条边。

一开始,所有边的权重为 0。你可以将每条边的权重设为 1 或 2。

两个节点 u 和 v 之间路径的 代价 是连接它们路径上所有边的权重之和。

选择任意一个 深度最大 的节点 x。返回从节点 1 到 x 的路径中,边权重之和为 奇数 的赋值方式数量。

由于答案可能很大,返回它对 10^9 + 7 取模的结果。

注意: 忽略从节点 1 到节点 x 的路径外的所有边。

示例 1:

输入: edges = [[1,2]]
输出: 1
解释:
从节点 1 到节点 2 的路径有一条边(1 → 2)。
将该边赋权为 1 会使代价为奇数,赋权为 2 则为偶数。因此,合法的赋值方式有 1 种。

示例 2:

输入: edges = [[1,2],[1,3],[3,4],[3,5]]
输出: 2
解释:
最大深度为 2,节点 4 和节点 5 都在该深度,可以选择任意一个。
例如,从节点 1 到节点 4 的路径包括两条边(1 → 3 和 3 → 4)。
将两条边赋权为 (1,2) 或 (2,1) 会使代价为奇数,因此合法赋值方式有 2 种。

提示:

  • 2 <= n <= 10^5
  • edges.length == n - 1
  • edges[i] == [ui, vi]
  • 1 <= ui, vi <= n
  • edges 表示一棵合法的树。

思路

有一颗含有 n 个节点的树,编号为 1 ~ n,根节点编号是 1。edges[i] = [ui, vi],表示节点 ui 与 vi 之间有一条边。定义节点 u 与 v 之间的代价为它们之间路径上边的权重之和。可以为每一条边赋予权重 1 或 2,求使得根节点到最远叶子节点代价为奇数的赋权方案数。

定义到达当前节点代价为奇数或偶数的方案数为 dp[i][1] 与 dp[i][0],状态转移方程为 dp[i][0] = dp[prev][0] + dp[prev][1],dp[i][1] = dp[prev][0] + dp[prev][1]。

观察发现 dp[i][0] == dp[i][1],因此代价为奇数的方案数为 dp[i][1] = 2 * dp[prev][1]。假设树的深度为 d,方案数为 2^(d - 1)。

求出树的深度 d,从中选取奇数条边赋值为 1,方案数为 2^(d - 1)。

代码


/**
 * @date 2026-06-11 10:16
 */
public class AssignEdgeWeights3558 {

    public int assignEdgeWeights(int[][] edges) {
        int n = edges.length + 1;
        List<Integer>[] g = new ArrayList[n + 1];
        Arrays.setAll(g, i -> new ArrayList<>());
        for (int[] edge : edges) {
            g[edge[0]].add(edge[1]);
            g[edge[1]].add(edge[0]);
        }
        int d = dfs(0, 1, g);
        return pow(2, d - 1, 1000000007);
    }

    public int dfs(int fa, int cur, List<Integer>[] g) {
        int res = 0;
        for (Integer next : g[cur]) {
            if (next == fa) {
                continue;
            }
            res = Math.max(res, dfs(cur, next, g) + 1);
        }
        return res;
    }

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

}

性能

3753.范围内总波动值II

目标

给你两个整数 num1 和 num2,表示一个 闭 区间 [num1, num2]。

一个数字的 波动值 定义为该数字中 峰 和 谷 的总数:

  • 如果一个数位 严格大于 其两个相邻数位,则该数位为 峰。
  • 如果一个数位 严格小于 其两个相邻数位,则该数位为 谷。
  • 数字的第一个和最后一个数位 不能 是峰或谷。
  • 任何少于 3 位的数字,其波动值均为 0。

返回范围 [num1, num2] 内所有数字的波动值之和。

示例 1:

输入: num1 = 120, num2 = 130
输出: 3
解释:
在范围 [120, 130] 内:
120:中间数位 2 是峰,波动值 = 1。
121:中间数位 2 是峰,波动值 = 1。
130:中间数位 3 是峰,波动值 = 1。
范围内所有其他数字的波动值均为 0。
因此,总波动值为 1 + 1 + 1 = 3。

示例 2:

输入: num1 = 198, num2 = 202
输出: 3
解释:
在范围 [198, 202] 内:
198:中间数位 9 是峰,波动值 = 1。
201:中间数位 0 是谷,波动值 = 1。
202:中间数位 0 是谷,波动值 = 1。
范围内所有其他数字的波动值均为 0。
因此,总波动值为 1 + 1 + 1 = 3。

示例 3:

输入: num1 = 4848, num2 = 4848
输出: 2
解释:
数字 4848:第二个数位 8 是峰,第三个数位 4 是谷,波动值为 2。

说明:

  • 1 <= num1 <= num2 <= 10^15

思路

代码

性能

3300.替换为数位和以后的最小元素

目标

给你一个整数数组 nums 。

请你将 nums 中每一个元素都替换为它的各个数位之 和 。

请你返回替换所有元素以后 nums 中的 最小 元素。

示例 1:

输入:nums = [10,12,13,14]
输出:1
解释:
nums 替换后变为 [1, 3, 4, 5] ,最小元素为 1 。

示例 2:

输入:nums = [1,2,3,4]
输出:1
解释:
nums 替换后变为 [1, 2, 3, 4] ,最小元素为 1 。

示例 3:

输入:nums = [999,19,199]
输出:10
解释:
nums 替换后变为 [27, 10, 19] ,最小元素为 10 。

说明:

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

思路

将数组中的元素换成其各位数字之和,返回替换后数组的最小值。

代码


/**
 * @date 2026-05-29 0:03
 */
public class MinElement3300 {

    public int minElement(int[] nums) {
        int res = Integer.MAX_VALUE;
        for (int num : nums) {
            int sum = 0;
            while (num > 0){
                sum += num % 10;
                num /= 10;
            }
            res = Math.min(res, sum);
        }
        return res;
    }
}

性能

3629.通过质数传送到达终点的最少跳跃次数

目标

给你一个长度为 n 的整数数组 nums。

你从下标 0 开始,目标是到达下标 n - 1。

在任何下标 i 处,你可以执行以下操作之一:

  • 移动到相邻格子:跳到下标 i + 1 或 i - 1,如果该下标在边界内。
  • 质数传送:如果 nums[i] 是一个质数 p,你可以立即跳到任何满足 nums[j] % p == 0 的下标 j 处,且下标 j != i 。

返回到达下标 n - 1 所需的 最少 跳跃次数。

质数 是一个大于 1 的自然数,只有两个因子,1 和它本身。

示例 1:

输入: nums = [1,2,4,6]
输出: 2
解释:
一个最优的跳跃序列是:
从下标 i = 0 开始。向相邻下标 1 跳一步。
在下标 i = 1,nums[1] = 2 是一个质数。因此,我们传送到索引 i = 3,因为 nums[3] = 6 可以被 2 整除。
因此,答案是 2。

示例 2:

输入: nums = [2,3,4,7,9]
输出: 2
解释:
一个最优的跳跃序列是:
从下标 i = 0 开始。向相邻下标 i = 1 跳一步。
在下标 i = 1,nums[1] = 3 是一个质数。因此,我们传送到下标 i = 4,因为 nums[4] = 9 可以被 3 整除。
因此,答案是 2。

示例 3:

输入: nums = [4,6,5,8]
输出: 3
解释:
由于无法进行传送,我们通过 0 → 1 → 2 → 3 移动。因此,答案是 3。

说明:

  • 1 <= n == nums.length <= 10^5
  • 1 <= nums[i] <= 10^6

思路

有一个数组 nums,从下标 0 开始移动,目标是到达下标 n - 1。每次移动可以移动到相邻的位置,如果 nums[i] 是质数,可以直接移动到满足 nums[j] % nums[i] == 0 的位置。返回最少移动次数。

枚举 2 ~ MAX,为每一个数记录质因子,得到哈希表 (num,primeFactorList),枚举数组中的元素,获取 primeFactorList,为其中的每一个质数建立一个跳转列表 jumpIndexList,将当前下标加进去。从 0 开始 bfs,记录跳转次数,直到到达下标 n - 1。

代码


/**
 * @date 2026-05-08 16:55
 */
public class MinJumps3629 {

    public static final int MAX = 1000000;
    public static Map<Integer, List<Integer>> primeFactorMap = new HashMap<>();

    static {
        primeFactorMap.computeIfAbsent(1, k -> new ArrayList<>());
        for (int i = 2; i <= MAX; i++) {
            if (primeFactorMap.get(i) == null) {
                for (int j = i; j <= MAX; j += i) {
                    primeFactorMap.computeIfAbsent(j, k -> new ArrayList<>()).add(i);
                }
            }
        }
    }

    public int minJumps(int[] nums) {
        int n = nums.length;
        Map<Integer, List<Integer>> jumpIndexList = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            List<Integer> primeFactors = primeFactorMap.get(nums[i]);
            for (Integer prime : primeFactors) {
                jumpIndexList.computeIfAbsent(prime, k -> new ArrayList<>()).add(i);
            }
        }
        boolean[] visited = new boolean[n];
        Deque<Integer> q = new ArrayDeque<>();
        q.offer(0);
        int res = 0;
        here:
        while (!q.isEmpty()) {
            int size = q.size();
            for (int i = 0; i < size; i++) {
                Integer index = q.poll();
                if (visited[index]) {
                    continue;
                }
                visited[index] = true;
                if (index == n - 1) {
                    break here;
                }
                int next = index + 1;
                int prev = index - 1;
                if (next < n) {
                    q.offer(next);
                }
                if (prev >= 0) {
                    q.offer(prev);
                }
                if (jumpIndexList.get(nums[index]) != null) {
                    q.addAll(jumpIndexList.get(nums[index]));
                    jumpIndexList.get(nums[index]).clear();
                }
            }
            res++;
        }
        return res;
    }

}

性能

48.旋转图像

目标

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]

示例 2:

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

说明:

n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000

思路

将 n x n 矩阵顺时针 原地 旋转 90°。

顺时针旋转 90° 的规律:交换行列坐标,将列坐标变为 n - 1 - ?。

i, j -> j, n - 1 - i
     -> n - 1 - i, n - 1 - j
     -> n - 1 - j, i,
     -> i, j

代码


/**
 * @date 2025-01-26 22:02
 */
public class Rotate48 {

    public void rotate(int[][] matrix) {
        int n = matrix.length;
        for (int i = 0; i < n / 2; i++) {
            for (int j = 0; j < (n + 1) / 2; j++) {
                int tmp = matrix[n - 1 - j][i];
                matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
                matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
                matrix[j][n - 1 - i] = matrix[i][j];
                matrix[i][j] = tmp;
            }
        }
    }

}

性能

396.旋转函数

目标

给定一个长度为 n 的整数数组 nums 。

假设 arrk 是数组 nums 顺时针旋转 k 个位置后的数组,我们定义 nums 的 旋转函数 F 为:

  • F(k) = 0 * arrk[0] + 1 * arrk[1] + ... + (n - 1) * arrk[n - 1]

返回 F(0), F(1), ..., F(n-1)中的最大值 。

生成的测试用例让答案符合 32 位 整数。

示例 1:

输入: nums = [4,3,2,6]
输出: 26
解释:
F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25
F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16
F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23
F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26
所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。

示例 2:

输入: nums = [100]
输出: 0

说明:

  • n == nums.length
  • 1 <= n <= 10^5
  • -100 <= nums[i] <= 100

思路

有一个循环数组 nums,arr_k 表示将数组 nums 循环右移 k 之后的数组。定义 F(k) = 0 * arr_k[0] + 1 * arr_k[1] + …… + (n - 1) * arr_k[n - 1],返回 F(0)、F(1)、……、F(n - 1) 的最大值。

F(k) = Σ(i * arr_k[i]),观察发现 F(k + 1) - F(k) = sum - n * arr_k[n - 1],其中 sum 是 nums 所有元素和。从 F(k) -> F(k + 1),除了最后一个元素,所有元素都增加了一个,最后一个元素减少了 n - 1 个。这里前面多加了最后一个元素,所以后面减去 n 个。arr_k 的最后一个元素为 nums[n - k]。

代码


/**
 * @date 2026-05-09 13:43
 */
public class MaxRotateFunction396 {

    public int maxRotateFunction(int[] nums) {
        int n = nums.length;
        int f = 0;
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += nums[i];
            f += i * nums[i];
        }
        int res = f;
        for (int i = 1; i < n; i++) {
            f += sum - n * nums[n - i];
            res = Math.max(res, f);
        }
        return res;
    }
}

性能

时间复杂度为 O(n)。