LeetCode 2841-2860. 题解合集
文章目录
- [LeetCode 2841-2860. 题解合集](#LeetCode 2841-2860. 题解合集)
-
- [2841. Maximum Sum of Almost Unique Subarray(几乎唯一子数组的最大和)](#2841. Maximum Sum of Almost Unique Subarray(几乎唯一子数组的最大和))
- [2842. Count K-Subsequences of a String With Maximum Beauty(统计一个字符串的 k 子序列美丽值最大的数目)](#2842. Count K-Subsequences of a String With Maximum Beauty(统计一个字符串的 k 子序列美丽值最大的数目))
- [2843. Count Symmetric Integers(统计对称整数的数目)](#2843. Count Symmetric Integers(统计对称整数的数目))
- [2844. Minimum Operations to Make a Special Number(生成特殊数字的最少操作)](#2844. Minimum Operations to Make a Special Number(生成特殊数字的最少操作))
- [2845. Count of Interesting Subarrays(统计趣味子数组的数目)](#2845. Count of Interesting Subarrays(统计趣味子数组的数目))
- [2846. Minimum Edge Weight Equilibrium Queries in a Tree(边权重均等查询)](#2846. Minimum Edge Weight Equilibrium Queries in a Tree(边权重均等查询))
- [2847. Smallest Number With Given Digit Product(给定数字乘积的最小数字)](#2847. Smallest Number With Given Digit Product(给定数字乘积的最小数字))
- [2848. Points That Intersect With Cars(与车相交的点)](#2848. Points That Intersect With Cars(与车相交的点))
- [2849. Determine if a Cell Is Reachable at a Given Time(判断能否在给定时间到达单元格)](#2849. Determine if a Cell Is Reachable at a Given Time(判断能否在给定时间到达单元格))
- [2850. Minimum Moves to Spread Stones Over Grid(将石头分散到网格图的最少移动次数)](#2850. Minimum Moves to Spread Stones Over Grid(将石头分散到网格图的最少移动次数))
- [2851. String Transformation(字符串转换)](#2851. String Transformation(字符串转换))
- [2852. Sum of Remoteness of All Cells(所有单元格的远离程度之和)](#2852. Sum of Remoteness of All Cells(所有单元格的远离程度之和))
- [2853. Highest Salaries Difference(最高薪水差异)](#2853. Highest Salaries Difference(最高薪水差异))
- [2854. Rolling Average Steps(滚动平均步数)](#2854. Rolling Average Steps(滚动平均步数))
- [2855. Minimum Right Shifts to Sort the Array(使数组成为递增数组的最少右移次数)](#2855. Minimum Right Shifts to Sort the Array(使数组成为递增数组的最少右移次数))
- [2856. Minimum Array Length After Pair Removals(删除数对后的最小数组长度)](#2856. Minimum Array Length After Pair Removals(删除数对后的最小数组长度))
- [2857. Count Pairs of Points With Distance k(统计距离为 k 的点对)](#2857. Count Pairs of Points With Distance k(统计距离为 k 的点对))
- [2858. Minimum Edge Reversals So Every Node Is Reachable(可以到达每一个节点的最少边反转次数)](#2858. Minimum Edge Reversals So Every Node Is Reachable(可以到达每一个节点的最少边反转次数))
- [2859. Sum of Values at Indices With K Set Bits(计算 K 置位下标对应元素的和)](#2859. Sum of Values at Indices With K Set Bits(计算 K 置位下标对应元素的和))
- [2860. Happy Students(让所有学生保持开心的分组方法数)](#2860. Happy Students(让所有学生保持开心的分组方法数))
2841. Maximum Sum of Almost Unique Subarray(几乎唯一子数组的最大和)
题目描述
给你一个整数数组 nums 和两个正整数 m 和 k。
请你返回 nums 中长度为 k 的「几乎唯一」子数组的最大和,如果不存在几乎唯一子数组则返回 0。
如果 nums 的一个子数组有至少 m 个互不相同的元素,我们称它是几乎唯一子数组。
示例 1
- 输入 :
nums= 2,6,7,3,1,7,m= 3,k= 4 - 输出:18
示例 2
- 输入 :
nums= 5,9,9,2,4,5,4,m= 1,k= 3 - 输出:23
示例 3
- 输入 :
nums= 1,2,1,2,1,2,1,m= 3,k= 3 - 输出 :0
提示: 1 <= nums.length <= 2 * 10^41 <= m <= k <= nums.length1 <= nums[i] <= 10^9
解题思路
解法一:定长滑动窗口
固定窗口长度 k,用一个 HashMap 维护窗口内元素频次,用 sum 维护窗口和,
进入一个新元素、移出一个旧元素后,若 map.size() >= m 则更新最大和。
时间复杂度 O(n),空间复杂度 O(k)。
解法二:暴力枚举所有 k 长子数组
小规模直接枚举并统计不同元素个数(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// ---------- 解法一:定长滑动窗口(主提交版本) ----------
public long maxSum(List<Integer> nums, int m, int k) {
int n = nums.size();
Map<Integer, Integer> cnt = new HashMap<>();
long sum = 0;
long ans = 0;
for (int i = 0; i < n; i++) {
int v = nums.get(i);
cnt.put(v, cnt.getOrDefault(v, 0) + 1);
sum += v;
if (i >= k) {
int out = nums.get(i - k);
int c = cnt.get(out);
if (c == 1) cnt.remove(out);
else cnt.put(out, c - 1);
sum -= out;
}
if (i >= k - 1 && cnt.size() >= m) ans = Math.max(ans, sum);
}
return ans;
}
// ---------- 解法二:暴力(对照) ----------
public long maxSumBrute(List<Integer> nums, int m, int k) {
int n = nums.size();
long ans = 0;
for (int l = 0; l + k <= n; l++) {
Map<Integer, Integer> cnt = new HashMap<>();
long sum = 0;
for (int i = l; i < l + k; i++) {
int v = nums.get(i);
cnt.put(v, cnt.getOrDefault(v, 0) + 1);
sum += v;
}
if (cnt.size() >= m) ans = Math.max(ans, sum);
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.maxSum(new ArrayList<>(java.util.Arrays.asList(2, 6, 7, 3, 1, 7)), 3, 4) == 18, "2841 例1");
check(s.maxSum(new ArrayList<>(java.util.Arrays.asList(5, 9, 9, 2, 4, 5, 4)), 1, 3) == 23, "2841 例2");
check(s.maxSum(new ArrayList<>(java.util.Arrays.asList(1, 2, 1, 2, 1, 2, 1)), 3, 3) == 0, "2841 例3");
java.util.Random rnd = new java.util.Random(61);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(12);
int k = 1 + rnd.nextInt(n);
int m = 1 + rnd.nextInt(k);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(6));
long x = s.maxSum(a, m, k);
long y = s.maxSumBrute(a, m, k);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " m=" + m + " k=" + k + " 滑窗=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2841 随机对拍");
System.out.println("2841 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(k)。
2842. Count K-Subsequences of a String With Maximum Beauty(统计一个字符串的 k 子序列美丽值最大的数目)
题目描述
给你一个字符串 s 和一个整数 k。
k 子序列指的是 s 的一个长度为 k 的子序列,且所有字符都是唯一的,即每个字符在子序列里只出现一次。
定义 f(c) 为字符 c 在 s 中出现的次数。k 子序列的美丽值定义为这个子序列中每一个字符 c 的 f(c) 之和。
返回所有 k 子序列里面美丽值是最大值的子序列数目,结果对 10^9 + 7 取余。
注意:两个 k 子序列如果有任何一个字符在原字符串中的下标不同,则它们是两个不同的子序列,
所以两个不同的 k 子序列可能产生相同的字符串。
示例 1
- 输入 :
s= "bcca",k= 2 - 输出:4
示例 2
- 输入 :
s= "abbcd",k= 4 - 输出 :2
提示: 1 <= s.length <= 2 * 10^51 <= k <= s.lengths只包含小写英文字母。
解题思路
解法一:贪心 + 组合数学
统计每个字符出现次数 freq,只保留 freq > 0 的,降序排序。
美丽值最大 = 选出现次数最多的 k 个字符。设第 k 个字符的出现次数为 boundary:
- 前 k 个中大于 boundary 的字符是「必选」;
- 等于 boundary 的字符共 total 个,其中正好有 inside 个位于前 k,
需要从 total 个中选 inside 个组合(C(total, inside))。
答案 = (Π 必选字符的 freq) × C(total, inside) × boundary^inside,全部 mod 1e9+7。
若不同字符总数 < k,返回 0。
时间复杂度 O(n + 26),空间复杂度 O(1)。
解法二:暴力组合枚举
字符集小时枚举所有 C(m, k) 个子集计算美丽值取最大并计数(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Solution {
static final int MOD = 1_000_000_007;
// ---------- 解法一:贪心 + 组合数(主提交版本) ----------
public int countKSubsequencesWithMaxBeauty(String s, int k) {
int[] freq = new int[26];
for (int i = 0; i < s.length(); i++) freq[s.charAt(i) - 'a']++;
List<Integer> list = new ArrayList<>();
for (int f : freq) if (f > 0) list.add(f);
if (list.size() < k) return 0;
Collections.sort(list, Collections.reverseOrder());
int boundary = list.get(k - 1);
int total = 0, inside = 0;
for (int i = 0; i < list.size(); i++) {
if (list.get(i) != boundary) continue;
total++;
if (i < k) inside++;
}
long ans = 1;
// 乘积:前 k 个中 > boundary 的必选字符
for (int i = 0; i < k; i++) {
if (list.get(i) > boundary) ans = ans * list.get(i) % MOD;
}
// C(total, inside):从 total 个同频字符中选 inside 个
ans = ans * comb(total, inside) % MOD;
// boundary^inside
ans = ans * modPow(boundary, inside) % MOD;
return (int) ans;
}
// 组合数 C(n, r)(n <= 26,杨辉三角)
static long comb(int n, int r) {
if (r < 0 || r > n) return 0;
long[][] C = new long[n + 1][n + 1];
for (int i = 0; i <= n; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
}
}
return C[n][r];
}
static long modPow(long a, long e) {
long r = 1;
while (e > 0) {
if ((e & 1) == 1) r = r * a % MOD;
a = a * a % MOD;
e >>= 1;
}
return r;
}
// ---------- 解法二:暴力组合枚举(对照,字符集小) ----------
public int countKSubsequencesWithMaxBeautyBrute(String s, int k) {
int[] freq = new int[26];
for (int i = 0; i < s.length(); i++) freq[s.charAt(i) - 'a']++;
List<Integer> chars = new ArrayList<>();
for (int i = 0; i < 26; i++) if (freq[i] > 0) chars.add(i);
if (chars.size() < k) return 0;
int m = chars.size();
long maxBeauty = -1, way = 0;
for (int mask = 0; mask < (1 << m); mask++) {
if (Integer.bitCount(mask) != k) continue;
long beauty = 0, ways = 1;
for (int i = 0; i < m; i++) {
if (((mask >> i) & 1) != 0) {
beauty += freq[chars.get(i)];
ways = ways * freq[chars.get(i)] % MOD; // 每个字符可任选一个出现下标
}
}
if (beauty > maxBeauty) { maxBeauty = beauty; way = ways; }
else if (beauty == maxBeauty) way = (way + ways) % MOD;
}
return (int) way;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.countKSubsequencesWithMaxBeauty("bcca", 2) == 4, "2842 例1");
check(s.countKSubsequencesWithMaxBeauty("abbcd", 4) == 2, "2842 例2");
java.util.Random rnd = new java.util.Random(62);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int len = 1 + rnd.nextInt(12);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) sb.append((char) ('a' + rnd.nextInt(5)));
String sstr = sb.toString();
int k = 1 + rnd.nextInt(Math.min(5, sstr.length()));
int a = s.countKSubsequencesWithMaxBeauty(sstr, k);
int b = s.countKSubsequencesWithMaxBeautyBrute(sstr, k);
if (a != b) {
allOk = false;
System.out.println("对拍失败 s=" + sstr + " k=" + k + " 主解=" + a + " 暴力=" + b);
break;
}
}
check(allOk, "2842 随机对拍");
System.out.println("2842 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n + 26),空间复杂度 O(1)。
2843. Count Symmetric Integers(统计对称整数的数目)
题目描述
给你两个正整数 low 和 high。
对于一个由 2 * n 位数字组成的整数 x,如果其前 n 位数字之和与后 n 位数字之和相等,
则认为这个数字是一个对称整数。
返回在 `low`, `high` 范围内的对称整数的数目。
示例 1
- 输入 :
low= 1,high= 100 - 输出:9
示例 2
- 输入 :
low= 1200,high= 1230 - 输出 :4
提示: 1 <= low <= high <= 10^4
解题思路
解法一:逐个枚举判断
直接遍历 low, high 的每个数,转成字符串,若长度为偶数,比较前半和与后半和。
长度≤5,复杂度 O((high-low) * 5)。
解法二:数学统计位数和
对每个长度(2/4 位)统计......这里直接用解法一即可(值域小),解法二做双指针式验证。
复杂度:
- 时间复杂度 :
O((high - low) * len),len为数字位数(≤ 5) - 空间复杂度 :
O(len),转字符串所需额外空间
代码实现
java
public class Solution {
// ---------- 解法一:枚举 + 判断(主提交版本) ----------
public int countSymmetricIntegers(int low, int high) {
int cnt = 0;
for (int x = low; x <= high; x++) {
if (isSym(x)) cnt++;
}
return cnt;
}
private boolean isSym(int x) {
String s = String.valueOf(x);
int n = s.length();
if ((n & 1) == 1) return false;
int sum = 0;
for (int i = 0; i < n / 2; i++) sum += s.charAt(i) - '0';
for (int i = n / 2; i < n; i++) sum -= s.charAt(i) - '0';
return sum == 0;
}
// ---------- 解法二:暴力拆分验证(对照) ----------
public int countSymmetricIntegersBrute(int low, int high) {
int cnt = 0;
for (int x = low; x <= high; x++) {
int[] d = new int[6];
int len = 0, tmp = x;
while (tmp > 0) { d[len++] = tmp % 10; tmp /= 10; }
if ((len & 1) == 1) continue;
int s1 = 0, s2 = 0;
for (int i = 0; i < len / 2; i++) s1 += d[i];
for (int i = len / 2; i < len; i++) s2 += d[i];
if (s1 == s2) cnt++;
}
return cnt;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.countSymmetricIntegers(1, 100) == 9, "2843 例1");
check(s.countSymmetricIntegers(1200, 1230) == 4, "2843 例2");
java.util.Random rnd = new java.util.Random(63);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int lo = 1 + rnd.nextInt(9999);
int hi = lo + rnd.nextInt(Math.min(5000, 10001 - lo));
int a = s.countSymmetricIntegers(lo, hi);
int b = s.countSymmetricIntegersBrute(lo, hi);
if (a != b) {
allOk = false;
System.out.println("对拍失败 low=" + lo + " high=" + hi + " 主解=" + a + " 暴力=" + b);
break;
}
}
check(allOk, "2843 随机对拍");
System.out.println("2843 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
- 时间复杂度 :
O((high - low) * len),len为数字位数(≤ 5) - 空间复杂度 :
O(len),转字符串所需额外空间
2844. Minimum Operations to Make a Special Number(生成特殊数字的最少操作)
题目描述
给你一个下标从 0 开始的字符串 num,表示一个非负整数。
在一次操作中,你可以选择 num 的任意一位数字并将其删除。如果删除 num 中的所有数字,则 num 变为 0。
返回最少需要多少次操作可以使 num 变成特殊数字。
如果整数 x 能被 25 整除,则该整数 x 被认为是特殊数字。
示例 1
- 输入 :
num= "2245047" - 输出:2
示例 2
- 输入 :
num= "2908305" - 输出:3
示例 3
- 输入 :
num= "10" - 输出 :1
提示: 1 <= num.length <= 100num仅由数字 '0' 到 '9' 组成num不含任何前导零
解题思路
解法一:枚举末两位模式
最少删除 = 保留最长"合法"子序列。能被 25 整除的数(≥2 位)末两位只能是
00、25、50、75。因此保留的字符串中,最后两位必须是其中一种模式,
且该模式由原串两个下标 i < j 构成(i、j 之间及 j 之后全部删除,i 之前全部保留),
删除数 = n - (i + 2)。要最小化删除数 → 最大化 i。
对四种模式分别找「最大的 i 满足 si=模式第一位、且存在 j>i 使 sj=模式第二位」。
特例:只保留一个 '0'(数字 0),删除 n-1 个;全删也合法但删除 n 个(更差)。
时间复杂度 O(4 * n^2),n <= 100;空间 O(1)。
解法二:BFS 删字符
小规模 BFS 逐步删除字符,判断当前剩余串对应整数能否被 25 整除(对照)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
public class Solution {
// ---------- 解法一:枚举末两位模式(主提交版本) ----------
public int minimumOperations(String num) {
int n = num.length();
int ans = n; // 全删变 0(需 n 次),作为上界
// 只保留一个 '0':删除 n-1 次
if (num.indexOf('0') >= 0) ans = Math.min(ans, n - 1);
String[] pats = {"00", "25", "50", "75"};
for (String pat : pats) {
int bestI = -1;
for (int i = 0; i < n; i++) {
if (num.charAt(i) != pat.charAt(0)) continue;
// 找 j > i 是否存在 pat.charAt(1)
for (int j = i + 1; j < n; j++) {
if (num.charAt(j) == pat.charAt(1)) {
bestI = Math.max(bestI, i);
break;
}
}
}
if (bestI >= 0) {
ans = Math.min(ans, n - bestI - 2); // 保留 bestI 之前 + bestI + j,删除其余
}
}
return ans;
}
// ---------- 解法二:BFS 删除(对照,小长度) ----------
public int minimumOperationsBrute(String num) {
if (isSpecial(num)) return 0;
Deque<String> q = new ArrayDeque<>();
Set<String> seen = new HashSet<>();
q.add(num);
seen.add(num);
int ops = 0;
while (!q.isEmpty()) {
int sz = q.size();
for (int s = 0; s < sz; s++) {
String cur = q.poll();
if (isSpecial(cur)) return ops;
int len = cur.length();
if (len == 0) continue;
for (int i = 0; i < len; i++) {
String ns = cur.substring(0, i) + cur.substring(i + 1);
if (seen.add(ns)) q.add(ns);
}
}
ops++;
if (ops > 14) break;
}
return num.length(); // 退化:全删(题目规定删空变 0)
}
private boolean isSpecial(String s) {
// 判断 s 对应整数能否被 25 整除:s="0"(或删空视为 0)或末两位为 00/25/50/75
int n = s.length();
if (n == 0) return true; // 删空 => 数字 0
if (n == 1) return s.equals("0");
String tail = s.substring(n - 2);
return tail.equals("00") || tail.equals("25") || tail.equals("50") || tail.equals("75");
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minimumOperations("2245047") == 2, "2844 例1");
check(s.minimumOperations("2908305") == 3, "2844 例2");
check(s.minimumOperations("10") == 1, "2844 例3");
java.util.Random rnd = new java.util.Random(64);
boolean allOk = true;
for (int t = 0; t < 800; t++) {
int len = 1 + rnd.nextInt(8);
StringBuilder sb = new StringBuilder();
sb.append((char) ('1' + rnd.nextInt(9)));
for (int i = 1; i < len; i++) sb.append((char) ('0' + rnd.nextInt(10)));
String num = sb.toString();
int a = s.minimumOperations(num);
int b = s.minimumOperationsBrute(num);
if (a != b) {
allOk = false;
System.out.println("对拍失败 num=" + num + " 主解=" + a + " BFS=" + b);
break;
}
}
check(allOk, "2844 随机对拍");
System.out.println("2844 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(4 * n^2),n <= 100;空间 O(1)。
2845. Count of Interesting Subarrays(统计趣味子数组的数目)
题目描述
给你一个下标从 0 开始的整数数组 nums,以及整数 modulo 和整数 k。
如果子数组 nums`l`...`r` 满足下述条件,则称其为趣味子数组:
在范围 `l`, `r` 内,设 cnt 为满足 nums[i] % modulo == k 的索引 i 的数量,
并且 cnt % modulo == k。
以整数形式返回趣味子数组的数目。
示例 1
- 输入 :
nums= 3,2,4,modulo= 2,k= 1 - 输出:3
示例 2
- 输入 :
nums= 3,1,9,6,modulo= 3,k= 0 - 输出 :2
提示: 1 <= nums.length <= 10^51 <= nums[i] <= 10^91 <= modulo <= 10^90 <= k < modulo
解题思路
解法一:前缀和 + 哈希计数
对每个位置定义 bi = (numsi % modulo == k) ? 1 : 0。
子数组 l, r 中 b 的和 cnt 满足 cnt % modulo == k。
设前缀和 pref,则 (prefr+1 - prefl) % modulo == k,
即 prefl % modulo == (prefr+1 - k) % modulo。
用哈希表统计每个前缀余数出现次数,边遍历边累加。
注意取 mod 需做正数处理((x % modulo + modulo) % modulo)。
时间复杂度 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 countInterestingSubarrays(List<Integer> nums, int modulo, int k) {
int n = nums.size();
long ans = 0;
int pref = 0;
Map<Long, Long> cnt = new HashMap<>();
cnt.put(0L, 1L);
for (int i = 0; i < n; i++) {
int b = ((nums.get(i) % modulo) == k) ? 1 : 0;
pref = (pref + b) % modulo;
long need = ((pref - k) % modulo + modulo) % modulo;
ans += cnt.getOrDefault(need, 0L);
cnt.put((long) pref, cnt.getOrDefault((long) pref, 0L) + 1);
}
return ans;
}
// ---------- 解法二:暴力(对照) ----------
public long countInterestingSubarraysBrute(List<Integer> nums, int modulo, int k) {
int n = nums.size();
long ans = 0;
for (int l = 0; l < n; l++) {
int cnt = 0;
for (int r = l; r < n; r++) {
int b = ((nums.get(r) % modulo) == k) ? 1 : 0;
cnt += b;
if (cnt % modulo == k) ans++;
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.countInterestingSubarrays(new ArrayList<>(java.util.Arrays.asList(3, 2, 4)), 2, 1) == 3, "2845 例1");
check(s.countInterestingSubarrays(new ArrayList<>(java.util.Arrays.asList(3, 1, 9, 6)), 3, 0) == 2, "2845 例2");
java.util.Random rnd = new java.util.Random(65);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(12);
int modulo = 1 + rnd.nextInt(6);
int k = rnd.nextInt(modulo);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(20));
long x = s.countInterestingSubarrays(a, modulo, k);
long y = s.countInterestingSubarraysBrute(a, modulo, k);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " modulo=" + modulo + " k=" + k + " 主解=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2845 随机对拍");
System.out.println("2845 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2846. Minimum Edge Weight Equilibrium Queries in a Tree(边权重均等查询)
题目描述
现有一棵由 n 个节点组成的无向树,节点按从 0 到 n - 1 编号。
给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges,
其中 edges[i] = `ui`, `vi`, `wi` 表示树中存在一条位于节点 ui 和 vi 之间、权重为 wi 的边。
另给你一个长度为 m 的二维整数数组 queries,其中 queries[i] = `ai`, `bi`。
对于每条查询,找出使从 ai 到 bi 路径上每条边的权重相等所需的最小操作次数。
在一次操作中,你可以选择树上的任意一条边,并将其权重更改为任意值。
注意:查询之间相互独立,每条新查询时树都回到初始状态。
返回长度为 m 的数组 answer,其中 answer[i] 是第 i 条查询的答案。
示例 1
- 输入 :
n= 7,edges= \[0,1,1,1,2,1,2,3,1,3,4,2,4,5,2,5,6,2],queries= \[0,3,3,6,2,6,0,6] - 输出:0,0,1,3
示例 2
- 输入 :
n= 8,edges= \[1,2,6,1,3,4,2,4,6,2,5,3,3,6,6,3,0,8,7,0,2],queries= \[4,6,0,4,6,5,7,4] - 输出 :1,2,2,3
提示: 1 <= n <= 10^4edges.length == n - 1edges[i].length == 30 <= ui, vi < n, 1 <= wi <= 26- 生成的输入满足
edges表示一棵有效的树 1 <= queries.length == m <= 2 * 10^40 <= ai, bi < n
解题思路
解法一:LCA 倍增 + 路径权重频次计数
对于查询 (a, b):设路径上的边总数为 d = deptha + depthb - 2depthlca。
若把路径上所有边改成相同权重 w,操作次数 = d - 路径上权重为 w 的边数。
要最小化,w 取路径上出现次数最多的权重,答案 = d - maxFreq。
预处理:倍增表 upjv(2^j 祖先)、每个节点到根路径上的
权重频次计数 cntvc(边权重 1...26)。
查询时 (a, b, l):cntA + cntB - 2 cntL 即路径上各权重出现次数,取 max。
时间复杂度 O(26 * (n + m) log n),空间复杂度 O(26 * n log n)。
解法二:逐个查询沿树爬
小规模直接找路径统计(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
static final int LOG = 15; // 2^14 > 10^4
// ---------- 解法一:倍增 LCA + 路径频次(主提交版本) ----------
public int[] minOperationsQueries(int n, int[][] edges, int[][] queries) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] e : edges) {
g[e[0]].add(new int[]{e[1], e[2]});
g[e[1]].add(new int[]{e[0], e[2]});
}
int[][] up = new int[LOG][n];
int[][] cnt = new int[n][27]; // cnt[v][c] = 根到 v 路径上权重 c 的边数
int[] depth = new int[n];
// DFS 预处理
dfs(0, -1, g, up, cnt, depth, 0);
for (int j = 1; j < LOG; j++) {
for (int v = 0; v < n; v++) {
int mid = up[j - 1][v];
up[j][v] = mid == -1 ? -1 : up[j - 1][mid];
}
}
int[] ans = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int a = queries[i][0], b = queries[i][1];
int l = lca(a, b, up, depth);
int d = depth[a] + depth[b] - 2 * depth[l];
int maxFreq = 0;
for (int c = 1; c <= 26; c++) {
int f = cnt[a][c] + cnt[b][c] - 2 * cnt[l][c];
maxFreq = Math.max(maxFreq, f);
}
ans[i] = d - maxFreq;
}
return ans;
}
private void dfs(int v, int p, List<int[]>[] g, int[][] up, int[][] cnt, int[] depth, int parentCntIndexOverload) {
// 不使用 parentCntIndexOverload,直接通过 p 传递
for (int[] e : g[v]) {
int to = e[0], w = e[1];
if (to == p) continue;
up[0][to] = v;
depth[to] = depth[v] + 1;
System.arraycopy(cnt[v], 0, cnt[to], 0, 27);
cnt[to][w]++;
dfs(to, v, g, up, cnt, depth, 0);
}
}
private int lca(int a, int b, int[][] up, int[] depth) {
if (depth[a] < depth[b]) { int t = a; a = b; b = t; }
int diff = depth[a] - depth[b];
for (int j = 0; diff > 0; j++) {
if ((diff & 1) != 0) a = up[j][a];
diff >>= 1;
}
if (a == b) return a;
for (int j = LOG - 1; j >= 0; j--) {
if (up[j][a] != up[j][b]) {
a = up[j][a];
b = up[j][b];
}
}
return up[0][a];
}
// ---------- 解法二:暴力路径统计(对照) ----------
public int[] minOperationsQueriesBrute(int n, int[][] edges, int[][] queries) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] e : edges) {
g[e[0]].add(new int[]{e[1], e[2]});
g[e[1]].add(new int[]{e[0], e[2]});
}
int[] ans = new int[queries.length];
for (int qi = 0; qi < queries.length; qi++) {
int a = queries[qi][0], b = queries[qi][1];
int[] freq = new int[27];
boolean[] visited = new boolean[n];
walk(a, -1, b, g, freq, visited);
int d = 0, mx = 0;
for (int c = 1; c <= 26; c++) { d += freq[c]; mx = Math.max(mx, freq[c]); }
ans[qi] = d - mx;
}
return ans;
}
private boolean walk(int v, int p, int target, List<int[]>[] g, int[] freq, boolean[] visited) {
visited[v] = true;
if (v == target) return true;
for (int[] e : g[v]) {
int to = e[0], w = e[1];
if (to == p || visited[to]) continue;
freq[w]++;
if (walk(to, v, target, g, freq, visited)) return true;
freq[w]--;
}
return false;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
int[][] e1 = {{0,1,1},{1,2,1},{2,3,1},{3,4,2},{4,5,2},{5,6,2}};
int[][] q1 = {{0,3},{3,6},{2,6},{0,6}};
check(Arrays.equals(s.minOperationsQueries(7, e1, q1), new int[]{0, 0, 1, 3}), "2846 例1");
int[][] e2 = {{1,2,6},{1,3,4},{2,4,6},{2,5,3},{3,6,6},{3,0,8},{7,0,2}};
int[][] q2 = {{4,6},{0,4},{6,5},{7,4}};
check(Arrays.equals(s.minOperationsQueries(8, e2, q2), new int[]{1, 2, 2, 3}), "2846 例2");
java.util.Random rnd = new java.util.Random(66);
boolean allOk = true;
for (int t = 0; t < 100; t++) {
int n = 2 + rnd.nextInt(9);
List<int[]> es = new ArrayList<>();
for (int v = 1; v < n; v++) {
int p = rnd.nextInt(v);
es.add(new int[]{p, v, 1 + rnd.nextInt(5)});
}
int[][] edges = es.toArray(new int[0][]);
int mq = 1 + rnd.nextInt(6);
int[][] qs = new int[mq][2];
for (int i = 0; i < mq; i++) { qs[i][0] = rnd.nextInt(n); qs[i][1] = rnd.nextInt(n); }
int[] a = s.minOperationsQueries(n, edges, qs);
int[] b = s.minOperationsQueriesBrute(n, edges, qs);
if (!Arrays.equals(a, b)) {
allOk = false;
System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(edges) + " queries=" + Arrays.deepToString(qs));
System.out.println(" 主解=" + Arrays.toString(a) + " 暴力=" + Arrays.toString(b));
break;
}
}
check(allOk, "2846 随机对拍");
System.out.println("2846 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(26 * (n + m) log n),空间复杂度 O(26 * n log n)。
2847. Smallest Number With Given Digit Product(给定数字乘积的最小数字)
题目描述
给定一个正整数 n。
返回一个字符串,表示使其各位数字的乘积等于 n 的最小正整数。
如果不存在这样的数字,则返回 "-1"。
示例 1
- 输入 :
n= 105 - 输出:"357"
示例 2
- 输入 :
n= 7 - 输出:"7"
示例 3
- 输入 :
n= 44 - 输出 :"-1"
提示: 1 <= n <= 10^15
解题思路
解法一:贪心分解因子
数字越小越好、位数越短越好。从 9 到 2 依次贪心分解 n:能整除就尽可能多除,
把数字记录下来。最后把得到的数字按升序排列组成最小的数。
若最终 n > 1(有大于 9 的质因子),返回 "-1"。
注意:若 n == 1 返回 "1"。
时间复杂度 O(log n),空间复杂度 O(log n)。
解法二:DFS 验证最小
小规模穷举所有数字组合,验证乘积等于 n(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Solution {
// ---------- 解法一:贪心分解(主提交版本) ----------
public String smallestNumber(long n) {
if (n == 1) return "1";
List<Integer> digits = new ArrayList<>();
long m = n;
for (int d = 9; d >= 2; d--) {
while (m % d == 0) {
digits.add(d);
m /= d;
}
}
if (m != 1) return "-1";
Collections.sort(digits);
StringBuilder sb = new StringBuilder();
for (int d : digits) sb.append((char) ('0' + d));
return sb.toString();
}
// ---------- 解法二:DFS 穷举(对照,小 n) ----------
public String smallestNumberBrute(long n) {
if (n == 1) return "1";
String best = null;
// 枚举数字个数 1..20(实际很小),DFS 构造
// 简化:直接枚举所有乘积等于 n 的数字串(回溯)
best = dfs(n, 9, "");
return best == null ? "-1" : best;
}
private String dfs(long rest, int maxDigit, String cur) {
if (rest == 1) return cur;
if (maxDigit <= 1) return null;
String best = null;
// 尝试从 maxDigit 往下填
for (int d = maxDigit; d >= 2; d--) {
if (rest % d == 0) {
String cand = dfs(rest / d, 9, cur + (char) ('0' + d));
if (cand != null) {
// 数字最小的:按字符序比较(相同长度比字典序,不同长度位数少的优先)
if (best == null || cand.length() < best.length()
|| (cand.length() == best.length() && cand.compareTo(best) < 0)) {
best = cand;
}
}
}
}
return best;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.smallestNumber(105).equals("357"), "2847 例1");
check(s.smallestNumber(7).equals("7"), "2847 例2");
check(s.smallestNumber(44).equals("-1"), "2847 例3");
check(s.smallestNumber(1).equals("1"), "2847 n=1");
java.util.Random rnd = new java.util.Random(67);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
long n = 1 + rnd.nextInt(100000);
String a = s.smallestNumber(n);
String b = s.smallestNumberBrute(n);
if (!a.equals(b)) {
allOk = false;
System.out.println("对拍失败 n=" + n + " 主解=" + a + " DFS=" + b);
break;
}
}
check(allOk, "2847 随机对拍");
System.out.println("2847 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(log n),空间复杂度 O(log n)。
2848. Points That Intersect With Cars(与车相交的点)
题目描述
给你一个下标从 0 开始的二维整数数组 nums 表示汽车停放在数轴上的坐标。
对于任意下标 i,nums[i] = `starti`, `endi`,其中 starti 是第 i 辆车的起点,
endi 是第 i 辆车的终点。
返回数轴上被车任意部分覆盖的整数点的数目。
示例 1
- 输入 :
nums= \[3,6,1,5,4,7] - 输出:7
示例 2
- 输入 :
nums= \[1,3,5,8] - 输出 :7
提示: 1 <= nums.length <= 100nums[i].length == 21 <= starti <= endi <= 100
解题思路
解法一:差分数组
值域 1,100,开长度 105 的差分数组,对每个区间 s, e 做 diffs++, diffe+1--,
前缀和统计正数个数。
时间复杂度 O(n + 100),空间复杂度 O(100)。
解法二:布尔数组合并
逐个区间标记,最后统计。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:差分数组(主提交版本) ----------
public int numberOfPoints(List<List<Integer>> nums) {
int[] diff = new int[105];
for (List<Integer> seg : nums) {
int s = seg.get(0), e = seg.get(1);
diff[s]++;
diff[e + 1]--;
}
int ans = 0, cur = 0;
for (int i = 1; i <= 100; i++) {
cur += diff[i];
if (cur > 0) ans++;
}
return ans;
}
// ---------- 解法二:布尔标记(对照) ----------
public int numberOfPointsBrute(List<List<Integer>> nums) {
boolean[] cover = new boolean[105];
for (List<Integer> seg : nums) {
for (int p = seg.get(0); p <= seg.get(1); p++) cover[p] = true;
}
int ans = 0;
for (int i = 1; i <= 100; i++) if (cover[i]) ans++;
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
List<List<Integer>> a1 = new ArrayList<>();
a1.add(java.util.Arrays.asList(3, 6));
a1.add(java.util.Arrays.asList(1, 5));
a1.add(java.util.Arrays.asList(4, 7));
check(s.numberOfPoints(a1) == 7, "2848 例1");
List<List<Integer>> a2 = new ArrayList<>();
a2.add(java.util.Arrays.asList(1, 3));
a2.add(java.util.Arrays.asList(5, 8));
check(s.numberOfPoints(a2) == 7, "2848 例2");
java.util.Random rnd = new java.util.Random(68);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 1 + rnd.nextInt(10);
List<List<Integer>> segs = new ArrayList<>();
for (int i = 0; i < n; i++) {
int st = 1 + rnd.nextInt(100);
int e = st + rnd.nextInt(101 - st);
segs.add(java.util.Arrays.asList(st, e));
}
int x = s.numberOfPoints(segs);
int y = s.numberOfPointsBrute(segs);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + segs + " 差分=" + x + " 布尔=" + y);
break;
}
}
check(allOk, "2848 随机对拍");
System.out.println("2848 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n + 100),空间复杂度 O(100)。
2849. Determine if a Cell Is Reachable at a Given Time(判断能否在给定时间到达单元格)
题目描述
给你四个整数 sx、sy、fx、fy 以及一个非负整数 t。
在一个无限的二维网格中,你从单元格 (sx, sy) 开始出发。
每一秒,你必须移动到任一与之前所处单元格相邻的单元格中。
如果你能在恰好 t 秒后到达单元格 (fx, fy),返回 true;否则返回 false。
单元格的相邻单元格是指该单元格周围与其至少共享一个角的 8 个单元格。
你可以多次访问同一个单元格。
示例 1
- 输入 :
sx= 2,sy= 4,fx= 7,fy= 7,t= 6 - 输出 :
true
示例 2
- 输入 :
sx= 3,sy= 1,fx= 7,fy= 3,t= 3 - 输出 :
false
提示: 1 <= sx, sy, fx, fy <= 10^90 <= t <= 10^9
解题思路
解法一:切比雪夫距离 + 冗余绕行
8 方向移动,最快需要 dist = max(|dx|, |dy|) 秒(切比雪夫距离)。
若 t >= dist 且 (t - dist) 为偶数或起点 == 终点,则可达:
- t == dist 直接到达;
- t > dist:多余的步数可以通过在终点绕一圈(来回)消耗,每消耗 2 步。
- 特殊:若起点 == 终点,dist = 0,但 t 必须 >= 2(t=1 必须离开再回来不可能,
因为必须移动一格);t=0 也可达。规则:起点终点时,t0 或 t>=2 可达,t==1 不可达。
更一般:起点 != 终点时,t >= dist 且任意多余步数都可用绕圈消耗(偶数)。
但 t >= dist 且 t - dist 为奇数时:可以在到达前绕一步(同行/列的相邻格),
所以只要 t >= dist 即可达(起点 != 终点)。统一结论:
if (sx == fx && sy == fy) return t != 1;
return t >= max(|dx|, |dy|);
时间复杂度 O(1),空间复杂度 O(1)。
解法二:BFS 小规模验证
小坐标下 BFS 恰好 t 步(对照)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
public class Solution {
// ---------- 解法一:数学(主提交版本) ----------
public boolean isReachableAtTime(int sx, int sy, int fx, int fy, int t) {
if (sx == fx && sy == fy) return t != 1;
int dist = Math.max(Math.abs(sx - fx), Math.abs(sy - fy));
return t >= dist;
}
// ---------- 解法二:BFS(对照,小坐标) ----------
public boolean isReachableAtTimeBrute(int sx, int sy, int fx, int fy, int t) {
if (t > 8) return isReachableAtTime(sx, sy, fx, fy, t); // 防爆
// BFS 恰好 t 步
Set<String> cur = new HashSet<>();
cur.add(sx + "," + sy);
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}};
for (int step = 0; step < t; step++) {
Set<String> nxt = new HashSet<>();
for (String pos : cur) {
String[] p = pos.split(",");
int x = Integer.parseInt(p[0]), y = Integer.parseInt(p[1]);
for (int[] d : dirs) {
nxt.add((x + d[0]) + "," + (y + d[1]));
}
}
cur = nxt;
if (cur.size() > 200000) break;
}
return cur.contains(fx + "," + fy);
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.isReachableAtTime(2, 4, 7, 7, 6), "2849 例1");
check(!s.isReachableAtTime(3, 1, 7, 3, 3), "2849 例2");
check(s.isReachableAtTime(1, 1, 1, 1, 0), "2849 同点t=0");
check(!s.isReachableAtTime(1, 1, 1, 1, 1), "2849 同点t=1");
java.util.Random rnd = new java.util.Random(69);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int sx = 1 + rnd.nextInt(8), sy = 1 + rnd.nextInt(8);
int fx = 1 + rnd.nextInt(8), fy = 1 + rnd.nextInt(8);
int tt = rnd.nextInt(9);
boolean a = s.isReachableAtTime(sx, sy, fx, fy, tt);
boolean b = s.isReachableAtTimeBrute(sx, sy, fx, fy, tt);
if (a != b) {
allOk = false;
System.out.println("对拍失败 sx=" + sx + " sy=" + sy + " fx=" + fx + " fy=" + fy + " t=" + tt + " 主解=" + a + " BFS=" + b);
break;
}
}
check(allOk, "2849 随机对拍");
System.out.println("2849 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(1),空间复杂度 O(1)。
2850. Minimum Moves to Spread Stones Over Grid(将石头分散到网格图的最少移动次数)
题目描述
给你一个大小为 3 * 3,下标从 0 开始的二维整数矩阵 grid,分别表示每一个格子里石头的数目。
网格图中总共恰好有 9 个石头,一个格子里可能会有多个石头。
每一次操作中,你可以将一个石头从它当前所在格子移动到一个至少有一条公共边的相邻格子。
返回每个格子恰好有一个石头的最少移动次数。
示例 1
- 输入 :
grid= \[1,1,0,1,1,1,1,2,1] - 输出:3
示例 2
- 输入 :
grid= \[1,3,0,1,0,0,1,0,3] - 输出 :4
提示: grid.length == grid[i].length == 30 <= grid[i][j] <= 9grid中元素之和为 9
解题思路
解法一:状压 DP / 最小费用分配
把"缺石头的格子"(gridij == 0)作为需求点,把"有富余石头的格子"
(gridij > 1)作为供给点(每个富余石头是 1 个单位供给)。
总供给 == 总需求。问题变成:把每个富余石头分配到某个缺石头的格子,费用 = 曼哈顿距离,
求最小总费用。小型指派问题可以用 DP over 供给集合(m <= 8):
dpmask = 已分配 mask 中的需求格子的最小费用,逐供给分配。
时间复杂度 O(3^m) 级别,m <= 8,非常快。
解法二:暴力全排列分配
枚举所有供给石头到需求格子的双射(全排列)求最小总距离(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// ---------- 解法一:状压 DP 指派(主提交版本) ----------
public int minimumMoves(int[][] grid) {
List<int[]> supply = new ArrayList<>(); // 富余石头(每个单位一个坐标)
List<int[]> demand = new ArrayList<>(); // 空缺格子
int[][] dirsAll = {{0,0}}; // 占位
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (grid[i][j] == 0) demand.add(new int[]{i, j});
else if (grid[i][j] > 1) {
for (int k = 0; k < grid[i][j] - 1; k++) supply.add(new int[]{i, j});
}
}
}
int m = supply.size();
int d = demand.size();
if (m == 0) return 0;
int[] dp = new int[1 << d];
Arrays.fill(dp, Integer.MAX_VALUE / 2);
dp[0] = 0;
for (int mask = 0; mask < (1 << d); mask++) {
int used = Integer.bitCount(mask);
if (used >= m) continue;
int[] s = supply.get(used);
for (int j = 0; j < d; j++) {
if (((mask >> j) & 1) != 0) continue;
int[] t = demand.get(j);
int cost = Math.abs(s[0] - t[0]) + Math.abs(s[1] - t[1]);
dp[mask | (1 << j)] = Math.min(dp[mask | (1 << j)], dp[mask] + cost);
}
}
return dp[(1 << d) - 1];
}
// ---------- 解法二:全排列暴力(对照) ----------
public int minimumMovesBrute(int[][] grid) {
List<int[]> supply = new ArrayList<>();
List<int[]> demand = new ArrayList<>();
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (grid[i][j] == 0) demand.add(new int[]{i, j});
else if (grid[i][j] > 1) {
for (int k = 0; k < grid[i][j] - 1; k++) supply.add(new int[]{i, j});
}
}
}
int m = supply.size();
int[] perm = new int[m];
for (int i = 0; i < m; i++) perm[i] = i;
best = Integer.MAX_VALUE;
permute(perm, 0, supply, demand);
return best;
}
private int best;
private void permute(int[] perm, int idx, List<int[]> supply, List<int[]> demand) {
if (idx == perm.length) {
int cost = 0;
for (int i = 0; i < perm.length; i++) {
int[] s = supply.get(i);
int[] t = demand.get(perm[i]);
cost += Math.abs(s[0] - t[0]) + Math.abs(s[1] - t[1]);
}
best = Math.min(best, cost);
return;
}
for (int j = idx; j < perm.length; j++) {
int tmp = perm[idx]; perm[idx] = perm[j]; perm[j] = tmp;
permute(perm, idx + 1, supply, demand);
tmp = perm[idx]; perm[idx] = perm[j]; perm[j] = tmp;
}
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minimumMoves(new int[][]{{1,1,0},{1,1,1},{1,2,1}}) == 3, "2850 例1");
check(s.minimumMoves(new int[][]{{1,3,0},{1,0,0},{1,0,3}}) == 4, "2850 例2");
java.util.Random rnd = new java.util.Random(70);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int[][] g = new int[3][3];
// 随机生成 9 个石头的分布
int[] cells = new int[9];
Arrays.fill(cells, 1);
// 随机搬移 4 个单位:选一个格子减一,另一个加一
for (int mv = 0; mv < 4; mv++) {
int a = rnd.nextInt(9), b = rnd.nextInt(9);
if (cells[a] > 0) { cells[a]--; cells[b]++; }
}
for (int i = 0; i < 9; i++) g[i / 3][i % 3] = cells[i];
int x = s.minimumMoves(g);
int y = s.minimumMovesBrute(g);
if (x != y) {
allOk = false;
System.out.println("对拍失败 grid=" + Arrays.deepToString(g) + " DP=" + x + " 全排列=" + y);
break;
}
}
check(allOk, "2850 随机对拍");
System.out.println("2850 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(3^m) 级别,m <= 8,非常快。
2851. String Transformation(字符串转换)
题目描述
给你两个长度都为 n 的字符串 s 和 t。你可以对字符串 s 执行以下操作:
将 s 长度为 l(0 < l < n)的后缀字符串删除,并将它添加在 s 的开头。
比方说,s = "abcd",一次操作中可删除后缀 "cd" 并添加到开头,得到 "cdab"。
给你一个整数 k,返回恰好 k 次操作将 s 变为 t 的方案数,对 10^9 + 7 取余。
示例 1
- 输入 :
s= "abcd",t= "cdab",k= 2 - 输出:2
示例 2
- 输入 :
s= "ababab",t= "ababab",k= 1 - 输出 :2
提示: 2 <= s.length <= 5 * 10^51 <= k <= 10^15s.length == t.lengths和t都只包含小写英文字母。
解题思路
解法一:循环位移类 + 矩阵快速幂
一次操作等价于把字符串循环位移 l(1 <= l <= n-1)。若 t 不是 s 的任意循环位移则答案 0。
设 s 的最小循环周期为 T,则与 s 相同的位移共有 x = n / T 个(S 类);
若 t 是 s 的位移 d 次,则与 t 相同的位移也有 x 个(T 类)。
将 n 个循环位移分为 S / T / 其余 C 三类,转移:
- 从 S 出发:到 S 有 x-1 种 l,到 T 有 x 种,其余去 C;
- 从 T 出发对称;从 C 出发到 S、T 各 x 种。
得 3x3 转移矩阵(t == s 时退化为 2x2),用矩阵快速幂求 k 次后的 T 类方案数。
时间复杂度 O(logn*3 + n)(n 用于计算周期),空间 O(1)。
解法二:DP 暴力模拟状态
小规模直接对字符串状态做 k 步 DP 转移(对照)。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
static final int MOD = 1_000_000_007;
// ---------- 解法一:矩阵快速幂(主提交版本) ----------
public int numberOfWays(String s, String t, long k) {
int n = s.length();
// 1. t 必须是 s 的循环位移
String ss = s + s;
int d = ss.indexOf(t);
if (d < 0 || d >= n) return 0; // 首出现在 [0, n-1] 之外等价于 d mod n,但循环位移只需一个代表
// 2. 最小周期 T
int T = n;
for (int p = 1; p <= n; p++) {
if (n % p != 0) continue;
boolean ok = true;
for (int i = 0; i < n; i++) {
if (s.charAt(i) != s.charAt(i % p)) { ok = false; break; }
}
if (ok) { T = p; break; }
}
long x = n / T; // S 类大小
if (d == 0) {
// t == s:两类 [S, C]
// f' = f*(x-1) + c*x
// c' = f*(n-x) + c*(n-1-x)
long[] v = {1, 0};
long[][] M = {
{mod(x - 1), mod(x)},
{mod(n - x), mod(n - 1 - x)}
};
v = pow(v, M, k);
return (int) v[0];
} else {
// t != s:三类 [S, T, C]
if (n - 2 * x == 0) {
// 只有 S、T 两类(如 n=2,x=1 或 n=4,x=2)
// f' = f*(x-1) + h*x ; h' = f*x + h*(x-1)
long[] v = {1, 0};
long[][] M = {
{mod(x - 1), mod(x)},
{mod(x), mod(x - 1)}
};
v = pow(v, M, k);
return (int) v[1];
}
long[] v = {1, 0, 0};
long[][] M = {
{mod(x - 1), mod(x), mod(x)},
{mod(x), mod(x - 1), mod(x)},
{mod(n - 2 * x), mod(n - 2 * x), mod(n - 1 - 2 * x)}
};
v = pow(v, M, k);
return (int) v[1]; // T 类
}
}
private long mod(long a) {
return ((a % MOD) + MOD) % MOD;
}
// 向量左乘矩阵 A^k(行向量 v)
private long[] pow(long[] v, long[][] A, long k) {
int m = v.length;
long[][] mp = A;
long e = k;
while (e > 0) {
if ((e & 1) == 1) v = mulVec(v, mp);
mp = mulMat(mp, mp);
e >>= 1;
}
return v;
}
private long[] mulVec(long[] v, long[][] A) {
int m = v.length;
long[] r = new long[m];
for (int j = 0; j < m; j++) {
long sum = 0;
for (int i = 0; i < m; i++) sum = (sum + v[i] * A[i][j]) % MOD;
r[j] = sum;
}
return r;
}
private long[][] mulMat(long[][] A, long[][] B) {
int m = A.length;
long[][] C = new long[m][m];
for (int i = 0; i < m; i++) {
for (int k = 0; k < m; k++) {
if (A[i][k] == 0) continue;
for (int j = 0; j < m; j++) {
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
}
}
}
return C;
}
// ---------- 解法二:暴力状态 DP(对照,小规模) ----------
public int numberOfWaysBrute(String s, String t, long k) {
int n = s.length();
if (n > 6 || k > 8) return numberOfWays(s, t, k);
Map<String, Long> cur = new HashMap<>();
cur.put(s, 1L);
for (long step = 0; step < k; step++) {
Map<String, Long> nxt = new HashMap<>();
for (Map.Entry<String, Long> e : cur.entrySet()) {
String w = e.getKey();
long cnt = e.getValue();
for (int l = 1; l < n; l++) {
String ns = w.substring(n - l) + w.substring(0, n - l);
long nv = (nxt.getOrDefault(ns, 0L) + cnt) % MOD;
nxt.put(ns, nv);
}
}
cur = nxt;
}
return (int) (cur.getOrDefault(t, 0L) % MOD);
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.numberOfWays("abcd", "cdab", 2) == 2, "2851 例1");
check(s.numberOfWays("ababab", "ababab", 1) == 2, "2851 例2");
check(s.numberOfWays("ab", "ba", 1) == 1, "2851 n=2 k=1");
check(s.numberOfWays("ab", "ba", 2) == 0, "2851 n=2 k=2");
check(s.numberOfWays("abab", "baba", 1) == 2, "2851 abab->baba k=1");
java.util.Random rnd = new java.util.Random(71);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(4);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.append((char) ('a' + rnd.nextInt(2)));
String base = sb.toString();
String ss = base + base;
int shift = rnd.nextInt(n);
String target = ss.substring(shift, shift + n);
long kk = 1 + rnd.nextInt(6);
int a = s.numberOfWays(base, target, kk);
int b = s.numberOfWaysBrute(base, target, kk);
if (a != b) {
allOk = false;
System.out.println("对拍失败 s=" + base + " t=" + target + " k=" + kk + " 矩阵=" + a + " DP=" + b);
break;
}
}
check(allOk, "2851 随机对拍");
// 大 k 冒烟测试
long big = s.numberOfWays("abcd", "cdab", 1000000000000000L);
System.out.println("大k结果=" + big);
System.out.println("2851 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(logn*3 + n)(n 用于计算周期),空间 O(1)。
2852. Sum of Remoteness of All Cells(所有单元格的远离程度之和)
题目描述
给定一个下标从 0 开始的大小为 n * n 的矩阵 grid,
其中每个单元格的值 grid[i][j] 要么是正整数,要么是表示阻塞单元格的值 -1。
你可以从一个非阻塞单元格移动到与其共享边的任何非阻塞单元格。
对于任何单元格 (i, j),定义其远离程度 R[i][j] 如下:
- 如果 (
i,j) 是非阻塞单元格,则R[i][j]是值grid[x][y]的总和,
其中没有从非阻塞单元格 (x,y) 到单元格 (i,j) 的路径。 - 对于阻塞单元格,
R[i][j]== 0。
返回所有单元格的R[i][j]之和。
示例 1
- 输入 :
grid= \[-1,1,-1,5,-1,4,-1,3,-1] - 输出:39
示例 2
- 输入 :
grid= \[-1,3,4,-1,-1,-1,3,-1,-1] - 输出:13
示例 3
- 输入 :
grid= \[1] - 输出 :0
提示: 1 <= n <= 300- 1 <=
grid[i][j]<= 10^6 或grid[i][j]== -1
解题思路
解法一:连通分量统计
设所有非阻塞单元格值之和为 total。对某个非阻塞单元格 (i, j) 所在的连通分量 C:
Rij = total - (C 内所有单元格值之和)。
因此答案 = Σ_{每个分量 C} sizeC * (total - sumC)。
用 BFS/DFS 求出每个连通分量的 size 与 sum 即可。
时间复杂度 O(n^2),空间复杂度 O(n^2)。
解法二:暴力逐对判可达
对每个非阻塞单元格,遍历所有其它非阻塞单元格,BFS 判是否可达(对照)。
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class Solution {
// ---------- 解法一:连通分量(主提交版本) ----------
public long sumRemoteness(int[][] grid) {
int n = grid.length;
long total = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] != -1) total += grid[i][j];
}
}
boolean[][] vis = new boolean[n][n];
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
long ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == -1 || vis[i][j]) continue;
Deque<int[]> q = new ArrayDeque<>();
q.add(new int[]{i, j});
vis[i][j] = true;
long sum = 0;
int size = 0;
while (!q.isEmpty()) {
int[] cur = q.poll();
int x = cur[0], y = cur[1];
sum += grid[x][y];
size++;
for (int[] d : dirs) {
int nx = x + d[0], ny = y + d[1];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
if (grid[nx][ny] == -1 || vis[nx][ny]) continue;
vis[nx][ny] = true;
q.add(new int[]{nx, ny});
}
}
ans += (long) size * (total - sum);
}
}
return ans;
}
// ---------- 解法二:暴力逐对判可达(对照) ----------
public long sumRemotenessBrute(int[][] grid) {
int n = grid.length;
List<int[]> cells = new ArrayList<>();
long total = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] != -1) { cells.add(new int[]{i, j}); total += grid[i][j]; }
}
}
long ans = 0;
for (int[] t : cells) {
long sum = 0;
for (int[] from : cells) {
if (!reachable(grid, from, t)) sum += grid[from[0]][from[1]];
}
ans += sum;
}
return ans;
}
private boolean reachable(int[][] grid, int[] from, int[] to) {
int n = grid.length;
boolean[][] vis = new boolean[n][n];
Deque<int[]> q = new ArrayDeque<>();
q.add(from);
vis[from[0]][from[1]] = true;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
while (!q.isEmpty()) {
int[] cur = q.poll();
if (cur[0] == to[0] && cur[1] == to[1]) return true;
for (int[] d : dirs) {
int nx = cur[0] + d[0], ny = cur[1] + d[1];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
if (grid[nx][ny] == -1 || vis[nx][ny]) continue;
vis[nx][ny] = true;
q.add(new int[]{nx, ny});
}
}
return false;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.sumRemoteness(new int[][]{{-1,1,-1},{5,-1,4},{-1,3,-1}}) == 39, "2852 例1");
check(s.sumRemoteness(new int[][]{{-1,3,4},{-1,-1,-1},{3,-1,-1}}) == 13, "2852 例2");
check(s.sumRemoteness(new int[][]{{1}}) == 0, "2852 例3");
java.util.Random rnd = new java.util.Random(72);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int n = 1 + rnd.nextInt(4);
int[][] g = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g[i][j] = rnd.nextInt(3) == 0 ? -1 : 1 + rnd.nextInt(9);
}
}
long a = s.sumRemoteness(g);
long b = s.sumRemotenessBrute(g);
if (a != b) {
allOk = false;
System.out.println("对拍失败 grid=" + java.util.Arrays.deepToString(g) + " 分量=" + a + " 暴力=" + b);
break;
}
}
check(allOk, "2852 随机对拍");
System.out.println("2852 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n^2),空间复杂度 O(n^2)。
2853. Highest Salaries Difference(最高薪水差异)
题目类型:数据库(SQL)
题目描述
表:Salaries
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| emp_name | varchar |
| department | varchar |
| salary | int |
+-------------+---------+
该表的主键(具有唯一值的列的组合)是 (emp_name, department)。
该表的每一行包含 emp_name、department 和 salary。工程部门和市场部门至少会有一条记录。
编写一个解决方案,计算 **市场部门 **和 **工程部门 **中 **最高 **工资之间的差异。输出工资的绝对差异。
返回结果表。
返回结果格式如下示例所示。
示例 1:**
输入:
Salaries table:
+----------+-------------+--------+
| emp_name | department | salary |
+----------+-------------+--------+
| Kathy | Engineering | 50000 |
| Roy | Marketing | 30000 |
| Charles | Engineering | 45000 |
| Jack | Engineering | 85000 |
| Benjamin | Marketing | 34000 |
| Anthony | Marketing | 42000 |
| Edward | Engineering | 102000 |
| Terry | Engineering | 44000 |
| Evelyn | Marketing | 53000 |
| Arthur | Engineering | 32000 |
+----------+-------------+--------+
输出:
+-------------------+
| salary_difference |
+-------------------+
| 49000 |
+-------------------+
解释:
- 工程部门和市场部门的最高工资分别为 102,000 和 53,000,因此绝对差异为 49,000。
解题思路
解法一:子查询取各部门最高薪再求差(主提交版本)
- 先按
department分组取MAX(salary)得到每个部门的最高薪,共 2 行; - 外层再对这两行的最高薪求
MAX - MIN即两部门最高薪之差; - 注意题目保证恰好两个部门。
代码实现
sql
SELECT MAX(s) - MIN(s) AS salary_difference
FROM
(
SELECT MAX(salary) AS s
FROM Salaries
GROUP BY department
) AS t;
复杂度分析
- 时间复杂度 :
O(n),n为表行数,单次扫描聚合(或窗口/连接)。 - 空间复杂度 :
O(n),分组/排序/窗口需要存储结果集。
2854. Rolling Average Steps(滚动平均步数)
题目类型:数据库(SQL)
题目描述
表: Steps
+-------------+------+
| Column Name | Type |
+-------------+------+
| user_id | int |
| steps_count | int |
| steps_date | date |
+-------------+------+
(user_id, steps_date) 是此表的主键。
该表的每一行包含 user_id、steps_count 和 steps_date。
编写一个解决方案,计算出每个用户的 3-day **滚动平均步数 **。
计算 n-day **滚动平均值 **的计算方式如下:
- 对于每一天,如果有可用数据的情况下,我们会计算以该天为结束的 `n` 天连续步数的平均值,否则,对于该天来说,`n` 天滚动平均步数是未定义的。
输出 user_id 、 steps_date 和滚动平均值。并将滚动平均值四舍五入到 两位小数。
返回结果表以user_id 和 steps_date 升序 排序。
结果的格式如下示例。
示例 1:**
输入:
Steps table:
+---------+-------------+------------+
| user_id | steps_count | steps_date |
+---------+-------------+------------+
| 1 | 687 | 2021-09-02 |
| 1 | 395 | 2021-09-04 |
| 1 | 499 | 2021-09-05 |
| 1 | 712 | 2021-09-06 |
| 1 | 576 | 2021-09-07 |
| 2 | 153 | 2021-09-06 |
| 2 | 171 | 2021-09-07 |
| 2 | 530 | 2021-09-08 |
| 3 | 945 | 2021-09-04 |
| 3 | 120 | 2021-09-07 |
| 3 | 557 | 2021-09-08 |
| 3 | 840 | 2021-09-09 |
| 3 | 627 | 2021-09-10 |
| 5 | 382 | 2021-09-05 |
| 6 | 480 | 2021-09-01 |
| 6 | 191 | 2021-09-02 |
| 6 | 303 | 2021-09-05 |
+---------+-------------+------------+
输出:
+---------+------------+-----------------+
| user_id | steps_date | rolling_average |
+---------+------------+-----------------+
| 1 | 2021-09-06 | 535.33 |
| 1 | 2021-09-07 | 595.67 |
| 2 | 2021-09-08 | 284.67 |
| 3 | 2021-09-09 | 505.67 |
| 3 | 2021-09-10 | 674.67 |
+---------+------------+-----------------+
解释:
- 对于 ID 为 1 的用户,截止到 2021-09-06 的三天连续的步数可用。因此,该日期的滚动平均值计算为 (395 + 499 + 712) / 3 = 535.33。
- 对于 ID 为 1 的用户,截止到 2021-09-07 的三天连续的步数可用。因此,该日期的滚动平均值计算为 (499 + 712 + 576) / 3 = 595.67。
- 对于 ID 为 2 的用户,截止到 2021-09-08 的三天连续的步数可用。因此,该日期的滚动平均值计算为 (153 + 171 + 530) / 3 = 284.67。
- 对于 ID 为 3 的用户,截止到 2021-09-09 的三天连续的步数可用。因此,该日期的滚动平均值计算为 (120 + 557 + 840) / 3 = 505.67。
- 对于 ID 为 3 的用户,截止到 2021-09-10 的三天连续的步数可用。因此,该日期的滚动平均值计算为 (557 + 840 + 627) / 3 = 674.67。
- 对于 ID 为 4 和 5 的用户,由于连续三天的数据不足,无法计算滚动平均值。结果表按 user_id 和 steps_date 升序排序。
解题思路
解法一:窗口函数 AVG + 日期连续性校验(主提交版本)
- 按
user_id分区、steps_date排序,AVG(steps_count) OVER (... ROWS 2 PRECEDING)
求当日与之前两天的三天滚动平均,ROUND(..., 2)保留两位; - 用
DATEDIFF(steps_date, LAG(steps_date, 2) OVER ...) = 2校验最近三天是否连续
(无缺失日期),过滤掉不满足的行,并按user_id、steps_date排序。
代码实现
sql
WITH
T AS (
SELECT
user_id,
steps_date,
ROUND(
AVG(steps_count) OVER (
PARTITION BY user_id
ORDER BY steps_date
ROWS 2 PRECEDING
),
2
) AS rolling_average,
DATEDIFF(
steps_date,
LAG(steps_date, 2) OVER (
PARTITION BY user_id
ORDER BY steps_date
)
) = 2 AS st
FROM Steps
)
SELECT
user_id,
steps_date,
rolling_average
FROM T
WHERE st = 1
ORDER BY 1, 2;
复杂度分析
- 时间复杂度 :
O(n),n为表行数,单次扫描聚合(或窗口/连接)。 - 空间复杂度 :
O(n),分组/排序/窗口需要存储结果集。
2855. Minimum Right Shifts to Sort the Array(使数组成为递增数组的最少右移次数)
题目描述
给你一个长度为 n 下标从 0 开始的数组 nums,数组中的元素为互不相同的正整数。
返回让 nums 成为递增数组的最少右移次数,如果无法得到递增数组,返回 -1。
一次右移指的是同时对所有下标进行操作,将下标为 i 的元素移动到下标 (i + 1) % n 处。
示例 1
- 输入 :
nums= 3,4,5,1,2 - 输出:2
示例 2
- 输入 :
nums= 1,3,5 - 输出:0
示例 3
- 输入 :
nums= 2,1,4 - 输出 :-1
提示: 1 <= nums.length <= 1001 <= nums[i] <= 100nums中的整数互不相同。
解题思路
解法一:找旋转点
数组元素互不相同且可右移排序,等价于原数组是一个「循环有序数组」。
找到唯一的下降点(numsi > numsi+1),若有多处下降则无法排序返回 -1。
若下降点为 p,则右移 (n-1-p) 次即可把最小值放到开头。
时间复杂度 O(n),空间复杂度 O(1)。
解法二:模拟右移
每次右移 1 次并判断是否递增,至多 n 次(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// ---------- 解法一:找旋转点(主提交版本) ----------
public int minimumRightShifts(List<Integer> nums) {
int n = nums.size();
int desc = -1;
for (int i = 0; i + 1 < n; i++) {
if (nums.get(i) > nums.get(i + 1)) {
if (desc != -1) return -1; // 两处下降,不可能
desc = i;
}
}
if (desc == -1) return 0; // 已经有序
// 旋转点:desc 处下降;还需要检查首尾:nums[0] 必须是全局最小(循环有序尾部衔接)
if (nums.get(0) < nums.get(n - 1)) return -1; // 尾比头小才允许旋转
// 右移 n-1-desc 次后首元素到达最小值处
return n - 1 - desc;
}
// ---------- 解法二:模拟(对照) ----------
public int minimumRightShiftsBrute(List<Integer> nums) {
int n = nums.size();
for (int shift = 0; shift < n; shift++) {
if (isSorted(nums)) return shift;
// 右移一次
int last = nums.remove(n - 1);
nums.add(0, last);
}
return -1;
}
private boolean isSorted(List<Integer> a) {
for (int i = 0; i + 1 < a.size(); i++) {
if (a.get(i) >= a.get(i + 1)) return false;
}
return true;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minimumRightShifts(new ArrayList<>(Arrays.asList(3, 4, 5, 1, 2))) == 2, "2855 例1");
check(s.minimumRightShifts(new ArrayList<>(Arrays.asList(1, 3, 5))) == 0, "2855 例2");
check(s.minimumRightShifts(new ArrayList<>(Arrays.asList(2, 1, 4))) == -1, "2855 例3");
java.util.Random rnd = new java.util.Random(73);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(8);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(50));
// 去重
java.util.Set<Integer> set = new java.util.HashSet<>(a);
a.clear();
a.addAll(set);
java.util.Collections.shuffle(a, rnd);
List<Integer> copy = new ArrayList<>(a);
int m1 = s.minimumRightShifts(a);
int m2 = s.minimumRightShiftsBrute(copy);
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + copy + " 主解=" + m1 + " 模拟=" + m2);
break;
}
}
check(allOk, "2855 随机对拍");
System.out.println("2855 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(1)。
2856. Minimum Array Length After Pair Removals(删除数对后的最小数组长度)
题目描述
给你一个下标从 0 开始的非递减整数数组 nums。
你可以执行以下操作任意次:
- 选择两个下标
i和j,满足nums[i]<nums[j]。 - 将
nums中下标在i和j处的元素删除。剩余元素保持原顺序形成新数组,下标重新编号。
返回执行以上操作任意次后(可以执行 0 次),nums数组的最小数组长度。
示例 1
- 输入 :
nums= 1,2,3,4 - 输出:0
示例 2
- 输入 :
nums= 1,1,2,2,3,3 - 输出:0
示例 3
- 输入 :
nums= 1000000000,1000000000 - 输出:2
示例 4
- 输入 :
nums= 2,3,4,4,4 - 输出 :1
提示: 1 <= nums.length <= 10^51 <= nums[i] <= 10^9nums是非递减数组。
解题思路
解法一:值频次 + 贪心配对
因为数组非递减,值相等元素可以任意删,值不同元素可以配对消除。
设频次最多的值为 maxFreq,其余数总数为 n - maxFreq。
每次操作本质是用一个「低频元素」消耗掉一个「高频元素」------
只要高频元素数量不超其余总数,可以全部配完得 0;
否则剩下 maxFreq - (n - maxFreq) 个高频元素无法配对。
答案 = max(maxFreq - (n - maxFreq), n % 2)。
更精确的做法:维护一个最小堆,每次取两个不同值的元素删除,
堆顶计数减一。最终堆顶计数(若有)即剩余长度。
这里用标准二分/双指针法:mid 处即最多重复数,剩余即可配对。
标准结论:剩余长度 = max(0, maxFreq * 2 - n),若 maxFreq2 <= n 则为 0,
否则为 maxFreq 2 - n。但注意奇数 n 且 maxFreq2 <= n 时,至少剩 1 个
(两个不同值可以配对消除,但奇数个元素最后必剩 1 个)。
综合:ans = max(2 maxFreq - n, n & 1)。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:直接模拟大根堆贪心
每次选频次最高与次高的两个不同值各删一个(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
public class Solution {
// ---------- 解法一:频次统计(主提交版本) ----------
public int minLengthAfterRemovals(List<Integer> nums) {
int n = nums.size();
Map<Integer, Integer> cnt = new HashMap<>();
int maxFreq = 0;
for (int v : nums) {
int c = cnt.merge(v, 1, Integer::sum);
maxFreq = Math.max(maxFreq, c);
}
return Math.max(2 * maxFreq - n, n & 1);
}
// ---------- 解法二:贪心堆模拟(对照) ----------
public int minLengthAfterRemovalsBrute(List<Integer> nums) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int v : nums) cnt.merge(v, 1, Integer::sum);
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
for (int c : cnt.values()) pq.add(c);
while (pq.size() >= 2) {
int a = pq.poll();
int b = pq.poll();
a--;
b--;
if (a > 0) pq.add(a);
if (b > 0) pq.add(b);
}
return pq.isEmpty() ? 0 : pq.peek();
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.minLengthAfterRemovals(new ArrayList<>(Arrays.asList(1, 2, 3, 4))) == 0, "2856 例1");
check(s.minLengthAfterRemovals(new ArrayList<>(Arrays.asList(1, 1, 2, 2, 3, 3))) == 0, "2856 例2");
check(s.minLengthAfterRemovals(new ArrayList<>(Arrays.asList(1000000000, 1000000000))) == 2, "2856 例3");
check(s.minLengthAfterRemovals(new ArrayList<>(Arrays.asList(2, 3, 4, 4, 4))) == 1, "2856 例4");
java.util.Random rnd = new java.util.Random(74);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(12);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(5));
java.util.Collections.sort(a);
int m1 = s.minLengthAfterRemovals(a);
int m2 = s.minLengthAfterRemovalsBrute(a);
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " 频次=" + m1 + " 堆=" + m2);
break;
}
}
check(allOk, "2856 随机对拍");
System.out.println("2856 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2857. Count Pairs of Points With Distance k(统计距离为 k 的点对)
题目描述
给你一个二维整数数组 coordinates 和一个整数 k,其中 coordinates[i] = `xi`, `yi`。
我们定义两个点 (x1, y1) 和 (x2, y2) 的距离为 (x1 XOR x2) + (y1 XOR y2)。
返回满足 i < j 且点 i 和点 j 之间距离为 k 的点对数目。
示例 1
- 输入 :
coordinates= \[1,2,4,2,1,3,5,2],k= 5 - 输出:2
示例 2
- 输入 :
coordinates= \[1,3,1,3,1,3,1,3,1,3],k= 0 - 输出 :10
提示: 2 <= coordinates.length <= 500000 <= xi, yi <= 10^60 <= k <= 100
解题思路
解法一:拆分 XOR 贡献 + 哈希
距离 = (x1^x2) + (y1^y2) = k。对每个点,枚举 a(0...k),
满足 x1^x2 = a 且 y1^y2 = k-a,即 x2 = x1^a, y2 = y1^(k-a)。
用哈希表统计已出现点的 (x,y) 频次,逐点累加匹配数。
时间复杂度 O(n * (k+1)),空间复杂度 O(n)。
解法二:暴力双重循环
小规模直接两两计算(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// ---------- 解法一:哈希拆分 XOR(主提交版本) ----------
public int countPairs(List<List<Integer>> coordinates, int k) {
Map<Long, Integer> cnt = new HashMap<>();
int ans = 0;
for (List<Integer> p : coordinates) {
int x = p.get(0), y = p.get(1);
for (int a = 0; a <= k; a++) {
int needX = x ^ a;
int needY = y ^ (k - a);
long key = ((long) needX << 32) | (needY & 0xFFFFFFFFL);
ans += cnt.getOrDefault(key, 0);
}
long self = ((long) x << 32) | (y & 0xFFFFFFFFL);
cnt.merge(self, 1, Integer::sum);
}
return ans;
}
// ---------- 解法二:暴力(对照) ----------
public int countPairsBrute(List<List<Integer>> coordinates, int k) {
int n = coordinates.size();
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int x1 = coordinates.get(i).get(0), y1 = coordinates.get(i).get(1);
int x2 = coordinates.get(j).get(0), y2 = coordinates.get(j).get(1);
if (((x1 ^ x2) + (y1 ^ y2)) == k) ans++;
}
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
List<List<Integer>> c1 = new ArrayList<>();
c1.add(Arrays.asList(1, 2)); c1.add(Arrays.asList(4, 2));
c1.add(Arrays.asList(1, 3)); c1.add(Arrays.asList(5, 2));
check(s.countPairs(c1, 5) == 2, "2857 例1");
List<List<Integer>> c2 = new ArrayList<>();
for (int i = 0; i < 5; i++) c2.add(Arrays.asList(1, 3));
check(s.countPairs(c2, 0) == 10, "2857 例2");
java.util.Random rnd = new java.util.Random(75);
boolean allOk = true;
for (int t = 0; t < 300; t++) {
int n = 2 + rnd.nextInt(10);
List<List<Integer>> pts = new ArrayList<>();
for (int i = 0; i < n; i++) pts.add(Arrays.asList(rnd.nextInt(16), rnd.nextInt(16)));
int k = rnd.nextInt(16);
int m1 = s.countPairs(new ArrayList<>(pts), k);
int m2 = s.countPairsBrute(new ArrayList<>(pts), k);
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 coordinates=" + pts + " k=" + k + " 哈希=" + m1 + " 暴力=" + m2);
break;
}
}
check(allOk, "2857 随机对拍");
System.out.println("2857 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n * (k+1)),空间复杂度 O(n)。
2858. Minimum Edge Reversals So Every Node Is Reachable(可以到达每一个节点的最少边反转次数)
题目描述
给你一个 n 个点的简单有向图(没有重复边的有向图),节点编号为 0 到 n - 1。
如果这些边是双向边,那么这个图形成一棵树。给你整数 n 和 edges,
其中 edges[i] = `ui`, `vi` 表示从 ui 到 vi 有一条有向边。
边反转指将一条边的方向反转。
对于范围 0, `n`-1 中的每个节点 i,独立计算最少需要多少次边反转,
从节点 i 出发经过一系列有向边可以到达所有的节点。
返回长度为 n 的数组 answer。
示例 1
- 输入 :
n= 4,edges= \[2,0,2,1,1,3] - 输出:1,1,0,2
示例 2
- 输入 :
n= 3,edges= \[1,2,2,0] - 输出 :2,0,1
提示: 2 <= n <= 10^5edges.length == n - 10 <= ui, vi < n, ui != vi- 输入保证如果边是双向边,可以得到一棵树。
解题思路
解法一:换根 DP
把有向边看作树上带方向的边。定义 dpv = 从 v 出发到达「以 v 为根的子树」
所有节点所需反转数。父节点换根时:
dpchild = dpparent + (parent->child 边方向所需调整)。
若边 parent->child 先天存在(不需反转),从 child 到 parent 需要反转 1;
若边 child->parent 存在,则从 child 到 parent 不需反转(parent 方向天然可走)。
更标准做法:先算 root=0 的 answer0(从 0 出发到全图所需反转数),
该值 = 所有「指向父节点」的边数(即拓扑上反向的边)。
再换根:从 u 换到 v(边 u->v 若存在,则对 v 而言该边不再需要反转,answerv = answeru - 1;
若边 v->u 存在,则 answerv = answeru + 1)。
时间复杂度 O(n),空间复杂度 O(n)。
解法二:对每个节点 BFS 算
小规模对每个源点做一次带权 BFS(反转算 1 代价)(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// ---------- 解法一:换根 DP(主提交版本) ----------
public int[] minEdgeReversals(int n, int[][] edges) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] e : edges) {
g[e[0]].add(new int[]{e[1], 0}); // 正向边 u->v,cost 0
g[e[1]].add(new int[]{e[0], 1}); // 反向边 v->u,cost 1(需反转)
}
int[] ans = new int[n];
// 1. 从 0 出发,统计到达所有节点所需反转数(即所有 cost 1 的边数)
dfs0(0, -1, g, ans);
// 2. 换根
reroot(0, -1, g, ans);
return ans;
}
private void dfs0(int v, int p, List<int[]>[] g, int[] ans) {
for (int[] e : g[v]) {
int to = e[0], cost = e[1];
if (to == p) continue;
ans[0] += cost;
dfs0(to, v, g, ans);
}
}
private void reroot(int v, int p, List<int[]>[] g, int[] ans) {
for (int[] e : g[v]) {
int to = e[0], cost = e[1];
if (to == p) continue;
// 换根到 to:如果 v->to 是正向(cost=0),原来从 v 到 to 不需要反转,
// 现在从 to 出发到 v 需要反转这 1 条,所以 ans[to] = ans[v] + 1;
// 如果 v->to 是反向(cost=1),原来需要反转,现在天然可走,ans[to] = ans[v] - 1。
ans[to] = ans[v] + (cost == 0 ? 1 : -1);
reroot(to, v, g, ans);
}
}
// ---------- 解法二:逐根 DFS(对照) ----------
public int[] minEdgeReversalsBrute(int n, int[][] edges) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] e : edges) {
g[e[0]].add(new int[]{e[1], 0}); // 正向 u->v:子到父不算反转
g[e[1]].add(new int[]{e[0], 1}); // 反向 v->u:子到父需反转
}
int[] ans = new int[n];
for (int s = 0; s < n; s++) {
// 以 s 为根 DFS,统计「指向父」的边数
ans[s] = countRooted(s, -1, g);
}
return ans;
}
private int countRooted(int v, int p, List<int[]>[] g) {
int cnt = 0;
for (int[] e : g[v]) {
int to = e[0], cost = e[1];
if (to == p) continue;
cnt += cost + countRooted(to, v, g);
}
return cnt;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(Arrays.equals(s.minEdgeReversals(4, new int[][]{{2,0},{2,1},{1,3}}), new int[]{1,1,0,2}), "2858 例1");
check(Arrays.equals(s.minEdgeReversals(3, new int[][]{{1,2},{2,0}}), new int[]{2,0,1}), "2858 例2");
java.util.Random rnd = new java.util.Random(76);
boolean allOk = true;
for (int t = 0; t < 200; t++) {
int n = 2 + rnd.nextInt(9);
List<int[]> es = new ArrayList<>();
// 随机生成有向树(每条边随机方向)
for (int v = 1; v < n; v++) {
int p = rnd.nextInt(v);
if (rnd.nextBoolean()) es.add(new int[]{p, v});
else es.add(new int[]{v, p});
}
int[][] edges = es.toArray(new int[0][]);
int[] a = s.minEdgeReversals(n, edges);
int[] b = s.minEdgeReversalsBrute(n, edges);
if (!Arrays.equals(a, b)) {
allOk = false;
System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(edges));
System.out.println(" 主解=" + Arrays.toString(a) + " 暴力=" + Arrays.toString(b));
break;
}
}
check(allOk, "2858 随机对拍");
System.out.println("2858 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n),空间复杂度 O(n)。
2859. Sum of Values at Indices With K Set Bits(计算 K 置位下标对应元素的和)
题目描述
给你一个下标从 0 开始的整数数组 nums 和一个整数 k。
返回 nums 中特定元素之和,这些特定元素满足:其对应下标的二进制表示中恰存在 k 个置位。
整数的二进制表示中的 1 就是这个整数的置位。
示例 1
- 输入 :
nums= 5,10,1,5,2,k= 1 - 输出:13
示例 2
- 输入 :
nums= 4,3,2,1,k= 2 - 输出 :1
提示: 1 <= nums.length <= 10001 <= nums[i] <= 10^50 <= k <= 10
解题思路
解法一:遍历 + Integer.bitCount
直接遍历下标,bitCount(i) == k 则累加 numsi。
时间复杂度 O(n log n),空间复杂度 O(1)。
解法二:手写 popcount 验证
用逐位统计的 popcount 实现(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// ---------- 解法一:bitCount(主提交版本) ----------
public int sumIndicesWithKSetBits(List<Integer> nums, int k) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
if (Integer.bitCount(i) == k) sum += nums.get(i);
}
return sum;
}
// ---------- 解法二:手写 popcount(对照) ----------
public int sumIndicesWithKSetBitsBrute(List<Integer> nums, int k) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) {
int c = 0, x = i;
while (x > 0) { c += (x & 1); x >>= 1; }
if (c == k) sum += nums.get(i);
}
return sum;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.sumIndicesWithKSetBits(new ArrayList<>(java.util.Arrays.asList(5, 10, 1, 5, 2)), 1) == 13, "2859 例1");
check(s.sumIndicesWithKSetBits(new ArrayList<>(java.util.Arrays.asList(4, 3, 2, 1)), 2) == 1, "2859 例2");
java.util.Random rnd = new java.util.Random(77);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(20);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(1 + rnd.nextInt(1000));
int k = rnd.nextInt(6);
int x = s.sumIndicesWithKSetBits(a, k);
int y = s.sumIndicesWithKSetBitsBrute(a, k);
if (x != y) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " k=" + k + " 主解=" + x + " 暴力=" + y);
break;
}
}
check(allOk, "2859 随机对拍");
System.out.println("2859 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n log n),空间复杂度 O(1)。
2860. Happy Students(让所有学生保持开心的分组方法数)
题目描述
给你一个下标从 0 开始、长度为 n 的整数数组 nums,其中 n 是班级中学生的总数。
班主任希望能够在让所有学生保持开心的情况下选出一组学生:
如果能够满足下述两个条件之一,则认为第 i 位学生将会保持开心:
- 这位学生被选中,并且被选中的学生人数严格大于
nums[i]。 - 这位学生没有被选中,并且被选中的学生人数严格小于
nums[i]。
返回能够满足让所有学生保持开心的分组方法的数目。
示例 1
- 输入 :
nums= 1,1 - 输出:2
示例 2
- 输入 :
nums= 6,0,3,3,6,7,2,7 - 输出 :3
提示: 1 <= nums.length <= 10^50 <= nums[i] < nums.length
解题思路
解法一:排序 + 枚举分组大小
设选 x 个学生。被选中者需 numsi < x,未选中者需 numsi > x。
排序后:选恰好 x 个合法 ⇔ (第 x 个(下标 x-1)nums < x)且(第 x+1 个(下标 x)nums > x)。
枚举 x 从 0 到 n,检查上述条件。注意 x=0(都不选)与 x=n(全选)也合法。
时间复杂度 O(n log n),空间复杂度 O(1)。
解法二:暴力枚举子集
小规模枚举所有子集并验证(对照)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Solution {
// ---------- 解法一:排序 + 枚举(主提交版本) ----------
public int countWays(List<Integer> nums) {
int n = nums.size();
Collections.sort(nums);
int ans = 0;
for (int x = 0; x <= n; x++) {
// 选 x 个:前 x 个被选(nums[i] < x),其余未选(nums[i] > x)
boolean ok = true;
if (x > 0 && nums.get(x - 1) >= x) ok = false; // 被选者要求 < x
if (x < n && nums.get(x) <= x) ok = false; // 未选者要求 > x
if (ok) ans++;
}
return ans;
}
// ---------- 解法二:暴力子集(对照,小规模) ----------
public int countWaysBrute(List<Integer> nums) {
int n = nums.size();
int ans = 0;
for (int mask = 0; mask < (1 << n); mask++) {
int sel = Integer.bitCount(mask);
boolean ok = true;
for (int i = 0; i < n; i++) {
boolean in = ((mask >> i) & 1) != 0;
if (in && !(sel > nums.get(i))) { ok = false; break; }
if (!in && !(sel < nums.get(i))) { ok = false; break; }
}
if (ok) ans++;
}
return ans;
}
// ---------- 本地自测 ----------
public static void main(String[] args) {
Solution s = new Solution();
check(s.countWays(new ArrayList<>(Arrays.asList(1, 1))) == 2, "2860 例1");
check(s.countWays(new ArrayList<>(Arrays.asList(6, 0, 3, 3, 6, 7, 2, 7))) == 3, "2860 例2");
java.util.Random rnd = new java.util.Random(78);
boolean allOk = true;
for (int t = 0; t < 500; t++) {
int n = 1 + rnd.nextInt(10);
List<Integer> a = new ArrayList<>();
for (int i = 0; i < n; i++) a.add(rnd.nextInt(n));
int m1 = s.countWays(new ArrayList<>(a));
int m2 = s.countWaysBrute(new ArrayList<>(a));
if (m1 != m2) {
allOk = false;
System.out.println("对拍失败 nums=" + a + " 排序=" + m1 + " 暴力=" + m2);
break;
}
}
check(allOk, "2860 随机对拍");
System.out.println("2860 all tests done");
}
static void check(boolean ok, String name) {
System.out.println((ok ? "PASS " : "FAIL ") + name);
}
}
复杂度分析
时间复杂度 O(n log n),空间复杂度 O(1)。