LeetCode 1881-1900. 题解合集
文章目录
- [LeetCode 1881-1900. 题解合集](#LeetCode 1881-1900. 题解合集)
-
- [1881. Maximum Value after Insertion(插入后的最大值)](#1881. Maximum Value after Insertion(插入后的最大值))
- [1882. Process Tasks Using Servers(使用服务器处理任务)](#1882. Process Tasks Using Servers(使用服务器处理任务))
- [1883. Minimum Skips to Arrive at Meeting On Time](#1883. Minimum Skips to Arrive at Meeting On Time)
- [1884. Egg Drop With 2 Eggs and N Floors(鸡蛋掉落-两枚鸡蛋)](#1884. Egg Drop With 2 Eggs and N Floors(鸡蛋掉落-两枚鸡蛋))
- [1885. Count Pairs in Two Arrays(统计数对)](#1885. Count Pairs in Two Arrays(统计数对))
- [1886. Determine Whether Matrix Can Be Obtained By Rotation](#1886. Determine Whether Matrix Can Be Obtained By Rotation)
- [1887. Reduction Operations to Make the Array Elements Equal](#1887. Reduction Operations to Make the Array Elements Equal)
- [1888. Minimum Number of Flips to Make the Binary String Alternating](#1888. Minimum Number of Flips to Make the Binary String Alternating)
- [1889. Minimum Space Wasted From Packaging(装包裹的最小浪费空间)](#1889. Minimum Space Wasted From Packaging(装包裹的最小浪费空间))
- [1890. The Latest Login in 2020(2020年最后一次登录)](#1890. The Latest Login in 2020(2020年最后一次登录))
- [1891. Cutting Ribbons(割绳子)](#1891. Cutting Ribbons(割绳子))
- [1892. Page Recommendations II(页面推荐Ⅱ)](#1892. Page Recommendations II(页面推荐Ⅱ))
- [1893. Check if All the Integers in a Range Are Covered](#1893. Check if All the Integers in a Range Are Covered)
- [1894. Find the Student that Will Replace the Chalk](#1894. Find the Student that Will Replace the Chalk)
- [1895. Largest Magic Square(最大的幻方)](#1895. Largest Magic Square(最大的幻方))
- [1896. Minimum Cost to Change the Final Value of Expression](#1896. Minimum Cost to Change the Final Value of Expression)
- [1897. Redistribute Characters to Make All Strings Equal](#1897. Redistribute Characters to Make All Strings Equal)
- [1898. Maximum Number of Removable Characters(可移除字符的最大数目)](#1898. Maximum Number of Removable Characters(可移除字符的最大数目))
- [1899. Merge Triplets to Form Target Triplet(合并若干三元组以形成目标三元组)](#1899. Merge Triplets to Form Target Triplet(合并若干三元组以形成目标三元组))
- [1900. The Earliest and Latest Rounds Where Players Compete](#1900. The Earliest and Latest Rounds Where Players Compete)
1881. Maximum Value after Insertion(插入后的最大值)
题目描述
给你一个非常大的整数 n 和一个整数数字 x,大整数 n 用一个字符串表示。n 中每一位数字和数字 x
都处于闭区间 1, 9 中,且 n 可能表示一个负数。
你打算通过在 n 的十进制表示的任意位置插入 x 来最大化 n 的数值。但不能在负号的左边插入 x。
例如,如果 n = 73 且 x = 6,那么最佳方案是将 6 插入 7 和 3 之间,使 n = 763。
如果 n = -55 且 x = 2,那么最佳方案是将 2 插在第一个 5 之前,使 n = -255。
返回插入操作后,用字符串表示的 n 的最大值。
示例 1
- 输入 :
n= "99",x= 9 - 输出:"999"
示例 2
- 输入 :
n= "-13",x= 2 - 输出:"-123"
- 解释 :插入
x可以得到 -213、-123 或者 -132,三者中最大的是 -123。
提示: - 1 <= n.length <= 10^5- 1 <= x <= 9-
n中每一位的数字都在闭区间 1, 9 中。
-
n代表一个有效的整数。
-
- 当
n表示负数时,将会以字符 '-' 开始。
- 当
解题思路
正数:想让数尽可能大,插入位置要尽量靠左,即找到第一个小于 x 的数字,把 x 插在它前面;
若没有,插到末尾。
负数:绝对值越小数值越大,等价于想让"正数部分"尽可能小,即找到第一个大于 x 的数字,
把 x 插在它前面(使该位之后数位右移、整体前段保持较小);若没有,插到末尾。
解法一:一次扫描贪心,主提交版本
区分正负号后找插入点,StringBuilder 插入。
O(n) 时间、O(n) 空间。
解法二:枚举全部位置取最大,对照
把 x 插入每个可行位置生成全部候选串,正数取数值/字典序
最大、负数取最小绝对值;仅作小规模验证,O(n^2)。
代码实现
java
public class Solution {
public String maxValue(String n, int x) {
boolean negative = n.charAt(0) == '-';
char xc = (char) ('0' + x);
int i;
if (negative) {
// 负数:绝对值越小越好 → 找到第一个大于 x 的数字,插在其前
i = 1;
while (i < n.length() && n.charAt(i) <= xc) {
i++;
}
} else {
// 正数:找到第一个小于 x 的数字,插在其前
i = 0;
while (i < n.length() && n.charAt(i) >= xc) {
i++;
}
}
return n.substring(0, i) + xc + n.substring(i);
}
public String maxValueAll(String n, int x) {
String best = null;
char xc = (char) ('0' + x);
int start = n.charAt(0) == '-' ? 1 : 0;
boolean negative = start == 1;
for (int i = start; i <= n.length(); i++) {
String cand = n.substring(0, i) + xc + n.substring(i);
if (best == null) {
best = cand;
continue;
}
// 负数比"绝对值",即去掉负号后字典序最小
if (negative) {
String a = cand.substring(1);
String b = best.substring(1);
if (a.compareTo(b) < 0) best = cand;
} else {
if (cand.compareTo(best) > 0) best = cand;
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
String[][] cases = {
{"99", "9"},
{"-13", "2"},
{"73", "6"},
{"-55", "2"},
{"1", "1"},
{"-9", "1"},
{"123", "9"},
{"987", "1"},
{"-555", "5"},
{"999", "5"}
};
boolean ok = true;
for (String[] c : cases) {
String r1 = s.maxValue(c[0], Integer.parseInt(c[1]));
String r2 = s.maxValueAll(c[0], Integer.parseInt(c[1]));
ok &= r1.equals(r2);
System.out.println("n=" + c[0] + " x=" + c[1] + " -> " + r1 + " | " + r2 + (r1.equals(r2) ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1882. Process Tasks Using Servers(使用服务器处理任务)
题目描述
给你两个下标从 0 开始的整数数组 servers 和 tasks,长度分别为 n 和 m。servers[i] 是第 i 台
服务器的权重,而 tasks[j] 是处理第 j 项任务所需要的时间(秒)。
你正在运行一个仿真系统,在处理完所有任务后,该系统将会关闭。每台服务器只能同时处理一项任务。
第 0 项任务在第 0 秒可以开始处理,第 j 项任务在第 j 秒可以开始处理。处理第 j 项任务时,你
需要为它分配一台权重最小的空闲服务器。如果存在多台相同权重的空闲服务器,请选择下标最小的
服务器。如果一台空闲服务器在第 t 秒分配到第 j 项任务,那么在 t + tasks[j] 时它将恢复空闲。
如果没有空闲服务器,则必须等待,直到出现一台空闲服务器,并尽可能早地处理剩余任务。如果有多项
任务等待分配,则按照下标递增的顺序完成分配。如果同一时刻存在多台空闲服务器,可以同时将多项
任务分别分配给它们。
构建长度为 m 的答案数组 ans,其中 ans[j] 是第 j 项任务分配的服务器的下标。返回 ans。
示例 1
- 输入 :
servers= 3,3,2,tasks= 1,2,3,2,1,2 - 输出:2,2,0,2,1,2
示例 2
- 输入 :
servers= 5,1,4,3,2,tasks= 2,1,2,4,5,2,1 - 输出 :1,4,1,4,1,3,2
提示: -
- servers.length ==
n,tasks.length ==m
- servers.length ==
- 1 <= n, m <= 10^5- 1 <= servers[i], tasks[j] <= 10^5
解题思路
解法一:双优先队列模拟,主提交版本
维护"空闲服务器堆"(按权重、下标排序)与"忙碌服务器堆"
(按结束时刻排序)。时间从 t=0 递增:先把所有按时结束的忙碌服务器归还到空闲堆,再给任务
分配:若空闲堆非空直接取最小权重服务器;否则让时间推进到忙碌堆最早结束时刻,归还后可分配。
注意同一空闲时刻可同时喂给多个等待任务。O((n+m) log n)。
解法二:预推进时间 + 队列,对照
不显式推进"等待时刻",而是对每个任务 j,用"当前时间
accTime"记录下一个可开始处理任务的时刻:若空闲堆空则 accTime = 最早结束时刻并归还;
否则在 accTime 与 j 都满足时直接分配。实现略有差异但语义相同,作为对照。
代码实现
java
import java.util.Arrays;
import java.util.PriorityQueue;
public class Solution {
public int[] assignTasks(int[] servers, int[] tasks) {
int n = servers.length, m = tasks.length;
// 空闲堆:权重小优先、下标小优先
PriorityQueue<int[]> idle = new PriorityQueue<>((a, b) ->
a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
// 忙碌堆:结束时刻早优先
PriorityQueue<int[]> busy = new PriorityQueue<>((a, b) -> a[0] - b[0]);
for (int i = 0; i < n; i++) {
idle.offer(new int[]{servers[i], i});
}
int[] ans = new int[m];
int time = 0;
for (int j = 0; j < m; j++) {
time = Math.max(time, j); // 任务 j 最早 j 秒可开始
// 归还截止到 time 的所有忙碌服务器
while (!busy.isEmpty() && busy.peek()[0] <= time) {
int[] sv = busy.poll();
idle.offer(new int[]{sv[1], sv[2]}); // [权重, 下标]
}
if (idle.isEmpty()) {
// 没有空闲服务器:跳到最早结束时刻
time = busy.peek()[0];
while (!busy.isEmpty() && busy.peek()[0] <= time) {
int[] sv = busy.poll();
idle.offer(new int[]{sv[1], sv[2]});
}
}
int[] choose = idle.poll();
ans[j] = choose[1];
busy.offer(new int[]{time + tasks[j], choose[0], choose[1]});
}
return ans;
}
public int[] assignTasksAlt(int[] servers, int[] tasks) {
int n = servers.length, m = tasks.length;
PriorityQueue<int[]> idle = new PriorityQueue<>((a, b) ->
a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
PriorityQueue<int[]> busy = new PriorityQueue<>((a, b) -> a[0] - b[0]);
for (int i = 0; i < n; i++) {
idle.offer(new int[]{servers[i], i});
}
int[] ans = new int[m];
long acc = 0; // 当前可分配时刻(虚拟时钟)
for (int j = 0; j < m; j++) {
long cur = Math.max(acc, j);
while (!busy.isEmpty() && busy.peek()[0] <= cur) {
int[] sv = busy.poll();
idle.offer(new int[]{sv[1], sv[2]});
}
if (idle.isEmpty()) {
acc = busy.peek()[0]; // 直接推到最早空闲时刻
while (!busy.isEmpty() && busy.peek()[0] <= acc) {
int[] sv = busy.poll();
idle.offer(new int[]{sv[1], sv[2]});
}
} else {
acc = cur;
}
int[] choose = idle.poll();
ans[j] = choose[1];
busy.offer(new int[]{(int) (acc + tasks[j]), choose[0], choose[1]});
}
return ans;
}
private static String str(int[] a) {
return Arrays.toString(a);
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] servers = {{3, 3, 2}, {5, 1, 4, 3, 2}, {1, 1}, {2, 1, 1}};
int[][] tasks = {{1, 2, 3, 2, 1, 2}, {2, 1, 2, 4, 5, 2, 1}, {1, 1, 1, 1}, {3, 3, 3, 3, 3}};
boolean ok = true;
for (int t = 0; t < servers.length; t++) {
int[] r1 = s.assignTasks(servers[t], tasks[t]);
int[] r2 = s.assignTasksAlt(servers[t], tasks[t]);
ok &= Arrays.equals(r1, r2);
System.out.println(str(r1) + " | " + str(r2) + (Arrays.equals(r1, r2) ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1883. Minimum Skips to Arrive at Meeting On Time
题目描述
(准时抵达会议现场的最小跳过休息次数)
给你一个整数 hoursBefore,表示你要前往会议所剩下的可用小时数。要想成功抵达会议现场,你必须
途经 n 条道路。道路的长度用一个长度为 n 的整数数组 dist 表示,其中 dist[i] 表示第 i 条道路的
长度(千米)。另给你一个整数 speed,表示你在道路上前进的速度(千米每小时)。
当你通过第 i 条路之后,就必须休息并等待,直到下一个整数小时才能开始继续通过下一条道路。
注意:你不需要在通过最后一条道路后休息,因为那时你已经抵达会议现场。
例如,如果你通过一条道路用去 1.4 小时,那你必须停下来等待,到 2 小时才可以继续通过下一条道路。
如果通过一条道路恰好用去 2 小时,就无需等待,可以直接继续。
然而,为了能准时到达,你可以选择跳过一些路的休息时间,这意味着你不必等待下一个整数小时。
返回准时抵达会议现场所需要的最小跳过次数;如果无法准时参会,返回 -1。
示例 1
- 输入 :
dist= 1,3,2,speed= 4,hoursBefore= 2 - 输出:1
示例 2
- 输入 :
dist= 7,3,5,5,speed= 2,hoursBefore= 10 - 输出:2
示例 3
- 输入 :
dist= 7,3,5,5,speed= 1,hoursBefore= 10 - 输出 :-1
提示: - n == dist.length- 1 <= n <= 1000- 1 <= dist[i] <= 10^5- 1 <= speed <= 10^6- 1 <= hoursBefore <= 10^7
解题思路
把时间单位放大 speed 倍(即用"每千米所需时间单位"计):第 i 条路耗时 disti 单位,
休息到下一整数小时等价于"向上取整到 speed 的倍数"(单位小时 * speed)。
设 dpik = 走完前 i 条路、跳过 k 次休息后的最短用时(整数单位,hoursBefore*speed 可直接比较)。
转移:不跳 → dpik = ceilToUnit(dpi-1k + disti);跳过 → dpik = dpi-1k-1 + disti。
边界 dp00 = 0;i 从 1...n,最后一趟不休息(不取整)。
最后找最小 k 使 dpnk <= hoursBefore * speed,否则 -1。
解法一:滚动数组 DP,主提交版本
O(n^2) 时间、O(n) 空间;用 long 防溢出。
解法二:二维表 DP,对照
不滚动、直接二维表,便于对照阅读。
代码实现
java
import java.util.Arrays;
public class Solution {
public int minSkips(int[] dist, int speed, int hoursBefore) {
int n = dist.length;
long limit = (long) hoursBefore * speed; // 单位化的时限
long[] dp = new long[n + 1]; // dp[k]:跳过 k 次时的最短用时
Arrays.fill(dp, Long.MAX_VALUE / 4);
dp[0] = 0;
for (int i = 0; i < n; i++) {
long[] ndp = new long[n + 1];
Arrays.fill(ndp, Long.MAX_VALUE / 4);
boolean last = (i == n - 1);
for (int k = 0; k <= i; k++) {
if (dp[k] == Long.MAX_VALUE / 4) continue;
// 不跳过本次休息(最后一条路无休息)
long cost = dp[k] + dist[i];
if (!last) {
cost = (cost + speed - 1) / speed * speed; // 取整到 speed 的倍数
}
ndp[k] = Math.min(ndp[k], cost);
// 跳过本次休息
if (!last && k + 1 <= n) {
ndp[k + 1] = Math.min(ndp[k + 1], dp[k] + dist[i]);
}
}
dp = ndp;
}
for (int k = 0; k <= n; k++) {
if (dp[k] <= limit) {
return k;
}
}
return -1;
}
public int minSkips2D(int[] dist, int speed, int hoursBefore) {
int n = dist.length;
long limit = (long) hoursBefore * speed;
long[][] dp = new long[n + 1][n + 1];
for (long[] row : dp) {
Arrays.fill(row, Long.MAX_VALUE / 4);
}
dp[0][0] = 0;
for (int i = 0; i < n; i++) {
boolean last = (i == n - 1);
for (int k = 0; k <= i; k++) {
if (dp[i][k] == Long.MAX_VALUE / 4) continue;
long cost = dp[i][k] + dist[i];
if (!last) {
cost = (cost + speed - 1) / speed * speed;
}
dp[i + 1][k] = Math.min(dp[i + 1][k], cost);
if (!last) {
dp[i + 1][k + 1] = Math.min(dp[i + 1][k + 1], dp[i][k] + dist[i]);
}
}
}
for (int k = 0; k <= n; k++) {
if (dp[n][k] <= limit) {
return k;
}
}
return -1;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] ds = {{1, 3, 2}, {7, 3, 5, 5}, {7, 3, 5, 5}, {1, 1, 1}, {5, 5, 5, 5}};
int[] speeds = {4, 2, 1, 1, 2};
int[] limits = {2, 10, 10, 3, 10};
boolean ok = true;
for (int t = 0; t < ds.length; t++) {
int r1 = s.minSkips(ds[t], speeds[t], limits[t]);
int r2 = s.minSkips2D(ds[t], speeds[t], limits[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1884. Egg Drop With 2 Eggs and N Floors(鸡蛋掉落-两枚鸡蛋)
题目描述
给你 2 枚相同的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f,满足 0 <= f <= n,任何从高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的
楼层落下的鸡蛋都不会碎。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,
你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中重复使用这枚鸡蛋。
请你计算并返回要确定 f 确切的值的最小操作次数是多少?
示例 1
- 输入 :
n= 2 - 输出:2
示例 2
- 输入 :
n= 100 - 输出 :14
提示: - 1 <= n <= 1000
解题思路
关键观察:第一枚鸡蛋"探路"采取递增间隔策略。若第一次从 k 层扔、第二次从 k+(k-1) 层扔、
第三次从 k+(k-1)+(k-2) 层扔......则每多一次试探可多覆盖一层;第 k 次试探可覆盖的总楼层数为
k*(k+1)/2。因此答案是使 k*(k+1)/2 >= n 的最小 k。
解法一:数学直接求解,主提交版本
对 k 从 1 递增(或解二次方程),找最小 k 满足
k(k+1)/2 >= n,k 最大不超过 45(1000 层)。O(sqrt(n))。
解法二:DP 一般化推导,对照
用更通用的"鸡蛋掉落"DP:dpte = 用 e 枚鸡蛋最多可
尝试 t 次时能确定的最大楼层数;dpte = dpt-1e-1 + dpt-1e + 1。
找到最小的 t 使 dpt2 >= n。对应第一类斯特林类似递推,便于推广到多枚鸡蛋。
代码实现
java
public class Solution {
public int twoEggDrop(int n) {
int k = 1;
while (k * (k + 1) / 2 < n) {
k++;
}
return k;
}
public int twoEggDropDP(int n) {
// dp[t][e] = 用 e 枚鸡蛋尝试 t 次能确定的最大楼层数
// dp[t][e] = dp[t-1][e-1] + dp[t-1][e] + 1;dp[t][1] = t
int t = 0;
long prev1 = 0; // dp[0][1] = 0
long cur2 = 0; // dp[0][2] = 0
while (cur2 < n) {
t++;
long next = prev1 + cur2 + 1; // dp[t][2]
prev1 = t; // dp[t][1] = t
cur2 = next;
}
return t;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] cases = {1, 2, 5, 10, 100, 1000, 45, 36};
boolean ok = true;
for (int c : cases) {
int r1 = s.twoEggDrop(c);
int r2 = s.twoEggDropDP(c);
ok &= (r1 == r2);
System.out.println("n=" + c + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1885. Count Pairs in Two Arrays(统计数对)
题目描述
给你两个长度为 n 的整数数组 nums1 和 nums2,找出所有满足 i < j 且
nums1[i] + nums1[j] > nums2[i] + nums2[j] 的数对 (i, j)。
返回满足条件数对的个数。
示例 1
- 输入 :
nums1= 2,1,2,1,nums2= 1,2,1,2 - 输出:1
- 解释:只有 (0,2) 满足 2+2 > 1+1。
示例 2
- 输入 :
nums1= 1,10,6,2,nums2= 1,4,1,5 - 输出 :5
提示: - n == nums1.length == nums2.length- 1 <= n <= 10^5- 1 <= nums1[i], nums2[i] <= 10^5
解题思路
移项:nums1i + nums1j > nums2i + nums2j
<=> (nums1i - nums2i) + (nums1j - nums2j) > 0。
记 di = nums1i - nums2i,问题变成"有序数组 d 中 i < j 且 di + dj > 0 的数对数"。
排序后经典双指针:l 从 0、r 从 n-1,若 dl + dr > 0,则 l...r-1 与 r 都配对,
累加 (r - l) 后 r--;否则 l++。
解法一:排序 + 双指针,主提交版本
O(n log n) 时间、O(n) 空间(diff 数组)。
解法二:排序 + 二分,对照
排序后对每个 i,用二分找满足 dj > -di 的最小下标 j,
累加 n - j,注意排除自身(j 必须 > i 不需特别排除,因为 i < j 条件通过下标关系体现:
遍历时只统计 j > i 的部分)。实现直观但对 O(n log n) 二分常数更大。
代码实现
java
import java.util.Arrays;
public class Solution {
public long countPairs(int[] nums1, int[] nums2) {
int n = nums1.length;
int[] d = new int[n];
for (int i = 0; i < n; i++) {
d[i] = nums1[i] - nums2[i];
}
Arrays.sort(d);
long ans = 0;
int l = 0, r = n - 1;
while (l < r) {
if (d[l] + d[r] > 0) {
ans += r - l; // d[l..r-1] 与 d[r] 均满足
r--;
} else {
l++;
}
}
return ans;
}
public long countPairsBinary(int[] nums1, int[] nums2) {
int n = nums1.length;
int[] d = new int[n];
for (int i = 0; i < n; i++) {
d[i] = nums1[i] - nums2[i];
}
Arrays.sort(d);
long ans = 0;
for (int i = 0; i < n; i++) {
// 找第一个 d[j] > -d[i] 的下标;j 可能等于 i,需保证 j > i。
// 由于二分找的是全数组第一个 > -d[i] 的下标 pos,pos 可能 <= i,
// 但 d[i] 与它配对数不依赖下标序,排序后"所有优于互补"的元素都可配对,
// 而 i<j 的约束通过等价形式(每个有序对选较小的处理一次)自然满足:
int lo = 0, hi = n;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (d[mid] > -d[i]) hi = mid;
else lo = mid + 1;
}
// 真正的配对数为 max(0, pos 右侧元素个数 - (自配对校正))
// 自配对校正:当 i 恰好落在满足区间内时需扣 1(d[i]+d[j]>0 且 j==i 是自己)
long add = n - lo;
if (d[i] > -d[i]) {
add--; // 自身被计入,扣除
}
ans += add;
}
return ans / 2; // 每个有序对 (i,j) 被计两次
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] a = {{2, 1, 2, 1}, {1, 10, 6, 2}, {1, 1, 1}, {5, 5, 5, 5}, {10, 20, 30}};
int[][] b = {{1, 2, 1, 2}, {1, 4, 1, 5}, {1, 1, 1}, {1, 1, 1, 1}, {30, 20, 10}};
boolean ok = true;
for (int t = 0; t < a.length; t++) {
long r1 = s.countPairs(a[t], b[t]);
long r2 = s.countPairsBinary(a[t], b[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1886. Determine Whether Matrix Can Be Obtained By Rotation
题目描述
(判断矩阵经轮转后是否一致)
给你两个大小为 n x n 的二进制矩阵 mat 和 target。现以 90 度顺时针轮转矩阵 mat 中的元素
若干次,如果能够使 mat 与 target 一致,返回 true;否则,返回 false。
示例 1
- 输入 :
mat= \[0,1,1,0],target= \[1,0,0,1] - 输出 :
true - 解释 :顺时针轮转 90 度一次可以使
mat和target一致。
示例 2
- 输入 :
mat= \[0,1,1,1],target= \[1,0,0,1] - 输出 :
false
示例 3
- 输入 :
mat= \[0,0,0,0,1,0,1,1,1],target= \[1,1,1,0,1,0,0,0,0] - 输出 :
true - 解释 :顺时针轮转 90 度两次可以使
mat和target一致。
提示: - n == mat.length == target.length- n == mat[i].length == target[i].length- 1 <= n <= 10-
mat[i][j]和target[i][j]不是 0 就是 1
解题思路
顺时针旋转 90° 一次:rotrc = matn-1-cr。旋转 0、1、2、3 次逐一与 target 比较。
解法一:显式旋转函数,主提交版本
实现 rotate90 返回新矩阵,比较 0~3 次旋转结果。
O(n^2) 空间与时间(每次旋转 O(n^2),共 4 次)。
解法二:坐标映射直接比较,对照
对旋转 k 次(k = 0...3),检查所有 (r, c) 是否满足
mat映射坐标 == targetrc,无需真实构造旋转矩阵,常数更小。
代码实现
java
import java.util.Arrays;
public class Solution {
public boolean findRotation(int[][] mat, int[][] target) {
int n = mat.length;
int[][] cur = mat;
for (int k = 0; k < 4; k++) {
if (Arrays.deepEquals(cur, target)) {
return true;
}
cur = rotate90(cur);
}
return false;
}
private int[][] rotate90(int[][] m) {
int n = m.length;
int[][] r = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
r[j][n - 1 - i] = m[i][j];
}
}
return r;
}
public boolean findRotationMap(int[][] mat, int[][] target) {
int n = mat.length;
for (int k = 0; k < 4; k++) {
boolean same = true;
for (int i = 0; i < n && same; i++) {
for (int j = 0; j < n; j++) {
int si = i, sj = j;
// 旋转 k 次后 target 的 (i,j) 来自原始 mat 的位置
for (int t = 0; t < k; t++) {
int ni = sj;
int nj = n - 1 - si;
si = ni;
sj = nj;
}
if (mat[si][sj] != target[i][j]) {
same = false;
break;
}
}
}
if (same) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][][] mats = {
{{0, 1}, {1, 0}},
{{0, 1}, {1, 1}},
{{0, 0, 0}, {0, 1, 0}, {1, 1, 1}},
{{1}},
{{1, 2}, {3, 4}}
};
int[][][] targets = {
{{1, 0}, {0, 1}},
{{1, 0}, {0, 1}},
{{1, 1, 1}, {0, 1, 0}, {0, 0, 0}},
{{1}},
{{3, 1}, {4, 2}}
};
boolean ok = true;
for (int t = 0; t < mats.length; t++) {
boolean r1 = s.findRotation(mats[t], targets[t]);
boolean r2 = s.findRotationMap(mats[t], targets[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1887. Reduction Operations to Make the Array Elements Equal
题目描述
(使数组元素相等的减少操作次数)
给你一个整数数组 nums,你的目标是令 nums 中的所有元素相等。完成一次减少操作需要遵照下面的
几个步骤:
- 找出
nums中的最大值。记这个值为largest并取其下标i(如果有多个元素都是最大值,则取最小
的下标i)。 - 找出
nums中的下一个最大值,这个值严格小于largest,记为nextLargest。 - 将
nums[i]减少到nextLargest。
返回使nums中的所有元素相等的操作次数。
示例 1
- 输入 :
nums= 5,1,3 - 输出:3
示例 2
- 输入 :
nums= 1,1,1 - 输出:0
示例 3
- 输入 :
nums= 1,1,2,2,3 - 输出 :4
提示: - 1 <= nums.length <= 5 * 10^4- 1 <= nums[i] <= 5 * 10^4
解题思路
观察:每次操作都把"当前最大值"降到"次大值"。若排序后有若干不同取值 v1 < v2 < ... < vm
(升序),那么取值 vj 的元素最终都要一路降到 v1,每个 vj 层的元素需要执行
(该层及更大层的元素个数 - 1) 次操作合计。等价地:从大到小扫,每次遇到新值切换层级时,
累加"前缀(较大的)元素个数"作为一次整层降低的代价。
解法一:排序 + 层级计数,主提交版本
升序排序后从右往左遍历,每当发现新值层级变化,
累加 i(该值及其右边所有更大值的个数);O(n log n)。
解法二:值域计数 + 前缀累计,对照
不排序,用值域数组统计频率,从大到小遍历不同取值,
用"已见更大元素数"累计操作;O(n + maxVal)。
代码实现
java
import java.util.Arrays;
public class Solution {
public int reductionOperations(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
int ans = 0;
int start = n - 1; // 当前层的起始(最右)下标
for (int i = n - 2; i >= 0; i--) {
if (nums[i] != nums[i + 1]) {
// 遇到更小的新层级:nums[i+1..] 这一整批(共 start - i 个... 实际是 n-1-i 个)
// 需要执行 1 次"降到 nums[i]";而更多层迭代会叠加
ans += n - 1 - i;
}
}
return ans;
}
public int reductionOperationsCount(int[] nums) {
int max = 0;
for (int v : nums) {
if (v > max) max = v;
}
int[] cnt = new int[max + 1];
for (int v : nums) cnt[v]++;
int ans = 0;
int bigger = 0; // 已经处理过的、更大的元素个数
// 从大到小遍历取值
for (int v = max; v >= 0; v--) {
if (cnt[v] > 0) {
if (bigger > 0) {
ans += bigger; // 把这一整层(bigger 个元素)降一级的次数
}
bigger += cnt[v];
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] cases = {{5, 1, 3}, {1, 1, 1}, {1, 1, 2, 2, 3}, {1, 2, 3, 4}, {2, 2, 2, 2}, {1, 2}, {1, 1, 1, 2}};
boolean ok = true;
for (int[] c : cases) {
int r1 = s.reductionOperations(c);
int r2 = s.reductionOperationsCount(c);
ok &= (r1 == r2);
System.out.println(Arrays.toString(c) + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1888. Minimum Number of Flips to Make the Binary String Alternating
题目描述
(使二进制字符串字符交替的最少反转次数)
给你一个二进制字符串 s。你可以按任意顺序执行以下两种操作任意次:
- 类型 1:删除字符串
s的第一个字符并将它添加到字符串结尾。 - 类型 2:选择字符串
s中任意一个字符并将该字符反转,即如果值为 '0',则反转得到 '1',
反之亦然。
请你返回使s变成交替字符串的前提下,类型 2 的最少操作次数。
我们称一个字符串是交替的,需要满足任意相邻字符都不同。比方说,字符串 "010" 和 "1010"
都是交替的,但是字符串 "0100" 不是。
示例 1
- 输入 :
s= "111000" - 输出:2
- 解释 :执行第一种操作两次,得到
s= "100011",然后对第三和第六个字符执行第二种操作,
得到 "101010"。
示例 2
- 输入 :
s= "010" - 输出:0
示例 3
- 输入 :
s= "1110" - 输出 :1
提示: - 1 <= s.length <= 10^5-
s[i]要么是 '0',要么是 '1'
解题思路
类型 1 可执行任意次,等价于"考虑 s 的所有循环移位"。设 t = s + s(长度为 2n)。
交替串只有两种形态:pattern0 = "0101..."、pattern1 = "1010..."。对任意窗口起点 l
(0 <= l < n),窗口 l, l+n-1 即 s 的一种循环移位,其最少反转次数 =
min(该窗口与 pattern0 的差异数, 与 pattern1 的差异数)。
关键推导:对 pattern0,定义 ai = (ti 与"按全局下标 i 奇偶的 pattern0"不符) ? 1 : 0。
窗口起点 l 的差异数 d0(l) = Σ_{i=l}^{l+n-1} t\[i 与"按窗口内位置奇偶的 pattern0"不符]。
由于"窗口内位置奇偶"与"全局位置奇偶"的关系是:l 偶时二者一致、l 奇时互补,
因此 d0(l) = l 偶 ? sumA(l) : n - sumA(l),其中 sumA(l)=Σ_{i=l}^{l+n-1} ai。
同 理 d1(l) 用 bi(按全局下标奇偶的 pattern1 差异)计算。滑动窗口 O(1) 维护 sumA/sumB。
解法一:滑动窗口差分,主提交版本
O(n) 时间、O(n) 空间。
解法二:逐起点枚举,对照
对每个起点直接逐位统计差异,O(n^2),小规模验证用。
代码实现
java
public class Solution {
public int minFlips(String s) {
int n = s.length();
String t = s + s;
int m = 2 * n;
int[] a = new int[m]; // a[i]:t[i] 与"全局下标奇偶定 pattern0"的差异
int[] b = new int[m]; // b[i]:t[i] 与"全局下标奇偶定 pattern1"的差异
for (int i = 0; i < m; i++) {
char c = t.charAt(i);
if (c != (i % 2 == 0 ? '0' : '1')) a[i] = 1;
if (c != (i % 2 == 0 ? '1' : '0')) b[i] = 1;
}
int sumA = 0, sumB = 0;
for (int i = 0; i < n; i++) {
sumA += a[i];
sumB += b[i];
}
// 起点 l = 0(原串不移位)
int ans = Math.min(Math.min(sumA, n - sumA), Math.min(sumB, n - sumB));
// 起点 l = 1..n-1
for (int l = 1; l < n; l++) {
sumA += a[l + n - 1] - a[l - 1];
sumB += b[l + n - 1] - b[l - 1];
int d0 = (l % 2 == 0) ? sumA : n - sumA;
int d1 = (l % 2 == 0) ? sumB : n - sumB;
ans = Math.min(ans, Math.min(d0, d1));
}
return ans;
}
public int minFlipsBrute(String s) {
int n = s.length();
String t = s + s;
int ans = Integer.MAX_VALUE;
for (int l = 0; l < n; l++) {
int d0 = 0, d1 = 0;
for (int k = 0; k < n; k++) {
char c = t.charAt(l + k);
if (c != (k % 2 == 0 ? '0' : '1')) d0++;
if (c != (k % 2 == 0 ? '1' : '0')) d1++;
}
ans = Math.min(ans, Math.min(d0, d1));
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] cases = {"111000", "010", "1110", "000", "111", "101", "01010", "1001", "1", "0",
"010010", "0011001", "1010101", "11001011"};
boolean ok = true;
for (String c : cases) {
int r1 = s.minFlips(c);
int r2 = s.minFlipsBrute(c);
ok &= (r1 == r2);
System.out.println(c + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1889. Minimum Space Wasted From Packaging(装包裹的最小浪费空间)
题目描述
给你 n 个包裹,你需要把它们装在箱子里,每个箱子装一个包裹。总共有 m 个供应商提供不同尺寸的
箱子(每个规格都有无数个箱子)。如果一个包裹的尺寸小于等于一个箱子的尺寸,那么这个包裹就可以
放入这个箱子之中。
包裹的尺寸用一个整数数组 packages 表示,其中 packages[i] 是第 i 个包裹的尺寸。供应商用二维
数组 boxes 表示,其中 boxes[j] 是第 j 个供应商提供的所有箱子尺寸的数组。
你想要选择一个供应商并只使用该供应商提供的箱子,使得总浪费空间最小。对于每个装了包裹的箱子,
我们定义浪费的空间等于 箱子的尺寸 - 包裹的尺寸。总浪费空间为所有箱子中浪费空间的总和。
请你选择最优箱子供应商,使得总浪费空间最小。如果无法将所有包裹放入箱子中,请你返回 -1。
由于答案可能会很大,请返回它对 10^9 + 7 取余的结果。
示例 1
- 输入 :
packages= 2,3,5,boxes= \[4,8,2,8] - 输出:6
示例 2
- 输入 :
packages= 2,3,5,boxes= \[1,4,2,3,3,4] - 输出:-1
示例 3
- 输入 :
packages= 3,5,8,10,11,12,boxes= \[12,11,9,10,5,14] - 输出 :9
提示: - n == packages.length- m == boxes.length- 1 <= n <= 10^5-
- 1 <=
packages[i],boxes[j]中各元素 <= 10^5
- 1 <=
- sum(boxes[j].length) <= 10^5-
boxes[j]中的元素互不相同
解题思路
对每个供应商,其最优装法:包裹排升序、箱子排升序,从小到大依次用最小的能装下包裹的箱子装。
浪费和 = Σ 每个箱子尺寸 * 用它装的包裹数 - Σ 包裹总尺寸。后者是常量,因此只需最小化
"Σ 箱子尺寸 * 装入数"。
对某供应商:把箱子排序,对每个箱子 b,用二分(排序后的包裹)找到"用前一个箱子装不下的首
个包裹下标"与"能装进 b 的最后一个包裹下标",区间内的包裹都用 b 装,贡献
b * 区间个数,用包裹前缀和快速得到包裹尺寸和,累加贡献与包裹和之差即浪费。
解法一:排序 + 二分 + 前缀和(主提交版本)
O((n + sum|boxes|) log n)。
解法二:相同框架、用指针扫描替代二分,对照
箱子升序时"能装下的包裹区间"用双指针维护,
避免重复二分,便于对照理解。
代码实现
java
import java.util.Arrays;
public class Solution {
private static final int MOD = 1000000007;
public int minWastedSpace(int[] packages, int[][] boxes) {
Arrays.sort(packages);
int n = packages.length;
long[] pref = new long[n + 1];
for (int i = 0; i < n; i++) {
pref[i + 1] = pref[i] + packages[i];
}
long total = pref[n];
long best = Long.MAX_VALUE;
for (int[] box : boxes) {
Arrays.sort(box);
if (box[box.length - 1] < packages[n - 1]) {
continue; // 该供应商装不下最大包裹
}
long used = 0;
int last = 0; // 已装入的包裹数
for (int b : box) {
// 找到 packages 中 <= b 的最大下标(全局二分)
int hi = upperBound(packages, b);
if (hi <= last) continue; // 这个箱子没有新的可用包裹
used += (long) b * (hi - last);
last = hi;
if (last == n) break;
}
if (last < n) {
continue; // 装不完(理论上不会,因为最大箱已覆盖)
}
best = Math.min(best, used - total);
}
return best == Long.MAX_VALUE ? -1 : (int) (best % MOD);
}
private int upperBound(int[] a, int key) {
int lo = 0, hi = a.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (a[mid] <= key) lo = mid + 1;
else hi = mid;
}
return lo;
}
public int minWastedSpaceTwoPointer(int[] packages, int[][] boxes) {
Arrays.sort(packages);
int n = packages.length;
long[] pref = new long[n + 1];
for (int i = 0; i < n; i++) {
pref[i + 1] = pref[i] + packages[i];
}
long total = pref[n];
long best = Long.MAX_VALUE;
for (int[] box : boxes) {
Arrays.sort(box);
if (box[box.length - 1] < packages[n - 1]) {
continue;
}
long used = 0;
int last = 0;
for (int b : box) {
int hi = last;
while (hi < n && packages[hi] <= b) {
hi++;
}
if (hi > last) {
used += (long) b * (hi - last);
last = hi;
}
if (last == n) break;
}
if (last == n) {
best = Math.min(best, used - total);
}
}
return best == Long.MAX_VALUE ? -1 : (int) (best % MOD);
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] packages = {{2, 3, 5}, {2, 3, 5}, {3, 5, 8, 10, 11, 12}, {1, 1}, {10}};
int[][][] boxes = {
{{4, 8}, {2, 8}},
{{1, 4}, {2, 3}, {3, 4}},
{{12}, {11, 9}, {10, 5, 14}},
{{1, 2}, {2, 2}},
{{5}, {10}}
};
boolean ok = true;
for (int t = 0; t < packages.length; t++) {
int r1 = s.minWastedSpace(packages[t], boxes[t]);
int r2 = s.minWastedSpaceTwoPointer(packages[t], boxes[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1890. The Latest Login in 2020(2020年最后一次登录)
题目类型:数据库(SQL)
题目描述
表:Logins
+----------------+----------+
| 列名 | 类型 |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
+----------------+----------+
(user_id, time_stamp) 是这个表的主键。每一行包含的信息是 user_id 这个用户的登录时间。
编写解决方案以获取在 2020 年登录过的所有用户的本年度最后一次登录时间。结果集不包含 2020 年没有登录过的用户。
示例 1:
输入:
Logins 表:
+---------+---------------------+
| user_id | time_stamp |
+---------+---------------------+
| 6 | 2020-06-30 15:06:07 |
| 6 | 2021-04-21 14:06:06 |
| 6 | 2019-03-07 00:18:15 |
| 8 | 2020-02-01 05:10:53 |
| 8 | 2020-12-30 00:46:50 |
| 2 | 2020-01-16 02:49:50 |
| 2 | 2019-08-25 07:59:08 |
| 14 | 2019-07-14 09:00:00 |
| 14 | 2021-01-06 11:59:59 |
+---------+---------------------+
输出:
+---------+---------------------+
| user_id | last_stamp |
+---------+---------------------+
| 6 | 2020-06-30 15:06:07 |
| 8 | 2020-12-30 00:46:50 |
| 2 | 2020-01-16 02:49:50 |
+---------+---------------------+
解题思路
必须先过滤 2020 年再取最大值 :先 WHERE time_stamp 限定在 2020 年内,再按 user_id 分组
取 MAX(time_stamp);否则用户 6 的 2021-04-21 会污染结果。
代码实现
sql
SELECT user_id, MAX(time_stamp) AS last_stamp
FROM Logins
WHERE time_stamp BETWEEN '2020-01-01 00:00:00' AND '2020-12-31 23:59:59'
GROUP BY user_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1891. Cutting Ribbons(割绳子)
题目描述
给定一个整数数组 ribbons 和一个整数 k,数组每项 ribbons[i] 表示第 i 条绳子的长度。对于每条
绳子,你可以将任意切割成一系列长度为正整数的部分,或者选择不进行切割。
例如,如果给你一条长度为 4 的绳子,你可以保持长度为 4 不变,或切割成 3+1、2+2、2+1+1、
1+1+1+1。
你的任务是找出最大 x 值,要求满足可以裁切出至少 k 条长度均为 x 的绳子。你可以丢弃裁切后剩余
的任意长度的绳子。如果不可能切割出 k 条相同长度的绳子,返回 0。
示例 1
- 输入 :
ribbons= 9,7,5,k= 3 - 输出:5
- 解释:第一条切成 5+4,第二条切成 5+2,第三条不切,得到 3 条长度为 5 的绳子。
示例 2
- 输入 :
ribbons= 7,5,9,k= 4 - 输出:4
示例 3
- 输入 :
ribbons= 5,7,9,k= 22 - 输出:0
- 解释 :由于绳子长度需要为正整数,无法得到 22 条长度相同的绳子。
提示: - 1 <= ribbons.length <= 10^5- 1 <= ribbons[i] <= 10^5- 1 <= k <= 10^9
解题思路
对目标长度 x,能从所有绳子上切出的段数为 Σ (ribbonsi / x);x 越大段数越少,单调可二分。
二分下界 1、上界 max(ribbons);若 x=1 时段数都 < k,返回 0。
解法一:二分答案,主提交版本
O(n log maxLen)。
解法二:线性递降,对照
从 maxLen 逐 1 下降尝试(小规模),验证二分正确性。
代码实现
java
public class Solution {
public int maxLength(int[] ribbons, int k) {
int max = 0;
for (int v : ribbons) {
if (v > max) max = v;
}
int lo = 1, hi = max, ans = 0;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
long cnt = 0;
for (int v : ribbons) {
cnt += v / mid;
if (cnt >= k) break; // 提前终止
}
if (cnt >= k) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
public int maxLengthLinear(int[] ribbons, int k) {
int max = 0;
for (int v : ribbons) {
if (v > max) max = v;
}
for (int x = max; x >= 1; x--) {
long cnt = 0;
for (int v : ribbons) {
cnt += v / x;
}
if (cnt >= k) return x;
}
return 0;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] rbs = {{9, 7, 5}, {7, 5, 9}, {5, 7, 9}, {10, 10, 10}, {1}, {100000}};
int[] ks = {3, 4, 22, 5, 1, 50000};
boolean ok = true;
for (int t = 0; t < rbs.length; t++) {
int r1 = s.maxLength(rbs[t], ks[t]);
int r2 = s.maxLengthLinear(rbs[t], ks[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1892. Page Recommendations II(页面推荐Ⅱ)
题目类型:数据库(SQL)
题目描述
表:Friendship
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| user1_id | int |
| user2_id | int |
+---------------+---------+
(user1_id, user2_id) 是 Friendship 表的主键。该表的每一行表示用户 user1_id 和 user2_id 是好友。
表:Likes
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| user_id | int |
| page_id | int |
+-------------+---------+
(user_id, page_id) 是 Likes 表的主键。该表的每一行表示 user_id 喜欢 page_id。
如果页面被 user_id 的至少一个朋友喜欢 ,而不被 user_id 喜欢 ,系统将推荐该页面给 user_id。编写解决方案查找针对每个用户的所有可能的页面建议:user_id(系统向其提出建议的用户)、page_id(推荐页面)、friends_likes(喜欢该页面的好友数)。
示例 1:
输入:
Friendship 表: Likes 表:
+----------+----------+ +---------+---------+
| user1_id | user2_id | | user_id | page_id |
+----------+----------+ +---------+---------+
| 1 | 2 | | 1 | 88 |
| 1 | 3 | | 2 | 23 |
| 1 | 4 | | 3 | 24 |
| 2 | 3 | | 4 | 56 |
| 2 | 4 | | 5 | 11 |
| 2 | 5 | | 6 | 33 |
| 6 | 1 | | 2 | 77 |
+----------+----------+ | 3 | 77 |
| 6 | 88 |
+---------+---------+
输出:
+---------+---------+---------------+
| user_id | page_id | friends_likes |
+---------+---------+---------------+
| 1 | 77 | 2 |
| 1 | 23 | 1 |
| ... | ... | ... |
| 5 | 77 | 1 |
| 5 | 23 | 1 |
+---------+---------+---------------+
解释:以用户 1 为例,其好友为 2、3、4、6;好友喜欢的页面中排除自己已喜欢的 88,得到 77(2 个好友喜欢)、23、24、56、33。
解题思路
① 把 Friendship 双向展开成 (user_id, friend_id) 视图(UNION 去重);
② 连接 Likes 取好友喜欢的页面,并按 (user_id, page_id) 分组统计好友数;
③ 用 LEFT JOIN 自己的 Likes 排除自己已喜欢的页面。
代码实现
sql
WITH friends AS (
SELECT user1_id AS user_id, user2_id AS friend_id FROM Friendship
UNION
SELECT user2_id, user1_id FROM Friendship
)
SELECT f.user_id, l.page_id, COUNT(*) AS friends_likes
FROM friends f
JOIN Likes l ON f.friend_id = l.user_id
LEFT JOIN Likes own
ON f.user_id = own.user_id AND own.page_id = l.page_id
WHERE own.user_id IS NULL
GROUP BY f.user_id, l.page_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1893. Check if All the Integers in a Range Are Covered
题目描述
(检查是否区域内所有整数都被覆盖)
给你一个二维整数数组 ranges 和两个整数 left 和 right。每个 ranges[i] = `starti`, `endi`
表示一个从 starti 到 endi 的闭区间。
如果闭区间 `left`, `right` 内每个整数都被 ranges 中至少一个区间覆盖,那么请你返回 true,
否则返回 false。
已知区间 ranges[i] = `starti`, `endi`,如果整数 x 满足 starti <= x <= endi,那么我们称整数 x
被覆盖了。
示例 1
- 输入 :
ranges= \[1,2,3,4,5,6],left= 2,right= 5 - 输出 :
true
示例 2
- 输入 :
ranges= \[1,10,10,20],left= 21,right= 21 - 输出 :
false
提示: - 1 <= ranges.length <= 50- 1 <= starti <= endi <= 50- 1 <= left <= right <= 50
解题思路
解法一:布尔数组覆盖标记,主提交版本
对每个区间标记覆盖点(值域 <= 50),最后检查
left, right 内每个点是否都被标记。O(50 * ranges.length)。
解法二:差分数组,对照
用差分数组对区间 +1/-1,做前缀和得到每个点的覆盖次数,
再检查 left, right 全 > 0;O(maxVal + ranges.length),语义更工程化。
代码实现
java
public class Solution {
public boolean isCovered(int[][] ranges, int left, int right) {
boolean[] cover = new boolean[55];
for (int[] r : ranges) {
for (int x = r[0]; x <= r[1]; x++) {
cover[x] = true;
}
}
for (int x = left; x <= right; x++) {
if (!cover[x]) {
return false;
}
}
return true;
}
public boolean isCoveredDiff(int[][] ranges, int left, int right) {
int[] diff = new int[55];
for (int[] r : ranges) {
diff[r[0]]++;
diff[r[1] + 1]--;
}
int cur = 0;
for (int x = 1; x <= 50; x++) {
cur += diff[x];
if (x >= left && x <= right && cur <= 0) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][][] rng = {
{{1, 2}, {3, 4}, {5, 6}},
{{1, 10}, {10, 20}},
{{1, 50}},
{{2, 4}},
{{1, 3}, {5, 8}}
};
int[] ls = {2, 21, 1, 1, 4};
int[] rs = {5, 21, 50, 4, 4};
boolean ok = true;
for (int t = 0; t < rng.length; t++) {
boolean r1 = s.isCovered(rng[t], ls[t], rs[t]);
boolean r2 = s.isCoveredDiff(rng[t], ls[t], rs[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1894. Find the Student that Will Replace the Chalk
题目描述
(找到需要补充粉笔的学生编号)
一个班级里有 n 个学生,编号为 0 到 n - 1。每个学生会依次回答问题,编号为 0 的学生先回答,
然后是编号为 1 的学生,以此类推,直到编号为 n - 1 的学生,然后老师会重复这个过程,重新从
编号为 0 的学生开始回答问题。
给你一个长度为 n 且下标从 0 开始的整数数组 chalk 和一个整数 k。一开始粉笔盒里总共有 k 支粉笔。
当编号为 i 的学生回答问题时,他会消耗 chalk[i] 支粉笔。如果剩余粉笔数量严格小于 chalk[i],
那么学生 i 需要补充粉笔。
请你返回需要补充粉笔的学生编号。
示例 1
- 输入 :
chalk= 5,1,5,k= 22 - 输出:0
示例 2
- 输入 :
chalk= 3,4,1,2,k= 25 - 输出 :1
提示: - chalk.length == n- 1 <= n <= 10^5- 1 <= chalk[i] <= 10^5- 1 <= k <= 10^9
解题思路
一轮完整循环消耗 sum(chalk)。k 可以先对 sum 取余:k %= sum,此时 k < sum,只需走不到一轮。
解法一:取余 + 线性扫描,主提交版本
先求总消耗并对 k 取余,然后顺序遍历 chalk 累减,
找到第一个 k < chalki 的学生。O(n)。
解法二:前缀和 + 二分,对照
构造前缀和(模 sum 后),对剩余 k 二分找第一个前缀和 > k
的下标即为答案。O(n) 建表 + O(log n) 查询。
代码实现
java
public class Solution {
public int chalkReplacer(int[] chalk, int k) {
long sum = 0;
for (int v : chalk) {
sum += v;
}
long remain = k % sum; // 完整循环后剩余的粉笔
for (int i = 0; i < chalk.length; i++) {
if (remain < chalk[i]) {
return i;
}
remain -= chalk[i];
}
return 0; // 理论上走不到
}
public int chalkReplacerBinary(int[] chalk, int k) {
int n = chalk.length;
long[] pref = new long[n];
pref[0] = chalk[0];
for (int i = 1; i < n; i++) {
pref[i] = pref[i - 1] + chalk[i];
}
long remain = k % pref[n - 1];
// 找第一个 pref[i] > remain 的下标
int lo = 0, hi = n - 1;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (pref[mid] > remain) hi = mid;
else lo = mid + 1;
}
return lo;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] chalks = {{5, 1, 5}, {3, 4, 1, 2}, {1, 1, 1}, {100000}, {2, 3, 5, 7}};
int[] ks = {22, 25, 1, 999999, 10};
boolean ok = true;
for (int t = 0; t < chalks.length; t++) {
int r1 = s.chalkReplacer(chalks[t], ks[t]);
int r2 = s.chalkReplacerBinary(chalks[t], ks[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1895. Largest Magic Square(最大的幻方)
题目描述
一个 k x k 的幻方指的是一个 k x k 填满整数的方格阵,且每一行、每一列以及两条对角线的和
全部相等。幻方中的整数不需要互不相同。显然,每个 1 x 1 的方格都是一个幻方。
给你一个 m x n 的整数矩阵 grid,请你返回矩阵中最大幻方的尺寸(即边长 k)。
示例 1
- 输入 :
grid= \[7,1,4,5,6,2,5,1,6,4,1,5,4,3,2,1,2,7,3,4] - 输出:3
- 解释:最大幻方尺寸为 3,其行、列、对角线之和都等于 12。
示例 2
- 输入 :
grid= \[5,1,3,1,9,3,3,1,1,3,3,8] - 输出 :2
提示: - m == grid.length- n == grid[i].length- 1 <= m, n <= 50- 1 <= grid[i][j] <= 10^5
解题思路
解法一:行/列前缀和 + 两条对角线前缀和,主提交版本
预计算行前缀 rowPref、列前缀 colPref
以及主、副对角线前缀 diag1/diag2。从大到小枚举边长 k,对每个左上角 (i, j) 检查:
行和、列和(用前缀 O(1))、两条对角线和(用 diagonal 前缀 O(1))都等于首行和基准。
找到的第一个可行 k 即最大。O(mn min(m,n))。
解法二:暴力逐格校验,对照
对每个候选方阵直接累加各行/列/对角线,O(k) 每次校验,
O(mnmin(m,n)^2),小规模对照验证。
代码实现
java
public class Solution {
public int largestMagicSquare(int[][] grid) {
int m = grid.length, n = grid[0].length;
long[][] rowPref = new long[m][n + 1];
long[][] colPref = new long[m + 1][n];
long[][] d1 = new long[m][n]; // 主对角线(↘)前缀和
long[][] d2 = new long[m][n]; // 副对角线(↙)前缀和
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
rowPref[i][j + 1] = rowPref[i][j] + grid[i][j];
colPref[i + 1][j] = colPref[i][j] + grid[i][j];
d1[i][j] = grid[i][j] + (i - 1 >= 0 && j - 1 >= 0 ? d1[i - 1][j - 1] : 0);
d2[i][j] = grid[i][j] + (i - 1 >= 0 && j + 1 < n ? d2[i - 1][j + 1] : 0);
}
}
for (int k = Math.min(m, n); k >= 1; k--) {
for (int i = 0; i + k <= m; i++) {
for (int j = 0; j + k <= n; j++) {
// 基准 = 首行和
long base = rowPref[i][j + k] - rowPref[i][j];
boolean ok = true;
// 各行
for (int r = i; r < i + k; r++) {
if (rowPref[r][j + k] - rowPref[r][j] != base) { ok = false; break; }
}
if (!ok) continue;
// 各列
for (int c = j; c < j + k; c++) {
if (colPref[i + k][c] - colPref[i][c] != base) { ok = false; break; }
}
if (!ok) continue;
// 主对角线:从 (i,j) 到 (i+k-1,j+k-1)
long dd1 = d1[i + k - 1][j + k - 1]
- (i - 1 >= 0 && j - 1 >= 0 ? d1[i - 1][j - 1] : 0);
if (dd1 != base) continue;
// 副对角线:从 (i,j+k-1) 到 (i+k-1,j)
long dd2 = d2[i + k - 1][j]
- (i - 1 >= 0 && j + k < n ? d2[i - 1][j + k] : 0);
if (dd2 != base) continue;
return k;
}
}
}
return 1;
}
public int largestMagicSquareBrute(int[][] grid) {
int m = grid.length, n = grid[0].length;
for (int k = Math.min(m, n); k >= 1; k--) {
for (int i = 0; i + k <= m; i++) {
for (int j = 0; j + k <= n; j++) {
long base = 0;
for (int c = j; c < j + k; c++) base += grid[i][c];
boolean ok = true;
for (int r = i; r < i + k; r++) {
long s = 0;
for (int c = j; c < j + k; c++) s += grid[r][c];
if (s != base) { ok = false; break; }
}
if (!ok) continue;
for (int c = j; c < j + k; c++) {
long s = 0;
for (int r = i; r < i + k; r++) s += grid[r][c];
if (s != base) { ok = false; break; }
}
if (!ok) continue;
long dd1 = 0, dd2 = 0;
for (int t = 0; t < k; t++) {
dd1 += grid[i + t][j + t];
dd2 += grid[i + t][j + k - 1 - t];
}
if (dd1 == base && dd2 == base) return k;
}
}
}
return 1;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] g1 = {{7, 1, 4, 5, 6}, {2, 5, 1, 6, 4}, {1, 5, 4, 3, 2}, {1, 2, 7, 3, 4}};
int[][] g2 = {{5, 1, 3, 1}, {9, 3, 3, 1}, {1, 3, 3, 8}};
int[][] g3 = {{2, 2, 2, 2}, {2, 2, 2, 2}, {2, 2, 2, 2}};
int[][] g4 = {{1}};
int[][][] cases = {g1, g2, g3, g4};
boolean ok = true;
for (int[][] c : cases) {
int r1 = s.largestMagicSquare(c);
int r2 = s.largestMagicSquareBrute(c);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1896. Minimum Cost to Change the Final Value of Expression
题目描述
(反转表达式值的最少操作次数)
给你一个有效的布尔表达式,用字符串 expression 表示。这个字符串包含字符 '1'、'0'、'&'
(按位与运算)、'|'(按位或运算)、'(' 和 ')'。
比方说,"()1|1" 和 "(1)&()" 不是有效布尔表达式。而 "1"、"(((1))|(0))" 和 "1|(0&(1))"
是有效布尔表达式。
你的目标是将布尔表达式的值反转(也就是将 0 变为 1,或者将 1 变为 0),请你返回达成目标需要的
最少操作次数。可执行的操作如下:
- 将一个 '1' 变成一个 '0'。
- 将一个 '0' 变成一个 '1'。
- 将一个 '&' 变成一个 '|'。
- 将一个 '|' 变成一个 '&'。
注意:'&' 的运算优先级与 '|' 相同。计算表达式时,括号优先级最高,然后按照从左到右的顺序运算。
示例 1
- 输入 :
expression= "1&(0|1)" - 输出:1
- 解释:将 '|' 变成 '&',得到 "1&(0&1)",值为 0。
示例 2
- 输入 :
expression= "(0&0)&(0&0&0)" - 输出:3
示例 3
- 输入 :
expression= "(0|(1|0&1))" - 输出 :1
提示: - 1 <= expression.length <= 10^5-
expression只包含 '1'、'0'、'&'、'|'、'(' 和 ')'
-
- 所有括号都有与之匹配的对应括号。
-
- 不会有空的括号(也就是说 "()" 不是
expression的子字符串)。
- 不会有空的括号(也就是说 "()" 不是
解题思路
自底向上 DP:对每个子表达式维护 (z, o) 两个数------z 是把它的值改成 0 的最小操作数,
o 是把它的值改成 1 的最小操作数。
- 叶子 '0':(0, 1);叶子 '1':(1, 0)。
- 组合 A op B(op 为 '&' 或 '|'):
& 的结果为 0 当且仅当至少一侧为 0:
z = min(A.z + min(B.z, B.o), min(A.z, A.o) + B.z)
& 的结果为 1 当且仅当两侧都为 1(或把运算符改成 '|' 后至少一侧为 1,代价 +1):
o = min(A.o + B.o, 1 + min(A.o + min(B.z, B.o), min(A.z, A.o) + B.o))
| 对称:
o = min(A.o + min(B.z, B.o), min(A.z, A.o) + B.o)
z = min(A.z + B.z, 1 + min(A.z + min(B.z, B.o), min(A.z, A.o) + B.z))
表达式求值用两个栈模拟(值栈 + 运算符栈),'&' 与 '|' 同级从左到右结合,遇新运算符先合
并前一个。最终答案:若根表达式当前值为 v,翻转代价 = v == 1 ? root.z : root.o。
解法一:栈模拟 + DP 合并(主提交版本)
O(n) 时间、O(n) 空间。
解法二:递归下降解析 + 同一 DP,对照
按括号递归求值,便于阅读理解组合规则。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
private static class Node {
int z;
int o;
Node(int z, int o) {
this.z = z;
this.o = o;
}
}
public int minOperationsToFlip(String expression) {
Deque<Node> vals = new ArrayDeque<>();
Deque<Character> ops = new ArrayDeque<>();
for (char c : expression.toCharArray()) {
if (c == '0' || c == '1') {
vals.push(c == '0' ? new Node(0, 1) : new Node(1, 0));
} else if (c == '(') {
ops.push(c);
} else if (c == ')') {
if (!ops.isEmpty() && ops.peek() != '(') {
char op = ops.pop();
Node right = vals.pop();
Node left = vals.pop();
vals.push(merge(left, right, op));
}
ops.pop(); // 弹出 '('
} else { // '&' 或 '|'
if (!ops.isEmpty() && ops.peek() != '(') {
char op = ops.pop(); // 同优先级从左到右:先合并前一运算符
Node right = vals.pop();
Node left = vals.pop();
vals.push(merge(left, right, op));
}
ops.push(c);
}
}
while (!ops.isEmpty()) { // 收尾:合并剩余
char op = ops.pop();
Node right = vals.pop();
Node left = vals.pop();
vals.push(merge(left, right, op));
}
Node root = vals.pop();
int curV = root.z == 0 ? 0 : 1; // 当前值
return curV == 1 ? root.z : root.o;
}
private Node merge(Node a, Node b, char op) {
if (op == '&') {
int z = Math.min(a.z + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.z);
int o = Math.min(a.o + b.o,
1 + Math.min(a.o + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.o));
return new Node(z, o);
}
// '|'
int o = Math.min(a.o + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.o);
int z = Math.min(a.z + b.z,
1 + Math.min(a.z + Math.min(b.z, b.o), Math.min(a.z, a.o) + b.z));
return new Node(z, o);
}
public int minOperationsToFlipParse(String expression) {
int[] idx = {0};
Node root = parse(expression, idx);
int curV = root.z == 0 ? 0 : 1;
return curV == 1 ? root.z : root.o;
}
private Node parse(String s, int[] idx) {
Node left = null;
char pending = 0;
while (idx[0] < s.length()) {
char c = s.charAt(idx[0]++);
if (c == '(') {
Node sub = parse(s, idx);
left = combine(left, sub, pending);
} else if (c == ')') {
break;
} else if (c == '0' || c == '1') {
Node num = new Node(c == '0' ? 0 : 1, c == '0' ? 1 : 0);
left = combine(left, num, pending);
} else { // '&' 或 '|'
pending = c;
}
}
return left;
}
private Node combine(Node a, Node b, char op) {
if (a == null) return b;
return merge(a, b, op);
}
public static void main(String[] args) {
Solution s = new Solution();
String[] cases = {
"1&(0|1)", // 1
"(0&0)&(0&0&0)", // 3
"(0|(1|0&1))", // 1
"1", // 1
"0", // 1
"1|1", // 1
"1&1", // 1
"0&1", // 1
"((1))", // 1
"1|0", // 1
"(1|0)&(0&1)", // ?
"0|0" // 1
};
boolean ok = true;
for (String c : cases) {
int r1 = s.minOperationsToFlip(c);
int r2 = s.minOperationsToFlipParse(c);
ok &= (r1 == r2);
System.out.println(c + " -> " + r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1897. Redistribute Characters to Make All Strings Equal
题目描述
(重新分配字符使所有字符串都相等)
给你一个字符串数组 words(下标从 0 开始计数)。
在一步操作中,需先选出两个不同下标 i 和 j,其中 words[i] 是一个非空字符串,接着将 words[i]
中的任一字符移动到 words[j] 中的任一位置上。
如果执行任意步操作可以使 words 中的每个字符串都相等,返回 true;否则,返回 false。
示例 1
- 输入 :
words= "`abc`","`aabc`","`bc`" - 输出 :
true - 解释 :将
words[1]中的第一个 'a' 移动到words[2]的最前面,所有字符串都等于 "abc"。
示例 2
- 输入 :
words= "`ab`","`a`" - 输出 :
false - 解释 :执行操作无法使所有字符串都相等。
提示: - 1 <= words.length <= 100- 1 <= words[i].length <= 100-
words[i]由小写英文字母组成
解题思路
字符可以在任意字符串之间自由移动,因此"能否全部相等"等价于"每个字符的总出现次数都能被
words.length 整除"(最终每个字符串会得到恰好 wordsi.length == 总长/m 个字符,
且每个字母均匀分配)。
解法一:26 字母频次整除判断,主提交版本
统计所有字符串中每个字母出现的总次数,
检查每个频次 % m == 0。O(总字符数)。
解法二:HashMap 等价实现,对照
用 Map<Character,Integer> 统计后遍历判断可整除,
与解法一逻辑完全一致、数据结构不同,便于理解。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
public boolean makeEqual(String[] words) {
int m = words.length;
int[] cnt = new int[26];
for (String w : words) {
for (char c : w.toCharArray()) {
cnt[c - 'a']++;
}
}
for (int v : cnt) {
if (v % m != 0) {
return false;
}
}
return true;
}
public boolean makeEqualMap(String[] words) {
int m = words.length;
Map<Character, Integer> freq = new HashMap<>();
for (String w : words) {
for (char c : w.toCharArray()) {
freq.put(c, freq.getOrDefault(c, 0) + 1);
}
}
for (int v : freq.values()) {
if (v % m != 0) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
String[][] cases = {
{"abc", "aabc", "bc"},
{"ab", "a"},
{"a", "a", "a"},
{"aa", "bb"},
{"ab", "ab", "cd"},
{"x"}
};
boolean ok = true;
for (String[] c : cases) {
boolean r1 = s.makeEqual(c);
boolean r2 = s.makeEqualMap(c);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1898. Maximum Number of Removable Characters(可移除字符的最大数目)
题目描述
给你两个字符串 s 和 p,其中 p 是 s 的一个子序列。同时,给你一个元素互不相同且下标从 0 开始
计数的整数数组 removable,该数组是 s 中下标的一个子集(s 的下标也从 0 开始计数)。
请你找出一个整数 k(0 <= k <= removable.length),使得在按顺序移除 removable 中前 k 个
下标对应的字符后,p 仍然是 s 的一个子序列。返回你可以找到的最大 k。
字符串的一个子序列是一个由原字符串生成的新字符串,生成过程中可能会移除原字符串中的一些字符
(也可能不移除)但不改变剩余字符之间的相对顺序。
示例 1
- 输入 :
s= "abcacb",p= "ab",removable= 3,1,0 - 输出:2
示例 2
- 输入 :
s= "abcbddddd",p= "abcd",removable= 3,2,1,4,5,6 - 输出:1
示例 3
- 输入 :
s= "abcab",p= "abc",removable= 0,1,2,3,4 - 输出 :0
提示: - 1 <= p.length < s.length <= 10^5- 0 <= removable.length < s.length- 0 <= removable[i] < s.length-
p是s的一个子序列
-
s和p都由小写英文字母组成
-
removable中的元素互不相同
解题思路
k 可行则 k-1 也可行(单调性),二分答案。check(k):标记 removable0...k-1 的下标为已删除,
然后双指针判断 p 是否仍是 s 的子序列。
解法一:二分 + 标记数组,主提交版本
O((n+m) log |removable|),标记用 boolean 每次重建。
解法二:二分 + 序号版本,对照
用额外数组 pos 标记"下标 i 的删除序号",check 时判断
当前字符是否属于前 k 个删除(posi < k),避免重复清空 boolean;语义等价。
代码实现
java
public class Solution {
public int maximumRemovals(String s, String p, int[] removable) {
int lo = 0, hi = removable.length, ans = 0;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
if (check(s, p, removable, mid)) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
private boolean check(String s, String p, int[] removable, int k) {
int n = s.length();
boolean[] removed = new boolean[n];
for (int i = 0; i < k; i++) {
removed[removable[i]] = true;
}
int j = 0;
for (int i = 0; i < n && j < p.length(); i++) {
if (!removed[i] && s.charAt(i) == p.charAt(j)) {
j++;
}
}
return j == p.length();
}
public int maximumRemovalsSet(String s, String p, int[] removable) {
int n = s.length();
int lo = 0, hi = removable.length, ans = 0;
while (lo <= hi) {
int mid = (lo + hi) >>> 1;
java.util.Set<Integer> removed = new java.util.HashSet<>();
for (int i = 0; i < mid; i++) {
removed.add(removable[i]);
}
int j = 0;
for (int i = 0; i < n && j < p.length(); i++) {
if (!removed.contains(i) && s.charAt(i) == p.charAt(j)) {
j++;
}
}
if (j == p.length()) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] ss = {"abcacb", "abcbddddd", "abcab", "qobftgcueho"};
String[] ps = {"ab", "abcd", "abc", "obue"};
int[][] rem = {{3, 1, 0}, {3, 2, 1, 4, 5, 6}, {0, 1, 2, 3, 4}, {5, 3, 0, 6, 4, 9, 10, 7, 2, 8}};
boolean ok = true;
for (int t = 0; t < ss.length; t++) {
int r1 = s.maximumRemovals(ss[t], ps[t], rem[t]);
int r2 = s.maximumRemovalsSet(ss[t], ps[t], rem[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1899. Merge Triplets to Form Target Triplet(合并若干三元组以形成目标三元组)
题目描述
三元组是一个由三个整数组成的数组。给你一个二维整数数组 triplets,其中 triplets[i] = `ai`, `bi`, `ci`
表示第 i 个三元组。同时,给你一个整数数组 target = `x`, `y`, `z`,表示你想要得到的三元组。
为了得到 target,你需要对 triplets 执行下面的操作任意次(可能零次):
- 选出两个下标
i和j(i!=j),并更新triplets[j]为 `max`(`ai`, `aj`), `max`(`bi`, `bj`), `max`(`ci`, `cj`)。
例如,triplets[i]= 2,5,3 且triplets[j]= 1,7,5,triplets[j]将会更新为 2,7,5。
如果通过以上操作我们可以使得目标三元组target成为triplets的一个元素,则返回true;否则,
返回false。
示例 1
- 输入 :
triplets= \[2,5,3,1,8,4,1,7,5],target= 2,7,5 - 输出 :
true
示例 2
- 输入 :
triplets= \[1,3,4,2,5,8],target= 2,5,8 - 输出 :
true
示例 3
- 输入 :
triplets= \[2,5,3,2,3,4,1,2,5,5,2,3],target= 5,5,5 - 输出 :
true
示例 4
- 输入 :
triplets= \[3,4,5,4,5,6],target= 3,2,5 - 输出 :
false
提示: - 1 <= triplets.length <= 10^5- triplets[i].length == target.length == 3- 1 <= ai, bi, ci, x, y, z <= 1000
解题思路
由于操作只会取 max,任何三元组只要某一维 > target 对应维,它就"有害"(参与合并会把该维
顶超过 target),必须丢弃。剩下的"候选"三元组每个维都 <= target,且它们通过多次合并可以
把各维叠到 target:只要候选并集中每一维的 max 恰好覆盖 target 即可(因为候选们共同参与
合并最终得到的三元组 = max(ai), max(bi), max(ci))。
解法一:筛选候选 + 逐维覆盖检查,主提交版本
一趟过滤 + 三个布尔/求最大值。O(n)。
解法二:全程维护叠加结果,对照
对每个候选三元组直接与"当前累计结果"取 max(类似模拟
合并),最终检查累计结果是否恰为 target;常数略大、可读性更好。
代码实现
java
public class Solution {
public boolean mergeTriplets(int[][] triplets, int[] target) {
int x = target[0], y = target[1], z = target[2];
int maxA = -1, maxB = -1, maxC = -1;
for (int[] t : triplets) {
if (t[0] <= x && t[1] <= y && t[2] <= z) {
maxA = Math.max(maxA, t[0]);
maxB = Math.max(maxB, t[1]);
maxC = Math.max(maxC, t[2]);
}
}
return maxA == x && maxB == y && maxC == z;
}
public boolean mergeTripletsAccum(int[][] triplets, int[] target) {
int[] acc = new int[3];
for (int[] t : triplets) {
// 只有全部维度都不超 target 的候选才可参与
if (t[0] <= target[0] && t[1] <= target[1] && t[2] <= target[2]) {
for (int d = 0; d < 3; d++) {
acc[d] = Math.max(acc[d], t[d]);
}
}
}
return acc[0] == target[0] && acc[1] == target[1] && acc[2] == target[2];
}
public static void main(String[] args) {
Solution s = new Solution();
int[][][] tripletsCases = {
{{2, 5, 3}, {1, 8, 4}, {1, 7, 5}},
{{1, 3, 4}, {2, 5, 8}},
{{2, 5, 3}, {2, 3, 4}, {1, 2, 5}, {5, 2, 3}},
{{3, 4, 5}, {4, 5, 6}},
{{1, 1, 1}},
{{2, 2, 2}}
};
int[][] targetCases = {{2, 7, 5}, {2, 5, 8}, {5, 5, 5}, {3, 2, 5}, {1, 1, 1}, {1, 1, 1}};
boolean ok = true;
for (int t = 0; t < tripletsCases.length; t++) {
boolean r1 = s.mergeTriplets(tripletsCases[t], targetCases[t]);
boolean r2 = s.mergeTripletsAccum(tripletsCases[t], targetCases[t]);
ok &= (r1 == r2);
System.out.println(r1 + " | " + r2 + (r1 == r2 ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}
复杂度分析
1900. The Earliest and Latest Rounds Where Players Compete
题目描述
(最佳运动员的比拼回合)
n 名运动员参与一场锦标赛,所有运动员站成一排,并根据最开始的站位从 1 到 n 编号。
锦标赛由多个回合组成(从回合 1 开始)。每一回合中,这一排从前往后数的第 i 名运动员需要与
从后往前数的第 i 名运动员比拼,获胜者将会进入下一回合。如果当前回合中运动员数目为奇数,那么
中间那位运动员将轮空晋级下一回合。
每回合结束后,获胜者将会基于最开始分配给他们的原始顺序(升序)重新排成一排。
编号为 firstPlayer 和 secondPlayer 的运动员是本场锦标赛中的最佳运动员。在他们开始比拼之前,
完全可以战胜任何其他运动员。而任意两个其他运动员进行比拼时,其中任意一个都有获胜的可能,
因此你可以裁定谁是这一回合的获胜者。
给你三个整数 n、firstPlayer 和 secondPlayer。返回一个由两个值组成的整数数组,分别表示两位
最佳运动员在本场锦标赛中比拼的最早回合数和最晚回合数。
示例 1
- 输入 :
n= 11,firstPlayer= 2,secondPlayer= 4 - 输出:3,4
示例 2
- 输入 :
n= 5,firstPlayer= 1,secondPlayer= 5 - 输出:1,1
- 解释 :两名最佳运动员 1 和 5 在回合 1 直接比拼。
提示: - 2 <= n <= 28- 1 <= firstPlayer < secondPlayer <= n
解题思路
把两名最佳运动员记为 F、S(0 基位置 l、r)。DFS 状态 (n, l, r) 表示"当前还有 n 名运动员,
F 在第 l 位、S 在第 r 位",返回 最早相遇回合, 最晚相遇回合(从当前回合起算)。
- 若 l + r == n - 1(两人本回合就被配成对称对手),返回 1, 1。
- 否则,本回合有 m = floor(n/2) 对比赛(奇数时中间位轮空直接晋级)。其他运动员的胜负可以
任意裁定:用 m 位二进制掩码枚举所有比赛结果,并强制 F、S 获胜(其对手淘汰),
由此可唯一确定两人在下一回合中的位置(新位置 = 排在该选手之前晋级的人数)。
对每个分支递归得到下一回合的 e, l,用 e+1 / l+1 更新当前状态的 最早, 最晚。
子状态规模极小(n <= 28 且每次约减半),配合记忆化即可。
解法一:位掩码枚举比赛结果 + 记忆化 DFS(主提交版本)
主提交版本。
解法二:回溯式逐对枚举胜者,对照
对每个普通配对依次选择胜者(DFS 递归),
与位掩码枚举完全等价,写法更贴近"递归枚举"的直觉,便于交叉验证。
代码实现
java
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class Solution {
private final Map<Integer, int[]> memo = new HashMap<>();
public int[] earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
memo.clear();
return dfs(n, firstPlayer - 1, secondPlayer - 1);
}
private int[] dfs(int n, int l, int r) {
int key = (n << 10) | (l << 5) | r;
int[] cached = memo.get(key);
if (cached != null) {
return cached;
}
if (l + r == n - 1) { // 本回合直接对决
int[] res = {1, 1};
memo.put(key, res);
return res;
}
int pairs = n / 2; // 本回合比赛对数
int minE = Integer.MAX_VALUE, maxL = 1;
for (int mask = 0; mask < (1 << pairs); mask++) {
boolean[] win = new boolean[n];
for (int j = 0; j < pairs; j++) { // 配对 (j, n-1-j)
if ((mask & (1 << j)) != 0) {
win[j] = true;
} else {
win[n - 1 - j] = true;
}
}
if ((n & 1) == 1) {
win[pairs] = true; // 中间位轮空晋级
}
// 两名最佳运动员必胜
win[l] = true;
win[r] = true;
// 他们的对手必败(注意避开轮空位与选手自身)
int ol = n - 1 - l;
int orr = n - 1 - r;
if (ol != l && ol != r) win[ol] = false;
if (orr != r && orr != l) win[orr] = false;
// 计算两人下一回合的位置(排在前面且晋级的人数)
int a = 0, b = 0, cnt = 0;
for (int j = 0; j < n; j++) {
if (j == l) a = cnt;
if (j == r) b = cnt;
if (win[j]) cnt++;
}
int[] sub = dfs((n + 1) / 2, a, b);
minE = Math.min(minE, sub[0] + 1);
maxL = Math.max(maxL, sub[1] + 1);
}
int[] res = {minE, maxL};
memo.put(key, res);
return res;
}
public int[] earliestAndLatestBacktrack(int n, int firstPlayer, int secondPlayer) {
return dfs2(n, firstPlayer - 1, secondPlayer - 1);
}
private final Map<Integer, int[]> memo2 = new HashMap<>();
private int[] dfs2(int n, int l, int r) {
int key = (n << 10) | (l << 5) | r;
int[] cached = memo2.get(key);
if (cached != null) {
return cached;
}
if (l + r == n - 1) {
int[] res = {1, 1};
memo2.put(key, res);
return res;
}
int[] ans = {Integer.MAX_VALUE, 1};
// 回溯枚举:按配对 (0,n-1), (1,n-2), ... 依次决定"前半赢/后半赢"
backtrack(0, n, l, r, new boolean[n], ans);
int[] res = {ans[0], ans[1]};
memo2.put(key, res);
return res;
}
private void backtrack(int pi, int n, int l, int r, boolean[] win, int[] ans) {
int pairs = n / 2;
if (pi == pairs) {
if ((n & 1) == 1) win[pairs] = true; // 中间轮空
win[l] = true;
win[r] = true;
int ol = n - 1 - l;
int orr = n - 1 - r;
if (ol != l && ol != r) win[ol] = false;
if (orr != r && orr != l) win[orr] = false;
int a = 0, b = 0, cnt = 0;
for (int j = 0; j < n; j++) {
if (j == l) a = cnt;
if (j == r) b = cnt;
if (win[j]) cnt++;
}
int[] sub = dfs2((n + 1) / 2, a, b);
ans[0] = Math.min(ans[0], sub[0] + 1);
ans[1] = Math.max(ans[1], sub[1] + 1);
return;
}
// 配对 (pi, n-1-pi):左侧赢
win[pi] = true;
win[n - 1 - pi] = false;
backtrack(pi + 1, n, l, r, win, ans);
win[pi] = false;
win[n - 1 - pi] = true;
backtrack(pi + 1, n, l, r, win, ans);
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] cases = {
{11, 2, 4}, {5, 1, 5}, {2, 1, 2}, {4, 1, 2},
{6, 1, 6}, {8, 3, 4}, {28, 1, 28}, {28, 13, 14},
{7, 2, 6}, {10, 3, 8}, {3, 1, 3}, {3, 1, 2}
};
boolean ok = true;
for (int[] c : cases) {
int[] r1 = s.earliestAndLatest(c[0], c[1], c[2]);
int[] r2 = s.earliestAndLatestBacktrack(c[0], c[1], c[2]);
boolean same = Arrays.equals(r1, r2);
ok &= same;
System.out.println("n=" + c[0] + " f=" + c[1] + " s=" + c[2] + " -> "
+ Arrays.toString(r1) + " | " + Arrays.toString(r2)
+ (same ? " OK" : " FAIL"));
}
System.out.println(ok ? "ALL OK" : "HAS FAIL");
}
}