LeetCode 2501-2520. 题解合集
文章目录
- [LeetCode 2501-2520. 题解合集](#LeetCode 2501-2520. 题解合集)
-
- [2501. Longest Square Streak in an Array(数组中最长的方波)](#2501. Longest Square Streak in an Array(数组中最长的方波))
- [2502. Design Memory Allocator(设计内存分配器)](#2502. Design Memory Allocator(设计内存分配器))
- [2503. Maximum Number of Points From Grid Queries(矩阵查询可获得的最大分数)](#2503. Maximum Number of Points From Grid Queries(矩阵查询可获得的最大分数))
- [2504. Concatenate the Name and the Profession(把名字和职业联系起来)](#2504. Concatenate the Name and the Profession(把名字和职业联系起来))
- [2505. Bitwise OR of All Subsequence Sums(所有子序列和的按位或)](#2505. Bitwise OR of All Subsequence Sums(所有子序列和的按位或))
- [2506. Count Pairs Of Similar Strings(统计相似字符串对的数目)](#2506. Count Pairs Of Similar Strings(统计相似字符串对的数目))
- [2507. Smallest Value After Replacing With Sum of Prime Factors](#2507. Smallest Value After Replacing With Sum of Prime Factors)
- [2508. Add Edges to Make Degrees of All Nodes Even(添加边使所有节点度数都为偶数)](#2508. Add Edges to Make Degrees of All Nodes Even(添加边使所有节点度数都为偶数))
- [2509. Cycle Length Queries in a Tree(查询树中环的长度)](#2509. Cycle Length Queries in a Tree(查询树中环的长度))
- [2510. Check if There is a Path With Equal Number of 0's And 1's](#2510. Check if There is a Path With Equal Number of 0's And 1's)
- [2511. Maximum Enemy Forts That Can Be Captured(最多可以摧毁的敌人城堡数目)](#2511. Maximum Enemy Forts That Can Be Captured(最多可以摧毁的敌人城堡数目))
- [2512. Reward Top K Students(奖励最顶尖的 K 名学生)](#2512. Reward Top K Students(奖励最顶尖的 K 名学生))
- [2513. Minimize the Maximum of Two Arrays(最小化两个数组中的最大值)](#2513. Minimize the Maximum of Two Arrays(最小化两个数组中的最大值))
- [2514. Count Anagrams(统计同位异构字符串数目)](#2514. Count Anagrams(统计同位异构字符串数目))
- [2515. Shortest Distance to Target String in a Circular Array](#2515. Shortest Distance to Target String in a Circular Array)
- [2516. Take K of Each Character From Left and Right(每种字符至少取 K 个)](#2516. Take K of Each Character From Left and Right(每种字符至少取 K 个))
- [2517. Maximum Tastiness of Candy Basket(礼盒的最大甜蜜度)](#2517. Maximum Tastiness of Candy Basket(礼盒的最大甜蜜度))
- [2518. Number of Great Partitions(好分区的数目)](#2518. Number of Great Partitions(好分区的数目))
- [2519. Count the Number of K-Big Indices(统计 K-Big 索引的数量)](#2519. Count the Number of K-Big Indices(统计 K-Big 索引的数量))
- [2520. Count the Digits That Divide a Number(统计能整除数字的数位)](#2520. Count the Digits That Divide a Number(统计能整除数字的数位))
2501. Longest Square Streak in an Array(数组中最长的方波)
题目描述
给你一个整数数组 nums。如果 nums 的子序列满足下述条件,则认为该子序列是一个「方波」:
- 子序列的长度至少为 2,并且
- 将子序列从小到大排序之后,除第一个元素外,每个元素都是前一个元素的平方。
返回nums中最长方波的长度;如果不存在方波,则返回 -1。
子序列也是一个数组,可以由另一个数组删除一些或不删除元素且不改变剩余元素的顺序得到。
示例 1
`nums = [4,3,6,16,8,2]` → 3(方波 [4,16,2],排序后 [2,4,16])
示例 2
`nums = [2,3,5,6,7]` → -1
提示:
2 <= nums.length <= 10^52 <= nums[i] <= 10^5
解题思路
解法一:哈希集合 + 动态规划统计以某数为起点的方波长度(主提交版本)
- 用 Set 记录所有数字,对每个数 x 不断平方(x2、x4...),只要平方后仍在集合中,长度 +1。
- 为避免重复统计,仅当 x 的平方根不在集合中(即 x 不能作为链中继)时才开始累加,保证每个
数字只作为链的起点被处理一次,总复杂度 O(n log max)。 - 若累加长度小于 2 则返回 -1。
解法二:排序 + 从小到大递推(对照) - 排序后用哈希表记录「以 x 结尾的方波长度」,fx = fsqrt(x) + 1(若 sqrt(x) 为整数且存在),
遍历时取 fx 的最大值。
复杂度: - 时间复杂度:O(n log max(nums)),其中 log 来自平方链的长度
- 空间复杂度:O(n)
代码实现
java
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Solution {
// 解法一:哈希集合 + 平方链(主提交版本)
public int longestSquareStreak(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int v : nums) {
set.add(v);
}
int ans = 0;
for (int x : nums) {
int r = (int) Math.sqrt(x);
if (r * r != x || !set.contains(r)) { // 不是某个数的平方 -> 链起点
int len = 0;
long cur = x;
while (cur <= 100000 && set.contains((int) cur)) {
len++;
cur *= cur; // 平方
}
ans = Math.max(ans, len);
}
}
return ans >= 2 ? ans : -1;
}
// 解法二:排序 + 哈希表递推(对照)
public int longestSquareStreakSort(int[] nums) {
Arrays.sort(nums);
java.util.Map<Integer, Integer> f = new java.util.HashMap<>();
int ans = 0;
for (int x : nums) {
int r = (int) Math.sqrt(x);
int len = 1;
if (r * r == x && f.containsKey(r)) {
len += f.get(r);
}
f.put(x, len);
ans = Math.max(ans, len);
}
return ans >= 2 ? ans : -1;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.longestSquareStreak(new int[]{4, 3, 6, 16, 8, 2}) == 3 : "2501 例1 主";
assert sol.longestSquareStreakSort(new int[]{4, 3, 6, 16, 8, 2}) == 3 : "2501 例1 对照";
assert sol.longestSquareStreak(new int[]{2, 3, 5, 6, 7}) == -1 : "2501 例2 主";
assert sol.longestSquareStreakSort(new int[]{2, 3, 5, 6, 7}) == -1 : "2501 例2 对照";
// 随机对拍
java.util.Random rnd = new java.util.Random(101);
for (int t = 0; t < 500; t++) {
int n = 2 + rnd.nextInt(30);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 2 + rnd.nextInt(300);
}
if (sol.longestSquareStreak(a) != sol.longestSquareStreakSort(a)) {
System.out.println("对拍失败 " + java.util.Arrays.toString(a));
System.exit(1);
}
}
System.out.println("2501 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n log max(nums)),其中 log 来自平方链的长度
- 空间复杂度:O(n)
2502. Design Memory Allocator(设计内存分配器)
题目描述
给你一个整数 n,表示下标从 0 开始的内存数组的大小。所有内存单元开始都是空闲的。
请你设计一个具备以下功能的内存分配器:
- allocate(
size,mID):找出大小为size个连续空闲内存单元且位于最左侧的块,分配并赋idmID。
返回块的第一个下标;如果不存在这样的块,返回 -1。 freeMemory(mID):释放idmID对应的所有内存单元,返回释放的内存单元数目。
注意:多个块可以被分配到同一个mID;释放时必须释放mID对应的所有内存单元。
示例
["Allocator","allocate","allocate","allocate","freeMemory","allocate","allocate","allocate","freeMemory","allocate","freeMemory"]
[[10],[1,1],[1,2],[1,3],[2],[3,4],[1,1],[1,1],[1],[10,2],[7]]
→ [null,0,1,2,1,3,1,6,3,-1,0]
提示:
1 <= n, size, mID <= 1000- 最多调用
allocate和free方法 1000 次
解题思路
解法一:数组标记 + 线性扫描最左空闲块(主提交版本)
- memoryi 记录内存单元 i 所属的 mID,0 表示空闲。
- allocate:从左到右扫描连续 size 个空闲单元(用 cnt 计数),找到后整体赋值 mID。
- freeMemory:遍历全部单元,凡是属于 mID 的单元清零并计数。
- 复杂度 O(n) 每次操作,n ≤ 1000、调用 ≤ 1000,完全可接受。
解法二:哈希表记录每块区间(对照) - 用 List<int\[\]> 存 (l, r, mID) 的已分配块,allocate 时合并查看空闲连续段,free 时按 mID 移除
块并统计长度。思路与解法一不同但结果一致。
复杂度: - 时间复杂度:allocate/freeMemory 均 O(n)
- 空间复杂度:O(n)
代码实现
java
public class Solution {
private final int[] mem;
private final int n;
// 解法一:数组标记(主提交版本,直接 inner class 使用)
public Solution() {
this.n = 0;
this.mem = null;
}
// 指定大小构造(供平台调用)
public Solution(int n) {
this.n = n;
this.mem = new int[n];
}
public int allocate(int size, int mID) {
// 找到最左侧连续 size 个空闲单元
int start = -1;
for (int i = 0; i <= n - size; i++) {
boolean ok = true;
for (int j = 0; j < size; j++) {
if (mem[i + j] != 0) {
ok = false;
break;
}
}
if (ok) {
start = i;
break;
}
}
if (start == -1) {
return -1;
}
for (int j = 0; j < size; j++) {
mem[start + j] = mID;
}
return start;
}
public int freeMemory(int mID) {
int cnt = 0;
for (int i = 0; i < n; i++) {
if (mem[i] == mID) {
mem[i] = 0;
cnt++;
}
}
return cnt;
}
// 解法二:哈希表记录已分配块区间(对照)
public static class AllocatorList {
private final java.util.List<int[]> blocks; // [l, r, mID]
private final int n;
public AllocatorList(int n) {
this.n = n;
this.blocks = new java.util.ArrayList<>();
}
public int allocate(int size, int mID) {
// 空闲区间 = 已排序块的间隙
java.util.Collections.sort(blocks, (a, b) -> Integer.compare(a[0], b[0]));
int prevEnd = -1; // 已占用到 prevEnd
for (int[] b : blocks) {
int l = b[0], r = b[1];
if (l - prevEnd - 1 >= size) { // [prevEnd+1, l-1] 空闲
int start = prevEnd + 1;
blocks.add(new int[]{start, start + size - 1, mID});
return start;
}
prevEnd = Math.max(prevEnd, r);
}
if (n - prevEnd - 1 >= size) {
int start = prevEnd + 1;
blocks.add(new int[]{start, start + size - 1, mID});
return start;
}
return -1;
}
public int freeMemory(int mID) {
int cnt = 0;
java.util.Iterator<int[]> it = blocks.iterator();
while (it.hasNext()) {
int[] b = it.next();
if (b[2] == mID) {
cnt += b[1] - b[0] + 1;
it.remove();
}
}
return cnt;
}
}
public static void main(String[] args) {
// 官方示例
Solution loc = new Solution(10);
assert loc.allocate(1, 1) == 0;
assert loc.allocate(1, 2) == 1;
assert loc.allocate(1, 3) == 2;
assert loc.freeMemory(2) == 1;
assert loc.allocate(3, 4) == 3;
assert loc.allocate(1, 1) == 1;
assert loc.allocate(1, 1) == 6;
assert loc.freeMemory(1) == 3;
assert loc.allocate(10, 2) == -1;
assert loc.freeMemory(7) == 0;
AllocatorList loc2 = new AllocatorList(10);
assert loc2.allocate(1, 1) == 0;
assert loc2.allocate(1, 2) == 1;
assert loc2.allocate(1, 3) == 2;
assert loc2.freeMemory(2) == 1;
assert loc2.allocate(3, 4) == 3;
assert loc2.allocate(1, 1) == 1;
assert loc2.allocate(1, 1) == 6;
assert loc2.freeMemory(1) == 3;
assert loc2.allocate(10, 2) == -1;
assert loc2.freeMemory(7) == 0;
// 随机对拍
java.util.Random rnd = new java.util.Random(202);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(20);
Solution a = new Solution(n);
AllocatorList b = new AllocatorList(n);
int ops = 1 + rnd.nextInt(60);
int nextId = 1;
for (int o = 0; o < ops; o++) {
if (rnd.nextBoolean()) {
int size = 1 + rnd.nextInt(n + 3);
int id = nextId++;
int ra = a.allocate(size, id);
int rb = b.allocate(size, id);
if (ra != rb) {
System.out.println("allocate 对拍失败: n=" + n + " size=" + size + " id=" + id + " a=" + ra + " b=" + rb);
System.exit(1);
}
} else {
int id = 1 + rnd.nextInt(nextId);
int fa = a.freeMemory(id);
int fb = b.freeMemory(id);
if (fa != fb) {
System.out.println("free 对拍失败: n=" + n + " id=" + id + " a=" + fa + " b=" + fb);
System.exit(1);
}
}
}
}
System.out.println("2502 全部 PASS");
}
}
复杂度分析
- 时间复杂度:allocate/freeMemory 均 O(n)
- 空间复杂度:O(n)
2503. Maximum Number of Points From Grid Queries(矩阵查询可获得的最大分数)
题目描述
给你一个大小为 m x n 的整数矩阵 grid 和一个大小为 k 的数组 queries。
找出一个大小为 k 的数组 answer,且满足对于每个整数 queries[i]:
从矩阵左上角单元格开始,重复以下过程:
- 如果
queries[i]严格大于当前所处位置单元格的值,若该单元格是第一次访问,则获得 1 分,
并且可以移动到所有 4 个方向上任一相邻单元格; - 否则,不能获得任何分,并且结束这一过程。
answer[i]是你可以获得的最大分数。注意,对于每个查询,你可以访问同一个单元格多次。
返回结果数组answer。
示例 1
`grid = [[1,2,3],[2,5,7],[3,5,1]], queries = [5,6,2]` → [5,8,1]
示例 2
`grid = [[5,2,1],[1,1,2]], queries = [3]` → [0]
提示:
2 <= m, n <= 1000,4 <= m * n <= 10^51 <= k <= 10^4,1 <= grid[i][j], queries[i] <= 10^6
解题思路
解法一:离线排序 + 最小堆 BFS(主提交版本)
- 将 queries 按值升序排序,从小到大依次处理。用最小堆维护边界上可达到的格子(按格子值排序),
每次把值小于当前查询的格子扩张(弹堆、四方向入堆),cnt 记录已访问格子数,即该查询的答案。 - 由于每个格子只会入堆一次(用 visited 去重),整体复杂度 O(mn log(mn) + k log k)。
解法二:并查集离线(对照) - 将格子按值升序排序,逐个加入并查集;查询也升序处理------当查询大于格子值时,把格子和四周已加入的
格子合并,答案即左上角格子 (0,0) 所在连通分量大小(若 (0,0) 未加入则为 0)。
复杂度: - 时间复杂度:O(mn log(mn) + k log k)
- 空间复杂度:O(mn)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
public class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// 解法一:离线排序 + 最小堆 BFS(主提交版本)
public int[] maxPoints(int[][] grid, int[] queries) {
int m = grid.length, n = grid[0].length;
int k = queries.length;
// 查询离线排序
Integer[] idx = new Integer[k];
for (int i = 0; i < k; i++) {
idx[i] = i;
}
Arrays.sort(idx, Comparator.comparingInt(a -> queries[a]));
boolean[][] vis = new boolean[m][n];
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
pq.offer(new int[]{grid[0][0], 0, 0});
vis[0][0] = true;
int cnt = 0;
int[] ans = new int[k];
for (int qi : idx) {
int q = queries[qi];
while (!pq.isEmpty() && pq.peek()[0] < q) {
int[] cur = pq.poll();
cnt++;
for (int[] d : DIRS) {
int nx = cur[1] + d[0], ny = cur[2] + d[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !vis[nx][ny]) {
vis[nx][ny] = true;
pq.offer(new int[]{grid[nx][ny], nx, ny});
}
}
}
ans[qi] = cnt;
}
return ans;
}
// 解法二:并查集离线(对照)
public int[] maxPointsUnion(int[][] grid, int[] queries) {
int m = grid.length, n = grid[0].length;
int k = queries.length;
// 格子按值排序
int[][] cells = new int[m * n][];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cells[i * n + j] = new int[]{grid[i][j], i, j};
}
}
Arrays.sort(cells, Comparator.comparingInt(a -> a[0]));
Integer[] idx = new Integer[k];
for (int i = 0; i < k; i++) {
idx[i] = i;
}
Arrays.sort(idx, Comparator.comparingInt(a -> queries[a]));
DSU dsu = new DSU(m * n);
boolean[][] on = new boolean[m][n];
int p = 0;
int[] ans = new int[k];
for (int qi : idx) {
int q = queries[qi];
while (p < m * n && cells[p][0] < q) {
int x = cells[p][1], y = cells[p][2];
on[x][y] = true;
for (int[] d : DIRS) {
int nx = x + d[0], ny = y + d[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && on[nx][ny]) {
dsu.union(x * n + y, nx * n + ny);
}
}
p++;
}
ans[qi] = on[0][0] ? dsu.size(0) : 0;
}
return ans;
}
private static class DSU {
int[] parent, sz;
DSU(int n) {
parent = new int[n];
sz = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
sz[i] = 1;
}
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return;
}
if (sz[ra] < sz[rb]) {
int tmp = ra;
ra = rb;
rb = tmp;
}
parent[rb] = ra;
sz[ra] += sz[rb];
}
int size(int x) {
return sz[find(x)];
}
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] r1 = sol.maxPoints(new int[][]{{1, 2, 3}, {2, 5, 7}, {3, 5, 1}}, new int[]{5, 6, 2});
int[] r1u = sol.maxPointsUnion(new int[][]{{1, 2, 3}, {2, 5, 7}, {3, 5, 1}}, new int[]{5, 6, 2});
assert Arrays.equals(r1, new int[]{5, 8, 1}) : "2503 例1 主";
assert Arrays.equals(r1u, new int[]{5, 8, 1}) : "2503 例1 并查集";
int[] r2 = sol.maxPoints(new int[][]{{5, 2, 1}, {1, 1, 2}}, new int[]{3});
int[] r2u = sol.maxPointsUnion(new int[][]{{5, 2, 1}, {1, 1, 2}}, new int[]{3});
assert Arrays.equals(r2, new int[]{0}) : "2503 例2 主";
assert Arrays.equals(r2u, new int[]{0}) : "2503 例2 并查集";
// 随机对拍
java.util.Random rnd = new java.util.Random(303);
for (int t = 0; t < 200; t++) {
int m = 2 + rnd.nextInt(5), n = 2 + rnd.nextInt(5);
int[][] g = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
g[i][j] = 1 + rnd.nextInt(20);
}
}
int qn = 1 + rnd.nextInt(6);
int[] qs = new int[qn];
for (int i = 0; i < qn; i++) {
qs[i] = 1 + rnd.nextInt(20);
}
if (!Arrays.equals(sol.maxPoints(g, qs), sol.maxPointsUnion(g, qs))) {
System.out.println("对拍失败 grid=" + Arrays.deepToString(g) + " q=" + Arrays.toString(qs));
System.exit(1);
}
}
System.out.println("2503 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(mn log(mn) + k log k)
- 空间复杂度:O(mn)
2504. Concatenate the Name and the Profession(把名字和职业联系起来)
题目类型:数据库(SQL)
题目描述
表: Person
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| person_id | int |
| name | varchar |
| profession | ENUM |
+-------------+---------+
person_id 是该表的主键(具有唯一值的列)。该表中的每一行都包含一个人的 id、姓名和职业。
profession 是 ENUM 类型,其值为 ('Doctor', 'Singer', 'Actor', 'Player', 'Engineer', 'Lawyer') 之一。
编写一个解决方案报告每个人的名字,后面是他们职业的第一个字母,用括号括起来。
返回按 person_id 降序排列的结果表。
示例 1:
输入:
Person 表:
+-----------+-------+------------+
| person_id | name | profession |
+-----------+-------+------------+
| 1 | Alex | Singer |
| 3 | Alice | Actor |
| 2 | Bob | Player |
| 4 | Messi | Doctor |
| 6 | Tyson | Engineer |
| 5 | Meir | Lawyer |
+-----------+-------+------------+
输出:
+-----------+----------+
| person_id | name |
+-----------+----------+
| 6 | Tyson(E) |
| 5 | Meir(L) |
| 4 | Messi(D) |
| 3 | Alice(A) |
| 2 | Bob(P) |
| 1 | Alex(S) |
+-----------+----------+
解释: 请注意,在名称和职业的第一个字母之间不应该有任何空白。
解题思路
解法一:CONCAT + LEFT 直接拼接(主提交版本)
- 利用
CONCAT把姓名、左括号、LEFT(profession, 1)职业首字母、右括号拼接成Tyson(E)的形式。 - 按
person_id降序排列完成题目要求。
解法二:|| 拼接运算符(对照)
- 部分数据库(如 PostgreSQL、SQLite、Oracle)支持
||字符串拼接,可替换CONCAT达到同样效果; - MySQL 需启用
PIPES_AS_CONCAT才能使用||,因此主解法采用通用性最好的CONCAT。
代码实现
sql
-- 解法一(主提交版本)
SELECT person_id,
CONCAT(name, '(', LEFT(profession, 1), ')') AS name
FROM Person
ORDER BY person_id DESC;
sql
-- 解法二(对照,使用 || 拼接)
SELECT person_id,
name || '(' || LEFT(profession, 1) || ')' AS name
FROM Person
ORDER BY person_id DESC;
复杂度分析
- 时间复杂度 :
O(n),n为Person表行数;拼接每行字符串、按person_id降序排序。 - 空间复杂度 :
O(n),排序期间需要存储结果行。
2505. Bitwise OR of All Subsequence Sums(所有子序列和的按位或)
题目描述
给你一个整数数组 nums,返回对数组中所有可能的子序列之和进行按位或运算后得到的值。
数组的子序列是从数组中删除零个或多个元素且不改变剩余元素的顺序得到的序列。
示例 1
`nums = [2,1,0,3]` → 7(所有子序列和 0~6 的或为 7)
示例 2
`nums = [0,0,0]` → 0
提示:
1 <= nums.length <= 10^50 <= nums[i] <= 10^9
解题思路
解法一:按位统计 + 进位模拟(主提交版本)
- 记 cntb 为数组中第 b 位为 1 的元素个数。任意子序列和可以写成 S = Σ m_k * 2^k,
其中 m_k ∈ 0, cnt\[k](每个第 k 位为 1 的元素独立决定是否被选中,最多贡献 cntk 个 2^k)。 - 由于「或」允许每一位来自不同的子序列,第 b 位可表达 ⟺ 存在子序列使和的第 b 位为 1。
从低位向高位模拟进位:carry 为低位累加进位到本位的 2 的幂个数,carry + cntb > 0 时
意味着本位上至少存在一个 2^b 可用(选奇数个即可让该位为 1,选 0 个以上组合恒可行),
因此答案第 b 位记为 1;随后 carry = (carry + cntb) / 2 向更高位进位。 - 每个位 O(n) 统计一次,总复杂度 O(n * 31)。
解法二:子序列和可达性 DP(对照,仅限小数据) - 用布尔数组记录可达的子序列和,最后对所有可达和求按位或。用于对拍验证主解法。
复杂度: - 时间复杂度:O(n * 31)
- 空间复杂度:O(1)(cnt 数组长度固定为 32)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:按位统计 + 进位模拟(主提交版本)
public int subsequenceSumOr(int[] nums) {
int[] cnt = new int[32];
for (int x : nums) {
for (int b = 0; b < 31; b++) {
if ((x >> b & 1) == 1) {
cnt[b]++;
}
}
}
int ans = 0;
long carry = 0;
for (int b = 0; b < 32; b++) {
carry += cnt[b];
if (carry > 0) {
ans |= 1 << b;
}
carry >>= 1;
}
return ans;
}
// 解法二:子序列和可达性 DP(对照,小数据对拍用)
public int subsequenceSumOrBrute(int[] nums) {
int total = 0;
for (int x : nums) {
total += x;
}
boolean[] reach = new boolean[total + 1];
reach[0] = true;
for (int x : nums) {
for (int s = total; s >= x; s--) {
if (reach[s - x]) {
reach[s] = true;
}
}
}
int or = 0;
for (int s = 0; s <= total; s++) {
if (reach[s]) {
or |= s;
}
}
return or;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.subsequenceSumOr(new int[]{2, 1, 0, 3}) == 7 : "2505 例1 主";
assert sol.subsequenceSumOrBrute(new int[]{2, 1, 0, 3}) == 7 : "2505 例1 对照";
assert sol.subsequenceSumOr(new int[]{0, 0, 0}) == 0 : "2505 例2 主";
assert sol.subsequenceSumOrBrute(new int[]{0, 0, 0}) == 0 : "2505 例2 对照";
assert sol.subsequenceSumOr(new int[]{2, 2}) == 6 : "2505 附加 2+2=4";
assert sol.subsequenceSumOr(new int[]{1, 1, 1}) == 3 : "2505 附加 三个1";
assert sol.subsequenceSumOr(new int[]{1, 2, 4}) == 7 : "2505 附加 幂次组合";
// 随机对拍(n <= 14, 元素 <= 60 保证 DP 可行)
java.util.Random rnd = new java.util.Random(404);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(14);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = rnd.nextInt(61);
}
int x1 = sol.subsequenceSumOr(a);
int x2 = sol.subsequenceSumOrBrute(a);
if (x1 != x2) {
System.out.println("对拍失败 " + Arrays.toString(a) + " 主=" + x1 + " 对照=" + x2);
System.exit(1);
}
}
System.out.println("2505 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n * 31)
- 空间复杂度:O(1)(cnt 数组长度固定为 32)
2506. Count Pairs Of Similar Strings(统计相似字符串对的数目)
题目描述
给你一个下标从 0 开始的字符串数组 words。
如果两个字符串由相同的字符组成,则认为这两个字符串相似。
例如,"abca" 和 "cba" 相似,因为它们都由字符 'a'、'b'、'c' 组成;
而 "abacba" 和 "bcfd" 不相似。
请你找出满足 words[i] 和 words[j] 相似的下标对 (i, j) 的数目,其中 0 <= i < j < words.length。
示例 1
`words = ["aba","aabb","abcd","bac","aabc"]` → 2
示例 2
`words = ["aabb","ab","ba"]` → 3
示例 3
`words = ["nba","cba","dba"]` → 0
提示:
1 <= words.length <= 1001 <= words[i].length <= 100words[i]仅由小写英文字母组成
解题思路
解法一:位掩码 + 哈希计数(主提交版本)
- 每个字符串用一个 26 位掩码表示含有哪些字符,相似 ⟺ 掩码相等。
- 遍历字符串,统计每种掩码的出现次数 cnt,答案累加已有的同掩码数量后把 cnt 加 1。
- 复杂度 O(Σ len(wordsi) + n)。
解法二:两两比较(对照) - 朴素 O(n^2) 双循环,每次比较两个字符串的去重字符集合是否相同(可用排序后去重比较)。
复杂度: - 时间复杂度:O(Σ len(wordsi) + n)
- 空间复杂度:O(n)
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// 解法一:位掩码 + 哈希计数(主提交版本)
public int similarPairs(String[] words) {
Map<Integer, Integer> cnt = new HashMap<>();
int ans = 0;
for (String w : words) {
int mask = 0;
for (int i = 0; i < w.length(); i++) {
mask |= 1 << (w.charAt(i) - 'a');
}
int c = cnt.getOrDefault(mask, 0);
ans += c;
cnt.put(mask, c + 1);
}
return ans;
}
// 解法二:两两比较字符集合(对照)
public int similarPairsBrute(String[] words) {
int n = words.length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (sameChars(words[i], words[j])) {
ans++;
}
}
}
return ans;
}
private boolean sameChars(String a, String b) {
boolean[] ca = new boolean[26];
boolean[] cb = new boolean[26];
for (int i = 0; i < a.length(); i++) {
ca[a.charAt(i) - 'a'] = true;
}
for (int i = 0; i < b.length(); i++) {
cb[b.charAt(i) - 'a'] = true;
}
for (int i = 0; i < 26; i++) {
if (ca[i] != cb[i]) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.similarPairs(new String[]{"aba", "aabb", "abcd", "bac", "aabc"}) == 2 : "2506 例1 主";
assert sol.similarPairsBrute(new String[]{"aba", "aabb", "abcd", "bac", "aabc"}) == 2 : "2506 例1 对照";
assert sol.similarPairs(new String[]{"aabb", "ab", "ba"}) == 3 : "2506 例2 主";
assert sol.similarPairsBrute(new String[]{"aabb", "ab", "ba"}) == 3 : "2506 例2 对照";
assert sol.similarPairs(new String[]{"nba", "cba", "dba"}) == 0 : "2506 例3 主";
assert sol.similarPairsBrute(new String[]{"nba", "cba", "dba"}) == 0 : "2506 例3 对照";
// 随机对拍
java.util.Random rnd = new java.util.Random(505);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(50);
String[] ws = new String[n];
for (int i = 0; i < n; i++) {
int len = 1 + rnd.nextInt(8);
StringBuilder sb = new StringBuilder();
for (int j = 0; j < len; j++) {
sb.append((char) ('a' + rnd.nextInt(5)));
}
ws[i] = sb.toString();
}
if (sol.similarPairs(ws) != sol.similarPairsBrute(ws)) {
System.out.println("对拍失败 " + java.util.Arrays.toString(ws));
System.exit(1);
}
}
System.out.println("2506 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(Σ len(wordsi) + n)
- 空间复杂度:O(n)
2507. Smallest Value After Replacing With Sum of Prime Factors
题目描述
(使用质因数之和替换后可以取到的最小值)
给你一个正整数 n。请你将 n 的值替换为 n 的质因数之和,重复这一过程。
注意,如果 n 能够被某个质因数多次整除,则在求和时应当包含这个质因数同样次数。
返回 n 可以取到的最小值。
示例 1
`n = 15` → 5(15=3×5 → 3+5=8 → 8=2×2×2 → 2+2+2=6 → 6=2×3 → 2+3=5)
示例 2
`n = 3` → 3
提示:
2 <= n <= 10^5
解题思路
解法一:试除质因数分解 + 循环(主提交版本)
- sum_prime_factors(x):对 x 从 2 开始试除,若 x 能被 p 整除,则把 p 计入和,同时 x /= p,
直到 x 为 1;若最后剩下的 x > 1 则它是质数因子,再加入和。 - 循环 n = sum_prime_factors(n),直到 n 不再变化。由于质因数之和 ≤ n(n 为质数时相等),
序列必然收敛(且最终停在某个质数上)。 - 每次分解 O(sqrt(n)),20 次以内必然收敛,总复杂度 O(sqrt(n) * 迭代次数)。
解法二:预筛最小质因子 SPF(对照) - 先线性筛出 1, 10\^5 内每个数的最小质因子,然后每次用 SPF 快速分解求和,
复杂度 O(maxN + log n * 迭代次数)。
复杂度: - 时间复杂度:O(sqrt(n) * log n)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:试除分解 + 循环(主提交版本)
public int smallestValue(int n) {
while (true) {
int s = primeFactorSum(n);
if (s == n) {
return n;
}
n = s;
}
}
private int primeFactorSum(int x) {
int sum = 0;
int d = 2;
while (d * d <= x) {
while (x % d == 0) {
sum += d;
x /= d;
}
d++;
}
if (x > 1) {
sum += x; // 剩余的是质数因子
}
return sum;
}
// 解法二:SPF 最小质因子分解(对照)
public int smallestValueSpf(int n) {
int maxN = 100000;
int[] spf = new int[maxN + 1];
// 线性筛
java.util.List<Integer> primes = new java.util.ArrayList<>();
for (int i = 2; i <= maxN; i++) {
if (spf[i] == 0) {
spf[i] = i;
primes.add(i);
}
for (int p : primes) {
if (p > spf[i] || (long) i * p > maxN) {
break;
}
spf[i * p] = p;
}
}
while (true) {
int x = n;
int sum = 0;
while (x > 1) {
int f = spf[x];
sum += f;
x /= f;
}
if (sum == n) {
return n;
}
n = sum;
}
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.smallestValue(15) == 5 : "2507 例1 主";
assert sol.smallestValueSpf(15) == 5 : "2507 例1 对照";
assert sol.smallestValue(3) == 3 : "2507 例2 主";
assert sol.smallestValueSpf(3) == 3 : "2507 例2 对照";
assert sol.smallestValue(4) == 4 : "2507 附加 4";
assert sol.smallestValue(16) == 4 : "2507 附加 16";
// 随机对拍
java.util.Random rnd = new java.util.Random(606);
for (int t = 0; t < 1000; t++) {
int v = 2 + rnd.nextInt(99999);
if (sol.smallestValue(v) != sol.smallestValueSpf(v)) {
System.out.println("对拍失败 n=" + v);
System.exit(1);
}
}
System.out.println("2507 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(sqrt(n) * log n)
- 空间复杂度:O(1)
2508. Add Edges to Make Degrees of All Nodes Even(添加边使所有节点度数都为偶数)
题目描述
给你一个有 n 个节点的无向图,节点编号为 1 到 n。再给你整数 n 和一个二维整数数组 edges,
其中 edges[i] = `ai`, `bi` 表示节点 ai 和 bi 之间有一条边。图不一定连通。
你可以给图中添加至多两条额外的边(也可以一条边都不添加),使得图中没有重边也没有自环。
如果添加额外的边后,可以使得图中所有点的度数都是偶数,返回 true,否则返回 false。
点的度数是连接一个点的边的数目。
示例 1
`n = 5, edges = [[1,2],[2,3],[3,4],[4,2],[1,4],[2,5]]` → true
示例 2
`n = 4, edges = [[1,2],[3,4]]` → true(补 [1,3] 和 [2,4])
示例 3
`n = 4, edges = [[1,2],[1,3],[1,4]]` → false
提示:
3 <= n <= 10^5,2 <= edges.length <= 10^51 <= ai, bi <= n,ai!=bi,图中无重边
解题思路
解法一:奇度点分类讨论(主提交版本)
- 只有度数为奇数的点需要被"修正",加一条边连接两个奇点可使二者度数变为偶数。
- 收集所有奇度点集合 o(大小必为偶数)。分情况:
a) |o| == 0:已经满足,返回 true。
b) |o| == 2:u、v 两个奇点------若 (u,v) 无边,直接连一条;若有边,则找任意点 w ∉ {u,v},
分别连 (u,w)、(v,w)(两条边),要求这两条边都不与已有边重复即可。
c) |o| == 4:四个奇点 a,b,c,d------检查能否配对成两条新边 (a,b)+(c,d)、(a,c)+(b,d) 或
(a,d)+(b,c),只要某一组中的两条边都不存在于原图即可。
d) |o| > 4:一条边最多修正两个奇点,至多两条边修正四个,返回 false。 - 用 Set 哈希存储已有边(key = (long)a << 32 | b,统一小号在前),O(1) 判重。
解法二:枚举配对 + 全量度数校验(对照) - 对 |o| ≤ 4 的每种可能加边组合(从所有可能边中选 ≤ 2 条)尝试,模拟加边后校验所有点度数为偶数。
复杂度: - 时间复杂度:O(n + m)
- 空间复杂度:O(n + m)
代码实现
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Solution {
// 解法一:奇度点分类讨论(主提交版本)
public boolean isPossible(int n, int[][] edges) {
int[] deg = new int[n + 1];
Set<Long> set = new HashSet<>();
for (int[] e : edges) {
int a = e[0], b = e[1];
deg[a]++;
deg[b]++;
set.add(key(a, b));
}
List<Integer> odd = new ArrayList<>();
for (int i = 1; i <= n; i++) {
if ((deg[i] & 1) == 1) {
odd.add(i);
}
}
int sz = odd.size();
if (sz == 0) {
return true;
}
if (sz == 2) {
int u = odd.get(0), v = odd.get(1);
if (!set.contains(key(u, v))) {
return true;
}
for (int w = 1; w <= n; w++) {
if (w != u && w != v
&& !set.contains(key(u, w)) && !set.contains(key(v, w))) {
return true;
}
}
return false;
}
if (sz == 4) {
int a = odd.get(0), b = odd.get(1), c = odd.get(2), d = odd.get(3);
return (canAddPair(a, b, c, d, set)
|| canAddPair(a, c, b, d, set)
|| canAddPair(a, d, b, c, set));
}
return false;
}
private boolean canAddPair(int a, int b, int c, int d, Set<Long> set) {
return !set.contains(key(a, b)) && !set.contains(key(c, d));
}
private long key(int a, int b) {
if (a > b) {
int t = a;
a = b;
b = t;
}
return ((long) a << 32) | b;
}
// 解法二:枚举加边组合 + 全量校验(对照,小图用)
public boolean isPossibleBrute(int n, int[][] edges) {
Set<Long> set = new HashSet<>();
for (int[] e : edges) {
set.add(key(e[0], e[1]));
}
// 收集所有可加的边(n 较小时)
List<long[]> cand = new ArrayList<>();
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (!set.contains(key(i, j))) {
cand.add(new long[]{i, j});
}
}
}
// 0 条
if (allEven(set, n)) {
return true;
}
// 1 条
for (long[] e : cand) {
Set<Long> s2 = new HashSet<>(set);
s2.add(key((int) e[0], (int) e[1]));
if (allEven(s2, n)) {
return true;
}
}
// 2 条
for (int i = 0; i < cand.size(); i++) {
for (int j = i + 1; j < cand.size(); j++) {
long[] e1 = cand.get(i), e2 = cand.get(j);
Set<Long> s2 = new HashSet<>(set);
s2.add(key((int) e1[0], (int) e1[1]));
s2.add(key((int) e2[0], (int) e2[1]));
if (allEven(s2, n)) {
return true;
}
}
}
return false;
}
private boolean allEven(Set<Long> set, int n) {
int[] deg = new int[n + 1];
for (long k : set) {
int a = (int) (k >> 32), b = (int) (k & 0xffffffffL);
deg[a]++;
deg[b]++;
}
for (int i = 1; i <= n; i++) {
if ((deg[i] & 1) == 1) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.isPossible(5, new int[][]{{1, 2}, {2, 3}, {3, 4}, {4, 2}, {1, 4}, {2, 5}}) : "2508 例1 主";
assert sol.isPossibleBrute(5, new int[][]{{1, 2}, {2, 3}, {3, 4}, {4, 2}, {1, 4}, {2, 5}}) : "2508 例1 对照";
assert sol.isPossible(4, new int[][]{{1, 2}, {3, 4}}) : "2508 例2 主";
assert sol.isPossibleBrute(4, new int[][]{{1, 2}, {3, 4}}) : "2508 例2 对照";
assert !sol.isPossible(4, new int[][]{{1, 2}, {1, 3}, {1, 4}}) : "2508 例3 主";
assert !sol.isPossibleBrute(4, new int[][]{{1, 2}, {1, 3}, {1, 4}}) : "2508 例3 对照";
assert sol.isPossible(3, new int[][]{{1, 2}, {2, 3}, {3, 1}}) : "2508 附加 完全三角";
assert sol.isPossible(6, new int[][]{}) : "2508 附加 无边图";
// 随机对拍(小图 n <= 6)
java.util.Random rnd = new java.util.Random(707);
for (int t = 0; t < 800; t++) {
int n = 3 + rnd.nextInt(4);
List<int[]> rel = new ArrayList<>();
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (rnd.nextBoolean()) {
rel.add(new int[]{i, j});
}
}
}
int[][] es = rel.toArray(new int[0][]);
boolean r1 = sol.isPossible(n, es);
boolean r2 = sol.isPossibleBrute(n, es);
if (r1 != r2) {
System.out.println("对拍失败 n=" + n + " " + java.util.Arrays.deepToString(es) + " 主=" + r1 + " 对照=" + r2);
System.exit(1);
}
}
System.out.println("2508 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n + m)
- 空间复杂度:O(n + m)
2509. Cycle Length Queries in a Tree(查询树中环的长度)
题目描述
给你一个整数 n,表示你有一棵含有 2^n - 1 个节点的完全二叉树。根节点的编号是 1,
编号在 1, 2\^(`n`-1) - 1 之间的节点都有两个子节点:左子节点编号 2*val,右子节点编号 2*val+1。
给你一个长度为 m 的查询数组 queries,其中 queries[i] = `ai`, `bi`。对于每个查询:
- 在节点
ai和bi之间添加一条边; - 求出图中环的长度(环中边的数目);
- 删除新添加的边,处理下一个查询。
返回长度为m的数组answer。
示例 1
`n = 3, queries = [[5,3],[4,7],[2,3]]` → [4,5,3]
示例 2
`n = 2, queries = [[1,2]]` → [2]
提示:
2 <= n <= 30,1 <= m <= 10^51 <= ai, bi <= 2^n - 1,ai != bi
解题思路
解法一:深度 + LCA(主提交版本)
- 在完全二叉树中,环 = 新增边 (ai, bi) + ai 到 bi 的树上唯一路径。
环长 = depth(ai) + depth(bi) - 2 * depth(lca(ai, bi)) + 1。 - 节点深度 = 二进制位数减 1(根深度 0):depth(x) = Integer.numberOfTrailingZeros(Integer.highestOneBit(x)),
即 floor(log2 x)。 - LCA:两个数不断向上(x / 2)直到相等,步数即深度差扁平化;更优法:先把较深者跳到与较浅者同层,
再同步上跳。由于完全二叉树的递推性质,直接用 while (a != b) { if (a > b) a >>= 1; else b >>= 1; }
即可(无需预处理)。 - 每次查询 O(log n),总复杂度 O(m log n),n ≤ 30 时完全足够。
解法二:路径逐级拼接(对照) - 收集 a 到根、b 到根的两条路径,环长 = pathA.size + pathB.size - 公共前缀长度*2 + 1。
复杂度: - 时间复杂度:O(m log n)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:深度 + LCA(主提交版本)
public int[] cycleLengthQueries(int n, int[][] queries) {
int m = queries.length;
int[] ans = new int[m];
for (int i = 0; i < m; i++) {
int a = queries[i][0], b = queries[i][1];
int lca = lca(a, b);
int len = depth(a) + depth(b) - 2 * depth(lca) + 1;
ans[i] = len;
}
return ans;
}
private int lca(int a, int b) {
while (a != b) {
if (a > b) {
a >>= 1;
} else {
b >>= 1;
}
}
return a;
}
private int depth(int x) {
int d = 0;
while (x > 1) {
x >>= 1;
d++;
}
return d;
}
// 解法二:路径集合拼接(对照)
public int[] cycleLengthQueriesPath(int n, int[][] queries) {
int m = queries.length;
int[] ans = new int[m];
for (int i = 0; i < m; i++) {
int a = queries[i][0], b = queries[i][1];
ans[i] = pathLen(a, b);
}
return ans;
}
private int pathLen(int a, int b) {
int da = 0, db = 0;
int x = a, y = b;
while (x != y) {
if (x > y) {
x >>= 1;
da++;
} else {
y >>= 1;
db++;
}
}
return da + db + 1;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] r1 = sol.cycleLengthQueries(3, new int[][]{{5, 3}, {4, 7}, {2, 3}});
int[] r1p = sol.cycleLengthQueriesPath(3, new int[][]{{5, 3}, {4, 7}, {2, 3}});
assert Arrays.equals(r1, new int[]{4, 5, 3}) : "2509 例1 主";
assert Arrays.equals(r1p, new int[]{4, 5, 3}) : "2509 例1 对照";
int[] r2 = sol.cycleLengthQueries(2, new int[][]{{1, 2}});
int[] r2p = sol.cycleLengthQueriesPath(2, new int[][]{{1, 2}});
assert Arrays.equals(r2, new int[]{2}) : "2509 例2 主";
assert Arrays.equals(r2p, new int[]{2}) : "2509 例2 对照";
// 随机对拍
java.util.Random rnd = new java.util.Random(808);
for (int t = 0; t < 2000; t++) {
int n = 2 + rnd.nextInt(12); // 2^n - 1 节点
int maxNode = (1 << n) - 1;
int qn = 1 + rnd.nextInt(30);
int[][] qs = new int[qn][2];
for (int i = 0; i < qn; i++) {
int a = 1 + rnd.nextInt(maxNode);
int b;
do {
b = 1 + rnd.nextInt(maxNode);
} while (b == a);
qs[i][0] = a;
qs[i][1] = b;
}
if (!Arrays.equals(sol.cycleLengthQueries(n, qs), sol.cycleLengthQueriesPath(n, qs))) {
System.out.println("对拍失败 n=" + n + " " + Arrays.deepToString(qs));
System.exit(1);
}
}
System.out.println("2509 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(m log n)
- 空间复杂度:O(1)
2510. Check if There is a Path With Equal Number of 0's And 1's
题目描述
(检查是否有路径经过相同数量的 0 和 1)
给定一个下标从 0 开始的 m x n 的二进制矩阵 grid,从坐标为 (row, col) 的元素可以向右走
(row, col+1) 或向下走 (row+1, col)。返回一个布尔值,表示从 (0, 0) 出发是否存在一条路径,
经过相同数量的 0 和 1,到达终点 (m-1, n-1)。如果存在这样的路径返回 true,否则返回 false。
示例 1
`grid = [[0,1,0,0],[0,1,0,0],[1,0,1,0]]` → true
示例 2
`grid = [[1,1,0],[0,0,1],[1,0,0]]` → false
提示:
2 <= m, n <= 100grid[i][j]不是 0 就是 1
解题思路
解法一:平衡值可达集合 DP(主提交版本)
- 记 balance = (0 的个数) - (1 的个数)。终点要求 balance == 0。
- 平衡值范围 -maxStep, maxStep,maxStep = m + n - 2(步数),用偏移量 off 压缩索引。
- 逐行扫描:curjb 表示到达 (i, j) 时平衡值为 b 是否可达。处理每个状态时向右推进到
(i, j+1)、向下推进到 (i+1, j)(写入下一行的 next 数组,避免污染当前行)。 - 最后一行扫描完后检查终点 (m-1, n-1) 处 balance == 0 是否可达。
- 复杂度 O(m * n * (m + n)),m,n ≤ 100 时约为 2e6 布尔运算,可行。
解法二:记忆化递归(对照) - dfs(i, j, b) 用 Boolean\[\]\[\]\[\] memo 缓存,b 用偏移存储,同样的可达性语义。
- 剪枝:剩余格子数 < |b| 时不可能回归 0,直接返回 false。
复杂度: - 时间复杂度:O(m * n * (m + n))
- 空间复杂度:O(n * (m + n))
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:行滚动 DP 平衡值集合(主提交版本)
public boolean isThereAPath(int[][] grid) {
int m = grid.length, n = grid[0].length;
int maxStep = m + n - 1; // 路径上的格子数(含起终点),平衡值极值
int off = maxStep;
int size = 2 * maxStep + 1;
boolean[][] cur = new boolean[n][size];
// 起点 (0,0) 计入自身值
int d0 = (grid[0][0] == 0 ? 1 : -1) + off;
cur[0][d0] = true;
for (int i = 0; i < m; i++) {
boolean[][] next = new boolean[n][size]; // 下一行的状态
for (int j = 0; j < n; j++) {
for (int b = 0; b < size; b++) {
if (!cur[j][b]) {
continue;
}
int bal = b - off;
if (j + 1 < n) { // 向右
int v = (grid[i][j + 1] == 0 ? 1 : -1);
cur[j + 1][bal + v + off] = true;
}
if (i + 1 < m) { // 向下 → 写入 next
int v = (grid[i + 1][j] == 0 ? 1 : -1);
next[j][bal + v + off] = true;
}
}
}
if (i == m - 1) {
break; // 已是最后一行,状态都在 cur 中
}
cur = next;
}
return cur[n - 1][off]; // 终点 balance == 0
}
// 解法二:记忆化递归(对照)
public boolean isThereAPathMemo(int[][] grid) {
int m = grid.length, n = grid[0].length;
int maxStep = m + n - 1;
int off = maxStep;
int size = 2 * maxStep + 1;
Boolean[][][] memo = new Boolean[m][n][size];
return dfs(grid, 0, 0, 0, off, memo);
}
private boolean dfs(int[][] g, int i, int j, int b, int off, Boolean[][][] memo) {
int m = g.length, n = g[0].length;
int idx = b + off;
if (memo[i][j][idx] != null) {
return memo[i][j][idx];
}
int remCells = (m - i) + (n - j) - 1; // 剩余还需经过的格子数(含当前)
if (Math.abs(b) > remCells) { // 即使全取同号也无法归零
memo[i][j][idx] = false;
return false;
}
int v = (g[i][j] == 0 ? 1 : -1);
if (i == m - 1 && j == n - 1) {
memo[i][j][idx] = (b + v == 0);
return memo[i][j][idx];
}
if (i + 1 < m && dfs(g, i + 1, j, b + v, off, memo)) {
memo[i][j][idx] = true;
return true;
}
if (j + 1 < n && dfs(g, i, j + 1, b + v, off, memo)) {
memo[i][j][idx] = true;
return true;
}
memo[i][j][idx] = false;
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.isThereAPath(new int[][]{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 0, 1, 0}}) : "2510 例1 主";
assert sol.isThereAPathMemo(new int[][]{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 0, 1, 0}}) : "2510 例1 对照";
assert !sol.isThereAPath(new int[][]{{1, 1, 0}, {0, 0, 1}, {1, 0, 0}}) : "2510 例2 主";
assert !sol.isThereAPathMemo(new int[][]{{1, 1, 0}, {0, 0, 1}, {1, 0, 0}}) : "2510 例2 对照";
assert sol.isThereAPath(new int[][]{{0, 0}, {1, 1}}) : "2510 附加 2x2";
assert sol.isThereAPath(new int[][]{{0, 1}, {1, 1}}) : "2510 附加 (0,0)=0,(1,1)=1";
assert !sol.isThereAPathMemo(new int[][]{{0, 1}, {1, 1}}) == sol.isThereAPath(new int[][]{{0, 1}, {1, 1}}) : "2510 附加一致性";
// 随机对拍(小矩阵)
java.util.Random rnd = new java.util.Random(909);
for (int t = 0; t < 400; t++) {
int m = 2 + rnd.nextInt(6), n = 2 + rnd.nextInt(6);
int[][] g = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
g[i][j] = rnd.nextInt(2);
}
}
boolean r1 = sol.isThereAPath(g);
boolean r2 = sol.isThereAPathMemo(g);
if (r1 != r2) {
System.out.println("对拍失败 " + Arrays.deepToString(g) + " 主=" + r1 + " 对照=" + r2);
System.exit(1);
}
}
System.out.println("2510 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(m * n * (m + n))
- 空间复杂度:O(n * (m + n))
2511. Maximum Enemy Forts That Can Be Captured(最多可以摧毁的敌人城堡数目)
题目描述
给你一个长度为 n、下标从 0 开始的整数数组 forts,表示一些城堡。forts[i] 可以是 -1、0 或者 1:
- -1 表示第
i个位置没有城堡; - 0 表示第
i个位置有一个敌人的城堡; - 1 表示第
i个位置有一个你控制的城堡。
现在你需要决定,将你的军队从某个你控制的城堡位置i移动到一个空的位置j,满足: - 0 <=
i,j<=n-1; - 军队经过的位置只有敌人的城堡,即对于所有
min(i,j) <k<max(i,j) 的k,都满足forts[k]== 0。
当军队移动时,所有途中经过的敌人城堡都会被摧毁。返回最多可以摧毁的敌人城堡数目;
如果无法移动,或没有你控制的城堡,返回 0。
示例 1
`forts = [1,0,0,-1,0,0,0,0,1]` → 4
示例 2
`forts = [0,0,1,-1]` → 0
提示:
1 <= forts.length <= 1000-1 <= forts[i] <= 1
解题思路
解法一:一次扫描统计两座己方城堡之间连续 0 的长度(主提交版本)
- 题意等价于:找到两个位置 i < j,满足 fortsi 与 fortsj 一个是 1、一个是 -1(军队从 1 出发
到达 -1 或反向),且中间全部是 0。摧毁数 = 中间 0 的个数。 - 扫描时记录上一个非 0 的位置 prev(值 1 或 -1)以及与 prev 之间的 0 计数;
当遇到新的非 0 值 v 时,若 v != 上一位非 0 值,则中间 0 的个数就是一个候选答案,
然后重置 0 计数并从当前位置重新开始。 - 复杂度 O(n)。
解法二:双指针朴素模拟(对照) - 对每个属于己方(值 1)的位置 i,向左右两个方向扫描中间全 0 且末端为空位(-1)的范围,
数出 0 的个数;取最大值。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:一次扫描相邻非零值(主提交版本)
public int captureForts(int[] forts) {
int n = forts.length;
int ans = 0;
int prevVal = 0; // 上一个非 0 值(1 或 -1)
int zeros = 0; // 从上个非 0 值位置之后累计的 0 的个数
boolean hasPrev = false;
for (int i = 0; i < n; i++) {
if (forts[i] == 0) {
zeros++;
continue;
}
if (hasPrev && forts[i] != prevVal) {
ans = Math.max(ans, zeros);
}
prevVal = forts[i];
hasPrev = true;
zeros = 0;
}
return ans;
}
// 解法二:双指针朴素模拟(对照)
public int captureFortsBrute(int[] forts) {
int n = forts.length;
int ans = 0;
for (int i = 0; i < n; i++) {
if (forts[i] != 1) {
continue;
}
// 向左
int cnt = 0;
for (int j = i - 1; j >= 0; j--) {
if (forts[j] == 0) {
cnt++;
} else if (forts[j] == -1) {
ans = Math.max(ans, cnt);
break;
} else {
break;
}
}
// 向右
cnt = 0;
for (int j = i + 1; j < n; j++) {
if (forts[j] == 0) {
cnt++;
} else if (forts[j] == -1) {
ans = Math.max(ans, cnt);
break;
} else {
break;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.captureForts(new int[]{1, 0, 0, -1, 0, 0, 0, 0, 1}) == 4 : "2511 例1 主";
assert sol.captureFortsBrute(new int[]{1, 0, 0, -1, 0, 0, 0, 0, 1}) == 4 : "2511 例1 对照";
assert sol.captureForts(new int[]{0, 0, 1, -1}) == 0 : "2511 例2 主";
assert sol.captureFortsBrute(new int[]{0, 0, 1, -1}) == 0 : "2511 例2 对照";
assert sol.captureForts(new int[]{1, 0, 0, 0}) == 0 : "2511 附加 无终点";
assert sol.captureForts(new int[]{-1, 0, 0, 0, 1}) == 3 : "2511 附加 反向";
assert sol.captureForts(new int[]{1, 0, -1}) == 1 : "2511 附加 一个敌人";
// 随机对拍
java.util.Random rnd = new java.util.Random(1011);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(30);
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = rnd.nextInt(3) - 1;
}
if (sol.captureForts(a) != sol.captureFortsBrute(a)) {
System.out.println("对拍失败 " + Arrays.toString(a));
System.exit(1);
}
}
System.out.println("2511 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2512. Reward Top K Students(奖励最顶尖的 K 名学生)
题目描述
给你两个字符串数组 positive_feedback 和 negative_feedback,分别包含表示正面和负面的词汇,
不会有单词同时是正面的和负面的。一开始每位学生分数为 0。每个正面的单词会给学生的分数加 3 分,
每个负面的词会给学生的分数减 1 分。
给你 n 个学生的评语,用一个下标从 0 开始的字符串数组 report 和一个下标从 0 开始的整数数组
student_id 表示,其中 student_id[i] 表示这名学生的 ID,这名学生的评语是 report[i]。
每名学生的 ID 互不相同。给你一个整数 k,请你返回按照得分从高到低最顶尖的 k 名学生。
如果有多名学生分数相同,ID 越小排名越前。
示例 1
`positive_feedback = ["smart","brilliant","studious"], negative_feedback = ["not"],
report = ["this student is studious","the student is smart"], student_id = [1,2], k = 2` → [1,2]
示例 2
report = ["this student is not studious","the student is smart"], k=2 → [2,1]
提示:
1 <= positive_feedback.length, negative_feedback.length <= 10^4n <= 10^4,1 <= k <= nreport[i]只包含小写英文字母和空格,连续单词之间有单个空格隔开1 <= student_id[i] <= 10^9,互不相同
解题思路
解法一:哈希表词库 + 统计 + 自定义排序(主提交版本)
- 用两个 HashSet 存正面/负面词汇。对每个学生,按空格切分评语逐词查询词库,累加 3 或 -1。
- 将 (score, id) 放入列表,按「分数降序、ID 升序」排序,取前 k 个 id。
- 复杂度 O(Σlen(report)),词数总量 ≤ 10^4 量级。
解法二:小顶堆维护 TopK(对照) - 同样先算分数,再用大小为 k 的堆保留前 k 名(堆内按分数升序、ID 降序),最后弹出反转。
复杂度: - 时间复杂度:O(Σ len(report) + n log n)
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
public class Solution {
private Set<String> pos = new HashSet<>();
private Set<String> neg = new HashSet<>();
// 解法一:哈希词库 + 排序(主提交版本)
public List<Integer> topStudents(String[] positive_feedback, String[] negative_feedback,
String[] report, int[] student_id, int k) {
pos.clear();
neg.clear();
pos.addAll(Arrays.asList(positive_feedback));
neg.addAll(Arrays.asList(negative_feedback));
int n = report.length;
List<int[]> list = new ArrayList<>(); // [score, id]
for (int i = 0; i < n; i++) {
int score = 0;
for (String w : report[i].split(" ")) {
if (pos.contains(w)) {
score += 3;
} else if (neg.contains(w)) {
score -= 1;
}
}
list.add(new int[]{score, student_id[i]});
}
list.sort((a, b) -> a[0] != b[0] ? Integer.compare(b[0], a[0]) : Integer.compare(a[1], b[1]));
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < k; i++) {
ans.add(list.get(i)[1]);
}
return ans;
}
// 解法二:小顶堆 TopK(对照)
public List<Integer> topStudentsHeap(String[] positive_feedback, String[] negative_feedback,
String[] report, int[] student_id, int k) {
pos.clear();
neg.clear();
pos.addAll(Arrays.asList(positive_feedback));
neg.addAll(Arrays.asList(negative_feedback));
int n = report.length;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) ->
a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(b[1], a[1]));
for (int i = 0; i < n; i++) {
int score = 0;
for (String w : report[i].split(" ")) {
if (pos.contains(w)) {
score += 3;
} else if (neg.contains(w)) {
score -= 1;
}
}
pq.offer(new int[]{score, student_id[i]});
if (pq.size() > k) {
pq.poll(); // 弹出最差(分数最低、ID 最大)的
}
}
List<int[]> tmp = new ArrayList<>(pq);
tmp.sort((a, b) -> a[0] != b[0] ? Integer.compare(b[0], a[0]) : Integer.compare(a[1], b[1]));
List<Integer> ans = new ArrayList<>();
for (int[] e : tmp) {
ans.add(e[1]);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
List<Integer> r1 = sol.topStudents(
new String[]{"smart", "brilliant", "studious"},
new String[]{"not"},
new String[]{"this student is studious", "the student is smart"},
new int[]{1, 2}, 2);
assert r1.equals(Arrays.asList(1, 2)) : "2512 例1 主";
List<Integer> r1h = sol.topStudentsHeap(
new String[]{"smart", "brilliant", "studious"},
new String[]{"not"},
new String[]{"this student is studious", "the student is smart"},
new int[]{1, 2}, 2);
assert r1h.equals(Arrays.asList(1, 2)) : "2512 例1 对照";
List<Integer> r2 = sol.topStudents(
new String[]{"smart", "brilliant", "studious"},
new String[]{"not"},
new String[]{"this student is not studious", "the student is smart"},
new int[]{1, 2}, 2);
assert r2.equals(Arrays.asList(2, 1)) : "2512 例2 主";
List<Integer> r2h = sol.topStudentsHeap(
new String[]{"smart", "brilliant", "studious"},
new String[]{"not"},
new String[]{"this student is not studious", "the student is smart"},
new int[]{1, 2}, 2);
assert r2h.equals(Arrays.asList(2, 1)) : "2512 例2 对照";
// 随机对拍
java.util.Random rnd = new java.util.Random(1112);
for (int t = 0; t < 300; t++) {
int wc = 1 + rnd.nextInt(5);
String[] posW = new String[wc];
for (int i = 0; i < wc; i++) {
posW[i] = "good" + i;
}
int nc = 1 + rnd.nextInt(3);
String[] negW = new String[nc];
for (int i = 0; i < nc; i++) {
negW[i] = "bad" + i;
}
int n = 1 + rnd.nextInt(20);
String[] rep = new String[n];
int[] ids = new int[n];
Set<Integer> seen = new HashSet<>();
for (int i = 0; i < n; i++) {
int cnt = 1 + rnd.nextInt(4);
StringBuilder sb = new StringBuilder();
for (int j = 0; j < cnt; j++) {
if (j > 0) {
sb.append(' ');
}
int pick = rnd.nextInt(3);
if (pick == 0) {
sb.append(posW[rnd.nextInt(wc)]);
} else if (pick == 1) {
sb.append(negW[rnd.nextInt(nc)]);
} else {
sb.append("plain" + rnd.nextInt(10));
}
}
rep[i] = sb.toString();
int id;
do {
id = 1 + rnd.nextInt(1000);
} while (!seen.add(id));
ids[i] = id;
}
int k = 1 + rnd.nextInt(n);
List<Integer> a = sol.topStudents(posW, negW, rep, ids, k);
List<Integer> b = sol.topStudentsHeap(posW, negW, rep, ids, k);
if (!a.equals(b)) {
System.out.println("对拍失败 pos=" + Arrays.toString(posW) + " neg=" + Arrays.toString(negW)
+ " k=" + k + " a=" + a + " b=" + b);
System.exit(1);
}
}
System.out.println("2512 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(Σ len(report) + n log n)
- 空间复杂度:O(n)
2513. Minimize the Maximum of Two Arrays(最小化两个数组中的最大值)
题目描述
给你两个数组 arr1 和 arr2,它们一开始都是空的。你需要往它们中添加正整数,使它们满足:
arr1包含uniqueCnt1个互不相同的正整数,每个整数都不能被divisor1整除;arr2包含uniqueCnt2个互不相同的正整数,每个整数都不能被divisor2整除;arr1和arr2中的元素互不相同。
给定divisor1、divisor2、uniqueCnt1和uniqueCnt2,返回两个数组中最大元素的最小值。
示例 1
`divisor1 = 2, divisor2 = 7, uniqueCnt1 = 1, uniqueCnt2 = 3` → 4(arr1=[1], arr2=[2,3,4])
示例 2
`divisor1 = 3, divisor2 = 5, uniqueCnt1 = 2, uniqueCnt2 = 1` → 3
示例 3
`divisor1 = 2, divisor2 = 4, uniqueCnt1 = 8, uniqueCnt2 = 2` → 15
提示:
2 <= divisor1, divisor2 <= 10^51 <= uniqueCnt1, uniqueCnt2 < 10^92 <= uniqueCnt1 + uniqueCnt2 <= 10^9
解题思路
解法一:二分答案 + 容斥计数(主提交版本)
- 对候选上界 X = mid,统计 1, X 内:
a = X - X / divisor1:不能被 divisor1 整除的个数(arr1 可用),
b = X - X / divisor2:不能被 divisor2 整除的个数(arr2 可用),
both = X - X/divisor1 - X/divisor2 + X/lcm(divisor1, divisor2):同时不被两者整除的个数。 - 可行性条件:a >= uniqueCnt1 且 b >= uniqueCnt2 且 a + b - both >= uniqueCnt1 + uniqueCnt2
(最后一个条件保证排除「只能放入一个数组」的重复计数后总数够用)。 - 二分下界 max(uniqueCnt1, uniqueCnt2),上界取 2 * (uniqueCnt1 + uniqueCnt2)(容斥安全界)。
- 防溢出:X、乘法用 long;lcm 用 (long) divisor1 / gcd * divisor2 再比较。
解法二:直接构造验证(对照,仅小数据) - 从小到大枚举 1,2,3,...,模拟分配:x 不能被 d1 整除则优先给 arr1(若还缺),否则再看能否给 arr2;
直到两个数组都填满,记录用到的最大值。适用于小数据对拍。
复杂度: - 时间复杂度:O(log(uniqueCnt1+uniqueCnt2))
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:二分答案 + 容斥计数(主提交版本)
public int minimizeSet(int divisor1, int divisor2, int uniqueCnt1, int uniqueCnt2) {
long lo = 1, hi = 4000000000L; // 上限足够大
long lcm = lcm(divisor1, divisor2);
while (lo < hi) {
long mid = (lo + hi) >>> 1;
// 不被 d1 整除
long a = mid - mid / divisor1;
// 不被 d2 整除
long b = mid - mid / divisor2;
// 同时不被两者整除
long both = mid - mid / divisor1 - mid / divisor2 + mid / lcm;
if (a >= uniqueCnt1 && b >= uniqueCnt2 && a + b - both >= (long) uniqueCnt1 + uniqueCnt2) {
hi = mid;
} else {
lo = mid + 1;
}
}
return (int) lo;
}
private long lcm(int a, int b) {
return (long) a / gcd(a, b) * b;
}
private int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a;
}
// 解法二:线性枚举 + 分类分配模拟(对照,不用二分公式)
public int minimizeSetBrute(int divisor1, int divisor2, int uniqueCnt1, int uniqueCnt2) {
long x = 1;
while (true) {
// 统计 [1, x] 内各类数
int both = 0; // 同时不被 d1、d2 整除
int only1 = 0; // 不被 d1 整除但被 d2 整除
int only2 = 0; // 被 d1 整除但不被 d2 整除
for (long v = 1; v <= x; v++) {
boolean n1 = (v % divisor1 != 0);
boolean n2 = (v % divisor2 != 0);
if (n1 && n2) {
both++;
} else if (n1) {
only1++;
} else if (n2) {
only2++;
}
}
// 先用"仅可给 arr1"的数填 arr1,再用公共池补;arr2 同理
int use1 = Math.min(uniqueCnt1, only1);
int use2 = Math.min(uniqueCnt2, only2);
int rem = (uniqueCnt1 - use1) + (uniqueCnt2 - use2);
if (rem <= both) {
return (int) x;
}
x++;
}
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.minimizeSet(2, 7, 1, 3) == 4 : "2513 例1 主";
assert sol.minimizeSetBrute(2, 7, 1, 3) == 4 : "2513 例1 对照";
assert sol.minimizeSet(3, 5, 2, 1) == 3 : "2513 例2 主";
assert sol.minimizeSetBrute(3, 5, 2, 1) == 3 : "2513 例2 对照";
assert sol.minimizeSet(2, 4, 8, 2) == 15 : "2513 例3 主";
assert sol.minimizeSetBrute(2, 4, 8, 2) == 15 : "2513 例3 对照";
assert sol.minimizeSet(2, 2, 2, 2) == 3 : "2513 附加 同除数";
assert sol.minimizeSet(1, 5, 0, 0) == 0 : "2513 附加 零需求";
// 随机对拍(小数据)
java.util.Random rnd = new java.util.Random(1213);
for (int t = 0; t < 500; t++) {
int d1 = 2 + rnd.nextInt(9);
int d2 = 2 + rnd.nextInt(9);
int c1 = 1 + rnd.nextInt(12);
int c2 = 1 + rnd.nextInt(12);
int a = sol.minimizeSet(d1, d2, c1, c2);
int b = sol.minimizeSetBrute(d1, d2, c1, c2);
if (a != b) {
System.out.println("对拍失败 d1=" + d1 + " d2=" + d2 + " c1=" + c1 + " c2=" + c2
+ " 主=" + a + " 对照=" + b);
System.exit(1);
}
}
System.out.println("2513 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(log(uniqueCnt1+uniqueCnt2))
- 空间复杂度:O(1)
2514. Count Anagrams(统计同位异构字符串数目)
题目描述
给你一个字符串 s,它包含一个或者多个单词,单词之间用单个空格 ' ' 隔开。
如果字符串 t 中第 i 个单词是 s 中第 i 个单词的一个排列,那么我们称字符串 t 是字符串 s 的
同位异构字符串。例如,"acb dfe" 是 "abc def" 的同位异构字符串,但是 "def cab" 不是。
请你返回 s 的同位异构字符串的数目,由于答案可能很大,请将它对 10^9 + 7 取余后返回。
示例 1
`s = "too hot"` → 18
示例 2
`s = "aa"` → 1
提示:
1 <= s.length <= 10^5s只包含小写英文字母和空格' ',相邻单词之间由单个空格隔开
解题思路
解法一:排列数公式 + 逆元(主提交版本)
- 每个单词的排列数 = len! / (Π cntc!),所有单词的排列数相乘即答案。
- 预处理阶乘 fac 与阶乘逆元 invFac(费马小定理:inv(faci) = pow(faci, MOD-2)),
一个单词贡献 = faclen * Π invFaccnt\[c](对每个出现过的字母)。 - 复杂度 O(n + Σ单词长度)。
解法二:逐个单词用杨辉三角组合数累积(对照) - 对每个单词,按「逐步摆放」用组合数 C(剩余位置, 该字母个数) 累积有多少种排列;
即 count = count * C(remaining, cnt) 逐字母乘。 - 杨辉三角预处理组合数表到 maxLen,再相乘。避免逆元思路的不同实现。
复杂度: - 时间复杂度:O(n + Σ len(单词))
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:阶乘 + 逆元(主提交版本)
public int countAnagrams(String s) {
int n = s.length();
long[] fac = new long[n + 1];
long[] invFac = new long[n + 1];
fac[0] = 1;
for (int i = 1; i <= n; i++) {
fac[i] = fac[i - 1] * i % MOD;
}
invFac[n] = pow(fac[n], MOD - 2);
for (int i = n; i >= 1; i--) {
invFac[i - 1] = invFac[i] * i % MOD;
}
long ans = 1;
for (String w : s.split(" ")) {
int len = w.length();
int[] cnt = new int[26];
for (int i = 0; i < len; i++) {
cnt[w.charAt(i) - 'a']++;
}
long ways = fac[len];
for (int c : cnt) {
ways = ways * invFac[c] % MOD;
}
ans = ans * ways % MOD;
}
return (int) ans;
}
private long pow(long a, long b) {
long res = 1;
while (b > 0) {
if ((b & 1) == 1) {
res = res * a % MOD;
}
a = a * a % MOD;
b >>= 1;
}
return res;
}
// 解法二:组合数逐步摆放(对照)
public int countAnagramsComb(String s) {
int n = s.length();
long[][] C = new long[n + 1][n + 1];
for (int i = 0; i <= n; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
}
}
long ans = 1;
for (String w : s.split(" ")) {
int len = w.length();
int[] cnt = new int[26];
for (int i = 0; i < len; i++) {
cnt[w.charAt(i) - 'a']++;
}
long ways = 1;
int remaining = len;
for (int c : cnt) {
if (c > 0) {
ways = ways * C[remaining][c] % MOD;
remaining -= c;
}
}
ans = ans * ways % MOD;
}
return (int) ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.countAnagrams("too hot") == 18 : "2514 例1 主";
assert sol.countAnagramsComb("too hot") == 18 : "2514 例1 对照";
assert sol.countAnagrams("aa") == 1 : "2514 例2 主";
assert sol.countAnagramsComb("aa") == 1 : "2514 例2 对照";
assert sol.countAnagrams("abc def") == 36 : "2514 附加 全异";
assert sol.countAnagrams("b ok") == 2 : "2514 附加 单字母";
// 随机对拍
java.util.Random rnd = new java.util.Random(1314);
for (int t = 0; t < 300; t++) {
int words = 1 + rnd.nextInt(4);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < words; i++) {
if (i > 0) {
sb.append(' ');
}
int len = 1 + rnd.nextInt(8);
for (int j = 0; j < len; j++) {
sb.append((char) ('a' + rnd.nextInt(5)));
}
}
String str = sb.toString();
int a = sol.countAnagrams(str);
int b = sol.countAnagramsComb(str);
if (a != b) {
System.out.println("对拍失败 " + str + " 主=" + a + " 对照=" + b);
System.exit(1);
}
}
System.out.println("2514 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n + Σ len(单词))
- 空间复杂度:O(n)
2515. Shortest Distance to Target String in a Circular Array
题目描述
(到目标字符串的最短距离)
给你一个下标从 0 开始的环形字符串数组 words 和一个字符串 target。环形数组意味着数组首尾相连。
形式上,words[i] 的下一个元素是 words(`i` + 1) % `n`,而 words[i] 的前一个元素是 words(`i` - 1 + `n`) % `n`,
其中 n 是 words 的长度。从 startIndex 开始,你一次可以用 1 步移动到下一个或者前一个单词。
返回到达目标字符串 target 所需的最短距离。如果 words 中不存在字符串 target,返回 -1。
示例 1
`words = ["hello","i","am","leetcode","hello"], target = "hello", startIndex = 1` → 1
示例 2
`words = ["a","b","leetcode"], target = "leetcode", startIndex = 0` → 1
示例 3
`words = ["i","eat","leetcode"], target = "ate", startIndex = 0` → -1
提示:
1 <= words.length <= 1001 <= words[i].length <= 1000 <= startIndex < words.length
解题思路
解法一:枚举所有出现位置取最短环形距离(主提交版本)
- 遍历数组,找到所有与 target 相等的下标 j,环形距离 = min(|j - i|, n - |j - i|),取最小值。
- 复杂度 O(n)。
解法二:双向扩散 BFS(对照) - 从 startIndex 向左右同时扩展,步数递增,首次碰到 target 即返回步数。
- 复杂度 O(n),可扩展到更大数据。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
import java.util.HashSet;
import java.util.Set;
public class Solution {
// 解法一:枚举位置 + 环形距离公式(主提交版本)
public int closetTarget(String[] words, String target, int startIndex) {
int n = words.length;
int ans = Integer.MAX_VALUE;
boolean found = false;
for (int j = 0; j < n; j++) {
if (words[j].equals(target)) {
found = true;
int d = Math.abs(j - startIndex);
d = Math.min(d, n - d);
ans = Math.min(ans, d);
}
}
return found ? ans : -1;
}
// 解法二:双向扩散(对照)
public int closetTargetBfs(String[] words, String target, int startIndex) {
int n = words.length;
Set<Integer> seen = new HashSet<>();
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
q.offer(startIndex);
seen.add(startIndex);
int step = 0;
while (!q.isEmpty()) {
int sz = q.size();
for (int k = 0; k < sz; k++) {
int cur = q.poll();
if (words[cur].equals(target)) {
return step;
}
int nxt = (cur + 1) % n;
if (seen.add(nxt)) {
q.offer(nxt);
}
nxt = (cur - 1 + n) % n;
if (seen.add(nxt)) {
q.offer(nxt);
}
}
step++;
}
return -1;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.closetTarget(new String[]{"hello", "i", "am", "leetcode", "hello"}, "hello", 1) == 1 : "2515 例1 主";
assert sol.closetTargetBfs(new String[]{"hello", "i", "am", "leetcode", "hello"}, "hello", 1) == 1 : "2515 例1 对照";
assert sol.closetTarget(new String[]{"a", "b", "leetcode"}, "leetcode", 0) == 1 : "2515 例2 主";
assert sol.closetTargetBfs(new String[]{"a", "b", "leetcode"}, "leetcode", 0) == 1 : "2515 例2 对照";
assert sol.closetTarget(new String[]{"i", "eat", "leetcode"}, "ate", 0) == -1 : "2515 例3 主";
assert sol.closetTargetBfs(new String[]{"i", "eat", "leetcode"}, "ate", 0) == -1 : "2515 例3 对照";
// 随机对拍
java.util.Random rnd = new java.util.Random(1515);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(30);
String[] ws = new String[n];
for (int i = 0; i < n; i++) {
ws[i] = "w" + rnd.nextInt(10);
}
String target = "w" + rnd.nextInt(12);
int start = rnd.nextInt(n);
int a = sol.closetTarget(ws, target, start);
int b = sol.closetTargetBfs(ws, target, start);
if (a != b) {
System.out.println("对拍失败 " + java.util.Arrays.toString(ws) + " t=" + target + " s=" + start);
System.exit(1);
}
}
System.out.println("2515 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2516. Take K of Each Character From Left and Right(每种字符至少取 K 个)
题目描述
给你一个由字符 'a'、'b'、'c' 组成的字符串 s 和一个非负整数 k。每分钟,你可以选择取走 s
最左侧还是最右侧的那个字符。你必须取走每种字符至少 k 个,返回需要的最少分钟数;
如果无法取到,则返回 -1。
示例 1
`s = "aabaaaacaabc", k = 2` → 8(左侧 3 个 + 右侧 5 个)
示例 2
`s = "a", k = 1` → -1
提示:
1 <= s.length <= 10^5s仅由字母 'a'、'b'、'c' 组成0 <= k <= s.length
解题思路
解法一:反向滑动窗口------求保留下的最长合法中间段(主提交版本)
- 总次数取 min(左侧次数 + 右侧次数) = n - 保留在中间的字符数。
设每个字符在 s 中的总数为 totalc,若某字符总数 < k 直接返回 -1。 - 需要在中间保留一段连续子串,使「s 中没被取走的 a/b/c 数」各自不超过 totalc - k
(即从两侧取走的恰好足够),等价于中间段中每个字符的计数 ≤ totalc - k。 - 用双指针维护窗口,当窗口内某个字符计数 > totalc - k 时收缩左边界;窗口合法时
答案 = min(n - (r - l + 1))。 - 复杂度 O(n)。
解法二:枚举左侧取几个字符,二分右侧(对照) - 预处理前缀计数。枚举 left 长度 L ∈ 0, n,计算左侧已取各字符数;对右侧二分最小长度 R,
使左侧 + 右侧覆盖各自 k 个字符。取最小 L + R。
复杂度: - 时间复杂度:O(n)
- 空间复杂度】**: O(1)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:反向滑动窗口(主提交版本)
public int takeCharacters(String s, int k) {
int n = s.length();
int[] total = new int[3];
for (int i = 0; i < n; i++) {
total[s.charAt(i) - 'a']++;
}
for (int c = 0; c < 3; c++) {
if (total[c] < k) {
return -1;
}
}
// 中间窗口内各字符最多 total[c] - k
int[] limit = new int[3];
for (int c = 0; c < 3; c++) {
limit[c] = total[c] - k;
}
int[] cnt = new int[3];
int l = 0;
int best = 0; // 最长合法中间段长度
for (int r = 0; r < n; r++) {
cnt[s.charAt(r) - 'a']++;
while (cnt[s.charAt(r) - 'a'] > limit[s.charAt(r) - 'a']) {
cnt[s.charAt(l) - 'a']--;
l++;
}
best = Math.max(best, r - l + 1);
}
return n - best;
}
// 解法二:枚举左侧 + 二分右侧(对照)
public int takeCharactersEnum(String s, int k) {
int n = s.length();
int[] total = new int[3];
for (int i = 0; i < n; i++) {
total[s.charAt(i) - 'a']++;
}
for (int c = 0; c < 3; c++) {
if (total[c] < k) {
return -1;
}
}
int[][] pre = new int[n + 1][3];
for (int i = 0; i < n; i++) {
System.arraycopy(pre[i], 0, pre[i + 1], 0, 3);
pre[i + 1][s.charAt(i) - 'a']++;
}
int ans = n;
for (int L = 0; L <= n; L++) {
int[] need = new int[3];
boolean ok = true;
for (int c = 0; c < 3; c++) {
int got = pre[L][c];
need[c] = Math.max(0, k - got);
}
// 二分右侧最小长度 R
int lo = 0, hi = n - L, res = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
boolean enough = true;
for (int c = 0; c < 3; c++) {
int rightGot = pre[n][c] - pre[n - mid][c];
if (rightGot < need[c]) {
enough = false;
break;
}
}
if (enough) {
res = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
if (res >= 0) {
ans = Math.min(ans, L + res);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.takeCharacters("aabaaaacaabc", 2) == 8 : "2516 例1 主";
assert sol.takeCharactersEnum("aabaaaacaabc", 2) == 8 : "2516 例1 对照";
assert sol.takeCharacters("a", 1) == -1 : "2516 例2 主";
assert sol.takeCharactersEnum("a", 1) == -1 : "2516 例2 对照";
assert sol.takeCharacters("abc", 1) == 3 : "2516 附加 全取";
assert sol.takeCharacters("abc", 0) == 0 : "2516 附加 k=0";
assert sol.takeCharacters("aabbcc", 2) == 6 : "2516 附加 全部";
// 随机对拍(小数据暴力枚举所有取法)
java.util.Random rnd = new java.util.Random(1616);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(14);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append((char) ('a' + rnd.nextInt(3)));
}
String str = sb.toString();
int k = rnd.nextInt(4);
int a = sol.takeCharacters(str, k);
int b = sol.takeCharactersEnum(str, k);
// 暴力求最小分钟
int brute = Integer.MAX_VALUE;
for (int left = 0; left <= n; left++) {
for (int right = 0; right <= n - left; right++) {
int[] cnt = new int[3];
for (int i = 0; i < left; i++) {
cnt[str.charAt(i) - 'a']++;
}
for (int i = 0; i < right; i++) {
cnt[str.charAt(n - 1 - i) - 'a']++;
}
if (cnt[0] >= k && cnt[1] >= k && cnt[2] >= k) {
brute = Math.min(brute, left + right);
}
}
}
if (brute == Integer.MAX_VALUE) {
brute = -1;
}
if (a != b || a != brute) {
System.out.println("对拍失败 s=" + str + " k=" + k + " 主=" + a + " 枚举=" + b + " 暴力=" + brute);
System.exit(1);
}
}
System.out.println("2516 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度】**: O(1)
2517. Maximum Tastiness of Candy Basket(礼盒的最大甜蜜度)
题目描述
给你一个正整数数组 price,其中 price[i] 表示第 i 类糖果的价格,另给你一个正整数 k。
商店组合 k 类不同糖果打包成礼盒出售。礼盒的甜蜜度是礼盒中任意两种糖果价格绝对差的最小值。
返回礼盒的最大甜蜜度。
示例 1
`price = [13,5,1,8,21,2], k = 3` → 8(选 [13,5,21],mindiff = 8)
示例 2
`price = [1,3,1], k = 2` → 2
示例 3
`price = [7,7,7,7], k = 2` → 0
提示:
2 <= k <= price.length <= 10^51 <= price[i] <= 10^9
解题思路
解法一:排序 + 二分答案 + 贪心检查(主提交版本)
- 将价格升序排序。若目标甜蜜度为 d,则贪心:从最小价格开始,取当前已选价格 + d 之后的第一个价格,
累计能选出的数量 cnt;cnt >= k 说明 d 可行。二分 d 的最大可行值。 - 检查 O(n),二分 log(maxPrice),总 O(n log maxPrice)。
解法二:直接构造验证(对照,小数据) - 枚举所有 k 组合的甜蜜度取最大(仅限小数据对拍)。
复杂度: - 时间复杂度:O(n log maxPrice)
- 空间复杂度:O(log n)(排序)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:排序 + 二分答案(主提交版本)
public int maximumTastiness(int[] price, int k) {
Arrays.sort(price);
int n = price.length;
int lo = 0, hi = price[n - 1] - price[0];
int ans = 0;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
if (can(price, k, mid)) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
private boolean can(int[] price, int k, int d) {
int cnt = 1;
int last = price[0];
for (int i = 1; i < price.length; i++) {
if (price[i] - last >= d) {
cnt++;
last = price[i];
if (cnt >= k) {
return true;
}
}
}
return cnt >= k;
}
// 解法二:暴力枚举组合(对照,小数据)
public int maximumTastinessBrute(int[] price, int k) {
int n = price.length;
int[] best = {0};
boolean[] used = new boolean[n];
dfs(price, k, 0, new int[k], used, 0, best);
return best[0];
}
private void dfs(int[] price, int k, int start, int[] sel, boolean[] used, int idx, int[] best) {
if (idx == k) {
int minD = Integer.MAX_VALUE;
for (int i = 0; i < k; i++) {
for (int j = i + 1; j < k; j++) {
minD = Math.min(minD, Math.abs(sel[i] - sel[j]));
}
}
best[0] = Math.max(best[0], minD);
return;
}
for (int i = start; i < price.length; i++) {
if (!used[i]) {
used[i] = true;
sel[idx] = price[i];
dfs(price, k, i + 1, sel, used, idx + 1, best);
used[i] = false;
}
}
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.maximumTastiness(new int[]{13, 5, 1, 8, 21, 2}, 3) == 8 : "2517 例1 主";
assert sol.maximumTastinessBrute(new int[]{13, 5, 1, 8, 21, 2}, 3) == 8 : "2517 例1 对照";
assert sol.maximumTastiness(new int[]{1, 3, 1}, 2) == 2 : "2517 例2 主";
assert sol.maximumTastinessBrute(new int[]{1, 3, 1}, 2) == 2 : "2517 例2 对照";
assert sol.maximumTastiness(new int[]{7, 7, 7, 7}, 2) == 0 : "2517 例3 主";
assert sol.maximumTastinessBrute(new int[]{7, 7, 7, 7}, 2) == 0 : "2517 例3 对照";
// 随机对拍(小数据)
java.util.Random rnd = new java.util.Random(1717);
for (int t = 0; t < 300; t++) {
int n = 2 + rnd.nextInt(8);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(30);
}
int k = 2 + rnd.nextInt(n - 1);
int a = sol.maximumTastiness(arr, k);
int b = sol.maximumTastinessBrute(arr, k);
if (a != b) {
System.out.println("对拍失败 " + Arrays.toString(arr) + " k=" + k + " 主=" + a + " 对照=" + b);
System.exit(1);
}
}
System.out.println("2517 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n log maxPrice)
- 空间复杂度:O(log n)(排序)
2518. Number of Great Partitions(好分区的数目)
题目描述
给你一个正整数数组 nums 和一个整数 k。
分区的定义是:将数组划分成两个有序的组,并满足每个元素恰好存在于某一个组中。
如果分区中每个组的元素和都大于等于 k,则认为该分区是一个好分区。
返回不同的好分区的数目,由于答案可能很大,请返回对 10^9 + 7 取余后的结果。
如果在两个分区中,存在某个元素 nums[i] 被分在不同的组中,则认为这两个分区不同。
示例 1
`nums = [1,2,3,4], k = 4` → 6
示例 2
`nums = [3,3,3], k = 4` → 0
示例 3
`nums = [6,6], k = 2` → 2
提示:
1 <= nums.length, k <= 10001 <= nums[i] <= 10^9
解题思路
解法一:总方案数 - 坏分区(主提交版本)
- 两个组视为有序,每个元素二选一,总方案数 = 2^n。
- 令 total = Σnums。若 total < 2k,则不可能有两组和同时 >= k,直接返回 0。
- 当 total >= 2k 时,「A 组和 < k」与「B 组和 < k」互斥(同时成立会推出 total < 2k)。
因此坏分区数 = #(A 组和 < k) + #(B 组和 < k) = 2 × #(A 组和 < k)(对称性:B/A 对调是双射)。 - 用 01 背包统计子集和 s ∈ 0, k-1 的方案数 dps(容量压缩到 k,v >= k 的元素放入子集后
和必 >= k,不参与统计,直接跳过),bad = Σ dp0...k-1。 - 答案 = (2^n - 2 × bad) mod MOD。
- 复杂度 O(n × k)。
解法二:掩码枚举所有分区(对照) - 对每个 mask 视为 A 组,校验 sumA >= k 且 total - sumA >= k,计数取模。
- 仅适合 n 较小的对拍。
复杂度: - 时间复杂度:O(n * k)
- 空间复杂度:O(k)
代码实现
java
import java.util.Random;
public class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:总方案 - 坏分区(主提交版本)
public int countPartitions(int[] nums, int k) {
long total = 0;
for (int v : nums) {
total += v;
}
if (total < 2L * k) {
return 0;
}
int n = nums.length;
long[] dp = new long[k]; // dp[s] = 子集和恰好为 s (< k) 的方案数
dp[0] = 1;
for (int v : nums) {
if (v >= k) {
continue; // 选入子集后和必 >= k,不进坏分区统计
}
for (int s = k - 1; s >= v; s--) {
dp[s] = (dp[s] + dp[s - v]) % MOD;
}
}
long bad = 0;
for (int s = 0; s < k; s++) {
bad = (bad + dp[s]) % MOD;
}
long totalWays = powMod(2, n);
long ans = ((totalWays - 2 * bad) % MOD + MOD) % MOD;
return (int) ans;
}
private long powMod(long a, long b) {
long res = 1;
while (b > 0) {
if ((b & 1) == 1) {
res = res * a % MOD;
}
a = a * a % MOD;
b >>= 1;
}
return res;
}
// 解法二:掩码枚举(对照,小数据)
public int countPartitionsBrute(int[] nums, int k) {
int n = nums.length;
long total = 0;
for (int v : nums) {
total += v;
}
int ans = 0;
for (int mask = 0; mask < (1 << n); mask++) {
long sumA = 0;
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) == 1) {
sumA += nums[i];
}
}
if (sumA >= k && total - sumA >= k) {
ans = (ans + 1) % MOD;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.countPartitions(new int[]{1, 2, 3, 4}, 4) == 6 : "2518 例1 主";
assert sol.countPartitionsBrute(new int[]{1, 2, 3, 4}, 4) == 6 : "2518 例1 对照";
assert sol.countPartitions(new int[]{3, 3, 3}, 4) == 0 : "2518 例2 主";
assert sol.countPartitionsBrute(new int[]{3, 3, 3}, 4) == 0 : "2518 例2 对照";
assert sol.countPartitions(new int[]{6, 6}, 2) == 2 : "2518 例3 主";
assert sol.countPartitionsBrute(new int[]{6, 6}, 2) == 2 : "2518 例3 对照";
assert sol.countPartitions(new int[]{1, 1, 1, 1}, 2) == 6 : "2518 附加 小和";
assert sol.countPartitions(new int[]{10, 10}, 11) == 0 : "2518 附加 和不足";
// 随机对拍
Random rnd = new Random(1818);
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(10);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(12);
}
int k = 1 + rnd.nextInt(15);
int a = sol.countPartitions(arr, k);
int b = sol.countPartitionsBrute(arr, k);
if (a != b) {
System.out.println("对拍失败 " + java.util.Arrays.toString(arr) + " k=" + k + " 主=" + a + " 对照=" + b);
System.exit(1);
}
}
System.out.println("2518 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n * k)
- 空间复杂度:O(k)
2519. Count the Number of K-Big Indices(统计 K-Big 索引的数量)
题目描述
给定一个下标从 0 开始的整数数组 nums 和一个正整数 k。
如果满足以下条件,我们称下标 i 为 k-big:
- 存在至少
k个不同的索引idx1,满足idx1<i且nums[idx1]<nums[i]; - 存在至少
k个不同的索引idx2,满足idx2>i且nums[idx2]<nums[i]。
返回k-big索引的数量。
示例 1
`nums = [2,3,6,5,2,3], k = 2` → 2(下标 2 和 3)
示例 2
`nums = [1,1,1], k = 3` → 0
提示:
1 <= nums.length <= 10^51 <= nums[i], k <= nums.length
解题思路
解法一:离散化 + 前缀/后缀树状数组(主提交版本)
- 对 nums 值域离散化(1...m)。左右各一棵树状数组:
先从左到右扫一遍,把每个位置 i 左侧「严格小于 numsi」的个数记入 lessLefti;
再从右到左扫一遍记录右侧严格小于的个数 lessRighti。 - 树状数组维护「各值出现次数」,查询 < v 的个数 = query(idx(v)-1)。
- 最终 lessLefti >= k && lessRighti >= k 的 i 计数。
- 复杂度 O(n log n)。
解法二:归并排序分治(对照) - 用归并排序的同时统计逆序信息(小于 numsi 的左侧个数),同样可得左右严格小于计数。
这里改用树状数组的一种变体:从右往左扫描建「后缀树状数组」镜像实现对称,逻辑同主解法但
独立实现一遍以交叉验证。
复杂度: - 时间复杂度:O(n log n)
- 空间复杂度:O(n)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:左右两次扫描 + 树状数组(主提交版本)
public int kBigIndices(int[] nums, int k) {
int n = nums.length;
// 离散化
int[] sorted = nums.clone();
Arrays.sort(sorted);
int[] idx = new int[n];
for (int i = 0; i < n; i++) {
idx[i] = Arrays.binarySearch(sorted, nums[i]) + 1;
}
int m = n;
Fenwick left = new Fenwick(m);
int[] lessL = new int[n];
for (int i = 0; i < n; i++) {
lessL[i] = left.query(idx[i] - 1); // 左侧严格小于
left.add(idx[i], 1);
}
Fenwick right = new Fenwick(m);
int[] lessR = new int[n];
for (int i = n - 1; i >= 0; i--) {
lessR[i] = right.query(idx[i] - 1); // 右侧严格小于
right.add(idx[i], 1);
}
int ans = 0;
for (int i = 0; i < n; i++) {
if (lessL[i] >= k && lessR[i] >= k) {
ans++;
}
}
return ans;
}
static class Fenwick {
int[] tree;
int n;
Fenwick(int n) {
this.n = n;
this.tree = new int[n + 1];
}
void add(int i, int v) {
while (i <= n) {
tree[i] += v;
i += i & -i;
}
}
int query(int i) {
int s = 0;
while (i > 0) {
s += tree[i];
i -= i & -i;
}
return s;
}
}
// 解法二:暴力统计左右两侧严格小于个数(对照,O(n^2) 仅对拍用)
public int kBigIndicesBrute(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; i++) {
int cntL = 0, cntR = 0;
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
cntL++;
}
}
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[i]) {
cntR++;
}
}
if (cntL >= k && cntR >= k) {
ans++;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.kBigIndices(new int[]{2, 3, 6, 5, 2, 3}, 2) == 2 : "2519 例1 主";
assert sol.kBigIndicesBrute(new int[]{2, 3, 6, 5, 2, 3}, 2) == 2 : "2519 例1 对照";
assert sol.kBigIndices(new int[]{1, 1, 1}, 3) == 0 : "2519 例2 主";
assert sol.kBigIndicesBrute(new int[]{1, 1, 1}, 3) == 0 : "2519 例2 对照";
assert sol.kBigIndices(new int[]{1, 2, 3, 4}, 1) == 2 : "2519 附加 递增";
assert sol.kBigIndices(new int[]{4, 3, 2, 1}, 1) == 2 : "2519 附加 递减";
// 随机对拍
java.util.Random rnd = new java.util.Random(1919);
for (int t = 0; t < 2000; t++) {
int n = 1 + rnd.nextInt(60);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(n);
}
int k = 1 + rnd.nextInt(n);
int a = sol.kBigIndices(arr, k);
int b = sol.kBigIndicesBrute(arr, k);
if (a != b) {
System.out.println("对拍失败 " + Arrays.toString(arr) + " k=" + k + " 主=" + a + " 对照=" + b);
System.exit(1);
}
}
System.out.println("2519 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n log n)
- 空间复杂度:O(n)
2520. Count the Digits That Divide a Number(统计能整除数字的数位)
题目描述
给你一个整数 num,返回 num 中能整除 num 的数位的数目。
如果满足 num % val == 0,则认为整数 val 可以整除 num。
示例 1
`num = 7` → 1
示例 2
`num = 121` → 2(1 出现两次)
示例 3
`num = 1248` → 4
提示:
1 <= num <= 10^9num的数位中不含 0
解题思路
解法一:逐位取出取模计数(主提交版本)
- 用 x = num 不断 x % 10 取出个位 digit,若 num % digit == 0 计数 +1,然后 x /= 10。
- 复杂度 O(digits)。
解法二:字符串遍历数位(对照) - 将 num 转为 String,逐字符转数字判断整除。
复杂度: - 时间复杂度:O(log num)
- 空间复杂度:O(1)
代码实现
java
import java.util.Random;
public class Solution {
// 解法一:逐位取模(主提交版本)
public int countDigits(int num) {
int x = num;
int ans = 0;
while (x > 0) {
int d = x % 10;
if (num % d == 0) {
ans++;
}
x /= 10;
}
return ans;
}
// 解法二:字符串遍历(对照)
public int countDigitsStr(int num) {
String s = String.valueOf(num);
int ans = 0;
for (int i = 0; i < s.length(); i++) {
int d = s.charAt(i) - '0';
if (num % d == 0) {
ans++;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.countDigits(7) == 1 : "2520 例1 主";
assert sol.countDigitsStr(7) == 1 : "2520 例1 对照";
assert sol.countDigits(121) == 2 : "2520 例2 主";
assert sol.countDigitsStr(121) == 2 : "2520 例2 对照";
assert sol.countDigits(1248) == 4 : "2520 例3 主";
assert sol.countDigitsStr(1248) == 4 : "2520 例3 对照";
// 随机对拍(数位不含 0)
Random rnd = new Random(2020);
for (int t = 0; t < 2000; t++) {
int digits = 1 + rnd.nextInt(9);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < digits; i++) {
sb.append(1 + rnd.nextInt(9));
}
int v = Integer.parseInt(sb.toString());
if (sol.countDigits(v) != sol.countDigitsStr(v)) {
System.out.println("对拍失败 " + v);
System.exit(1);
}
}
System.out.println("2520 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(log num)
- 空间复杂度:O(1)