LeetCode 2481-2500. 题解合集
文章目录
- [LeetCode 2481-2500. 题解合集](#LeetCode 2481-2500. 题解合集)
-
- [2481. Minimum Cuts to Divide a Circle(分割圆的最少切割次数)](#2481. Minimum Cuts to Divide a Circle(分割圆的最少切割次数))
- [2482. Difference Between Ones and Zeros in Row and Column(行和列中一和零的差值)](#2482. Difference Between Ones and Zeros in Row and Column(行和列中一和零的差值))
- [2483. Minimum Penalty for a Shop(商店的最少代价)](#2483. Minimum Penalty for a Shop(商店的最少代价))
- [2484. Count Palindromic Subsequences(统计回文子序列数目)](#2484. Count Palindromic Subsequences(统计回文子序列数目))
- [2485. Find the Pivot Integer(找出中枢整数)](#2485. Find the Pivot Integer(找出中枢整数))
- [2486. Append Characters to String to Make Subsequence(追加字符以获得子序列)](#2486. Append Characters to String to Make Subsequence(追加字符以获得子序列))
- [2487. Remove Nodes From Linked List(从链表中移除节点)](#2487. Remove Nodes From Linked List(从链表中移除节点))
- [2488. Count Subarrays With Median K(统计中位数为 K 的子数组)](#2488. Count Subarrays With Median K(统计中位数为 K 的子数组))
- [2489. Number of Substrings With Fixed Ratio(固定比率的子字符串数)](#2489. Number of Substrings With Fixed Ratio(固定比率的子字符串数))
- [2490. Circular Sentence(回环句)](#2490. Circular Sentence(回环句))
- [2491. Divide Players Into Teams of Equal Skill(划分技能点相等的团队)](#2491. Divide Players Into Teams of Equal Skill(划分技能点相等的团队))
- [2492. Minimum Score of a Path Between Two Cities(两个城市间路径的最小分数)](#2492. Minimum Score of a Path Between Two Cities(两个城市间路径的最小分数))
- [2493. Divide Nodes Into the Maximum Number of Groups(将节点分成尽可能多的组)](#2493. Divide Nodes Into the Maximum Number of Groups(将节点分成尽可能多的组))
- [2494. Merge Overlapping Events in the Same Hall(合并在同一个大厅重叠的活动)](#2494. Merge Overlapping Events in the Same Hall(合并在同一个大厅重叠的活动))
- [2495. Number of Subarrays Having Even Product(乘积为偶数的子数组数)](#2495. Number of Subarrays Having Even Product(乘积为偶数的子数组数))
- [2496. Maximum Value of a String in an Array(数组中字符串的最大值)](#2496. Maximum Value of a String in an Array(数组中字符串的最大值))
- [2497. Maximum Star Sum of a Graph(图中最大星和)](#2497. Maximum Star Sum of a Graph(图中最大星和))
- [2498. Frog Jump II(青蛙过河 II)](#2498. Frog Jump II(青蛙过河 II))
- [2499. Minimum Total Cost to Make Arrays Unequal(让数组不相等的最小总代价)](#2499. Minimum Total Cost to Make Arrays Unequal(让数组不相等的最小总代价))
- [2500. Delete Greatest Value in Each Row(删除每行中的最大值)](#2500. Delete Greatest Value in Each Row(删除每行中的最大值))
2481. Minimum Cuts to Divide a Circle(分割圆的最少切割次数)
题目描述
圆内一个有效切割,符合以下二者之一:
- 该切割是两个端点在圆上的线段,且该线段经过圆心。
- 该切割是一端在圆心另一端在圆上的线段。
给你一个整数n,请你返回将圆切割成相等的n等分的最少切割次数。
示例 1
`n = 4` → 2(两次直径切割可得四等分)
示例 2
`n = 3` → 3(需要三次半径切割)
示例 3
`n = 1` → 0(无需切割)
提示:
1 <= n <= 100
解题思路
解法一:奇偶讨论(主提交版本)
- 直径切割一刀同时产生两条分界线,半径切割一刀只产生一条分界线。
- 若 n 为偶数:用 n/2 条直径即可分出 2*(n/2) = n 块 → 答案 n/2。
- 若 n 为奇数:无法只用直径(直径总是产生偶数块),需 n 条半径 → 答案 n。
- n = 1 时不需要任何刀 → 0。
解法二:统一公式(对照) n == 1 ? 0 : (n % 2 == 0 ? n / 2 : n),与解法一逻辑等价。
复杂度:- 时间复杂度:O(1)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:奇偶讨论(主提交版本)
public int numberOfCuts(int n) {
if (n == 1) {
return 0;
}
if (n % 2 == 1) {
return n;
}
return n / 2;
}
// 解法二:统一公式(对照)
public int numberOfCutsFormula(int n) {
return n == 1 ? 0 : (n % 2 == 0 ? n / 2 : n);
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.numberOfCuts(4) == 2 : "2481 例1 主";
assert sol.numberOfCutsFormula(4) == 2 : "2481 例1 公式";
assert sol.numberOfCuts(3) == 3 : "2481 例2 主";
assert sol.numberOfCutsFormula(3) == 3 : "2481 例2 公式";
assert sol.numberOfCuts(1) == 0 : "2481 例3 主";
assert sol.numberOfCutsFormula(1) == 0 : "2481 例3 公式";
for (int n = 1; n <= 100; n++) {
int a = sol.numberOfCuts(n);
int b = sol.numberOfCutsFormula(n);
assert a == b : "2481 不一致 n=" + n;
}
System.out.println("2481 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(1)
- 空间复杂度:O(1)
2482. Difference Between Ones and Zeros in Row and Column(行和列中一和零的差值)
题目描述
给你一个下标从 0 开始的 m x n 二进制矩阵 grid。
定义 diff[i][j] = onesRowi + onesColj - zerosRowi - zerosColj,
其中 onesRowi 为第 i 行 1 的个数,onesColj 为第 j 列 1 的个数,
zerosRowi 为第 i 行 0 的个数,zerosColj 为第 j 列 0 的个数。
返回差值矩阵 diff。
示例 1
`grid = [[0,1,1],[1,0,1],[0,0,1]]` → `[[0,0,4],[0,0,4],[-2,-2,2]]`
示例 2
`grid = [[1,1,1],[1,1,1]]` → `[[5,5,5],[5,5,5]]`
提示:
m == grid.length,n == grid[i].length1 <= m, n <= 10^5,1 <= m * n <= 10^5grid[i][j]要么是 0,要么是 1
解题思路
解法一:按行/列统计 1 的个数(主提交版本)
- 统计每行 1 的个数
rowOnes[i]、每列 1 的个数colOnes[j]。 - 由于一行的 0 数 = n - rowOnesi,一列的 0 数 = m - colOnesj,代入公式:
diff[i][j] = rowOnes[i] + colOnes[j] - (n - rowOnes[i]) - (m - colOnes[j]) = 2 * (rowOnes[i] + colOnes[j]) - m - n
解法二:直接按定义累加(对照) - 每格用公式原样计算(预统计行/列 1、0 数),写法更直白。
复杂度: - 时间复杂度:O(m * n)
- 空间复杂度:O(m + n)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:行/列 1 计数 + 化简公式(主提交版本)
public int[][] onesMinusZeros(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[] rowOnes = new int[m];
int[] colOnes = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
rowOnes[i]++;
colOnes[j]++;
}
}
}
int[][] diff = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
diff[i][j] = 2 * (rowOnes[i] + colOnes[j]) - m - n;
}
}
return diff;
}
// 解法二:按定义原样计算(对照)
public int[][] onesMinusZerosBrute(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[] onesRow = new int[m];
int[] zerosRow = new int[m];
int[] onesCol = new int[n];
int[] zerosCol = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
onesRow[i]++;
onesCol[j]++;
} else {
zerosRow[i]++;
zerosCol[j]++;
}
}
}
int[][] diff = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
diff[i][j] = onesRow[i] + onesCol[j] - zerosRow[i] - zerosCol[j];
}
}
return diff;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] g1 = {{0, 1, 1}, {1, 0, 1}, {0, 0, 1}};
int[][] r1 = sol.onesMinusZeros(g1);
int[][] r1b = sol.onesMinusZerosBrute(g1);
int[][] exp1 = {{0, 0, 4}, {0, 0, 4}, {-2, -2, 2}};
assert Arrays.deepEquals(r1, exp1) : "2482 例1 主: " + Arrays.deepToString(r1);
assert Arrays.deepEquals(r1b, exp1) : "2482 例1 暴力: " + Arrays.deepToString(r1b);
int[][] g2 = {{1, 1, 1}, {1, 1, 1}};
int[][] exp2 = {{5, 5, 5}, {5, 5, 5}};
assert Arrays.deepEquals(sol.onesMinusZeros(g2), exp2) : "2482 例2 主";
assert Arrays.deepEquals(sol.onesMinusZerosBrute(g2), exp2) : "2482 例2 暴力";
// 随机对拍
java.util.Random rnd = new java.util.Random(13);
for (int t = 0; t < 300; t++) {
int m = 1 + rnd.nextInt(8);
int n = 1 + rnd.nextInt(8);
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);
}
}
if (!Arrays.deepEquals(sol.onesMinusZeros(g), sol.onesMinusZerosBrute(g))) {
System.out.println("对拍失败 " + Arrays.deepToString(g));
System.exit(1);
}
}
System.out.println("2482 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(m * n)
- 空间复杂度:O(m + n)
2483. Minimum Penalty for a Shop(商店的最少代价)
题目描述
给你一个顾客访问商店的日志,用一个下标从 0 开始且只包含字符 'N' 和 'Y' 的字符串 customers 表示:
- 如果第
i个字符是 'Y',表示第i小时有顾客到达。 - 如果第
i个字符是 'N',表示第i小时没有顾客到达。
如果商店在第j小时关门(0 <=j<=n),代价按如下方式计算: - 在开门期间,如果某一个小时没有顾客到达,代价增加 1。
- 在关门期间,如果某一个小时有顾客到达,代价增加 1。
返回在确保代价最小的前提下,商店的最早关门时间。
注意,商店在第j小时关门表示在第j小时以及之后商店处于关门状态。
示例 1
`customers = "YYNY"` → 2(第 2 或第 4 小时关门代价都最小,取最早的 2)
示例 2
`customers = "NNNNN"` → 0
示例 3
`customers = "YYYY"` → 4
提示:
1 <= customers.length <= 10^5customers只包含字符 'Y' 和 'N'
解题思路
解法一:前缀 Y 计数(主提交版本)
- 设在第 j 小时关门(0 <= j <= n):
- 开门段 [0, j):未关闭时遇到 'N' 计 1 → 这段代价 = 区间内 'N' 的个数 = j - 区间内 'Y' 个数。
- 关门段 [j, n):关门时遇到 'Y' 计 1 → 这段代价 = 区间内 'Y' 的个数。
- 用前缀 Y 计数 preYi = 前 i 个字符中 'Y' 的个数:
代价(j) = (j - preYj) + (preYn - preYj) = preYn + j - 2 * preYj。 - 枚举 j 求最小代价,相同代价取最早 j。
解法二:两遍扫描(对照) - 先统计总 'Y' 数 totalY;从左向右扫描,维护左侧 N 数、右侧 Y 数,
代价 = leftN + rightY,逐步取最小 j。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)(解法一 O(n) 也可,解法二 O(1))
代码实现
java
public class Solution {
// 解法一:前缀 Y 计数公式(主提交版本)
public int bestClosingTime(String customers) {
int n = customers.length();
int[] preY = new int[n + 1];
for (int i = 0; i < n; i++) {
preY[i + 1] = preY[i] + (customers.charAt(i) == 'Y' ? 1 : 0);
}
int totalY = preY[n];
int bestIdx = 0;
int bestCost = Integer.MAX_VALUE;
for (int j = 0; j <= n; j++) {
int cost = totalY + j - 2 * preY[j];
if (cost < bestCost) {
bestCost = cost;
bestIdx = j;
}
}
return bestIdx;
}
// 解法二:两遍扫描(对照)
public int bestClosingTimeScan(String customers) {
int n = customers.length();
int totalY = 0;
for (int i = 0; i < n; i++) {
if (customers.charAt(i) == 'Y') {
totalY++;
}
}
int leftN = 0;
int rightY = totalY;
int bestIdx = 0;
int bestCost = Integer.MAX_VALUE;
for (int j = 0; j <= n; j++) {
int cost = leftN + rightY;
if (cost < bestCost) {
bestCost = cost;
bestIdx = j;
}
if (j < n) {
if (customers.charAt(j) == 'N') {
leftN++; // j 小时后进入关门段,不再计 N
} else {
rightY--; // 其变成开门段遇到的 Y,关闭代价取消
}
}
}
return bestIdx;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.bestClosingTime("YYNY") == 2 : "2483 例1 主: " + sol.bestClosingTime("YYNY");
assert sol.bestClosingTimeScan("YYNY") == 2 : "2483 例1 扫描";
assert sol.bestClosingTime("NNNNN") == 0 : "2483 例2 主";
assert sol.bestClosingTimeScan("NNNNN") == 0 : "2483 例2 扫描";
assert sol.bestClosingTime("YYYY") == 4 : "2483 例3 主";
assert sol.bestClosingTimeScan("YYYY") == 4 : "2483 例3 扫描";
assert sol.bestClosingTime("Y") == 1 : "2483 例4 主";
assert sol.bestClosingTimeScan("Y") == 1 : "2483 例4 扫描";
assert sol.bestClosingTime("N") == 0 : "2483 例5 主";
assert sol.bestClosingTimeScan("N") == 0 : "2483 例5 扫描";
// 穷举所有长度 <= 12 的字符串对拍
for (int len = 1; len <= 12; len++) {
int total = 1 << len;
for (int mask = 0; mask < total; mask++) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) {
sb.append(((mask >> i) & 1) == 1 ? 'Y' : 'N');
}
String s = sb.toString();
int a = sol.bestClosingTime(s);
int b = sol.bestClosingTimeScan(s);
if (a != b) {
System.out.println("对拍失败 s=" + s + " 主=" + a + " 扫描=" + b);
System.exit(1);
}
}
}
System.out.println("2483 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)(解法一 O(n) 也可,解法二 O(1))
2484. Count Palindromic Subsequences(统计回文子序列数目)
题目描述
给你数字字符串 s,返回 s 中长度为 5 的回文子序列数目。由于答案可能很大,请将答案对 10^9 + 7 取余后返回。
回文字符串:从前往后和从后往前读相同的字符串。
子序列:删除若干个字符后,不改变字符顺序,剩余字符构成的字符串。
示例 1
`s = "103301"` → 2(两个 "10301")
示例 2
`s = "0000000"` → 21(所有 C(7,5) = 21 个长度 5 子序列都是 "00000")
示例 3
`s = "9999900000"` → 2("99999" 和 "00000")
提示:
1 <= s.length <= 10^4s只包含数字字符
解题思路
解法一:左右双向计数(主提交版本,O(10^2 * n))
- 长度为 5 的回文形如
a b c b a:两端字符相同(a = a)、内侧字符相同(b = b),中间为 c。 - 枚举中间位置 i(字符 c),则需要在左侧(0...i-1)选两个位置 p < q 使字符为 (a, b),
在右侧(i+1...n-1)选两个位置 p' < q' 使字符为 (b, a)。 - 维护:
right[i][x][y]:si...n-1 中形如 x y 的子序列个数(从右向左构建)。- 从左向右扫描,动态维护
left[x][y]= s0...i-1 中形如 x y 的子序列个数。 - 对每个位置 i:
ans += Σ_{x,y} left[x][y] * right[i+1][y][x],
因为左侧需 (a, b) = (x, y),右侧需 (b, a) = (y, x)。
解法二:暴力五重循环(对照,仅小规模对拍) - 枚举五个下标 i<j<k<l<m,判断 si==sm 且 sj==sl。
复杂度: - 时间复杂度:O(100 * n)
- 空间复杂度:O(100 * n)
代码实现
java
public class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:左右双向计数(主提交版本)
public int countPalindromes(String s) {
int n = s.length();
int[] d = new int[n];
for (int i = 0; i < n; i++) {
d[i] = s.charAt(i) - '0';
}
// right[i][x][y] = s[i..n-1] 中形如 x y(存在位置 p<q,s[p]=x, s[q]=y)的子序列数
long[][][] right = new long[n + 1][10][10];
int[] cntR = new int[10];
for (int i = n - 1; i >= 0; i--) {
for (int x = 0; x < 10; x++) {
System.arraycopy(right[i + 1][x], 0, right[i][x], 0, 10);
}
int c = d[i];
// 以位置 i 的字符 c 作为子序列的第一个字符,y 在 i 之后出现
for (int y = 0; y < 10; y++) {
right[i][c][y] += cntR[y]; // c y 子序列新增 cntR[y] 个
}
cntR[c]++;
}
long ans = 0;
long[][] left = new long[10][10]; // left[x][y] = 已扫过前缀中形如 x y 的子序列数
int[] cntL = new int[10];
for (int i = 0; i < n; i++) {
int c = d[i];
// i 作为正中间字符:左侧取 (x, y),右侧取 (y, x),构成 x y c y x
for (int x = 0; x < 10; x++) {
for (int y = 0; y < 10; y++) {
if (left[x][y] > 0 && right[i + 1][y][x] > 0) {
ans = (ans + left[x][y] * right[i + 1][y][x]) % MOD;
}
}
}
// 更新 left:以 c 结尾的 x c 子序列新增 cntL[x] 个
for (int x = 0; x < 10; x++) {
left[x][c] += cntL[x];
}
cntL[c]++;
}
return (int) ans;
}
// 解法二:暴力五重循环(对照,仅小规模对拍)
public int countPalindromesBrute(String s) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
for (int l = k + 1; l < n; l++) {
for (int m = l + 1; m < n; m++) {
if (s.charAt(i) == s.charAt(m) && s.charAt(j) == s.charAt(l)) {
ans++;
}
}
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.countPalindromes("103301") == 2 : "2484 例1 主: " + sol.countPalindromes("103301");
assert sol.countPalindromesBrute("103301") == 2 : "2484 例1 暴力";
assert sol.countPalindromes("0000000") == 21 : "2484 例2 主: " + sol.countPalindromes("0000000");
assert sol.countPalindromesBrute("0000000") == 21 : "2484 例2 暴力";
assert sol.countPalindromes("9999900000") == 2 : "2484 例3 主: " + sol.countPalindromes("9999900000");
assert sol.countPalindromesBrute("9999900000") == 2 : "2484 例3 暴力";
// 随机对拍(小规模)
java.util.Random rnd = new java.util.Random(31);
for (int t = 0; t < 150; t++) {
int n = 5 + rnd.nextInt(7);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append((char) ('0' + rnd.nextInt(4))); // 4 种数字保证有回文
}
String s = sb.toString();
int a = sol.countPalindromes(s);
int b = sol.countPalindromesBrute(s);
if (a != b) {
System.out.println("对拍失败 s=" + s + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2484 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(100 * n)
- 空间复杂度:O(100 * n)
2485. Find the Pivot Integer(找出中枢整数)
题目描述
给你一个正整数 n,找出满足下述条件的中枢整数 x:
- 1 和
x之间的所有元素之和等于x和n之间所有元素之和。
返回中枢整数x;如果不存在中枢整数,则返回 -1。
题目保证对于给定的输入,至多存在一个中枢整数。
示例 1
`n = 8` → 6(1+2+3+4+5+6 = 6+7+8 = 21)
示例 2
`n = 1` → 1
示例 3
`n = 4` → -1
提示:
1 <= n <= 1000
解题思路
解法一:数学公式(主提交版本)
- 1 到 n 的总和
total = n*(n+1)/2。 - 若存在 x,则 1 到 x 的和 = x 到 n 的和 = total / 2 与 x 需要满足:
x*(x+1)/2 >= total/2。更直接:x²可能成立,因为:
1+...+x = x+...+n ⇔ sum(1...x) = sum(x...n)
展开:x(x+1)/2 = (x+n)(n-x+1)/2
化简得 x^2 = n(n+1)/2 − 反推:本式等价于 2x² = n²+n,即 x = sqrt(n(n+1)/2)。 - 计算
t = sqrt(n*(n+1)/2),若t*t == n*(n+1)/2(允许整数开方检查,注意用 long),返回 t,否则 -1。
解法二:枚举(对照) - 直接枚举 x 从 1 到 n,比较 1...x 和 x...n 两个累加和。
复杂度: - 时间复杂度:O(1)(解法二 O(n))
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:数学公式(主提交版本)
public int pivotInteger(int n) {
long total = (long) n * (n + 1) / 2;
long t = (long) Math.sqrt(total);
// 浮点开方可能向下取整,做一次 ±1 校验
for (long x = Math.max(1, t - 1); x <= t + 1; x++) {
if (x * x == total) {
return (int) x;
}
}
return -1;
}
// 解法二:枚举(对照)
public int pivotIntegerBrute(int n) {
int[] pre = new int[n + 1];
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1] + i;
}
for (int x = 1; x <= n; x++) {
int left = pre[x]; // 1..x
int right = pre[n] - pre[x - 1]; // x..n
if (left == right) {
return x;
}
}
return -1;
}
public static void main(String[] args) {
Solution sol = new Solution();
for (int n = 1; n <= 1000; n++) {
int a = sol.pivotInteger(n);
int b = sol.pivotIntegerBrute(n);
assert a == b : "2485 不一致 n=" + n + " 主=" + a + " 枚举=" + b;
}
assert sol.pivotInteger(8) == 6 : "2485 例1";
assert sol.pivotInteger(1) == 1 : "2485 例2";
assert sol.pivotInteger(4) == -1 : "2485 例3";
System.out.println("2485 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(1)(解法二 O(n))
- 空间复杂度:O(1)
2486. Append Characters to String to Make Subsequence(追加字符以获得子序列)
题目描述
给你两个仅由小写英文字母组成的字符串 s 和 t。
现在需要通过向 s 末尾追加字符的方式使 t 变成 s 的一个子序列,返回需要追加的最少字符数。
子序列是一个可以由其他字符串删除部分(或不删除)字符但不改变剩下字符顺序得到的字符串。
示例 1
`s = "coaching", t = "coding"` → 4
示例 2
`s = "abcde", t = "a"` → 0
示例 3
`s = "z", t = "abcde"` → 5
提示:
1 <= s.length, t.length <= 10^5s和t仅由小写英文字母组成
解题思路
解法一:双指针贪心(主提交版本)
- 用指针 i 扫 s、指针 j 扫 t:若 si == tj,则 j 前进(t 中该字符已被匹配)。
- 扫描完成后 t 还有 j...t.length-1 未匹配,这些字符只能追加到 s 末尾,
追加数量 = t.length - j。 - 等价于:t 中能被 s 作为子序列匹配掉的最大前缀长度为 j,剩余全要追加。
解法二:朴素逐字符搜索(对照) - 对 t 的每个字符在 s 中顺序查找,找不到的第一个位置以后全部要追加。
复杂度: - 时间复杂度:O(n + m)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:双指针(主提交版本)
public int appendCharacters(String s, String t) {
int i = 0, j = 0;
int n = s.length(), m = t.length();
while (i < n && j < m) {
if (s.charAt(i) == t.charAt(j)) {
j++;
}
i++;
}
return m - j;
}
// 解法二:逐字符推进(对照)
public int appendCharactersBrute(String s, String t) {
int idx = 0;
for (int i = 0; i < s.length(); i++) {
if (idx < t.length() && s.charAt(i) == t.charAt(idx)) {
idx++;
}
}
return t.length() - idx;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.appendCharacters("coaching", "coding") == 4 : "2486 例1 主";
assert sol.appendCharactersBrute("coaching", "coding") == 4 : "2486 例1 暴力";
assert sol.appendCharacters("abcde", "a") == 0 : "2486 例2 主";
assert sol.appendCharactersBrute("abcde", "a") == 0 : "2486 例2 暴力";
assert sol.appendCharacters("z", "abcde") == 5 : "2486 例3 主";
assert sol.appendCharactersBrute("z", "abcde") == 5 : "2486 例3 暴力";
assert sol.appendCharacters("cba", "abc") == 3 : "2486 特例主";
assert sol.appendCharactersBrute("cba", "abc") == 3 : "2486 特例暴力";
// 随机对拍
java.util.Random rnd = new java.util.Random(19);
for (int t = 0; t < 300; t++) {
int ns = 1 + rnd.nextInt(40);
int nt = 1 + rnd.nextInt(40);
StringBuilder sb1 = new StringBuilder();
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < ns; i++) {
sb1.append((char) ('a' + rnd.nextInt(6)));
}
for (int i = 0; i < nt; i++) {
sb2.append((char) ('a' + rnd.nextInt(6)));
}
String s = sb1.toString(), tt = sb2.toString();
int a = sol.appendCharacters(s, tt);
int b = sol.appendCharactersBrute(s, tt);
if (a != b) {
System.out.println("对拍失败 s=" + s + " t=" + tt + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2486 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n + m)
- 空间复杂度:O(1)
2487. Remove Nodes From Linked List(从链表中移除节点)
题目描述
给你一个链表的头节点 head。
移除每个右侧有一个更大数值的节点。
返回修改后链表的头节点 head。
示例 1
`head = [5,2,13,3,8]` → `[13,8]`(5、2、3 右侧均有更大值被移除)
示例 2
`head = [1,1,1,1]` → `[1,1,1,1]`(没有需要移除的节点)
提示:
- 列表中的节点数目在范围
[1, 10^5] 1 <= Node.val <= 10^5
解题思路
解法一:单调栈(主提交版本)
- 栈中自底向上保持「严格递减」:依次遍历节点,若栈顶值 < 当前值,弹出(被移除),
直到栈顶值 ≥ 当前值或栈空,再把当前节点压栈。 - 遍历完栈中自底向上 递减(非严格,相等保留),正好是答案链表;重新链接 next 指针返回。
解法二:反转链表 + 一趟清扫(对照) - 先反转链表,从右到左处理:记右侧见过最大值 maxRight,若当前值 < maxRight 则删除,
否则保留并更新 maxRight;最后再反转回来。等价于「删除比右侧最大值小的节点」。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(n)(栈)/ O(1)(反转版)
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
// 解法一:单调栈(主提交版本)
public ListNode removeNodes(ListNode head) {
Deque<ListNode> st = new ArrayDeque<>();
ListNode cur = head;
while (cur != null) {
while (!st.isEmpty() && st.peek().val < cur.val) {
st.pop();
}
st.push(cur);
cur = cur.next;
}
// 栈中自底向上递减,重新串联
ListNode result = null;
while (!st.isEmpty()) {
ListNode node = st.pop();
node.next = result;
result = node;
}
return result;
}
// 解法二:反转 + 清扫(对照)
public ListNode removeNodesReverse(ListNode head) {
ListNode rev = reverse(head);
ListNode dummy = new ListNode(0);
dummy.next = rev;
ListNode prev = dummy;
ListNode cur = rev;
int maxRight = -1;
while (cur != null) {
if (cur.val < maxRight) {
prev.next = cur.next; // 删除 cur
} else {
maxRight = cur.val;
prev = cur;
}
cur = cur.next;
}
return reverse(dummy.next);
}
private ListNode reverse(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = prev;
prev = cur;
cur = nxt;
}
return prev;
}
// ---- 测试辅助 ----
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
for (int v : arr) {
cur.next = new ListNode(v);
cur = cur.next;
}
return dummy.next;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] r1 = toArr(sol.removeNodes(build(new int[]{5, 2, 13, 3, 8})));
int[] r1b = toArr(sol.removeNodesReverse(build(new int[]{5, 2, 13, 3, 8})));
assert java.util.Arrays.equals(r1, new int[]{13, 8}) : "2487 例1 主: " + java.util.Arrays.toString(r1);
assert java.util.Arrays.equals(r1b, new int[]{13, 8}) : "2487 例1 反转: " + java.util.Arrays.toString(r1b);
int[] r2 = toArr(sol.removeNodes(build(new int[]{1, 1, 1, 1})));
int[] r2b = toArr(sol.removeNodesReverse(build(new int[]{1, 1, 1, 1})));
assert java.util.Arrays.equals(r2, new int[]{1, 1, 1, 1}) : "2487 例2 主";
assert java.util.Arrays.equals(r2b, new int[]{1, 1, 1, 1}) : "2487 例2 反转";
int[] r3 = toArr(sol.removeNodes(build(new int[]{5, 4, 3, 2, 1})));
assert java.util.Arrays.equals(r3, new int[]{5, 4, 3, 2, 1}) : "2487 递减主";
// 随机对拍
java.util.Random rnd = new java.util.Random(23);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(20);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(50);
}
int[] a = toArr(sol.removeNodes(build(arr)));
int[] b = toArr(sol.removeNodesReverse(build(arr)));
if (!java.util.Arrays.equals(a, b)) {
System.out.println("对拍失败 " + java.util.Arrays.toString(arr)
+ " 主=" + java.util.Arrays.toString(a) + " 反转=" + java.util.Arrays.toString(b));
System.exit(1);
}
}
System.out.println("2487 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)(栈)/ O(1)(反转版)
2488. Count Subarrays With Median K(统计中位数为 K 的子数组)
题目描述
给你一个长度为 n 的数组 nums,该数组由从 1 到 n 的不同整数组成。另给你一个正整数 k。
统计并返回 nums 中的中位数等于 k 的非空子数组的数目。
注意:
- 数组的中位数是按递增顺序排列后位于中间的那个元素;如果长度为偶数,则中位数是中间靠左的那个元素。
例:[2,3,1,4]中位数为 2,[8,4,3,5,1]中位数为 4。
示例 1
`nums = [3,2,1,4,5], k = 4` → 3([4]、[4,5]、[1,4,5])
示例 2
`nums = [2,3,1], k = 3` → 1([3] 是唯一)
提示:
n==nums.length,1 <= n <= 10^51 <= nums[i], k <= n,nums中的整数互不相同
解题思路
解法一:哈希表映射「大-小」前缀差(主提交版本)
- 中位数是 k 当且仅当子数组中 > k 的个数与 < k 的个数满足特定关系:
- 由于元素互异且 k 出现在数组中唯一位置 pos,含 k 的子数组一定有 pos。
- 对包含 k 的区间 l, r(l ≤ pos ≤ r):
令 greater = 区间内 > k 的个数,less = 区间内 < k 的个数,长度 len = greater + less + 1。 - len 奇数:中位数 = k ⟺ greater == less。
- len 偶数:中位数(靠左) = k ⟺ greater == less + 1。
- 把条件统一:记 b = greater - less,
满足 b == 0(奇长)或 b == 1(偶长,即 greater = less + 1)。
两个条件都要求区间同时包含 k;统计时以 pos 为界分成左右两半: - 左半(含 pos)到右侧扩展,用哈希表记录从 pos 往左的 (b) 出现次数,
往右扩展并累加:ans += leftb + leftb-1。
解法二:暴力枚举(对照,仅小规模对拍) - 枚举全部 (l, r) 并统计大于/小于 k 的个数按中位数定义判断。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(n)
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// 解法一:前缀差计数(主提交版本)
public int countSubarrays(int[] nums, int k) {
int n = nums.length;
int pos = -1;
for (int i = 0; i < n; i++) {
if (nums[i] == k) {
pos = i;
break;
}
}
// 以 pos 为中心的左侧:从 pos 向左累计 (大于-小于)
Map<Integer, Integer> left = new HashMap<>();
left.put(0, 1); // 左半只含 k 自身(空延伸)
int bal = 0;
for (int i = pos - 1; i >= 0; i--) {
bal += nums[i] > k ? 1 : -1;
left.put(bal, left.getOrDefault(bal, 0) + 1);
}
// 右侧扩展
int ans = 0;
bal = 0;
for (int j = pos; j < n; j++) {
if (j > pos) {
bal += nums[j] > k ? 1 : -1;
}
// 右侧前缀差与左侧某 bal0 组合:
// 总差 = bal0 + bal;奇数长需 0,偶数长需 1
ans += left.getOrDefault(-bal, 0); // 奇长 → 总差 0
ans += left.getOrDefault(1 - bal, 0); // 偶长 → 总差 1
}
return ans;
}
// 解法二:暴力(对照,小规模对拍)
public int countSubarraysBrute(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int l = 0; l < n; l++) {
int greater = 0, less = 0;
boolean hasK = false;
for (int r = l; r < n; r++) {
if (nums[r] == k) {
hasK = true;
} else if (nums[r] > k) {
greater++;
} else {
less++;
}
if (!hasK) {
continue;
}
int len = r - l + 1;
// 中位数靠左:排序后下标 (len-1)/2
int[] sorted = new int[len]; // 小规模可用
int idx = 0;
for (int t = l; t <= r; t++) {
sorted[idx++] = nums[t];
}
java.util.Arrays.sort(sorted);
if (sorted[(len - 1) / 2] == k) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.countSubarrays(new int[]{3, 2, 1, 4, 5}, 4) == 3 : "2488 例1 主: " + sol.countSubarrays(new int[]{3, 2, 1, 4, 5}, 4);
assert sol.countSubarraysBrute(new int[]{3, 2, 1, 4, 5}, 4) == 3 : "2488 例1 暴力";
assert sol.countSubarrays(new int[]{2, 3, 1}, 3) == 1 : "2488 例2 主";
assert sol.countSubarraysBrute(new int[]{2, 3, 1}, 3) == 1 : "2488 例2 暴力";
// 随机排列对拍
java.util.Random rnd = new java.util.Random(37);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(12);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// Fisher-Yates 洗牌
for (int i = n - 1; i > 0; i--) {
int j = rnd.nextInt(i + 1);
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
int k = 1 + rnd.nextInt(n);
int a = sol.countSubarrays(arr, k);
int b = sol.countSubarraysBrute(arr, k);
if (a != b) {
System.out.println("对拍失败 nums=" + java.util.Arrays.toString(arr) + " k=" + k + " 主=" + a + " 暴力=" + b);
System.exit(1);
}
}
System.out.println("2488 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2489. Number of Substrings With Fixed Ratio(固定比率的子字符串数)
题目描述
给定一个二进制字符串 s 和两个整数 num1 和 num2(互质)。
比率子串是 s 的子串,其中子串中 0 的数量与 1 的数量之比正好是 num1 : num2。
返回 s 的非空比率子串的个数。
示例 1
`s = "0110011", num1 = 1, num2 = 2` → 4
示例 2
`s = "10101", num1 = 3, num2 = 1` → 0
提示:
1 <= s.length <= 10^51 <= num1, num2 <= s.lengthnum1和num2互质
解题思路
解法一:前缀计数 + 关键值哈希(主提交版本)
- 前缀统计 zerosi、onesi:子串 [l, r) 的比为 zeros®-zeros(l) : ones®-ones(l) = num1:num2
⟺ (zeros®-zeros(l)) * num2 == (ones®-ones(l)) * num1
⟺ zeros®*num2 - ones®*num1 == zeros(l)*num2 - ones(l)*num1。 - 定义 key(i) = zerosi num2 - onesnum1,则子串合法 ⟺ key(l) == key®。
- 用哈希表统计每个 key 的出现次数,对每个位置 r,贡献 = 此前同 key 的 l 个数。
(l 中可以允许 0/1 个数为 0 吗?若 num1, num2 都为正,则子串必须同时含 0、1;
但 key 相同已经保证比例正确。当 num1>0、num2>0 时不需额外条件,若恰好两者为 0 的情况
由 key 唯一性自然排除------两个正数比例不为 0。)
解法二:暴力枚举(对照,仅小规模对拍) - 直接枚举所有子串并统计 0、1 数量按比例判断。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(n)
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// 解法一:前缀 key 哈希(主提交版本)
public long fixedRatio(String s, int num1, int num2) {
long zeros = 0, ones = 0;
Map<Long, Long> cnt = new HashMap<>();
cnt.put(0L, 1L); // 空前缀
long ans = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '0') {
zeros++;
} else {
ones++;
}
long key = zeros * num2 - ones * num1;
long prev = cnt.getOrDefault(key, 0L);
ans += prev;
cnt.put(key, prev + 1);
}
return ans;
}
// 解法二:暴力(对照,小规模对拍)
public long fixedRatioBrute(String s, int num1, int num2) {
int n = s.length();
long ans = 0;
for (int l = 0; l < n; l++) {
long z = 0, o = 0;
for (int r = l; r < n; r++) {
if (s.charAt(r) == '0') {
z++;
} else {
o++;
}
if (z * num2 == o * num1) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.fixedRatio("0110011", 1, 2) == 4 : "2489 例1 主: " + sol.fixedRatio("0110011", 1, 2);
assert sol.fixedRatioBrute("0110011", 1, 2) == 4 : "2489 例1 暴力";
assert sol.fixedRatio("10101", 3, 1) == 0 : "2489 例2 主";
assert sol.fixedRatioBrute("10101", 3, 1) == 0 : "2489 例2 暴力";
assert sol.fixedRatio("0", 1, 1) == 0 : "2489 单0 主";
assert sol.fixedRatioBrute("0", 1, 1) == 0 : "2489 单0 暴力";
// 随机对拍
java.util.Random rnd = new java.util.Random(41);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(20);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append((char) ('0' + rnd.nextInt(2)));
}
String s = sb.toString();
int a1 = 1 + rnd.nextInt(8);
int a2 = 1 + rnd.nextInt(8);
long x = sol.fixedRatio(s, a1, a2);
long y = sol.fixedRatioBrute(s, a1, a2);
if (x != y) {
System.out.println("对拍失败 s=" + s + " num1=" + a1 + " num2=" + a2 + " 主=" + x + " 暴力=" + y);
System.exit(1);
}
}
System.out.println("2489 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2490. Circular Sentence(回环句)
题目描述
句子是由单个空格分隔的一组单词,且不含前导或尾随空格。单词仅由大写和小写英文字母组成,
且大小写视作不同字符。
如果句子满足下述全部条件,则认为它是一个回环句:
- 句子中每个单词的最后一个字符等于下一个单词的第一个字符。
- 最后一个单词的最后一个字符和第一个单词的第一个字符相等。
返回判定结果。
示例 1
`sentence = "leetcode exercises sound delightful"` → true
示例 2
`sentence = "eetcode"` → true
示例 3
`sentence = "Leetcode is cool"` → false
示例 4
`sentence = "Leetcode"` → false(单单词也要首尾相等)
提示:
1 <= sentence.length <= 500sentence仅由大小写英文字母和空格组成- 单词由单个空格分隔,不含前导或尾随空格
解题思路
解法一:相邻字符判定(主提交版本)
- 回环条件等价于:对每个空格位置 i(si==' '),其左侧字符 si-1 必须等于右侧字符 si+1。
(这同时覆盖「每个单词末 == 下一单词首」) - 另外首字符必须等于末字符(覆盖首尾环接)。
解法二:split 分词检查(对照) - 按空格切分句子,检查相邻词的末/首字符及首词首=末词末。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:相邻字符判定(主提交版本)
public boolean isCircularSentence(String sentence) {
int n = sentence.length();
if (sentence.charAt(0) != sentence.charAt(n - 1)) {
return false;
}
for (int i = 0; i < n; i++) {
if (sentence.charAt(i) == ' ') {
if (sentence.charAt(i - 1) != sentence.charAt(i + 1)) {
return false;
}
}
}
return true;
}
// 解法二:split 分词(对照)
public boolean isCircularSentenceSplit(String sentence) {
String[] words = sentence.split(" ");
int m = words.length;
for (int i = 0; i < m; i++) {
char last = words[i].charAt(words[i].length() - 1);
char first = words[(i + 1) % m].charAt(0);
if (last != first) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.isCircularSentence("leetcode exercises sound delightful") : "2490 例1 主";
assert sol.isCircularSentenceSplit("leetcode exercises sound delightful") : "2490 例1 split";
assert sol.isCircularSentence("eetcode") : "2490 例2 主";
assert sol.isCircularSentenceSplit("eetcode") : "2490 例2 split";
assert !sol.isCircularSentence("Leetcode is cool") : "2490 例3 主";
assert !sol.isCircularSentenceSplit("Leetcode is cool") : "2490 例3 split";
assert !sol.isCircularSentence("Leetcode") : "2490 例4 主";
assert !sol.isCircularSentenceSplit("Leetcode") : "2490 例4 split";
assert sol.isCircularSentence("a") : "2490 单字符主";
assert sol.isCircularSentenceSplit("a") : "2490 单字符 split";
System.out.println("2490 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2491. Divide Players Into Teams of Equal Skill(划分技能点相等的团队)
题目描述
给你一个正整数数组 skill,数组长度为偶数 n,其中 skill[i] 表示第 i 个玩家的技能点。
将所有玩家分成 n/2 个 2 人团队,使每一个团队的技能点之和相等。
团队的化学反应等于团队中玩家的技能点乘积。
返回所有团队的化学反应之和;如果无法使每个团队的技能点之和相等,则返回 -1。
示例 1
`skill = [3,2,5,1,3,4]` → 22((1,5),(2,4),(3,3) 和均为 6)
示例 2
`skill = [3,4]` → 12
示例 3
`skill = [1,1,2,3]` → -1
提示:
2 <= skill.length <= 10^5,长度是偶数1 <= skill[i] <= 1000
解题思路
解法一:排序 + 双指针(主提交版本)
- 排序后,和相等的配对必然是「最小 + 最大」:若存在可行配对,任意最优配对均可化为
最小配最大(若最小 a 配 b、最大 c 配 d,可交换使 a 与 c 配对,和不变)。 - 排序后首尾配对,校验每对和都等于
target = 总和 * 2 / n(先整体除以队数),
乘积累加即可;一旦有对和不等于 target 返回 -1。
解法二:频次哈希(对照) - 统计每个技能点出现频次,若 skilli=x 则必须有 cnttarget-x 相应匹配,校验配对并累乘。
复杂度: - 时间复杂度:O(n log n)
- 空间复杂度:O(1)(排序原地)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:排序 + 双指针(主提交版本)
public long dividePlayers(int[] skill) {
int n = skill.length;
long total = 0;
for (int v : skill) {
total += v;
}
int teams = n / 2;
if (total % teams != 0) {
return -1;
}
long target = total / teams;
Arrays.sort(skill);
long ans = 0;
int i = 0, j = n - 1;
while (i < j) {
if (skill[i] + skill[j] != target) {
return -1;
}
ans += (long) skill[i] * skill[j];
i++;
j--;
}
return ans;
}
// 解法二:频次哈希(对照)
public long dividePlayersFreq(int[] skill) {
int n = skill.length;
int maxV = 1000;
int[] cnt = new int[maxV + 1];
long total = 0;
for (int v : skill) {
cnt[v]++;
total += v;
}
int teams = n / 2;
if (total % teams != 0) {
return -1;
}
long target = total / teams;
long ans = 0;
for (int x = 1; x <= maxV; x++) {
if (cnt[x] == 0) {
continue;
}
int y = (int) (target - x);
if (y < 1 || y > maxV || cnt[y] < cnt[x]) {
return -1;
}
if (y == x) {
if (cnt[x] % 2 != 0) {
return -1;
}
ans += (long) x * y * (cnt[x] / 2);
cnt[x] = 0;
} else {
ans += (long) x * y * cnt[x];
cnt[y] -= cnt[x];
cnt[x] = 0;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.dividePlayers(new int[]{3, 2, 5, 1, 3, 4}) == 22 : "2491 例1 主";
assert sol.dividePlayersFreq(new int[]{3, 2, 5, 1, 3, 4}) == 22 : "2491 例1 频次";
assert sol.dividePlayers(new int[]{3, 4}) == 12 : "2491 例2 主";
assert sol.dividePlayersFreq(new int[]{3, 4}) == 12 : "2491 例2 频次";
assert sol.dividePlayers(new int[]{1, 1, 2, 3}) == -1 : "2491 例3 主";
assert sol.dividePlayersFreq(new int[]{1, 1, 2, 3}) == -1 : "2491 例3 频次";
java.util.Random rnd = new java.util.Random(43);
for (int t = 0; t < 300; t++) {
int n = 2 + 2 * rnd.nextInt(20);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(50);
}
long a = sol.dividePlayers(arr);
long b = sol.dividePlayersFreq(arr);
if (a != b) {
System.out.println("对拍失败 " + Arrays.toString(arr) + " 主=" + a + " 频次=" + b);
System.exit(1);
}
}
System.out.println("2491 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n log n)
- 空间复杂度:O(1)(排序原地)
2492. Minimum Score of a Path Between Two Cities(两个城市间路径的最小分数)
题目描述
给你一个正整数 n,表示总共有 n 个城市,城市从 1 到 n 编号。
roads[i] = [ai, bi, distancei] 表示城市 ai 和 bi 之间有一条双向道路,道路距离为 distancei。
城市构成的图不一定是连通的。
两个城市之间一条路径的分数定义为这条路径中道路的最小距离。
返回城市 1 和城市 n 之间的所有路径的最小分数。
注意:一条路径可以多次包含同一条道路,也可以多次到达城市 1 和城市 n。
测试数据保证城市 1 和城市 n 之间至少有一条路径。
示例 1
`n = 4, roads = [[1,2,9],[2,3,6],[2,4,5],[1,4,7]]` → 5
示例 2
`n = 4, roads = [[1,2,2],[1,3,4],[3,4,7]]` → 2(路径 1->2->1->3->4,分数 min(2,2,4,7)=2)
提示:
2 <= n <= 10^51 <= roads.length <= 10^51 <= distancei <= 10^4
解题思路
解法一:连通分量内最小边(主提交版本)
- 题目允许来回走,因此「连通分量」内任意两点都可到达:城市 1 与城市 n 同属一个连通分量
(题目保证有路径),该分量内所有边都可以被某条路径覆盖(重复走即可)。 - 因此答案 = 1 所在连通分量中权重最小的边。
- 实现:BFS/DFS 从 1 出发遍历并记录最小边权。
解法二:并查集(对照) - 并查集合并所有边,找到 1 与 n 的共同根后扫描全部边,取连接同分量两点中的最小权。
复杂度: - 时间复杂度:O(n + m)
- 空间复杂度:O(n + m)
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// 解法一:BFS 连通分量(主提交版本)
public int minScore(int n, int[][] roads) {
List<int[]>[] g = new List[n + 1];
for (int i = 1; i <= n; i++) {
g[i] = new ArrayList<>();
}
for (int[] r : roads) {
g[r[0]].add(new int[]{r[1], r[2]});
g[r[1]].add(new int[]{r[0], r[2]});
}
boolean[] vis = new boolean[n + 1];
ArrayDeque<Integer> q = new ArrayDeque<>();
q.offer(1);
vis[1] = true;
int ans = Integer.MAX_VALUE;
while (!q.isEmpty()) {
int u = q.poll();
for (int[] e : g[u]) {
ans = Math.min(ans, e[1]); // 覆盖该边(进/出都算)
if (!vis[e[0]]) {
vis[e[0]] = true;
q.offer(e[0]);
}
}
}
return ans;
}
// 解法二:并查集(对照)
public int minScoreUnion(int n, int[][] roads) {
int[] parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int[] r : roads) {
union(parent, r[0], r[1]);
}
int root = find(parent, 1);
int ans = Integer.MAX_VALUE;
for (int[] r : roads) {
if (find(parent, r[0]) == root) { // 两点都在 1 的分量(r[1] 也必然同根)
ans = Math.min(ans, r[2]);
}
}
return ans;
}
private int find(int[] p, int x) {
while (p[x] != x) {
p[x] = p[p[x]];
x = p[x];
}
return x;
}
private void union(int[] p, int a, int b) {
int ra = find(p, a), rb = find(p, b);
if (ra != rb) {
p[ra] = rb;
}
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.minScore(4, new int[][]{{1, 2, 9}, {2, 3, 6}, {2, 4, 5}, {1, 4, 7}}) == 5 : "2492 例1 主";
assert sol.minScoreUnion(4, new int[][]{{1, 2, 9}, {2, 3, 6}, {2, 4, 5}, {1, 4, 7}}) == 5 : "2492 例1 并查集";
assert sol.minScore(4, new int[][]{{1, 2, 2}, {1, 3, 4}, {3, 4, 7}}) == 2 : "2492 例2 主";
assert sol.minScoreUnion(4, new int[][]{{1, 2, 2}, {1, 3, 4}, {3, 4, 7}}) == 2 : "2492 例2 并查集";
// 随机对拍
java.util.Random rnd = new java.util.Random(47);
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(10);
java.util.Set<Long> used = new java.util.HashSet<>();
List<int[]> roads = new ArrayList<>();
// 保证 1 与 n 连通:建一条链
for (int i = 1; i < n; i++) {
int w = 1 + rnd.nextInt(50);
roads.add(new int[]{i, i + 1, w});
used.add(key(i, i + 1));
}
int extra = rnd.nextInt(10);
for (int i = 0; i < extra; i++) {
int a = 1 + rnd.nextInt(n);
int b = 1 + rnd.nextInt(n);
if (a == b || !used.add(key(a, b))) {
continue;
}
roads.add(new int[]{a, b, 1 + rnd.nextInt(50)});
}
int[][] rr = roads.toArray(new int[0][]);
int x = sol.minScore(n, rr);
int y = sol.minScoreUnion(n, rr);
if (x != y) {
System.out.println("对拍失败 n=" + n + " roads=" + Arrays.deepToString(rr) + " 主=" + x + " 并查集=" + y);
System.exit(1);
}
}
System.out.println("2492 全部 PASS");
}
private static long key(int a, int b) {
return a < b ? a * 10000L + b : b * 10000L + a;
}
}
复杂度分析
- 时间复杂度:O(n + m)
- 空间复杂度:O(n + m)
2493. Divide Nodes Into the Maximum Number of Groups(将节点分成尽可能多的组)
题目描述
给你一个正整数 n,表示一个无向图中的节点数目,节点编号从 1 到 n。
edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条双向边。注意给定的图可能是不连通的。
请你将图划分为 m 个组(编号从 1 开始),满足:
- 图中每个节点都只属于一个组。
- 图中每条边连接的两个点 `ai`, `bi`,若
ai属于x组、bi属于y组,则 |y-x| = 1。
返回最多可以将节点分为多少个组(最大的m);如果没办法在给定条件下分组,返回 -1。
示例 1
`n = 6, edges = [[1,2],[1,4],[1,5],[2,6],[2,3],[4,6]]` → 4
示例 2
`n = 3, edges = [[1,2],[2,3],[3,1]]` → -1(奇环不满足条件)
提示:
1 <= n <= 5001 <= edges.length <= 10^4
解题思路
解法一:BFS 分层 + 按连通块取最大值(主提交版本)
- 分组条件 |组号差| = 1 等价于给图做「二分染色且相邻层号连续」:
固定某个根 u 的组号为 1,则 u 可达的每个节点组号由最短路长度唯一决定(BFS 层数)。 - 由于可能有多种起点,对每个连通分量:
- 先检查是否二分图(奇环 → -1)。
- 枚举分量内每个节点作为起点做 BFS,得到的最大层数即该起点的「直径层数」;
取所有起点中的最大值作为该分量的贡献。
- 各分量贡献相加即答案。
解法二:并查集分组 + 逐起点 BFS(对照) - 并查集把边两端合并确定分量,对每个分量枚举起点 BFS 计算层数,逻辑同解法一,写法不同。
复杂度: - 时间复杂度:O(n * (n + m)),n ≤ 500 可行
- 空间复杂度:O(n + m)
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
private List<Integer>[] g;
// 解法一:连通分量 + 枚举起点 BFS(主提交版本)
public int magnificentSets(int n, int[][] edges) {
g = new List[n + 1];
for (int i = 1; i <= n; i++) {
g[i] = new ArrayList<>();
}
for (int[] e : edges) {
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
int[] comp = new int[n + 1];
int compCnt = 0;
List<List<Integer>> comps = new ArrayList<>();
for (int i = 1; i <= n; i++) {
if (comp[i] == 0) {
compCnt++;
List<Integer> nodes = new ArrayList<>();
// BFS 收集分量
ArrayDeque<Integer> q = new ArrayDeque<>();
q.offer(i);
comp[i] = compCnt;
while (!q.isEmpty()) {
int u = q.poll();
nodes.add(u);
for (int v : g[u]) {
if (comp[v] == 0) {
comp[v] = compCnt;
q.offer(v);
}
}
}
comps.add(nodes);
}
}
int ans = 0;
for (List<Integer> nodes : comps) {
int best = -1;
for (int start : nodes) {
int depth = bfsDepth(start); // 从 start 出发的最大层数
if (depth == -1) {
return -1; // 奇环 → 不可染色
}
best = Math.max(best, depth);
}
ans += best;
}
return ans;
}
// 返回以 start 为第 1 层时最大层数;若发现奇环(同层边)返回 -1
private int bfsDepth(int start) {
int[] level = new int[g.length];
Arrays.fill(level, -1);
level[start] = 1;
ArrayDeque<Integer> q = new ArrayDeque<>();
q.offer(start);
int maxLevel = 1;
while (!q.isEmpty()) {
int u = q.poll();
for (int v : g[u]) {
if (level[v] == -1) {
level[v] = level[u] + 1;
q.offer(v);
} else if (level[v] != level[u] - 1 && level[v] != level[u] + 1) {
// 相邻节点层号差必须为 1:违反则奇环
return -1;
}
}
maxLevel = Math.max(maxLevel, level[u]);
}
return maxLevel;
}
// 解法二:并查集确定分量 + BFS(对照)
public int magnificentSetsUnion(int n, int[][] edges) {
g = new List[n + 1];
for (int i = 1; i <= n; i++) {
g[i] = new ArrayList<>();
}
int[] parent = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
}
for (int[] e : edges) {
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
union(parent, e[0], e[1]);
}
// root -> nodes
java.util.Map<Integer, List<Integer>> compMap = new java.util.HashMap<>();
for (int i = 1; i <= n; i++) {
int r = find(parent, i);
compMap.computeIfAbsent(r, k -> new ArrayList<>()).add(i);
}
int ans = 0;
for (List<Integer> nodes : compMap.values()) {
int best = -1;
for (int start : nodes) {
int depth = bfsDepth(start);
if (depth == -1) {
return -1;
}
best = Math.max(best, depth);
}
ans += best;
}
return ans;
}
private int find(int[] p, int x) {
while (p[x] != x) {
p[x] = p[p[x]];
x = p[x];
}
return x;
}
private void union(int[] p, int a, int b) {
int ra = find(p, a), rb = find(p, b);
if (ra != rb) {
p[ra] = rb;
}
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.magnificentSets(6, new int[][]{{1, 2}, {1, 4}, {1, 5}, {2, 6}, {2, 3}, {4, 6}}) == 4 : "2493 例1 主";
assert sol.magnificentSetsUnion(6, new int[][]{{1, 2}, {1, 4}, {1, 5}, {2, 6}, {2, 3}, {4, 6}}) == 4 : "2493 例1 并查集";
assert sol.magnificentSets(3, new int[][]{{1, 2}, {2, 3}, {3, 1}}) == -1 : "2493 例2 主";
assert sol.magnificentSetsUnion(3, new int[][]{{1, 2}, {2, 3}, {3, 1}}) == -1 : "2493 例2 并查集";
// 链式图(二分)对拍:n=4 链 1-2-3-4 → 最大分组 4
assert sol.magnificentSets(4, new int[][]{{1, 2}, {2, 3}, {3, 4}}) == 4 : "2493 链主";
assert sol.magnificentSetsUnion(4, new int[][]{{1, 2}, {2, 3}, {3, 4}}) == 4 : "2493 链并查集";
// 偶环:4 环 1-2-3-4-1 → 最大 4
assert sol.magnificentSets(4, new int[][]{{1, 2}, {2, 3}, {3, 4}, {4, 1}}) == 4 : "2493 偶环主";
// 随机二分图对拍
java.util.Random rnd = new java.util.Random(53);
for (int t = 0; t < 120; t++) {
int n = 2 + rnd.nextInt(10);
java.util.Set<Long> used = new java.util.HashSet<>();
List<int[]> es = new ArrayList<>();
for (int i = 1; i < n; i++) { // 先随机树(保证连通二分)
int p = 1 + rnd.nextInt(i);
es.add(new int[]{p, i + 1});
used.add(key(p, i + 1));
}
int extra = rnd.nextInt(6);
for (int i = 0; i < extra; i++) {
int a = 1 + rnd.nextInt(n), b = 1 + rnd.nextInt(n);
if (a == b || !used.add(key(a, b))) {
continue;
}
es.add(new int[]{a, b});
}
int[][] ee = es.toArray(new int[0][]);
int x = sol.magnificentSets(n, ee);
int y = sol.magnificentSetsUnion(n, ee);
if (x != y) {
System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(ee) + " 主=" + x + " 并查集=" + y);
System.exit(1);
}
}
System.out.println("2493 全部 PASS");
}
private static long key(int a, int b) {
return a < b ? a * 1000L + b : b * 1000L + a;
}
}
复杂度分析
- 时间复杂度:O(n * (n + m)),n ≤ 500 可行
- 空间复杂度:O(n + m)
2494. Merge Overlapping Events in the Same Hall(合并在同一个大厅重叠的活动)
题目类型:数据库(SQL)
题目描述
表: HallEvents
+-------------+------+
| Column Name | Type |
+-------------+------+
| hall_id | int |
| start_day | date |
| end_day | date |
+-------------+------+
该表可能包含重复字段。该表的每一行表示活动的开始日期和结束日期,以及活动举行的大厅。
编写解决方案,合并在同一个大厅 举行的所有重叠活动。如果两个活动至少有一天相同,那么它们就是重叠的。
以任意顺序返回结果表。
示例 1:
输入:
HallEvents 表:
+---------+------------+------------+
| hall_id | start_day | end_day |
+---------+------------+------------+
| 1 | 2023-01-13 | 2023-01-14 |
| 1 | 2023-01-14 | 2023-01-17 |
| 1 | 2023-01-18 | 2023-01-25 |
| 2 | 2022-12-09 | 2022-12-23 |
| 2 | 2022-12-13 | 2022-12-17 |
| 3 | 2022-12-01 | 2023-01-30 |
+---------+------------+------------+
输出:
+---------+------------+------------+
| hall_id | start_day | end_day |
+---------+------------+------------+
| 1 | 2023-01-13 | 2023-01-17 |
| 1 | 2023-01-18 | 2023-01-25 |
| 2 | 2022-12-09 | 2022-12-23 |
| 3 | 2022-12-01 | 2023-01-30 |
+---------+------------+------------+
解释:
大厅 1:
- 两个活动 ["2023-01-13", "2023-01-14"] 和 ["2023-01-14", "2023-01-17"] 重叠,合并为 ["2023-01-13", "2023-01-17"]。
- 活动 ["2023-01-18", "2023-01-25"] 不与任何其他活动重叠,保持原样。
大厅 2:
- 两个活动 ["2022-12-09", "2022-12-23"] 和 ["2022-12-13", "2022-12-17"] 重叠,合并为 ["2022-12-09", "2022-12-23"]。
大厅 3:
- 大厅只有一个活动,原样返回。注意:只分别考虑每个大厅的活动。
解题思路
解法一:窗口求「组起始标记」后分组(推荐)
- 区间合并经典套路:按
hall_id分组、按start_day(再按end_day)排序后,用窗口函数把每个活动与前一行比较。 - 用
MAX(end_day) OVER (PARTITION BY hall_id ORDER BY start_day, end_day ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)取「此前所有活动的最远结束日」prev_max_end。若当前start_day <= prev_max_end,说明与前序段重叠(含端点相接:>=重叠是指至少一天相同,即start_day <= prev_max_end即重叠),应归入同一组;否则开启新组。 - 用累计和(
SUM(flag) OVER (...)) 生成组号:flag = (start_day > prev_max_end) ? 1 : 0,组号 = 该行及其之前flag的累加。 - 最后按
hall_id, 组号聚合成MIN(start_day)、MAX(end_day)。
解法二(对照):NOT EXISTS 找每组合并起点 + 终点同起点归并
- 先找出「不与同一大厅任何更早活动重叠」的活动作为合并段的起点(每条记录是一个组的左端点);对每个组起点,在其左侧段内取
MAX(end_day)作为组终点------但终点归属需要通过「在同一组内但不超过起点所在组的连通范围」判断,处理较繁琐。这里为便于理解改用自连接传递闭包思路:start_day落在某个已合并段内则吸收。 - 实际推荐直接使用解法一窗口法,简洁且易读。
代码实现
sql
-- 解法一(推荐):窗口 prev_max_end + 组号累加
WITH ordered AS (
SELECT hall_id, start_day, end_day,
MAX(end_day) OVER (
PARTITION BY hall_id
ORDER BY start_day, end_day
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
) AS prev_max_end
FROM HallEvents
),
flagged AS (
SELECT hall_id, start_day, end_day,
CASE WHEN prev_max_end IS NULL OR start_day > prev_max_end
THEN 1 ELSE 0 END AS is_new_group
FROM ordered
),
grouped AS (
SELECT hall_id, start_day, end_day,
SUM(is_new_group) OVER (
PARTITION BY hall_id
ORDER BY start_day, end_day
ROWS UNBOUNDED PRECEDING
) AS grp
FROM flagged
)
SELECT hall_id,
MIN(start_day) AS start_day,
MAX(end_day) AS end_day
FROM grouped
GROUP BY hall_id, grp
ORDER BY hall_id, start_day;
sql
-- 解法二(对照):NOT EXISTS 找组起点 + 组号传播
-- 每个活动的组号定义为:向前追溯到第一个不与更早活动重叠的活动的行号
WITH ordered AS (
SELECT hall_id, start_day, end_day,
ROW_NUMBER() OVER (PARTITION BY hall_id ORDER BY start_day, end_day) AS rn
FROM HallEvents
),
is_start AS (
SELECT o1.hall_id, o1.start_day, o1.end_day, o1.rn,
CASE WHEN NOT EXISTS (
SELECT 1 FROM ordered o2
WHERE o2.hall_id = o1.hall_id
AND o2.rn < o1.rn
AND o2.start_day <= o1.end_day
AND o1.start_day <= o2.end_day
) THEN 1 ELSE 0 END AS is_new
FROM ordered o1
),
grouped AS (
SELECT hall_id, start_day, end_day,
SUM(is_new) OVER (PARTITION BY hall_id ORDER BY rn) AS grp
FROM is_start
)
SELECT hall_id,
MIN(start_day) AS start_day,
MAX(end_day) AS end_day
FROM grouped
GROUP BY hall_id, grp
ORDER BY hall_id, start_day;
复杂度分析
- 时间复杂度 :
O(n log n),n为HallEvents行数;每大厅按日期排序是主要开销。 - 空间复杂度 :
O(n),窗口函数的中间结果与行数成正比。
2495. Number of Subarrays Having Even Product(乘积为偶数的子数组数)
题目描述
给定一个整数数组 nums,返回具有偶数乘积的 nums 子数组的数目。
示例 1
`nums = [9,6,7,13]` → 6(乘积为偶数的子数组共 6 个)
示例 2
`nums = [7,3,5]` → 0(没有乘积为偶数的子数组)
提示:
1 <= nums.length <= 10^51 <= nums[i] <= 10^5
解题思路
解法一:偶数组计数取反(主提交版本)
- 乘积为偶数 ⟺ 子数组中至少含一个偶数元素。
- 总数 = n*(n+1)/2,减去「全是奇数」的子数组数。
- 对每段连续的奇数,长度为 len,贡献的纯奇子数组 = len*(len+1)/2。
解法二:滑窗计数「以 r 结尾且乘积为偶数」的子数组(对照) - 维护 lastEven = 最后一个偶数位置:以 r 结尾的乘积偶数子数组起点 ∈ 0...lastEven,
共 lastEven + 1 个;累加。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:总数 - 纯奇数子数组(主提交版本)
public long evenProduct(int[] nums) {
int n = nums.length;
long total = (long) n * (n + 1) / 2;
long odd = 0;
long cur = 0;
for (int v : nums) {
if (v % 2 == 1) {
cur++;
} else {
odd += cur * (cur + 1) / 2;
cur = 0;
}
}
odd += cur * (cur + 1) / 2;
return total - odd;
}
// 解法二:以 r 结尾累加(对照)
public long evenProductLast(int[] nums) {
long ans = 0;
int lastEven = -1;
for (int r = 0; r < nums.length; r++) {
if (nums[r] % 2 == 0) {
lastEven = r;
}
if (lastEven != -1) {
ans += lastEven + 1;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.evenProduct(new int[]{9, 6, 7, 13}) == 6 : "2495 例1 主";
assert sol.evenProductLast(new int[]{9, 6, 7, 13}) == 6 : "2495 例1 滑窗";
assert sol.evenProduct(new int[]{7, 3, 5}) == 0 : "2495 例2 主";
assert sol.evenProductLast(new int[]{7, 3, 5}) == 0 : "2495 例2 滑窗";
assert sol.evenProduct(new int[]{2}) == 1 : "2495 单偶 主";
assert sol.evenProductLast(new int[]{2}) == 1 : "2495 单偶 滑窗";
// 随机对拍
java.util.Random rnd = new java.util.Random(57);
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(60);
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = 1 + rnd.nextInt(30);
}
long a = sol.evenProduct(arr);
long b = sol.evenProductLast(arr);
if (a != b) {
System.out.println("对拍失败 " + java.util.Arrays.toString(arr) + " 主=" + a + " 滑窗=" + b);
System.exit(1);
}
}
System.out.println("2495 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2496. Maximum Value of a String in an Array(数组中字符串的最大值)
题目描述
一个由字母和数字组成的字符串的值定义如下:
- 如果字符串只包含数字,那么值为该字符串在 10 进制下所表示的数字。
- 否则,值为字符串的长度。
给你一个字符串数组strs,每个字符串都只由字母和数字组成,返回strs中字符串的最大值。
示例 1
`strs = ["alic3","bob","3","4","00000"]` → 5("00000" 纯数字值为 0,最大为 "alic3" 的长度 5)
示例 2
`strs = ["1","01","001","0001"]` → 1(全是纯数字,值都为 1)
提示:
1 <= strs.length <= 1001 <= strs[i].length <= 9strs[i]只包含小写英文字母和数字
解题思路
解法一:逐串判定(主提交版本)
- 对每个字符串:检查是否全为数字;是则用 Integer.parseInt 求值,否则取长度。
- 维护全局最大值。
解法二:正则匹配(对照) - 用
matches("\\d+")判断纯数字,逻辑等价。
复杂度: - 时间复杂度:O(总字符数)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:逐串判定(主提交版本)
public int maximumValue(String[] strs) {
int ans = 0;
for (String s : strs) {
boolean allDigit = true;
for (int i = 0; i < s.length(); i++) {
if (!Character.isDigit(s.charAt(i))) {
allDigit = false;
break;
}
}
int val = allDigit ? Integer.parseInt(s) : s.length();
ans = Math.max(ans, val);
}
return ans;
}
// 解法二:正则(对照)
public int maximumValueRegex(String[] strs) {
int ans = 0;
for (String s : strs) {
int val = s.matches("\\d+") ? Integer.parseInt(s) : s.length();
ans = Math.max(ans, val);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.maximumValue(new String[]{"alic3", "bob", "3", "4", "00000"}) == 5 : "2496 例1 主";
assert sol.maximumValueRegex(new String[]{"alic3", "bob", "3", "4", "00000"}) == 5 : "2496 例1 正则";
assert sol.maximumValue(new String[]{"1", "01", "001", "0001"}) == 1 : "2496 例2 主";
assert sol.maximumValueRegex(new String[]{"1", "01", "001", "0001"}) == 1 : "2496 例2 正则";
assert sol.maximumValue(new String[]{"000"}) == 0 : "2496 全零主";
assert sol.maximumValueRegex(new String[]{"000"}) == 0 : "2496 全零正则";
System.out.println("2496 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(总字符数)
- 空间复杂度:O(1)
2497. Maximum Star Sum of a Graph(图中最大星和)
题目描述
给你一个 n 个点的无向图,节点从 0 到 n-1 编号。给你一个长度为 n 的下标从 0 开始的整数数组
vals,其中 vals[i] 表示第 i 个节点的值。
edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条双向边。
星图是给定图中的一个子图,它包含一个中心节点和 0 个或更多个邻居。
星和定义为星图中所有节点值的和。
给你一个整数 k,返回至多包含 k 条边的星图中的最大星和。
示例 1
`vals = [1,2,3,4,10,-10,-20], edges = [[0,1],[1,2],[1,3],[3,4],[3,5],[3,6]], k = 2` → 16(中心 3 + 邻居 1、4)
示例 2
`vals = [-5], edges = [], k = 0` → -5(只有一个星图,节点 0 自己)
提示:
n == vals.length,1 <= n <= 10^5-10^4 <= vals[i] <= 10^40 <= edges.length <= min(n*(n-1)/2, 10^5)0 <= k <= n - 1
解题思路
解法一:贪心取正邻居(主提交版本)
- 星图以某节点 u 为中心,可选的邻居为 u 的邻接点。为最大化星和:
- 中心值固定要取(无论正负,中心都计入该星图)。
- 邻居中最多取 k 个,显然取「值为正且最大」的若干邻居;值为负的邻居不取(除非必须,
但 k 是「至多」,可取 0 条边,因此负邻居一律不取)。 - 实现:为每个节点收集邻接点值,排序后累加前 min(k, 正数个数) 个正数,加上中心值取 max。
解法二:小顶堆维护 Top-k 正邻居(对照) - 对每个节点维护 size ≤ k 的小顶堆只存正邻居,堆和为候选星和,取 max。
复杂度: - 时间复杂度:O(m + Σ deg(u) log deg(u)),可简化 O(m log m)
- 空间复杂度:O(n + m)
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:邻接值排序贪心(主提交版本)
public int maxStarSum(int[] vals, int[][] edges, int k) {
int n = vals.length;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
for (int[] e : edges) {
adj.get(e[0]).add(vals[e[1]]);
adj.get(e[1]).add(vals[e[0]]);
}
int ans = Integer.MIN_VALUE;
for (int u = 0; u < n; u++) {
List<Integer> neighbors = adj.get(u);
neighbors.sort((a, b) -> b - a); // 降序
int sum = vals[u];
int take = Math.min(k, neighbors.size());
for (int i = 0; i < take; i++) {
if (neighbors.get(i) > 0) {
sum += neighbors.get(i);
} else {
break; // 降序,后续都 ≤ 0
}
}
ans = Math.max(ans, sum);
}
return ans;
}
// 解法二:小顶堆 Top-k(对照)
public int maxStarSumHeap(int[] vals, int[][] edges, int k) {
int n = vals.length;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
for (int[] e : edges) {
adj.get(e[0]).add(vals[e[1]]);
adj.get(e[1]).add(vals[e[0]]);
}
int ans = Integer.MIN_VALUE;
for (int u = 0; u < n; u++) {
java.util.PriorityQueue<Integer> pq = new java.util.PriorityQueue<>(); // 小顶堆
int sum = vals[u];
for (int w : adj.get(u)) {
if (w <= 0) {
continue;
}
if (pq.size() < k) {
pq.offer(w);
sum += w;
} else if (!pq.isEmpty() && w > pq.peek()) {
sum += w - pq.poll();
pq.offer(w);
}
}
ans = Math.max(ans, sum);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.maxStarSum(new int[]{1, 2, 3, 4, 10, -10, -20},
new int[][]{{0, 1}, {1, 2}, {1, 3}, {3, 4}, {3, 5}, {3, 6}}, 2) == 16 : "2497 例1 主";
assert sol.maxStarSumHeap(new int[]{1, 2, 3, 4, 10, -10, -20},
new int[][]{{0, 1}, {1, 2}, {1, 3}, {3, 4}, {3, 5}, {3, 6}}, 2) == 16 : "2497 例1 堆";
assert sol.maxStarSum(new int[]{-5}, new int[][]{}, 0) == -5 : "2497 例2 主";
assert sol.maxStarSumHeap(new int[]{-5}, new int[][]{}, 0) == -5 : "2497 例2 堆";
// 随机对拍
java.util.Random rnd = new java.util.Random(59);
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(15);
int[] vals = new int[n];
for (int i = 0; i < n; i++) {
vals[i] = -10 + rnd.nextInt(21);
}
java.util.Set<Long> used = new java.util.HashSet<>();
List<int[]> es = new ArrayList<>();
int m = rnd.nextInt(Math.max(1, n * (n - 1) / 2));
for (int i = 0; i < m; i++) {
int a = rnd.nextInt(n), b = rnd.nextInt(n);
if (a == b || !used.add(key(a, b))) {
continue;
}
es.add(new int[]{a, b});
}
int[][] ee = es.toArray(new int[0][]);
int k = rnd.nextInt(Math.max(1, n));
int x = sol.maxStarSum(vals, ee, k);
int y = sol.maxStarSumHeap(vals, ee, k);
if (x != y) {
System.out.println("对拍失败 vals=" + java.util.Arrays.toString(vals)
+ " edges=" + java.util.Arrays.deepToString(ee) + " k=" + k + " 主=" + x + " 堆=" + y);
System.exit(1);
}
}
System.out.println("2497 全部 PASS");
}
private static long key(int a, int b) {
return a < b ? a * 1000L + b : b * 1000L + a;
}
}
复杂度分析
- 时间复杂度:O(m + Σ deg(u) log deg(u)),可简化 O(m log m)
- 空间复杂度:O(n + m)
2498. Frog Jump II(青蛙过河 II)
题目描述
给你一个下标从 0 开始的整数数组 stones,数组中的元素严格递增,表示一条河中石头的位置。
一只青蛙一开始在第一块石头上,它想到达最后一块石头,然后回到第一块石头。同时每块石头至多到达一次。
一次跳跃的长度是青蛙跳跃前和跳跃后所在两块石头之间的距离。
一条路径的代价是这条路径里的最大跳跃长度。
返回这只青蛙的最小代价。
示例 1
`stones = [0,2,5,6,7]` → 5
示例 2
`stones = [0,3,9]` → 9
提示:
2 <= stones.length <= 10^50 <= stones[i] <= 10^9stones[0]== 0,元素严格递增
解题思路
解法一:贪心跳「隔一格」(主提交版本)
- 去程最优策略是「跳过一块石头落下一块」:从 0 → 2 → 4 → ...,回程落在奇数位 → ... → 1 → 0。
这样每块石头恰好踩一次(终点/起点除外),且任意相邻两次使用的石头不相邻,最大跳跃长度
出现在「隔着的那一跳」。 - 形式化:最小代价 = max(stonesi - stonesi-2)(i 从 2 到 n-1),即隔一块石头的最大间距。
因为:去程走偶数下标(0,2,4,...),回程走奇数下标(...,5,3,1,0),任何一跳都是
stonesi - stonesi-2(或回程同样)。这些覆盖了所有连续两次使用的石头,故答案是隔距最大值。
解法二:二分答案 + 可行性检查(对照) - 对答案 X 二分:检查是否存在一条路径使每跳 ≤ X。可行性:去程贪心跳最远可行石头(不同时被
回程占用)。标准判定:每隔一块石头能被「隔跳」覆盖------直接暴力 check:用双数组奇偶交替模拟。
更简单:若 X ≥ max(stonesi - stonesi-2) 即可行。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(1)
代码实现
java
public class Solution {
// 解法一:隔一跳即答案(主提交版本)
public int maxJump(int[] stones) {
int n = stones.length;
int ans = 0;
// 去程:0,2,4,...;回程:...,3,1,0 → 每跳都是隔一块
// 还需考虑最后一跳:无论去程到终点 n-1,回程从 n-2 起?终点 n-1 只到达一次。
// 最小的最大跳 = 全局「隔一格」最大(含首尾特殊处理:去程 0->2 的最大 gap)
// 标准解法:ans = max over i>=2 of (stones[i] - stones[i-2]),
// 另考虑 n=2 时只有直接跳:ans = stones[1] - stones[0]。
for (int i = 2; i < n; i++) {
ans = Math.max(ans, stones[i] - stones[i - 2]);
}
// 首尾边界:从 0 跳到终点(n-1)与从 n-1 跳回 n-2(若只有两块则涵盖在 i=2 None)
if (n == 2) {
ans = Math.max(ans, stones[1] - stones[0]);
} else {
ans = Math.max(ans, stones[1] - stones[0]); // 回程最后一跳 1 -> 0
ans = Math.max(ans, stones[n - 1] - stones[n - 2]); // 去程最后一段(若 n-1 偶)
}
return ans;
}
// 解法二:二分答案 + 可行性(对照)
public int maxJumpBinary(int[] stones) {
int n = stones.length;
int lo = 0, hi = stones[n - 1];
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (feasible(stones, mid)) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
// 可行性:存在一条去程+回程路径每跳<=X,每块石头至多一次
private boolean feasible(int[] stones, int x) {
int n = stones.length;
boolean[] used = new boolean[n]; // 模拟去程偶位置、回程奇位置
// 去程:从 0 出发尽量跳到 <= x 的最远未用石头;由于要保证回程也有路,
// 标准构造:去程走 0,2,4,...;回程 1,3,5,...
int last = 0;
used[0] = true;
for (int i = 1; i < n; i++) {
if (i >= 2 && stones[i] - stones[i - 2] > x) {
return false;
}
}
// 上述判断只覆盖隔跳;还需对去程/回程相邻跳(i 与 i-1 无跳跃,回程相邻差同):
// 去程与回程相邻使用的两石之间最大为 stones[i]-stones[i-2];首尾特殊:
if (n == 2) {
return stones[1] - stones[0] <= x;
}
// 回程最后:1->0,差 stones[1]-stones[0]
if (stones[1] - stones[0] > x) {
return false;
}
// 去程第一跳 0->(最近的>=2 的石头) 由隔跳覆盖;去程到终点的跳由 i>=2 覆盖(n-1 与 n-3)
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.maxJump(new int[]{0, 2, 5, 6, 7}) == 5 : "2498 例1 主: " + sol.maxJump(new int[]{0, 2, 5, 6, 7});
assert sol.maxJumpBinary(new int[]{0, 2, 5, 6, 7}) == 5 : "2498 例1 二分";
assert sol.maxJump(new int[]{0, 3, 9}) == 9 : "2498 例2 主";
assert sol.maxJumpBinary(new int[]{0, 3, 9}) == 9 : "2498 例2 二分";
assert sol.maxJump(new int[]{0, 1}) == 1 : "2498 两块 主";
assert sol.maxJumpBinary(new int[]{0, 1}) == 1 : "2498 两块 二分";
// 随机对拍
java.util.Random rnd = new java.util.Random(61);
for (int t = 0; t < 300; t++) {
int n = 2 + rnd.nextInt(20);
int[] stones = new int[n];
stones[0] = 0;
for (int i = 1; i < n; i++) {
stones[i] = stones[i - 1] + 1 + rnd.nextInt(20);
}
int a = sol.maxJump(stones);
int b = sol.maxJumpBinary(stones);
if (a != b) {
System.out.println("对拍失败 " + java.util.Arrays.toString(stones) + " 主=" + a + " 二分=" + b);
System.exit(1);
}
}
// 暴力 BFS 验证一个小例子的正确性(每块至多一次)
assert bruteVerify(new int[]{0, 2, 5, 6, 7}) == 5 : "2498 例1 暴力验证";
System.out.println("2498 全部 PASS");
}
// 暴力:全排列式 DFS 验证(仅小规模)
private static int bruteVerify(int[] stones) {
int n = stones.length;
int[] best = {Integer.MAX_VALUE};
boolean[] used = new boolean[n];
used[0] = true;
dfs(0, n - 1, 0, 0, stones, used, false, best);
return best[0];
}
private static void dfs(int pos, int target, int curMax, int step, int[] stones,
boolean[] used, boolean returning, int[] best) {
if (step == stones.length - 1) {
// 必须回到 0
if (pos == 0 && returning) {
best[0] = Math.min(best[0], curMax);
return;
}
if (pos == target && !returning) {
// 终点 → 开始回程
dfs(pos, 0, curMax, step + 1, stones, used, true, best);
}
return;
}
if (returning && pos == 0) {
best[0] = Math.min(best[0], curMax);
return;
}
for (int i = 0; i < stones.length; i++) {
if (used[i]) {
continue;
}
int jump = Math.abs(stones[pos] - stones[i]);
if (returning) {
if (i != 0 && i != target) { /* 一般位置可跳 */ }
}
used[i] = true;
dfs(i, target, Math.max(curMax, jump), step + 1, stones, used, returning, best);
used[i] = false;
}
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2499. Minimum Total Cost to Make Arrays Unequal(让数组不相等的最小总代价)
题目描述
给你两个下标从 0 开始的整数数组 nums1 和 nums2,两者长度都为 n。
每次操作中,你可以选择交换 nums1 中任意两个下标处的值。操作的开销为两个下标的和。
你的目标是对于所有的 0 <= i <= n-1,都满足 nums1[i] != nums2[i],可以进行任意次操作,
返回达到这个目标的最小总代价。如果无法达成目标,返回 -1。
示例 1
`nums1 = [1,2,3,4,5], nums2 = [1,2,3,4,5]` → 10
示例 2
`nums1 = [2,2,2,1,3], nums2 = [1,2,2,3,3]` → 10
示例 3
`nums1 = [1,2,2], nums2 = [1,2,2]` → -1
提示:
n == nums1.length == nums2.length1 <= n <= 10^51 <= nums1[i], nums2[i] <= n
解题思路
解法一:冲突位分析 + 贪心凑对(主提交版本)
- 冲突位置集合 bad(nums1i == nums2i)。每个冲突位 i 的值 nums1i 最终必须被换走,
因此 i 至少要参与一次交换,代价贡献 ≥ i → 总代价下界 = sum(bad)。 - 冲突位之间可以直接互换:两个冲突位 i、j(对应冲突值 v1 != v2)互换后双双变好,
每次交换刚好覆盖两个冲突位 → 下界可达。 - 但若某个冲突值 maxV 出现次数 c > m - c(m = |bad|),先让非 maxV 冲突位与 maxV 冲突位
两两互配(最多 m-c 对),还剩 extra = 2c - m 个 maxV 冲突位找不到冲突位伙伴,
只能与非冲突位 j 交换:要求交换后 j 不产生新冲突,即
nums1[j] != maxV(j 换入 maxV 后不能等于自身新值?不------j 得到 maxV,要求 maxV != nums2j)
且nums2[j] != maxV(防止新冲突)。此类外部位每个解决一个冲突位,代价额外 + j。 - 为最小化选最小下标的 extra 个外部位;不够则返回 -1。
最终答案 = sum(bad) + sum(选中的外部位下标)。
解法二:Dijkstra 置换图暴力(对照,仅小规模对拍) - 把「数组状态」作为节点、交换为边(代价 i+j),从初始状态跑最短路,
在所有 nums1i != nums2i 的合法状态中取最小代价。
复杂度: - 时间复杂度:O(n)
- 空间复杂度:O(n)
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// 解法一:冲突位分析 + 贪心凑对(主提交版本)
public long minimumTotalCost(int[] nums1, int[] nums2) {
int n = nums1.length;
long base = 0;
int m = 0;
int[] freqBad = new int[n + 1];
List<Integer> badIdx = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (nums1[i] == nums2[i]) {
int v = nums1[i];
freqBad[v]++;
badIdx.add(i);
base += i;
m++;
}
}
if (m == 0) {
return 0;
}
int maxV = 0;
for (int v = 1; v <= n; v++) {
if (freqBad[v] > freqBad[maxV]) {
maxV = v;
}
}
int c = freqBad[maxV];
int extra = c - (m - c); // 超出内部可互配数量的 maxV 冲突位
if (extra > 0) {
// 收集外部(非冲突)位:nums1[j] != maxV 且 nums2[j] != maxV
List<Integer> outIdx = new ArrayList<>();
for (int j = 0; j < n; j++) {
if (nums1[j] != nums2[j] && nums1[j] != maxV && nums2[j] != maxV) {
outIdx.add(j);
if (outIdx.size() == extra) {
break;
}
}
}
if (outIdx.size() < extra) {
return -1;
}
for (int j : outIdx) {
base += j;
}
}
return base;
}
// 解法二:Dijkstra 置换图(对照,小规模对拍)
public long minimumTotalCostDijkstra(int[] nums1, int[] nums2) {
int n = nums1.length;
long[] init = new long[n];
for (int i = 0; i < n; i++) {
init[i] = nums1[i];
}
Map<String, Long> dist = new HashMap<>();
String start = encode(init);
dist.put(start, 0L);
java.util.PriorityQueue<long[]> pq = new java.util.PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
// 编码成本太高,改用 String -> long 代价,这里用简单状态: 直接塞入编码后的长整数
// 为了简化,返回一个由递归深搜代替(n<=7 时状态数 ≤ 5040,可直接 BFS 全部枚举)。
best4 = Long.MAX_VALUE;
dfs(new long[n], 0, nums1, nums2, 0, new boolean[n]);
return best4 == Long.MAX_VALUE ? -1 : best4;
}
private long best4 = Long.MAX_VALUE;
// 深搜构造「当前排列 cur」------复杂度 n! 太大;改用 BFS 交换。这里用置换目标枚举 + 环代价计算。
// 简化实现:直接用「枚举目标排列 + Dijkstra 距离」在 main 里用更小规模。
private void dfs(long[] cur, int pos, int[] nums1, int[] nums2, long cost, boolean[] used) {
// 未使用;实际对拍走 minimumTotalCostSearch
}
// 正确暴力:从初始数组 BFS 全部可达状态,取合法最小代价(n ≤ 6)
public long minimumTotalCostSearch(int[] nums1, int[] nums2) {
int n = nums1.length;
Map<Long, Long> dist = new HashMap<>();
long start = encodeLong(nums1);
dist.put(start, 0L);
java.util.PriorityQueue<long[]> pq = new java.util.PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
pq.offer(new long[]{start, 0});
long best = Long.MAX_VALUE;
while (!pq.isEmpty()) {
long[] top = pq.poll();
long state = top[0];
long d = top[1];
if (d > dist.getOrDefault(state, Long.MAX_VALUE)) {
continue;
}
int[] arr = decodeLong(state, n);
boolean valid = true;
for (int i = 0; i < n; i++) {
if (arr[i] == nums2[i]) {
valid = false;
break;
}
}
if (valid) {
best = Math.min(best, d);
// 不能提前终止:还要继续探索更优路径(Dijkstra 首弹即最优,但合法状态未必首弹)
// 继续即可
}
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int[] nxt = arr.clone();
int tmp = nxt[i];
nxt[i] = nxt[j];
nxt[j] = tmp;
long ns = encodeLong(nxt);
long nd = d + i + j;
if (nd < dist.getOrDefault(ns, Long.MAX_VALUE)) {
dist.put(ns, nd);
pq.offer(new long[]{ns, nd});
}
}
}
}
return best == Long.MAX_VALUE ? -1 : best;
}
private String encode(long[] a) {
StringBuilder sb = new StringBuilder();
for (long x : a) {
sb.append(x).append(',');
}
return sb.toString();
}
private long encodeLong(int[] a) {
long key = 0;
for (int i = 0; i < a.length; i++) {
key = key * 37 + a[i]; // 值 ≤ n ≤ 6,37 进制足够
}
return key;
}
private int[] decodeLong(long key, int n) {
int[] a = new int[n];
for (int i = n - 1; i >= 0; i--) {
a[i] = (int) (key % 37);
key /= 37;
}
return a;
}
public static void main(String[] args) {
Solution sol = new Solution();
// 官方示例
assert sol.minimumTotalCost(new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5}) == 10 : "2499 例1 主: " + sol.minimumTotalCost(new int[]{1, 2, 3, 4, 5}, new int[]{1, 2, 3, 4, 5});
assert sol.minimumTotalCost(new int[]{2, 2, 2, 1, 3}, new int[]{1, 2, 2, 3, 3}) == 10 : "2499 例2 主: " + sol.minimumTotalCost(new int[]{2, 2, 2, 1, 3}, new int[]{1, 2, 2, 3, 3});
assert sol.minimumTotalCost(new int[]{1, 2, 2}, new int[]{1, 2, 2}) == -1 : "2499 例3 主: " + sol.minimumTotalCost(new int[]{1, 2, 2}, new int[]{1, 2, 2});
assert sol.minimumTotalCost(new int[]{1, 2, 3}, new int[]{2, 3, 1}) == 0 : "2499 无冲突主";
// 小规模随机对拍(n ≤ 6)
java.util.Random rnd = new java.util.Random(67);
for (int t = 0; t < 400; t++) {
int n = 1 + rnd.nextInt(6);
int[] a = new int[n];
int[] b = new int[n];
for (int i = 0; i < n; i++) {
a[i] = 1 + rnd.nextInt(Math.max(1, n)); // 值域小 → 冲突概率高
b[i] = 1 + rnd.nextInt(Math.max(1, n));
}
long x = sol.minimumTotalCost(a, b);
long y = sol.minimumTotalCostSearch(a, b);
if (x != y) {
System.out.println("对拍失败 nums1=" + java.util.Arrays.toString(a)
+ " nums2=" + java.util.Arrays.toString(b) + " 主=" + x + " 搜索=" + y);
System.exit(1);
}
}
// 特别验证官方可能反例:大量相同冲突值
assert sol.minimumTotalCost(new int[]{1, 1, 1}, new int[]{1, 1, 1}) == -1 : "2499 全同主";
assert sol.minimumTotalCost(new int[]{1, 1, 2, 2}, new int[]{1, 1, 2, 2}) == 2 : "2499 双对主: " + sol.minimumTotalCost(new int[]{1, 1, 2, 2}, new int[]{1, 1, 2, 2});
System.out.println("2499 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2500. Delete Greatest Value in Each Row(删除每行中的最大值)
题目描述
给你一个 m x n 大小的矩阵 grid,由若干正整数组成。
执行下述操作,直到 grid 变为空矩阵:
- 从每一行删除值最大的元素。如果存在多个这样的值,删除其中任何一个。
- 将删除元素中的最大值与答案相加。
注意每执行一次操作,矩阵中列的数据就会减 1。
返回执行上述操作后的答案。
示例 1
`grid = [[1,2,4],[3,3,1]]` → 8(4+3+1)
示例 2
`grid = [[10]]` → 10
提示:
m==grid.length,n==grid[i].length1 <= m, n <= 501 <= grid[i][j] <= 100
解题思路
解法一:逐行排序 + 列最大值累加(主提交版本)
- 每轮删除「每行当前最大值」等价于把每行降序排列后取第 t 列的值(t 从 0 到 n-1),
累加该列的最大值。 - 实现:每行升序排序,对每一列 j 求所有行 gridij 的最大值并累加。
解法二:直接模拟删除(对照) - 实际循环 n 轮,每轮从每行找出未删的最大值(标记删除),累加本轮最大值。
复杂度: - 时间复杂度:O(m * n log n)
- 空间复杂度:O(1)
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:逐行排序 + 列最大值累加(主提交版本)
public int deleteGreatestValue(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for (int i = 0; i < m; i++) {
Arrays.sort(grid[i]); // 升序,第 t 列即第 t 轮删除值
}
int ans = 0;
for (int j = 0; j < n; j++) {
int colMax = 0;
for (int i = 0; i < m; i++) {
colMax = Math.max(colMax, grid[i][j]);
}
ans += colMax;
}
return ans;
}
// 解法二:直接模拟(对照)
public int deleteGreatestValueSim(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
boolean[][] removed = new boolean[m][n];
int ans = 0;
for (int round = 0; round < n; round++) {
int roundMax = 0;
for (int i = 0; i < m; i++) {
int bestVal = -1, bestJ = -1;
for (int j = 0; j < n; j++) {
if (!removed[i][j] && grid[i][j] > bestVal) {
bestVal = grid[i][j];
bestJ = j;
}
}
removed[i][bestJ] = true;
roundMax = Math.max(roundMax, bestVal);
}
ans += roundMax;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
assert sol.deleteGreatestValue(new int[][]{{1, 2, 4}, {3, 3, 1}}) == 8 : "2500 例1 主";
assert sol.deleteGreatestValueSim(new int[][]{{1, 2, 4}, {3, 3, 1}}) == 8 : "2500 例1 模拟";
assert sol.deleteGreatestValue(new int[][]{{10}}) == 10 : "2500 例2 主";
assert sol.deleteGreatestValueSim(new int[][]{{10}}) == 10 : "2500 例2 模拟";
// 随机对拍
java.util.Random rnd = new java.util.Random(71);
for (int t = 0; t < 300; t++) {
int m = 1 + rnd.nextInt(8);
int n = 1 + rnd.nextInt(8);
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(50);
}
}
int a = sol.deleteGreatestValue(g);
int b = sol.deleteGreatestValueSim(g);
if (a != b) {
System.out.println("对拍失败 " + Arrays.deepToString(g) + " 主=" + a + " 模拟=" + b);
System.exit(1);
}
}
System.out.println("2500 全部 PASS");
}
}
复杂度分析
- 时间复杂度:O(m * n log n)
- 空间复杂度:O(1)