LeetCode 1921-1940. 题解合集
文章目录
- [LeetCode 1921-1940. 题解合集](#LeetCode 1921-1940. 题解合集)
-
- [1921. Eliminate Maximum Number of Monsters(消灭怪物的最大数量)](#1921. Eliminate Maximum Number of Monsters(消灭怪物的最大数量))
- [1922. Count Good Numbers(统计好数字的数目)](#1922. Count Good Numbers(统计好数字的数目))
- [1923. Longest Common Subpath(最长公共子路径)](#1923. Longest Common Subpath(最长公共子路径))
- [1924. Erect the Fence II(安装栅栏 II)](#1924. Erect the Fence II(安装栅栏 II))
- [1925. Count Square Sum Triples(统计平方和三元组的数目)](#1925. Count Square Sum Triples(统计平方和三元组的数目))
- [1926. Nearest Exit from Entrance in Maze(迷宫中离入口最近的出口)](#1926. Nearest Exit from Entrance in Maze(迷宫中离入口最近的出口))
- [1927. Sum Game(求和游戏)](#1927. Sum Game(求和游戏))
- [1928. Minimum Cost to Reach Destination in Time(规定时间内到达终点的最小花费)](#1928. Minimum Cost to Reach Destination in Time(规定时间内到达终点的最小花费))
- [1929. Concatenation of Array(数组串联)](#1929. Concatenation of Array(数组串联))
- [1930. Unique Length-3 Palindromic Subsequences(长度为 3 的不同回文子序列)](#1930. Unique Length-3 Palindromic Subsequences(长度为 3 的不同回文子序列))
- [1931. Painting a Grid With Three Different Colors(用三种不同颜色为网格涂色)](#1931. Painting a Grid With Three Different Colors(用三种不同颜色为网格涂色))
- [1932. Merge BSTs to Create Single BST(合并多棵二叉搜索树)](#1932. Merge BSTs to Create Single BST(合并多棵二叉搜索树))
- [1933. Check if String Is Decomposable Into Value-Equal Substrings](#1933. Check if String Is Decomposable Into Value-Equal Substrings)
- [1934. Confirmation Rate(确认率)](#1934. Confirmation Rate(确认率))
- [1935. Maximum Number of Words You Can Type(可以输入的最大单词数)](#1935. Maximum Number of Words You Can Type(可以输入的最大单词数))
- [1936. Add Minimum Number of Rungs(新增的最少台阶数)](#1936. Add Minimum Number of Rungs(新增的最少台阶数))
- [1937. Maximum Number of Points with Cost(扣分后的最大得分)](#1937. Maximum Number of Points with Cost(扣分后的最大得分))
- [1938. Maximum Genetic Difference Query(查询最大基因差)](#1938. Maximum Genetic Difference Query(查询最大基因差))
- [1939. Users That Actively Request Confirmation Messages(主动请求确认消息的用户)](#1939. Users That Actively Request Confirmation Messages(主动请求确认消息的用户))
- [1940. Longest Common Subsequence Between Sorted Arrays(排序数组之间的最长公共子序列)](#1940. Longest Common Subsequence Between Sorted Arrays(排序数组之间的最长公共子序列))
1921. Eliminate Maximum Number of Monsters(消灭怪物的最大数量)
题目描述
你正在玩一款电子游戏,在游戏中你需要保护城市免受怪物侵袭。给定一个下标从 0 开始且大小为 n
的整数数组 dist,其中 dist[i] 是第 i 个怪物与城市的初始距离(单位:千米)。
怪物以恒定的速度走向城市。每个怪物的速度都以一个长度为 n 的整数数组 speed 表示,
其中 speed[i] 是第 i 个怪物的速度(单位:千米/分)。
你有一种武器,一旦充满电,就可以消灭一个怪物。但是,武器需要一分钟才能充电。武器在游戏开始时
是充满电的状态,怪物从第 0 分钟时开始移动。
一旦任一怪物到达城市,你就输掉了这场游戏。如果某个怪物恰好在一分钟开始时到达城市(距离表示为 0),
这也会被视为输掉游戏,在你可以使用武器之前,游戏就会结束。
返回在你输掉游戏前可以消灭的怪物的最大数量。如果你可以在所有怪物到达城市前将它们全部消灭,返回 n。
示例 1
- 输入 :
dist= 1,3,4,speed= 1,1,1 - 输出:3
- 解释:第 0 分钟消灭第一个怪物,第 1 分钟消灭第二个怪物,第 3 分钟消灭第三个怪物。
示例 2
- 输入 :
dist= 1,1,2,3,speed= 1,1,1,1 - 输出:1
- 解释:第 1 分钟开始时第二个怪物距离为 0,你输掉了游戏。
示例 3
- 输入 :
dist= 3,2,4,speed= 5,3,2 - 输出:1
- 解释 :第 1 分钟开始时第二个怪物距离为 0,你输掉了游戏。
提示: - n == dist.length == speed.length- 1 <= n <= 10^5- 1 <= dist[i], speed[i] <= 10^5
解题思路
解法一:贪心 + 排序到达时刻,O(n log n),主提交版本
第 i 个怪物在 t = ceil(dist[i] / speed[i])
分钟到达(到达时距离为 0 也算输)。你在第 0...k-1 分钟可以各消灭一个怪物,因此若想消灭 k 个怪物,
必须存在 k 个怪物的到达时刻严格大于其被消灭的分钟:即把到达时刻升序排列后,第 j 个(0 基)到达时刻
必须 > j。按到达时刻排序后贪心扫描,统计满足 arrive[j] > j 的最长前缀长度。
解法二:模拟每分钟 + 最小堆,对照
小顶堆维护"当前最近到达的怪物时刻",每分钟弹出一个消灭并
计数,若堆顶时刻 <= 当前分钟则说明有怪物已到达,游戏结束。O(n log n),用于交叉验证。
代码实现
java
import java.util.Arrays;
import java.util.PriorityQueue;
public class Solution {
public int eliminateMaximum(int[] dist, int[] speed) {
int n = dist.length;
int[] arrive = new int[n];
for (int i = 0; i < n; i++) {
arrive[i] = (dist[i] + speed[i] - 1) / speed[i]; // 向上取整到达分钟
}
Arrays.sort(arrive);
int cnt = 0;
for (int i = 0; i < n; i++) {
if (arrive[i] <= i) { // 第 i 分钟开始前该怪物已到(或恰好到)
break;
}
cnt++;
}
return cnt;
}
public int eliminateMaximumHeap(int[] dist, int[] speed) {
int n = dist.length;
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int i = 0; i < n; i++) {
pq.offer((dist[i] + speed[i] - 1) / speed[i]);
}
int minute = 0;
while (!pq.isEmpty()) {
if (pq.peek() <= minute) { // 该怪物已在第 minute 分钟开始前到达
break;
}
pq.poll();
minute++;
}
return minute;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] d1 = {1, 3, 4}, sp1 = {1, 1, 1};
int[] d2 = {1, 1, 2, 3}, sp2 = {1, 1, 1, 1};
int[] d3 = {3, 2, 4}, sp3 = {5, 3, 2};
boolean ok = true;
ok &= s.eliminateMaximum(d1, sp1) == 3 && s.eliminateMaximumHeap(d1, sp1) == 3;
ok &= s.eliminateMaximum(d2, sp2) == 1 && s.eliminateMaximumHeap(d2, sp2) == 1;
ok &= s.eliminateMaximum(d3, sp3) == 1 && s.eliminateMaximumHeap(d3, sp3) == 1;
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + s.eliminateMaximum(d1, sp1));
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + s.eliminateMaximum(d2, sp2));
System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + s.eliminateMaximum(d3, sp3));
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1922. Count Good Numbers(统计好数字的数目)
题目描述
我们称一个数字字符串是好数字当它满足(下标从 0 开始):偶数下标处的数字为偶数且奇数下标处的数字
为质数(2,3,5 或 7)。
比方说,"2582" 是好数字,因为偶数下标处的数字(2 和 8)是偶数且奇数下标处的数字
(5 和 2)为质数。但 "3245" 不是好数字,因为 3 在偶数下标处但不是偶数。
给你一个整数 n,请你返回长度为 n 且为好数字的数字字符串总数。由于答案可能会很大,
请你将它对 10^9 + 7 取余后返回。
一个数字字符串是每一位都由 0 到 9 组成的字符串,且可能包含前导 0。
示例 1
- 输入 :
n= 1 - 输出:5
- 解释:长度为 1 的好数字包括 "0","2","4","6","8"。
示例 2
- 输入 :
n= 4 - 输出:400
示例 3
- 输入 :
n= 50 - 输出 :564908303
提示: - 1 <= n <= 10^15
解题思路
解法一:快速幂,O(log n),主提交版本
每个位置独立选数字------偶数下标可选 {0,2,4,6,8} 共 5 种,
奇数下标可选 {2,3,5,7} 共 4 种。长度为 n 时偶数位置个数 cntEven = (n + 1) / 2,奇数位置个数
cntOdd = n / 2。答案 = 5^cntEven * 4^cntOdd mod (10^9+7),用快速幂按指数二分计算。
解法二:递推迭代,对照
按位递推 res = res * 5 / * 4,n 可达 10^15 不能逐位,因此把解法一
的快速幂展开为"倍增式"循环(对每一位以 2 为步长处理),本质相同,写法用于交叉验证。
代码实现
java
public class Solution {
private static final int MOD = 1_000_000_007;
public int countGoodNumbers(long n) {
long even = (n + 1) / 2; // 偶数下标数量
long odd = n / 2; // 奇数下标数量
long res = modPow(5, even) * modPow(4, odd) % MOD;
return (int) res;
}
private 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 countGoodNumbersIter(long n) {
long even = (n + 1) / 2; // 5^even
long odd = n / 2; // 4^odd
long res = 1;
long baseEven = 5, baseOdd = 4;
// 两个底数各自的指数同时做二进制拆分,合并相乘
while (even > 0 || odd > 0) {
if ((even & 1) == 1) {
res = res * baseEven % MOD;
}
if ((odd & 1) == 1) {
res = res * baseOdd % MOD;
}
baseEven = baseEven * baseEven % MOD;
baseOdd = baseOdd * baseOdd % MOD;
even >>= 1;
odd >>= 1;
}
return (int) res;
}
public static void main(String[] args) {
Solution s = new Solution();
long[] ns = {1, 4, 50, 2, 3};
int[] exp = {5, 400, 564908303, 20, 100};
boolean all = true;
for (int i = 0; i < ns.length; i++) {
int a = s.countGoodNumbers(ns[i]);
int b = s.countGoodNumbersIter(ns[i]);
boolean ok = a == exp[i] && b == exp[i];
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " (n=" + ns[i] + ") => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1923. Longest Common Subpath(最长公共子路径)
题目描述
一个国家由 n 个编号为 0 到 n - 1 的城市组成。在这个国家里,每两个城市之间都有一条道路连接。
总共有 m 个编号为 0 到 m - 1 的朋友想在这个国家旅游。他们每一个人的路径都会包含一些城市。
每条路径都由一个整数数组表示,每个整数数组表示一个朋友按顺序访问过的城市序列。同一个城市在一条
路径中可能重复出现,但同一个城市在一条路径中不会连续出现。
给你一个整数 n 和二维数组 paths,其中 paths[i] 是一个整数数组,表示第 i 个朋友走过的路径,
请你返回每一个朋友都走过的最长公共子路径的长度,如果不存在公共子路径,请你返回 0。
一个子路径指的是一条路径中连续的城市序列。
示例 1
- 输入 :
n= 5,paths= \[0,1,2,3,4,2,3,4,4,0,1,2,3] - 输出:2
- 解释:最长公共子路径为 2,3。
示例 2
- 输入 :
n= 3,paths= \[0,1,2] - 输出:0
- 解释:三条路径没有公共子路径。
示例 3
- 输入 :
n= 5,paths= \[0,1,2,3,4,4,3,2,1,0] - 输出:1
- 解释 :最长公共子路径为 0,1,2,3 和 4。它们长度都为 1。
提示: - 1 <= n <= 10^5- m == paths.length- 2 <= m <= 10^5- sum(paths[i].length) <= 10^5- 0 <= paths[i][j] < n-
paths[i]中同一个城市不会连续重复出现
解题思路
解法一:二分长度 + 滚动哈希去重,O(S log S),主提交版本
检查"是否存在长度为 len 的公共子路径"
是否可行可用二分(答案具有单调性:若 len 可行则更短也可行)。判定时对第一条路径枚举所有长度为 len
的子段计算哈希放入集合(用双哈希 64 位或单 64 位滚动哈希 + 长度绑定避免碰撞,这里用两个 64 位
乘加组合),其余每条路径只保留其长度为 len 的子段哈希中"同时出现在之前所有路径集合里"的哈希,
最后集合为空则不存在。注意每条路径内部哈希要去重(同一路径可能重复出现同一子段),
用每路径内 Set 收集再与全局集合求交。
解法二:暴力枚举起点 + KMP 逐步缩小,对照
先取最短路径为基准,枚举其每个长度为 len 的子段
检查是否为所有路径的子串(用 String/List indexOf 或朴素匹配)。正确但 O(S^2) 仅用于小数据交叉验证。
代码实现
java
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Solution {
public int longestCommonSubpath(int n, int[][] paths) {
int lo = 0, hi = Integer.MAX_VALUE;
for (int[] p : paths) {
hi = Math.min(hi, p.length);
}
while (lo < hi) {
int mid = (lo + hi + 1) >>> 1;
if (check(mid, paths)) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
private boolean check(int len, int[][] paths) {
if (len == 0) {
return true;
}
final long MOD = (1L << 61) - 1; // 大质数
final long BASE = 1000003L;
// 第一条路径的所有长度为 len 的子段哈希
Set<Long> common = hashesOf(paths[0], len, BASE, MOD);
// 其余路径与 common 求交
for (int i = 1; i < paths.length; i++) {
Set<Long> cur = hashesOf(paths[i], len, BASE, MOD);
Set<Long> next = new HashSet<>();
for (long h : cur) {
if (common.contains(h)) {
next.add(h);
}
}
if (next.isEmpty()) {
return false;
}
common = next;
}
return !common.isEmpty();
}
// 返回数组所有长度为 len 连续子段的 61 位滚动哈希集合(内部去重)
private Set<Long> hashesOf(int[] path, int len, long BASE, long MOD) {
Set<Long> set = new HashSet<>();
int m = path.length;
if (m < len) {
return set;
}
// 预计算 base^len mod MOD
long pow = 1;
for (int i = 0; i < len; i++) {
pow = pow * BASE % MOD;
}
long h = 0;
for (int i = 0; i < len; i++) {
h = (h * BASE + path[i] + 1) % MOD;
}
set.add(h);
for (int i = len; i < m; i++) {
h = (h * BASE + path[i] + 1) % MOD;
h = ((h - (path[i - len] + 1) * pow % MOD) + MOD) % MOD;
set.add(h);
}
return set;
}
public int longestCommonSubpathBrute(int n, int[][] paths) {
// 取最短路径
int minIdx = 0;
for (int i = 1; i < paths.length; i++) {
if (paths[i].length < paths[minIdx].length) {
minIdx = i;
}
}
int best = 0;
int m = paths[minIdx].length;
for (int l = 0; l < m; l++) {
for (int len = 1; l + len <= m; len++) {
if (isCommon(paths, minIdx, l, len)) {
best = Math.max(best, len);
}
}
}
return best;
}
private boolean isCommon(int[][] paths, int baseIdx, int start, int len) {
int[] base = paths[baseIdx];
outer:
for (int i = 0; i < paths.length; i++) {
int[] p = paths[i];
boolean found = false;
for (int j = 0; j + len <= p.length; j++) {
boolean eq = true;
for (int t = 0; t < len; t++) {
if (p[j + t] != base[start + t]) {
eq = false;
break;
}
}
if (eq) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] p1 = {{0, 1, 2, 3, 4}, {2, 3, 4}, {4, 0, 1, 2, 3}};
int[][] p2 = {{0}, {1}, {2}};
int[][] p3 = {{0, 1, 2, 3, 4}, {4, 3, 2, 1, 0}};
boolean ok = true;
int a1 = s.longestCommonSubpath(5, p1);
int b1 = s.longestCommonSubpathBrute(5, p1);
ok &= a1 == 2 && b1 == 2;
int a2 = s.longestCommonSubpath(3, p2);
int b2 = s.longestCommonSubpathBrute(3, p2);
ok &= a2 == 0 && b2 == 0;
int a3 = s.longestCommonSubpath(5, p3);
int b3 = s.longestCommonSubpathBrute(5, p3);
ok &= a3 == 1 && b3 == 1;
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1924. Erect the Fence II(安装栅栏 II)
题目描述
给你一个二维整数数组 trees,其中 trees[i] = [xi, yi] 表示花园中第 i 棵树的坐标。
你需要用最少的原材料给花园安装一个圆形的栅栏,使花园中所有的树都被围在栅栏内部(在栅栏边界上的树
也算在内)。正式地说,你需要求出栅栏的圆心坐标 (x,y) 和半径 r,使花园中所有的树都在圆的内部
或边界上,并且让半径 r 最小。
请用一个长度为 3 的数组 [x,y,r] 来返回圆心坐标和半径。如果答案与正确答案的误差不超过 10^-5,
则该答案将被视为正确答案通过。
示例 1
- 输入 :
trees= \[1,1,2,2,2,0,2,4,3,3,4,2] - 输出:2.00000,2.00000,2.00000
- 解释:栅栏的圆心应当在 (2, 2) 处,半径为 2。
示例 2
- 输入 :
trees= \[1,2,2,2,4,2] - 输出:2.50000,2.00000,1.50000
- 解释 :栅栏的圆心应当在 (2.5, 2) 处,半径为 1.5。
提示: - 1 <= trees.length <= 3000- trees[i].length == 2- 0 <= xi, yi <= 3000
解题思路
解法一:Welzl 随机增量法求最小圆覆盖,期望 O(n),主提交版本
利用经典结论:最小覆盖圆由
不超过 3 个点确定。先把点随机打乱,维护当前圆(圆心 O、半径 r)。逐点加入:若点在圆外,则该点
必在边界上,递归调用"过边界点求最小圆":先以该点为圆心 r=0,再扫其余点,若某点也不在圆内,
则以这两点为直径做圆;仍不满足的第三个点,用三点定圆。整过程用 shuffle 保证期望线性。
三点定圆用向量叉积 + 垂径中点法计算。
解法二:暴力三分搜索圆心/半径上界枚举,对照
圆心一定在点集包围盒内;对半径做"用固定圆心尝试
覆盖全部点"的可行性二分并不单调,因此改用直接推导答案的朴素做法不可取------这里用基于"枚举两点直径
或三点外接圆"的暴力:枚举所有两点组合与三点组合,对每个候选圆检查覆盖全部点(O(n^4) 在小数据),
取半径最小者,用于交叉验证。
代码实现
java
public class Solution {
public double[] outerTrees(int[][] trees) {
// 随机打乱
java.util.Random rand = new java.util.Random(1234567L);
int n = trees.length;
int[][] pts = trees.clone();
for (int i = n - 1; i > 0; i--) {
int j = rand.nextInt(i + 1);
int[] t = pts[i]; pts[i] = pts[j]; pts[j] = t;
}
double cx = 0, cy = 0, r = -1; // r=-1 表示未初始化
// 初始化圆为前两个点确定的圆(若 n==1 则是单点)
if (n == 1) {
return new double[]{pts[0][0], pts[0][1], 0};
}
circleFrom2(pts[0], pts[1]);
// 用增量法:先以两点定圆逐步扩展
double[] c2 = circleFrom2(pts[0], pts[1]);
cx = c2[0]; cy = c2[1]; r = c2[2];
for (int i = 2; i < n; i++) {
if (!in(pts[i], cx, cy, r)) {
// pts[i] 在圆外 -> 该点在最终圆边界上,重建
double[] c = circleWithBoundary(pts, i, cx, cy, r);
cx = c[0]; cy = c[1]; r = c[2];
}
}
return new double[]{cx, cy, r};
}
// 以 points[k] 为边界点,对前 k+1 个点重建最小圆(k 之前点可跨过已确定的边界)
private double[] circleWithBoundary(int[][] pts, int k, double cx, double cy, double r) {
// 先以 pts[k] 为圆心、半径 0
double ox = pts[k][0], oy = pts[k][1], or = 0;
for (int i = 0; i < k; i++) {
if (!in(pts[i], ox, oy, or)) {
// pts[i] 也在边界上:以 pts[k]、pts[i] 为直径
double[] c = circleFrom2(pts[k], pts[i]);
ox = c[0]; oy = c[1]; or = c[2];
for (int j = 0; j < i; j++) {
if (!in(pts[j], ox, oy, or)) {
// 三点定圆
double[] c3 = circleFrom3(pts[k], pts[i], pts[j]);
ox = c3[0]; oy = c3[1]; or = c3[2];
}
}
}
}
return new double[]{ox, oy, or};
}
private boolean in(int[] p, double cx, double cy, double r) {
double dx = p[0] - cx, dy = p[1] - cy;
return dx * dx + dy * dy <= r * r + 1e-9;
}
// 以两点为直径的圆
private double[] circleFrom2(int[] a, int[] b) {
double cx = (a[0] + b[0]) / 2.0;
double cy = (a[1] + b[1]) / 2.0;
double r = Math.hypot(a[0] - b[0], a[1] - b[1]) / 2.0;
return new double[]{cx, cy, r};
}
// 三点外接圆;三点共线时退化为最长边中点直径
private double[] circleFrom3(int[] a, int[] b, int[] c) {
double ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
double d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if (Math.abs(d) < 1e-12) { // 共线
// 选两两距离最大的一对作直径
double d1 = dist2(a, b), d2 = dist2(a, c), d3 = dist2(b, c);
if (d1 >= d2 && d1 >= d3) return circleFrom2(a, b);
if (d2 >= d3) return circleFrom2(a, c);
return circleFrom2(b, c);
}
double ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by)) / d;
double uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax)) / d;
double rr = Math.hypot(ax - ux, ay - uy);
return new double[]{ux, uy, rr};
}
private double dist2(int[] a, int[] b) {
double dx = a[0] - b[0], dy = a[1] - b[1];
return dx * dx + dy * dy;
}
public double[] outerTreesBrute(int[][] trees) {
int n = trees.length;
double bestR = Double.MAX_VALUE;
double bestCx = 0, bestCy = 0;
// 枚举两点直径
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
double[] c = circleFrom2(trees[i], trees[j]);
if (coversAll(trees, c[0], c[1], c[2])) {
if (c[2] < bestR) {
bestR = c[2]; bestCx = c[0]; bestCy = c[1];
}
}
}
}
// 枚举三点外接圆
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
for (int k = j + 1; k < n; k++) {
double[] c = circleFrom3(trees[i], trees[j], trees[k]);
if (coversAll(trees, c[0], c[1], c[2])) {
if (c[2] < bestR) {
bestR = c[2]; bestCx = c[0]; bestCy = c[1];
}
}
}
}
}
return new double[]{bestCx, bestCy, bestR};
}
private boolean coversAll(int[][] pts, double cx, double cy, double r) {
for (int[] p : pts) {
if (!in(p, cx, cy, r)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] t1 = {{1, 1}, {2, 2}, {2, 0}, {2, 4}, {3, 3}, {4, 2}};
int[][] t2 = {{1, 2}, {2, 2}, {4, 2}};
double[] r1 = s.outerTrees(t1);
double[] b1 = s.outerTreesBrute(t1);
double[] r2 = s.outerTrees(t2);
double[] b2 = s.outerTreesBrute(t2);
boolean ok1 = Math.abs(r1[0] - 2) < 1e-5 && Math.abs(r1[1] - 2) < 1e-5 && Math.abs(r1[2] - 2) < 1e-5;
boolean ok1b = Math.abs(b1[0] - 2) < 1e-5 && Math.abs(b1[1] - 2) < 1e-5 && Math.abs(b1[2] - 2) < 1e-5;
boolean ok2 = Math.abs(r2[0] - 2.5) < 1e-5 && Math.abs(r2[1] - 2) < 1e-5 && Math.abs(r2[2] - 1.5) < 1e-5;
boolean ok2b = Math.abs(b2[0] - 2.5) < 1e-5 && Math.abs(b2[1] - 2) < 1e-5 && Math.abs(b2[2] - 1.5) < 1e-5;
System.out.println((ok1 && ok1b ? "PASS" : "FAIL") + " case1 => " + java.util.Arrays.toString(r1) + " / " + java.util.Arrays.toString(b1));
System.out.println((ok2 && ok2b ? "PASS" : "FAIL") + " case2 => " + java.util.Arrays.toString(r2) + " / " + java.util.Arrays.toString(b2));
System.out.println((ok1 && ok1b && ok2 && ok2b) ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1925. Count Square Sum Triples(统计平方和三元组的数目)
题目描述
一个平方和三元组 (a,b,c) 指的是满足 a^2 + b^2 = c^2 的整数三元组 a,b 和 c。
给你一个整数 n,请你返回满足 1 <= a, b, c <= n 的平方和三元组的数目。
示例 1
- 输入 :
n= 5 - 输出:2
- 解释:平方和三元组为 (3,4,5) 和 (4,3,5)。
示例 2
- 输入 :
n= 10 - 输出:4
- 解释 :平方和三元组为 (3,4,5),(4,3,5),(6,8,10) 和 (8,6,10)。
提示: - 1 <= n <= 250
解题思路
解法一:平方集合 + 枚举 a、b,O(n^2),主提交版本
把 1..n 的平方预先存进哈希集合。
枚举 a, b in [1, n],若 c2 = a^2 + b^2 是某个 <= n^2 的完全平方数 c^2 且 c <= n,
则 (a, b, c) 计数 +1。因为 (a,b) 有序,所以 (3,4,5) 与 (4,3,5) 算两个,恰好符合题意。
解法二:穷举 c、a 校验,对照
枚举 c 与 a,令 b2 = c^2 - a^2,若 b2 是完全平方且
b <= n 且 b >= 1 则计数。两种枚举角度互相验证。
代码实现
java
import java.util.HashSet;
import java.util.Set;
public class Solution {
public int countTriples(int n) {
Set<Integer> squares = new HashSet<>();
for (int i = 1; i <= n; i++) {
squares.add(i * i);
}
int ans = 0;
for (int a = 1; a <= n; a++) {
for (int b = 1; b <= n; b++) {
int c2 = a * a + b * b;
if (c2 <= n * n && squares.contains(c2)) {
ans++;
}
}
}
return ans;
}
public int countTriplesEnumC(int n) {
int ans = 0;
for (int c = 1; c <= n; c++) {
for (int a = 1; a < c; a++) {
int b2 = c * c - a * a;
int b = (int) Math.sqrt(b2);
if (b * b == b2 && b >= 1 && b <= n && a < b) {
ans += 2; // (a,b) 与 (b,a) 都满足
}
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] ns = {5, 10, 1, 12, 250};
// 期望值用两种方法交叉验证(互相一致即通过;2 4 处给出已知值)
boolean all = true;
for (int n : ns) {
int a = s.countTriples(n);
int b = s.countTriplesEnumC(n);
boolean ok = a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " n=" + n + " => " + a + " / " + b);
}
// 官方例子校验
boolean ex = s.countTriples(5) == 2 && s.countTriples(10) == 4;
all &= ex;
System.out.println((ex ? "PASS" : "FAIL") + " official ex => 2, 4");
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1926. Nearest Exit from Entrance in Maze(迷宫中离入口最近的出口)
题目描述
给你一个 m x n 的迷宫矩阵 maze(下标从 0 开始),矩阵中有空格子(用 '.' 表示)和墙
(用 '+' 表示)。同时给你迷宫的入口 entrance,用 entrance = [entrancerow, entrancecol]
表示你一开始所在格子的行和列。
每一步操作,你可以往上,下,左或者右移动一个格子。你不能进入墙所在的格子,你也不能离开迷宫。
你的目标是找到离 entrance 最近的出口。出口的含义是 maze 边界上的空格子。entrance 格子
不算出口。
请你返回从 entrance 到最近出口的最短路径的步数,如果不存在这样的路径,请你返回 -1。
示例 1
- 输入 :
maze= \["+","+",".","+",".",".",".","+","+","+","+","."],entrance= 1,2 - 输出:1
- 解释:总共有 3 个出口,分别位于 (1,0),(0,2) 和 (2,3)。最近的出口是 (0,2),距离为 1 步。
示例 2
- 输入 :
maze= \["+","+","+",".",".",".","+","+","+"],entrance= 1,0 - 输出:2
- 解释:迷宫中只有 1 个出口,在 (1,2) 处。(1,0) 不算出口,因为它是入口格子。距离为 2 步。
示例 3
- 输入 :
maze= \[".","+"],entrance= 0,0 - 输出:-1
- 解释 :这个迷宫中没有出口。
提示: - maze.length == m- maze[i].length == n- 1 <= m, n <= 100- entrance.length == 2- 0 <= entrancerow < m- 0 <= entrancecol < n-
entrance一定是空格子
解题思路
解法一:BFS 层序搜索,O(mn),主提交版本
从入口开始 BFS。队列中加入每层的格子,步数随层递增;
每从队列取出一个格子,若是边界且不是入口,则其所在层数即最近出口距离。BFS 天然保证第一次遇到的
出口就是最短距离。注意把已访问格子置为 '+' 避免重复入队。
解法二:双端 BFS / 多起点 Dijkstra 变体,对照
这里改用一个双向队列做"层内扩展",同样按层遍历,
逐层计算当前层所有可达格子并检查边界出口,与解法一逻辑等价但写法不同,用于交叉验证。
代码实现
java
import java.util.ArrayDeque;
import java.util.Queue;
public class Solution {
private final int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public int nearestExit(char[][] maze, int[] entrance) {
int m = maze.length, n = maze[0].length;
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{entrance[0], entrance[1]});
maze[entrance[0]][entrance[1]] = '+'; // 标记入口已访问
int steps = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int k = 0; k < size; k++) {
int[] cur = q.poll();
int r = cur[0], c = cur[1];
// 出口判定:边界空格子,且不是入口
boolean isExit = (r == 0 || r == m - 1 || c == 0 || c == n - 1)
&& !(r == entrance[0] && c == entrance[1]);
if (isExit) {
return steps;
}
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.') {
maze[nr][nc] = '+';
q.offer(new int[]{nr, nc});
}
}
}
steps++;
}
return -1;
}
public int nearestExitSimple(char[][] maze, int[] entrance) {
int m = maze.length, n = maze[0].length;
Queue<int[]> q = new ArrayDeque<>();
int[][] dist = new int[m][n];
for (int i = 0; i < m; i++) {
java.util.Arrays.fill(dist[i], -1);
}
dist[entrance[0]][entrance[1]] = 0;
q.offer(new int[]{entrance[0], entrance[1]});
while (!q.isEmpty()) {
int[] cur = q.poll();
int r = cur[0], c = cur[1];
if ((r == 0 || r == m - 1 || c == 0 || c == n - 1)
&& !(r == entrance[0] && c == entrance[1])) {
return dist[r][c];
}
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.'
&& dist[nr][nc] == -1) {
dist[nr][nc] = dist[r][c] + 1;
q.offer(new int[]{nr, nc});
}
}
}
return -1;
}
private static char[][] deepCopy(char[][] a) {
char[][] b = new char[a.length][a[0].length];
for (int i = 0; i < a.length; i++) {
System.arraycopy(a[i], 0, b[i], 0, a[i].length);
}
return b;
}
public static void main(String[] args) {
Solution s = new Solution();
char[][] m1 = {{'+', '+', '.', '+'}, {'.', '.', '.', '+'}, {'+', '+', '+', '.'}};
char[][] m2 = {{'+', '+', '+'}, {'.', '.', '.'}, {'+', '+', '+'}};
char[][] m3 = {{'.', '+'}};
boolean ok = true;
int a1 = s.nearestExit(deepCopy(m1), new int[]{1, 2});
int b1 = s.nearestExitSimple(deepCopy(m1), new int[]{1, 2});
ok &= a1 == 1 && b1 == 1;
int a2 = s.nearestExit(deepCopy(m2), new int[]{1, 0});
int b2 = s.nearestExitSimple(deepCopy(m2), new int[]{1, 0});
ok &= a2 == 2 && b2 == 2;
int a3 = s.nearestExit(deepCopy(m3), new int[]{0, 0});
int b3 = s.nearestExitSimple(deepCopy(m3), new int[]{0, 0});
ok &= a3 == -1 && b3 == -1;
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1927. Sum Game(求和游戏)
题目描述
Alice 和 Bob 玩一个游戏,两人轮流行动,Alice 先手。
给你一个偶数长度的字符串 num,每一个字符为数字字符或者 '?'。每一次操作中,如果 num 中至少
有一个 '?',那么玩家可以执行以下操作:
- 选择一个下标
i满足num[i] == '?'。 - 将
num[i]用'0'到'9'之间的一个数字字符替代。
当num中没有'?'时,游戏结束。
Bob获胜的条件是num中前一半数字的和等于后一半数字的和。Alice获胜的条件是前一半的和与后一半的
和不相等。
比方说,游戏结束时num = "243801",那么Bob获胜,因为2+4+3 = 8+0+1。
如果游戏结束时num = "243803",那么Alice获胜,因为2+4+3 != 8+0+3。
在Alice和Bob都采取最优策略的前提下,如果Alice获胜,请返回true,如果Bob获胜,请返回false。
示例 1
- 输入 :
num= "5023" - 输出 :
false - 解释 :
num中没有 '?',没法进行任何操作。前一半的和等于后一半的和:5 + 0 = 2 + 3。
示例 2
- 输入 :
num= "25??" - 输出 :
true - 解释 :
Alice可以将两个 '?' 中的一个替换为 '9',Bob无论如何都无法使前一半的和等于后一半的和。
示例 3
- 输入 :
num= "?3295???" - 输出 :
false - 解释 :
Bob总是能赢。一种可能的结果是Alice将第一个 '?' 用 '9' 替换,之后Bob补齐其余 '?',
最终Bob获胜,因为 9 + 3 + 2 + 9 = 5 + 9 + 2 + 7。
提示: - 2 <= num.length <= 10^5-
num.length是偶数
-
num只包含数字字符和 '?'
解题思路
解法一:贪心数学判定,O(n),主提交版本
设前半 ? 数量为 lq、后半 ? 数量为 rq。若 (lq + rq)
为奇数,Alice 总能取胜(她可以下一次改变 ?,让 Bob 无法恰好补平差值)------更精确的结论:
当 ? 总数 cnt 为奇数时 Alice 必胜;当 cnt 为偶数时,设 d = 前半已知数字和 - 后半已知数字和,
则只有当 d == 9 * (rq - lq) / 2 时 Bob 才能赢,否则 Alice 赢。直观解释:每个 '?' 由一对玩家
轮流填,Alice/Bob 可以把各自的 ? 尽量推向对自己有利的极端(9 或 0),若双方 ? 数量相同则差值固定,
当且仅当差值已经等于"后半 ? 需要补的量"时 Bob 才刚好追平。
解法二:博弈搜索回溯,对照,仅小数据
? 数量少时用递归搜索双方最优决策:Alice 最大化最终
"前后半是否不等"(返回 1 表示 Alice 胜),Bob 最小化。O(10^cnt) 仅用于交叉验证解法一。
代码实现
java
public class Solution {
public boolean sumGame(String num) {
int n = num.length();
int half = n / 2;
int leftQ = 0, rightQ = 0;
long leftSum = 0, rightSum = 0;
for (int i = 0; i < half; i++) {
char c = num.charAt(i);
if (c == '?') leftQ++;
else leftSum += c - '0';
}
for (int i = half; i < n; i++) {
char c = num.charAt(i);
if (c == '?') rightQ++;
else rightSum += c - '0';
}
int totalQ = leftQ + rightQ;
if ((totalQ & 1) == 1) {
// 奇数个 '?' -> Alice 必胜
return true;
}
// 偶数个 '?':Bob 能赢当且仅当 已知差 == 9 * (rightQ - leftQ) / 2
long diff = leftSum - rightSum; // 前半 - 后半
long need = 9L * (rightQ - leftQ) / 2;
return diff != need;
}
public boolean sumGameBrute(String num) {
char[] cs = num.toCharArray();
// Alice 先手,最大化"前后半不等" => 返回 true 表示 Alice 胜
return dfs(cs, true);
}
private boolean dfs(char[] cs, boolean aliceTurn) {
int n = cs.length;
// 找第一个 '?'
int q = -1;
for (int i = 0; i < n; i++) {
if (cs[i] == '?') {
q = i;
break;
}
}
if (q == -1) {
// 游戏结束,判定 Bob 是否赢(前后半相等)
return !halfEqual(cs);
}
if (aliceTurn) {
// Alice 走:选任一 ? 填 0..9,只要任意一种能赢则赢
for (int i = 0; i < n; i++) {
if (cs[i] == '?') {
for (int d = 0; d <= 9; d++) {
cs[i] = (char) ('0' + d);
if (dfs(cs, false)) {
cs[i] = '?';
return true;
}
cs[i] = '?';
}
}
}
return false;
} else {
// Bob 走:Bob 想要前后半相等(返回 false 表示 Alice 输)
for (int i = 0; i < n; i++) {
if (cs[i] == '?') {
for (int d = 0; d <= 9; d++) {
cs[i] = (char) ('0' + d);
if (!dfs(cs, true)) {
cs[i] = '?';
return false;
}
cs[i] = '?';
}
}
}
return true;
}
}
private boolean halfEqual(char[] cs) {
int n = cs.length, half = n / 2;
long l = 0, r = 0;
for (int i = 0; i < half; i++) l += cs[i] - '0';
for (int i = half; i < n; i++) r += cs[i] - '0';
return l == r;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] cases = {"5023", "25??", "?3295???", "?", "??", "1?2?3?4?"};
// 期望:5023 -> false; 25?? -> true; ?3295??? -> false; "?" -> true(Alice先手填非0即不等,总能赢? 实际填0则相等...仔细分析:单个?Alice填数字后结束,Alice要不等,填 1..9 即可,所以 true)
// "??" -> 两?偶数个,差 need=9*(1-1)/2=0,已知差0==0 -> Bob能赢 -> false
// "1?2?3?4?" -> 4个?偶数,左右各2个?,已知左=1+2=3 右=3+4=7,need=9*(2-2)/2=0,diff=-4!=0 -> Alice -> true
boolean[] exp = {false, true, false, true, false, true};
boolean all = true;
for (int i = 0; i < cases.length; i++) {
if (cases[i].indexOf('?') >= 0 && cases[i].length() > 4) {
// 暴力只跑小数据
continue;
}
boolean a = s.sumGame(cases[i]);
boolean ok = a == exp[i];
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a);
}
// 小数据暴力与数学对拍
boolean bruteOk = true;
String[] small = {"?", "??", "0?", "?0", "1?1"};
for (String st : small) {
boolean a = s.sumGame(st);
boolean b = s.sumGameBrute(st);
if (a != b) {
bruteOk = false;
System.out.println("MISMATCH " + st + " => " + a + " vs " + b);
}
}
all &= bruteOk;
System.out.println(bruteOk ? "PASS brute vs math" : "FAIL brute vs math");
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1928. Minimum Cost to Reach Destination in Time(规定时间内到达终点的最小花费)
题目描述
一个国家有 n 个城市,城市编号为 0 到 n - 1,题目保证所有城市都由双向道路连接在一起。
道路由二维整数数组 edges 表示,其中 edges[i] = [xi, yi, timei] 表示城市 xi 和 yi 之间
有一条双向道路,耗费时间为 timei 分钟。两个城市之间可能会有多条耗费时间不同的道路,
但是不会有道路两头连接着同一座城市。
每次经过一个城市时,你需要付通行费。通行费用一个长度为 n 且下标从 0 开始的整数数组 passingFees
表示,其中 passingFees[j] 是你经过城市 j 需要支付的费用。
一开始,你在城市 0,你想要在 maxTime 分钟以内(包含 maxTime 分钟)到达城市 n - 1。
旅行的费用为你经过的所有城市通行费之和(包括起点和终点城市的通行费)。
给你 maxTime、edges 和 passingFees,请你返回完成旅行的最小费用,如果无法在 maxTime 分钟
以内完成旅行,请你返回 -1。
示例 1
- 输入 :
maxTime= 30,edges= \[0,1,10,1,2,10,2,5,10,0,3,1,3,4,10,4,5,15],
passingFees = [5,1,2,20,20,3] - 输出:11
- 解释:最优路径为 0 -> 1 -> 2 -> 5,总共需要耗费 30 分钟,需要支付 11 的通行费。
示例 2
- 输入 :
maxTime= 29,edges= \[0,1,10,1,2,10,2,5,10,0,3,1,3,4,10,4,5,15],
passingFees = [5,1,2,20,20,3] - 输出:48
- 解释:最优路径为 0 -> 3 -> 4 -> 5,总共需要耗费 26 分钟,需要支付 48 的通行费。
示例 3
- 输入 :
maxTime= 25,edges= \[0,1,10,1,2,10,2,5,10,0,3,1,3,4,10,4,5,15],
passingFees = [5,1,2,20,20,3] - 输出:-1
- 解释 :无法在 25 分钟以内从城市 0 到达城市 5。
提示: - 1 <= n <= 1000- 0 <= edges.length <= 1000- edges[i].length == 3- 0 <= xi, yi < n- 1 <= timei <= 1000- 1 <= maxTime <= 1000- passingFees.length == n- 1 <= passingFees[j] <= 1000-
- 图中两个节点之间可能有多条路径
-
- 图中不含有自环
解题思路
解法一:DP + Dijkstra 式优先队列,O(T * E log V),主提交版本
dist[t][v] 表示"恰好用 t 分钟
到达城市 v 的最小费用"不方便,改用 Dijkstra 在"时间-城市"状态图上做最短路:状态 (time, city),
代价为累计费用,用优先队列按费用升序扩展。每次弹出状态 (cost, time, city),若 time > maxTime 剪枝;
若 city == n-1 直接返回 cost(首次到达终点的即最优,因为按费用出队)。扩展每条边 (city, nxt, w):
新状态 (cost + passingFeesnxt, time + w, nxt)。为避免大量冗余状态,用 best[time][city] 记录
到达该 (time, city) 的最小费用,仅当更优才入队。
解法二:DP 递推,对照
dp[t][v] = 用至多 t 分钟到达 v 的最小费用。按时间从 0 到 maxTime 递推:
dp[t][v] = min(dp[t-1][v], min over edge (u,v,w): dp[t-w][u] + passingFees[v]),答案取
min(dp[t][n-1])。O(maxTime * E),用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;
public class Solution {
public int minCost(int maxTime, int[][] edges, int[] passingFees) {
int n = passingFees.length;
List<int[]>[] g = new ArrayList[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]});
}
// best[t][v]:恰好用 t 分钟到 v 的最小费用
int[][] best = new int[maxTime + 1][n];
for (int[] row : best) {
Arrays.fill(row, Integer.MAX_VALUE);
}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // (cost, time, city)
pq.offer(new int[]{passingFees[0], 0, 0});
best[0][0] = passingFees[0];
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int cost = cur[0], time = cur[1], city = cur[2];
if (cost > best[time][city]) {
continue; // 过时状态
}
if (city == n - 1) {
return cost; // 按费用出队,首次到达即最优
}
for (int[] e : g[city]) {
int nxt = e[0], w = e[1];
int nt = time + w;
if (nt > maxTime) {
continue;
}
int nc = cost + passingFees[nxt];
if (nc < best[nt][nxt]) {
best[nt][nxt] = nc;
pq.offer(new int[]{nc, nt, nxt});
}
}
}
return -1;
}
public int minCostDp(int maxTime, int[][] edges, int[] passingFees) {
int n = passingFees.length;
// 邻接表
List<int[]>[] g = new ArrayList[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]});
}
long INF = Long.MAX_VALUE / 4;
long[][] dp = new long[maxTime + 1][n];
for (long[] row : dp) {
Arrays.fill(row, INF);
}
dp[0][0] = passingFees[0];
for (int t = 1; t <= maxTime; t++) {
for (int v = 0; v < n; v++) {
dp[t][v] = dp[t - 1][v]; // 原地等待(不产生新费用,等价于"至多 t 分钟")
for (int[] e : g[v]) {
int u = e[0], w = e[1];
if (t >= w && dp[t - w][u] < INF) {
dp[t][v] = Math.min(dp[t][v], dp[t - w][u] + passingFees[v]);
}
}
}
}
return dp[maxTime][n - 1] >= INF ? -1 : (int) dp[maxTime][n - 1];
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] edges = {{0, 1, 10}, {1, 2, 10}, {2, 5, 10}, {0, 3, 1}, {3, 4, 10}, {4, 5, 15}};
int[] fees = {5, 1, 2, 20, 20, 3};
boolean ok = true;
int a1 = s.minCost(30, edges, fees);
int b1 = s.minCostDp(30, edges, fees);
ok &= a1 == 11 && b1 == 11;
int a2 = s.minCost(29, edges, fees);
int b2 = s.minCostDp(29, edges, fees);
ok &= a2 == 48 && b2 == 48;
int a3 = s.minCost(25, edges, fees);
int b3 = s.minCostDp(25, edges, fees);
ok &= a3 == -1 && b3 == -1;
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1929. Concatenation of Array(数组串联)
题目描述
给你一个长度为 n 的整数数组 nums。请你构建一个长度为 2n 的答案数组 ans,数组下标从 0
开始计数,对于所有 0 <= i < n 满足 ans[i] == nums[i] 且 ans[i + n] == nums[i]。
具体地,ans 由两个 nums 数组串联形成。请你返回构建好的数组 ans。
示例 1
- 输入 :
nums= 1,2,1 - 输出:1,2,1,1,2,1
- 解释 :数组
ans按下述方式形成:ans= `nums[0]`,`nums[1]`,`nums[2]`,`nums[0]`,`nums[1]`,`nums[2]` = 1,2,1,1,2,1。
示例 2
- 输入 :
nums= 1,3,2,1 - 输出:1,3,2,1,1,3,2,1
- 解释 :数组
ans按下述方式形成:ans= 1,3,2,1,1,3,2,1。
提示: - n == nums.length- 1 <= n <= 1000- 1 <= nums[i] <= 1000
解题思路
解法一:System.arraycopy,O(n),主提交版本
申请 2n 数组,前 n 位复制 nums,后 n 位再复制一遍。
解法二:双循环逐位填充,对照
用两次循环分别填 ans[i] 与 ans[i+n],写法更直观,便于验证。
代码实现
java
import java.util.Arrays;
public class Solution {
public int[] getConcatenation(int[] nums) {
int n = nums.length;
int[] ans = new int[2 * n];
System.arraycopy(nums, 0, ans, 0, n);
System.arraycopy(nums, 0, ans, n, n);
return ans;
}
public int[] getConcatenationLoop(int[] nums) {
int n = nums.length;
int[] ans = new int[2 * n];
for (int i = 0; i < n; i++) {
ans[i] = nums[i];
ans[i + n] = nums[i];
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] c1 = {1, 2, 1};
int[] e1 = {1, 2, 1, 1, 2, 1};
int[] c2 = {1, 3, 2, 1};
int[] e2 = {1, 3, 2, 1, 1, 3, 2, 1};
boolean ok = Arrays.equals(s.getConcatenation(c1), e1)
&& Arrays.equals(s.getConcatenationLoop(c1), e1)
&& Arrays.equals(s.getConcatenation(c2), e2)
&& Arrays.equals(s.getConcatenationLoop(c2), e2);
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + Arrays.toString(s.getConcatenation(c1)));
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + Arrays.toString(s.getConcatenation(c2)));
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1930. Unique Length-3 Palindromic Subsequences(长度为 3 的不同回文子序列)
题目描述
给你一个字符串 s,返回 s 中长度为 3 的不同回文子序列的个数。
即便存在多种方法来构建相同的子序列,但相同的子序列只计数一次。
回文是正着读和反着读一样的字符串。
子序列是由原字符串删除其中部分字符(也可以不删除)且不改变剩余字符之间相对顺序形成的一个新字符串。
例如,"ace" 是 "abcde" 的一个子序列。
示例 1
- 输入 :
s= "aabca" - 输出:3
- 解释 :长度为 3 的 3 个回文子序列分别是:"
aba"、"aaa"、"aca"。
示例 2
- 输入 :
s= "adc" - 输出:0
- 解释 :"
adc" 不存在长度为 3 的回文子序列。
示例 3
- 输入 :
s= "bbcbaba" - 输出:4
- 解释 :长度为 3 的 4 个回文子序列分别是:"
bbb"、"bcb"、"bab"、"aba"。
提示: - 3 <= s.length <= 10^5-
s仅由小写英文字母组成
解题思路
解法一:首末字符枚举 + 中间存在性,O(26n),主提交版本
长度为 3 的回文子序列形如 x _ y,
其中必须 x == y,中间可以是任意字符。对每个字母 c,找到它第一次出现与最后一次出现的位置
first[c]、last[c];若两者之间(开区间)至少还有一个位置,则 c 可以作为回文的两端,
中间字符可以是两端之间出现的任一字母。因此对每个字母 c,统计区间 (first[c], last[c]) 内
出现的不同字符数即可(用前缀和或位掩码)。总计数 = 各 c 的该区间内不同字符数之和。
解法二:位掩码前缀统计,对照
维护 pre[i] 为前 i 个字符出现字母的位掩码(26 位),
对每个字母 c 的 (first, last) 区间,区间内字母集合 = pre[last] XOR pre[first+1] 的二进制 1 个数
(注意 pre 记录的是字符集合,异或得到 (first, last] 区间新出现的字母,实际上是出现的字母集合差)。
用 Integer.bitCount 统计,写法更紧凑,用于交叉验证。
代码实现
java
public class Solution {
public int countPalindromicSubsequence(String s) {
int n = s.length();
int[] first = new int[26];
int[] last = new int[26];
java.util.Arrays.fill(first, -1);
for (int i = 0; i < n; i++) {
int c = s.charAt(i) - 'a';
if (first[c] == -1) {
first[c] = i;
}
last[c] = i;
}
// prefix[i][c] = s[0..i-1] 中字母 c 是否出现
boolean[][] pre = new boolean[n + 1][26];
for (int i = 1; i <= n; i++) {
for (int c = 0; c < 26; c++) {
pre[i][c] = pre[i - 1][c];
}
pre[i][s.charAt(i - 1) - 'a'] = true;
}
int ans = 0;
for (int c = 0; c < 26; c++) {
if (first[c] == -1 || first[c] + 1 >= last[c]) {
continue; // 没有可放中间字符的空间
}
int l = first[c] + 1, r = last[c]; // 开区间 (first, last)
for (int mid = 0; mid < 26; mid++) {
// 中间字符 mid 在 (first, last) 内出现过?
boolean appeared = pre[r][mid] && !pre[l - 1][mid];
if (appeared) {
ans++;
}
}
}
return ans;
}
public int countPalindromicSubsequenceMask(String s) {
int n = s.length();
int[] first = new int[26];
int[] last = new int[26];
java.util.Arrays.fill(first, -1);
for (int i = 0; i < n; i++) {
int c = s.charAt(i) - 'a';
if (first[c] == -1) first[c] = i;
last[c] = i;
}
int[] preMask = new int[n + 1]; // 前 i 个字符的字母集合掩码
for (int i = 1; i <= n; i++) {
preMask[i] = preMask[i - 1] | (1 << (s.charAt(i - 1) - 'a'));
}
int ans = 0;
for (int c = 0; c < 26; c++) {
if (first[c] == -1 || first[c] + 1 >= last[c]) {
continue;
}
// 区间 (first, last] 的字母集合(左开右闭)
int mask = preMask[last[c]] & ~preMask[first[c]];
ans += Integer.bitCount(mask);
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] cases = {"aabca", "adc", "bbcbaba", "abc", "aaaa", "abcdef"};
int[] exp = {3, 0, 4, 0, 1, 0};
boolean all = true;
for (int i = 0; i < cases.length; i++) {
int a = s.countPalindromicSubsequence(cases[i]);
int b = s.countPalindromicSubsequenceMask(cases[i]);
boolean ok = a == exp[i] && b == exp[i];
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1931. Painting a Grid With Three Different Colors(用三种不同颜色为网格涂色)
题目描述
给你两个整数 m 和 n。构造一个 m x n 的网格,其中每个单元格最开始是白色。
请你用红、绿、蓝三种颜色为每个单元格涂色。所有单元格都需要被涂色。
涂色方案需要满足:不存在相邻两个单元格颜色相同的情况。返回网格涂色的方法数。
因为答案可能非常大,返回对 10^9 + 7 取余的结果。
示例 1
- 输入 :
m= 1,n= 1 - 输出:3
- 解释:存在三种可能的涂色方案。
示例 2
- 输入 :
m= 1,n= 2 - 输出:6
- 解释:存在六种可能的涂色方案。
示例 3
- 输入 :
m= 5,n= 5 - 输出 :580986
提示: - 1 <= m <= 5- 1 <= n <= 1000
解题思路
解法一:按列状态 DP,O(n * 4^m * 合法态数),主提交版本
m <= 5,一列最多 3^m = 243 种
涂色,但合法的列内状态(上下格颜色不同)远少于这个数。预处理所有合法列状态
(把三种颜色编码为 0/1/2,每格占 2 位),以及状态间是否可相邻(逐位比较上下对应格颜色不同)。
DP:dp[j][s] = 前 j 列以状态 s 结尾的方案数,转移 dp[j][s] = sum over t 可接 s 的 dp[j-1][t]。
第一列状态全为 1。答案 = 最后一列所有状态之和 mod。
解法二:公式化枚举行三元组计数,对照,仅 m <= 3
当 m 很小(1/2/3)时,可以直接按行枚举
相邻行颜色组合再乘列方向,但实现复杂;这里改用"构建转移矩阵 + 快速幂"作为对照------
若 n 特别大可对转移矩阵做快速幂,本解法直接使用同样的合法态 DP 但换用矩阵乘法形式,
用于交叉验证解法一。由于矩阵快速幂代码量大,这里用逐列迭代版本(与首解法一样的 DP
但缓存转移表 + 用 int\[\]\[\]),仅实现角度差异。
代码实现
java
public class Solution {
private static final int MOD = 1_000_000_007;
public int colorTheGrid(int m, int n) {
// 1) 枚举状态:m 个格每格颜色 0/1/2(2 位/格),枚举全部 4^m 种 2 位组合再过滤合法列。
// ⚠️ 不能只枚举 3^m 个(对 m=4/5,3^m < 4^m,高行在 2 位编码下将被漏算永远为颜色 0)
int total = 1 << (2 * m); // 4^m
// 2) 收集合法列状态
java.util.List<Integer> states = new java.util.ArrayList<>();
for (int s = 0; s < total; s++) {
if (isValidCol(s, m)) {
states.add(s);
}
}
int K = states.size();
int[] st = new int[K];
for (int i = 0; i < K; i++) st[i] = states.get(i);
// 3) 转移表:t 能否接在 s 之后(逐位颜色不同)
boolean[][] can = new boolean[K][K];
for (int i = 0; i < K; i++) {
for (int j = 0; j < K; j++) {
can[i][j] = compatible(st[i], st[j], m);
}
}
// 4) DP
long[] dp = new long[K];
for (int i = 0; i < K; i++) dp[i] = 1;
for (int col = 1; col < n; col++) {
long[] ndp = new long[K];
for (int j = 0; j < K; j++) {
long sum = 0;
for (int i = 0; i < K; i++) {
if (can[i][j]) {
sum += dp[i];
}
}
ndp[j] = sum % MOD;
}
dp = ndp;
}
long ans = 0;
for (long v : dp) ans = (ans + v) % MOD;
return (int) ans;
}
private boolean isValidCol(int s, int m) {
int prev = -1;
for (int i = 0; i < m; i++) {
int c = (s >> (2 * i)) & 3;
if (c == prev || c > 2) {
return false;
}
prev = c;
}
return true;
}
private boolean compatible(int s1, int s2, int m) {
for (int i = 0; i < m; i++) {
int c1 = (s1 >> (2 * i)) & 3;
int c2 = (s2 >> (2 * i)) & 3;
if (c1 == c2) {
return false;
}
}
return true;
}
public int colorTheGridCache(int m, int n) {
int total = 1 << (2 * m); // 4^m(同样必须枚举全部 2 位组合)
java.util.List<Integer> states = new java.util.ArrayList<>();
for (int s = 0; s < total; s++) {
if (isValidCol(s, m)) states.add(s);
}
int K = states.size();
// 预计算每个状态的可后继列表
java.util.List<java.util.List<Integer>> nxt = new java.util.ArrayList<>();
for (int i = 0; i < K; i++) {
java.util.List<Integer> list = new java.util.ArrayList<>();
for (int j = 0; j < K; j++) {
if (compatible(states.get(i), states.get(j), m)) {
list.add(j);
}
}
nxt.add(list);
}
long[] dp = new long[K];
java.util.Arrays.fill(dp, 1);
for (int col = 1; col < n; col++) {
long[] ndp = new long[K];
for (int i = 0; i < K; i++) {
long v = dp[i];
if (v == 0) continue;
for (int j : nxt.get(i)) {
ndp[j] = (ndp[j] + v) % MOD;
}
}
dp = ndp;
}
long ans = 0;
for (long v : dp) ans = (ans + v) % MOD;
return (int) ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] cs = {{1, 1}, {1, 2}, {2, 1}, {2, 2}, {2, 3}, {3, 2}, {3, 3}, {4, 2}, {5, 5}, {5, 1000}};
// 官方样例:m=1,n=1 -> 3;m=1,n=2 -> 6;m=5,n=5 -> 580986。其余小规模用暴力对照验证
int[] exp = {3, 6, 6, 0, 0, 0, 0, 0, 580986, 0};
boolean all = true;
for (int i = 0; i < cs.length; i++) {
int a = s.colorTheGrid(cs[i][0], cs[i][1]);
int b = s.colorTheGridCache(cs[i][0], cs[i][1]);
boolean ok = a == b && (exp[i] == 0 || a == exp[i]);
int br = -1;
if (cs[i][0] * cs[i][1] <= 9) { // 小规模暴力对照
br = brute(cs[i][0], cs[i][1]);
ok &= a == br;
}
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " m=" + cs[i][0] + " n=" + cs[i][1]
+ " => dp=" + a + " / cache=" + b + (br >= 0 ? " / brute=" + br : ""));
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
private static int brute(int m, int n) {
int cells = m * n;
int total = 1;
for (int i = 0; i < cells; i++) total *= 3;
int cnt = 0;
for (int s = 0; s < total; s++) {
int[] g = new int[cells];
int x = s;
for (int i = 0; i < cells; i++) {
g[i] = x % 3;
x /= 3;
}
boolean ok = true;
for (int r = 0; r < m && ok; r++) {
for (int c = 0; c < n && ok; c++) {
int id = r * n + c;
if (c + 1 < n && g[id] == g[id + 1]) ok = false; // 右邻
if (r + 1 < m && g[id] == g[id + n]) ok = false; // 下邻
}
}
if (ok) cnt++;
}
return cnt;
}
}
复杂度分析
1932. Merge BSTs to Create Single BST(合并多棵二叉搜索树)
题目描述
给你 n 个二叉搜索树的根节点,存储在数组 trees 中(下标从 0 开始),对应 n 棵不同的二叉搜索树。
trees 中的每棵二叉搜索树最多有 3 个节点,且不存在值相同的两个根节点。在一步操作中,将会完成
下述步骤:
- 选择两个不同的下标
i和j,要求满足在trees[i]中的某个叶节点的值等于trees[j]的根节点的值。 - 用
trees[j]替换trees[i]中的那个叶节点。 - 从
trees中移除trees[j]。
如果在执行n - 1次操作后,能形成一棵有效的二叉搜索树,则返回结果二叉树的根节点;
如果无法构造一棵有效的二叉搜索树,返回null。
二叉搜索树是一种二叉树,且树中每个节点均满足下述属性: - 任意节点的左子树中的值都严格小于此节点的值。
- 任意节点的右子树中的值都严格大于此节点的值。
叶节点是不含子节点的节点。
示例 1
- 输入 :
trees= \[2,1,3,2,5,5,4] - 输出 :3,2,5,1,`null`,4
- 解释 :第一步把 2,1 合并到 3,2,5,第二步把 5,4 合并到 3,2,5,1,得到有效
BST。
示例 2
- 输入 :
trees= \[5,3,8,3,2,6] - 输出:\[\]
- 解释 :合并后 5,3,8,2,6 不是有效二叉搜索树,返回
null。
示例 3
- 输入 :
trees= \[5,4,3] - 输出:\[\]
- 解释 :无法执行任何操作。
提示: - n == trees.length- 1 <= n <= 2 * 10^4-
- 每棵树中节点数目在范围 1, 3 内
-
- 输入数据的每个节点可能有子节点但不存在子节点的子节点
-
trees中不存在两棵树根节点值相同的情况
-
- 输入中的所有树都是有效的二叉搜索树
- 1 <= TreeNode.val <= 5 * 10^4
解题思路
解法一:直接挂接 + 全树校验,O(n),主提交版本
- 用 map 记录 根值 -> 树(每棵树按根值唯一标识)。
- 对每棵树的每个叶节点(无左右孩子),若其值恰好等于另一棵树的根值,则该叶节点可被那棵树替换,
把被替换的树从 map 中删除并"挂"进去(直接修改树指针)。因为操作要恰好 n-1 次,所以最终 map
只剩 1 棵树;任一步有多个可选项都不会影响最终结果(题目保证存在唯一可行合并时,任意顺序
合并结果相同,因为合并是确定性的:每个叶值至多匹配一棵树的根,反之亦然)。 - 得到唯一根后做一次全树 BST 校验(中序遍历严格递增)并核对总节点数,合法则返回根,否则 null。
解法二:参考官方解法:先构建 children 映射再组装,对照
用
child2parent[value] = (树index, 该节点)记录哪些叶节点可以被其他树替换;随后同样把那些
子树挂到对应叶节点下;最后校验所得树是否为 BST 且包含全部 n 棵树的所有节点(节点总数一致)。
与解法一的组装方式略有不同(解法一直接改指针,解法二记录映射后统一挂接),用于交叉验证。
代码实现
java
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Solution {
public TreeNode canMerge(java.util.List<TreeNode> trees) {
Map<Integer, TreeNode> rootMap = new HashMap<>();
Set<Integer> allVals = new HashSet<>();
for (TreeNode t : trees) {
rootMap.put(t.val, t);
collectVals(t, allVals); // 修改前收集全部节点值(避免挂接后重复计数)
}
// 对每棵非叶树,其叶节点可以被以该值为根的树替换
for (TreeNode t : trees) {
// 树 t 的每个叶节点
for (TreeNode leaf : leaves(t)) {
TreeNode sub = rootMap.get(leaf.val);
if (sub != null && sub != t) {
leaf.left = sub.left; // 用 sub 替换 leaf(直接挂接整棵子树)
leaf.right = sub.right;
rootMap.remove(sub.val); // sub 已被合并,从 map 移除
}
}
}
// 最终应只剩一棵未合并的树
if (rootMap.size() != 1) {
return null;
}
TreeNode root = rootMap.values().iterator().next();
// 全树必须是合法 BST
long[] prev = {Long.MIN_VALUE};
if (!isBST(root, prev)) {
return null;
}
// 值集合必须与全部原始树节点值集合一致(含全部节点)
Set<Integer> got = new HashSet<>();
collectVals(root, got);
if (!got.equals(allVals)) {
return null;
}
return root;
}
private void collectVals(TreeNode node, Set<Integer> set) {
if (node == null) return;
set.add(node.val);
collectVals(node.left, set);
collectVals(node.right, set);
}
private java.util.List<TreeNode> leaves(TreeNode root) {
java.util.List<TreeNode> res = new java.util.ArrayList<>();
collectLeaves(root, res);
return res;
}
private void collectLeaves(TreeNode node, java.util.List<TreeNode> res) {
if (node.left == null && node.right == null) {
res.add(node);
return;
}
if (node.left != null) collectLeaves(node.left, res);
if (node.right != null) collectLeaves(node.right, res);
}
private boolean isBST(TreeNode node, long[] prev) {
if (node == null) return true;
if (!isBST(node.left, prev)) return false;
if (node.val <= prev[0]) return false;
prev[0] = node.val;
return isBST(node.right, prev);
}
private int countNodes(TreeNode node) {
if (node == null) return 0;
return 1 + countNodes(node.left) + countNodes(node.right);
}
public TreeNode canMergeMap(java.util.List<TreeNode> trees) {
Map<Integer, TreeNode> roots = new HashMap<>();
Set<Integer> allVals = new HashSet<>();
for (TreeNode t : trees) {
roots.put(t.val, t);
collectVals(t, allVals);
}
// leafParent: 叶节点值 -> [父节点]
Map<Integer, TreeNode> leafParent = new HashMap<>();
Set<Integer> usedRoots = new HashSet<>();
for (TreeNode t : trees) {
for (TreeNode leaf : leaves(t)) {
if (roots.containsKey(leaf.val) && roots.get(leaf.val) != t) {
leafParent.put(leaf.val, t);
usedRoots.add(roots.get(leaf.val).val);
}
}
}
for (TreeNode t : trees) {
if (usedRoots.contains(t.val)) continue; // 这棵树作为根被挂在别处
for (TreeNode leaf : leaves(t)) {
TreeNode sub = roots.get(leaf.val);
if (sub != null && sub != t) {
leaf.left = sub.left;
leaf.right = sub.right;
}
}
}
// 最终根:未被挂接的树
TreeNode root = null;
for (TreeNode t : trees) {
if (!usedRoots.contains(t.val)) {
if (root != null) return null; // 多于一个根
root = t;
}
}
if (root == null) return null;
long[] prev = {Long.MIN_VALUE};
if (!isBST(root, prev)) return null;
Set<Integer> got = new HashSet<>();
collectVals(root, got);
if (!got.equals(allVals)) return null;
return root;
}
public static void main(String[] args) {
Solution s = new Solution();
boolean all = true;
// ===== 解法一:直接挂接 =====
// 用例 1:trees = [[2,1],[3,2,5],[5,4]] -> 可合并为 BST [3,2,5,1,null,4]
// ⚠️ 注意 [3,2,5] 的节点 2、5 在层序输入中是叶子,1、4 分别位于独立树 [2,1]、[5,4] 中
java.util.List<TreeNode> l1 = new java.util.ArrayList<>();
l1.add(new TreeNode(2, new TreeNode(1), null));
l1.add(new TreeNode(3, new TreeNode(2), new TreeNode(5)));
l1.add(new TreeNode(5, new TreeNode(4), null));
TreeNode r1 = s.canMerge(l1);
boolean ok1 = r1 != null && r1.val == 3 && isBstCheck(r1) && countN(r1) == 5;
all &= ok1;
System.out.println((ok1 ? "PASS" : "FAIL") + " case1(sol1) => " + (r1 == null ? "null" : "root=" + r1.val));
// 用例 2:trees = [[5,3,8],[3,2,6]] -> 合并后 [5,3,8,2,6] 不是 BST,返回 null
java.util.List<TreeNode> l2 = new java.util.ArrayList<>();
l2.add(new TreeNode(5, new TreeNode(3), new TreeNode(8))); // [5,3,8]:3、8 均为叶子
l2.add(new TreeNode(3, new TreeNode(2), new TreeNode(6))); // [3,2,6]:将被挂到 5 的左子
TreeNode r2 = s.canMerge(l2);
boolean ok2 = r2 == null;
all &= ok2;
System.out.println((ok2 ? "PASS" : "FAIL") + " case2(sol1) => null");
// 用例 3:trees = [[5,4],[3]] -> 无任何叶值与根值匹配,无法操作
java.util.List<TreeNode> l3 = new java.util.ArrayList<>();
l3.add(new TreeNode(5, new TreeNode(4), null));
l3.add(new TreeNode(3, null, null));
TreeNode r3 = s.canMerge(l3);
boolean ok3 = r3 == null;
all &= ok3;
System.out.println((ok3 ? "PASS" : "FAIL") + " case3(sol1) => null");
// ===== 解法二:映射挂接(需重建 fresh 树,避免解法一已原地改指针) =====
java.util.List<TreeNode> l4 = new java.util.ArrayList<>();
l4.add(new TreeNode(2, new TreeNode(1), null));
l4.add(new TreeNode(3, new TreeNode(2), new TreeNode(5)));
l4.add(new TreeNode(5, new TreeNode(4), null));
TreeNode r4 = s.canMergeMap(l4);
boolean ok4 = r4 != null && r4.val == 3 && isBstCheck(r4) && countN(r4) == 5;
all &= ok4;
System.out.println((ok4 ? "PASS" : "FAIL") + " case4(sol2) => " + (r4 == null ? "null" : "root=" + r4.val));
java.util.List<TreeNode> l5 = new java.util.ArrayList<>();
l5.add(new TreeNode(5, new TreeNode(3), new TreeNode(8)));
l5.add(new TreeNode(3, new TreeNode(2), new TreeNode(6)));
boolean ok5 = s.canMergeMap(l5) == null;
all &= ok5;
System.out.println((ok5 ? "PASS" : "FAIL") + " case5(sol2) => null");
java.util.List<TreeNode> l6 = new java.util.ArrayList<>();
l6.add(new TreeNode(5, new TreeNode(4), null));
l6.add(new TreeNode(3, null, null));
boolean ok6 = s.canMergeMap(l6) == null;
all &= ok6;
System.out.println((ok6 ? "PASS" : "FAIL") + " case6(sol2) => null");
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
private static int countN(TreeNode n) {
if (n == null) return 0;
return 1 + countN(n.left) + countN(n.right);
}
private static boolean isBstCheck(TreeNode r) {
long[] prev = {Long.MIN_VALUE};
return check(r, prev);
}
private static boolean check(TreeNode n, long[] prev) {
if (n == null) return true;
if (!check(n.left, prev)) return false;
if (n.val <= prev[0]) return false;
prev[0] = n.val;
return check(n.right, prev);
}
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
}
复杂度分析
1933. Check if String Is Decomposable Into Value-Equal Substrings
题目描述
(判断字符串是否可分解为值均等的子串)
一个字符串的所有字符都是一样的,被称作等值字符串。
举例,"1111" 和 "33" 就是等值字符串。相比之下,"123" 就不是等值字符串。
规则:给出一个数字字符串 s,将字符串分解成一些等值字符串,如果有且仅有一个等值子字符串长度为 2,
其他的等值子字符串的长度都是 3。
如果能够按照上面的规则分解字符串 s,就返回真,否则返回假。
子串就是原字符串中连续的字符序列。
示例 1
- 输入 :
s= "000111000" - 输出 :
false - 解释 :
s只能被分解为长度为 3 的等值子字符串。
示例 2
- 输入 :
s= "00011111222" - 输出 :
true - 解释 :
s能被分解为 "000","111","11","222"。
示例 3
- 输入 :
s= "01110002223300" - 输出 :
false - 解释 :一个不能被分解的原因是在开头有一个 0。
提示: - 1 <= s.length <= 1000-
s仅包含数字
解题思路
解法一:按连续段长度分情况处理,O(n),主提交版本
把 s 按相同字符切成若干连续段,记录每段长度。
规则等价于:每个连续段长度 len 必须满足 len % 3 == 0 或 len % 3 == 2
(因为段内要么拆成若干个 3,要么恰好用一个 2 + 若干 3,均要求 len % 3 != 1),
并且"恰好有一个长度为 2 的等值子串"意味着这些段中 len % 3 == 2 的段必须恰有一个。
注意:len 可以是 2、5、8...(都拆出 3+3+...+2),所以这里有且仅有一段给出那个 2。
若满足则 true,否则 false。
解法二:贪心模拟切割,对照
从左往右贪心:当前连续相同字符长度 c,若 c % 3 == 2 则此段必须
贡献那个唯一的 2,标记 used2 并跳过;若 c % 3 == 1 则该段不合法直接 false(长度 1、4、7...)。
逐段处理完检查是否恰好用了一次 2。与解法一等价但实现角度不同,用于交叉验证。
代码实现
java
public class Solution {
public boolean isDecomposable(String s) {
int n = s.length();
boolean usedTwo = false;
int i = 0;
while (i < n) {
int j = i;
while (j < n && s.charAt(j) == s.charAt(i)) {
j++;
}
int len = j - i;
int mod = len % 3;
if (mod == 1) {
return false; // 长度 1/4/7... 无法拆成 2+3k
}
if (mod == 2) {
if (usedTwo) {
return false; // 有两个段需要那个唯一的 2
}
usedTwo = true;
}
i = j;
}
return usedTwo; // 必须恰好有一个长度为 2 的段
}
public boolean isDecomposableGreedy(String s) {
int n = s.length();
boolean usedTwo = false;
for (int i = 0; i < n; ) {
int j = i;
while (j < n && s.charAt(j) == s.charAt(i)) {
j++;
}
int len = j - i;
// 每个段必须能拆成 3+3+...+2 或 3+3+...
if (len % 3 == 1) {
return false;
}
if (len % 3 == 2) {
if (usedTwo) {
return false;
}
usedTwo = true;
}
i = j;
}
return usedTwo;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] cs = {"000111000", "00011111222", "01110002223300", "00", "000", "000111", "1", "11", "1111"};
boolean[] exp = {false, true, false, true, false, false, false, true, false};
boolean all = true;
for (int i = 0; i < cs.length; i++) {
boolean a = s.isDecomposable(cs[i]);
boolean b = s.isDecomposableGreedy(cs[i]);
boolean ok = a == exp[i] && b == exp[i];
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1934. Confirmation Rate(确认率)
题目类型:数据库(SQL)
题目描述
表:Signups
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
+----------------+----------+
user_id 是该表的主键。每一行都包含 ID 为 user_id 的用户的注册时间信息。
表:Confirmations
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
| action | ENUM |
+----------------+----------+
(user_id, time_stamp) 是该表的主键。action 是类型为 ('confirmed', 'timeout') 的 ENUM。
用户的确认率 是 'confirmed' 消息的数量除以请求的确认消息的总数。没有请求任何确认消息的用户的确认率为 0。确认率四舍五入到小数点后两位 。编写一个 SQL 查询来查找每个用户的确认率。
示例 1:
输入:
Signups 表: Confirmations 表:
+---------+---------------------+ +---------+---------------------+-----------+
| user_id | time_stamp | | user_id | time_stamp | action |
+---------+---------------------+ +---------+---------------------+-----------+
| 3 | 2020-03-21 10:16:13 | | 3 | 2021-01-06 03:30:46 | timeout |
| 7 | 2020-01-04 13:57:59 | | 3 | 2021-07-14 14:00:00 | timeout |
| 2 | 2020-07-29 23:09:44 | | 7 | 2021-06-12 11:57:29 | confirmed |
| 6 | 2020-12-09 10:39:37 | | 7 | 2021-06-13 12:58:28 | confirmed |
+---------+---------------------+ | 7 | 2021-06-14 13:59:27 | confirmed |
| 2 | 2021-01-22 00:00:00 | confirmed |
| 2 | 2021-02-28 23:59:59 | timeout |
+---------+---------------------+-----------+
输出:
+---------+-------------------+
| user_id | confirmation_rate |
+---------+-------------------+
| 6 | 0.00 |
| 3 | 0.00 |
| 7 | 1.00 |
| 2 | 0.50 |
+---------+-------------------+
解题思路
Signups 与 Confirmations 左连接后按用户分组:
AVG(action = 'confirmed') 在 MySQL 中会把布尔表达式转成 0/1 求平均即确认率;没有请求的用户
AVG 为 NULL,用 IFNULL(..., 0) 兜底为 0,最后 ROUND(..., 2) 保留两位小数。
代码实现
sql
SELECT s.user_id,
ROUND(IFNULL(AVG(c.action = 'confirmed'), 0), 2) AS confirmation_rate
FROM Signups s
LEFT JOIN Confirmations c ON s.user_id = c.user_id
GROUP BY s.user_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1935. Maximum Number of Words You Can Type(可以输入的最大单词数)
题目描述
键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。
给你一个由若干单词组成的字符串 text,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串
brokenLetters,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。
示例 1
- 输入 :
text= "helloworld",brokenLetters= "ad" - 输出:1
- 解释 :无法输入 "
world",因为字母键 'd' 已损坏。
示例 2
- 输入 :
text= "leetcode",brokenLetters= "lt" - 输出:1
- 解释 :无法输入 "
leet",因为字母键 'l' 和 't' 已损坏。
示例 3
- 输入 :
text= "leetcode",brokenLetters= "e" - 输出:0
- 解释 :无法输入任何单词,因为字母键 '
e' 已损坏。
提示: - 1 <= text.length <= 10^4- 0 <= brokenLetters.length <= 26-
text由若干用单个空格分隔的单词组成,且不含任何前导和尾随空格
-
- 每个单词仅由小写英文字母组成
-
brokenLetters由互不相同的小写英文字母组成
解题思路
解法一:单词拆分 + 字符集合,O(len),主提交版本
把 brokenLetters 存进布尔数组(26 位),
按空格拆分 text 为单词,统计完全不含坏键的单词数。
解法二:单次扫描按空格切词,对照
不调用 split,用一个循环扫描 text,遇到空格结束当前单词判断,
单词内若出现坏键则标记本词不可输入;最后再处理最后一个单词。与 split 等价但实现不同,用于交叉验证。
代码实现
java
public class Solution {
public int canBeTypedWords(String text, String brokenLetters) {
boolean[] broken = new boolean[26];
for (char c : brokenLetters.toCharArray()) {
broken[c - 'a'] = true;
}
int ans = 0;
for (String w : text.split(" ")) {
boolean ok = true;
for (char c : w.toCharArray()) {
if (broken[c - 'a']) {
ok = false;
break;
}
}
if (ok) {
ans++;
}
}
return ans;
}
public int canBeTypedWordsScan(String text, String brokenLetters) {
boolean[] broken = new boolean[26];
for (char c : brokenLetters.toCharArray()) {
broken[c - 'a'] = true;
}
int ans = 0;
int n = text.length();
boolean bad = false;
for (int i = 0; i <= n; i++) {
if (i == n || text.charAt(i) == ' ') {
if (!bad) {
ans++;
}
bad = false;
continue;
}
if (broken[text.charAt(i) - 'a']) {
bad = true;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] t = {"hello world", "leet code", "leet code", "a b c d", "abc", "zzz"};
String[] b = {"ad", "lt", "e", "abcde", "z", ""};
int[] exp = {1, 1, 0, 0, 1, 1};
boolean all = true;
for (int i = 0; i < t.length; i++) {
int a = s.canBeTypedWords(t[i], b[i]);
int c = s.canBeTypedWordsScan(t[i], b[i]);
boolean ok = a == exp[i] && c == exp[i];
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + c);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1936. Add Minimum Number of Rungs(新增的最少台阶数)
题目描述
给你一个严格递增的整数数组 rungs,用于表示梯子上每一台阶的高度。当前你正站在高度为 0 的地板上,
并打算爬到最后一个台阶。
另给你一个整数 dist。每次移动中,你可以到达下一个距离你当前位置(地板或台阶)不超过 dist 高度的
台阶。当然,你也可以在任何正整数高度处插入尚不存在的新台阶。
返回爬到最后一阶时必须添加到梯子上的最少台阶数。
示例 1
- 输入 :
rungs= 1,3,5,10,dist= 2 - 输出:2
- 解释 :现在无法到达最后一阶。在高度为 7 和 8 的位置增设新的台阶,以爬上梯子。
梯子在高度为 1,3,5,7,8,10 的位置上有台阶。
示例 2
- 输入 :
rungs= 3,6,8,10,dist= 3 - 输出:0
- 解释:这个梯子无需增设新台阶也可以爬上去。
示例 3
- 输入 :
rungs= 3,4,6,7,dist= 2 - 输出:1
- 解释:现在无法从地板到达梯子的第一阶。在高度为 1 的位置增设新的台阶,以爬上梯子。
示例 4
- 输入 :
rungs= 5,dist= 10 - 输出:0
- 解释 :这个梯子无需增设新台阶也可以爬上去。
提示: - 1 <= rungs.length <= 10^5- 1 <= rungs[i] <= 10^9- 1 <= dist <= 10^9-
rungs严格递增
解题思路
解法一:贪心差分,O(n),主提交版本
从当前位置 cur(初始 0)推进到每个台阶 next,若
next - cur > dist,则需要在中间补台阶。每补一个台阶最多跨越 dist 高度,所以要补
(next - cur - 1) / dist 个(向上取整再减 1)。补完后把 cur 更新为 next。
解法二:逐段模拟插入位置,对照
维护当前可达高度 reach = cur + dist,若 reach < next,
则在 cur + dist 处插入台阶并累加计数,重复直到 reach >= next。逻辑等价,写法更贴近模拟,
用于交叉验证。
代码实现
java
public class Solution {
public int addRungs(int[] rungs, int dist) {
long ans = 0;
long cur = 0;
for (int next : rungs) {
if (next - cur > dist) {
// 需要 (next - cur) 被 dist 分成 ceil 段,中间插入 ceil-1 个台阶
ans += (next - cur - 1) / dist;
}
cur = next;
}
return (int) ans;
}
public int addRungsSim(int[] rungs, int dist) {
long ans = 0;
long cur = 0;
for (int next : rungs) {
while (next - cur > dist) {
cur += dist; // 在 cur+dist 处补一个台阶并站上去
ans++;
}
cur = next; // 直接上到真实台阶
}
return (int) ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] rs = {{1, 3, 5, 10}, {3, 6, 8, 10}, {3, 4, 6, 7}, {5}, {1, 2, 3, 4}, {1000000000}};
int[] ds = {2, 3, 2, 10, 1, 1};
int[] exp = {2, 0, 1, 0, 0, 999999999};
boolean all = true;
for (int i = 0; i < rs.length; i++) {
int a = s.addRungs(rs[i], ds[i]);
int b = s.addRungsSim(rs[i], ds[i]);
boolean ok = a == exp[i] && b == exp[i];
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1937. Maximum Number of Points with Cost(扣分后的最大得分)
题目描述
给你一个 m x n 的整数矩阵 points(下标从 0 开始)。一开始你的得分为 0,你想最大化从矩阵中
得到的分数。
你的得分方式为:每一行中选取一个格子,选中坐标为 (r, c) 的格子会给你的总得分增加 points[r][c]。
然而,相邻行之间被选中的格子如果隔得太远,你会失去一些得分。对于相邻行 r 和 r + 1
(其中 0 <= r < m - 1),选中格子 (r, c1) 和 (r + 1, c2),你的总得分减少 abs(c1 - c2)。
请你返回你能得到的最大得分。
abs(x) 定义为:如果 x >= 0,那么值为 x;如果 x < 0,那么值为 -x。
提示:
- m == points.length- n == points[i].length- 1 <= m, n <= 10^5- 1 <= m * n <= 10^5- 0 <= points[i][j] <= 10^5
解题思路
解法一:前后缀最大值优化 DP,O(mn),主提交版本
设 dp[c] 为上一行选第 c 列时的最大得分。转移:ndp[c] = points[i][c] + max( max over c1 <= c 的 dp[c1] - (c - c1), // 从左向右:dp[c1] + c1 - c max over c1 >= c 的 dp[c1] - (c1 - c) // 从右向左:dp[c1] - c1 + c )。
预处理 leftBest[c] = max(dp[c1] + c1)(左扫)与 rightBest[c] = max(dp[c1] - c1)(右扫),
则 ndp[c] = points[i][c] + max(leftBest[c] - c, rightBest[c] + c)。O(n) 每行,总 O(mn)。
解法二:暴力 O(mn^2),对照
直接按定义 ndp[c] = points[i][c] + max_j(dp[j] - |j - c|),
小数据交叉验证。
代码实现
java
public class Solution {
public long maxPoints(int[][] points) {
int m = points.length, n = points[0].length;
long[] dp = new long[n];
for (int j = 0; j < n; j++) {
dp[j] = points[0][j];
}
for (int i = 1; i < m; i++) {
long[] left = new long[n];
long[] right = new long[n];
long best = Long.MIN_VALUE;
for (int c = 0; c < n; c++) {
best = Math.max(best, dp[c] + c);
left[c] = best;
}
best = Long.MIN_VALUE;
for (int c = n - 1; c >= 0; c--) {
best = Math.max(best, dp[c] - c);
right[c] = best;
}
long[] ndp = new long[n];
for (int c = 0; c < n; c++) {
ndp[c] = points[i][c] + Math.max(left[c] - c, right[c] + c);
}
dp = ndp;
}
long ans = 0;
for (long v : dp) {
ans = Math.max(ans, v);
}
return ans;
}
public long maxPointsBrute(int[][] points) {
int m = points.length, n = points[0].length;
long[] dp = new long[n];
for (int j = 0; j < n; j++) {
dp[j] = points[0][j];
}
for (int i = 1; i < m; i++) {
long[] ndp = new long[n];
for (int c = 0; c < n; c++) {
long best = Long.MIN_VALUE;
for (int c1 = 0; c1 < n; c1++) {
best = Math.max(best, dp[c1] - Math.abs(c1 - c));
}
ndp[c] = points[i][c] + best;
}
dp = ndp;
}
long ans = 0;
for (long v : dp) ans = Math.max(ans, v);
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] p1 = {{1, 2, 3}, {1, 5, 1}, {3, 1, 1}};
int[][] p2 = {{1, 5}, {2, 3}, {4, 2}};
int[][] p3 = {{0, 0, 0}};
boolean ok = true;
long a1 = s.maxPoints(p1);
long b1 = s.maxPointsBrute(p1);
ok &= a1 == 9 && b1 == 9;
long a2 = s.maxPoints(p2);
long b2 = s.maxPointsBrute(p2);
ok &= a2 == 11 && b2 == 11;
long a3 = s.maxPoints(p3);
long b3 = s.maxPointsBrute(p3);
ok &= a3 == 0 && b3 == 0;
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1938. Maximum Genetic Difference Query(查询最大基因差)
题目描述
给你一棵 n 个节点的有根树,节点编号从 0 到 n - 1。每个节点的编号表示这个节点的独一无二的
基因值(也就是说节点 x 的基因值为 x)。两个基因值的基因差是两者的异或和。给你整数数组 parents,
其中 parents[i] 是节点 i 的父节点。如果节点 x 是树的根,那么 parents[x] == -1。
给你查询数组 queries,其中 queries[i] = [nodei, vali]。对于查询 i,请你找到 vali 和 pi
的最大基因差,其中 pi 是节点 nodei 到根之间的任意节点(包含 nodei 和根节点)。
更正式的,你想要最大化 vali XOR pi。
请你返回数组 ans,其中 ans[i] 是第 i 个查询的答案。
示例 1
- 输入 :
parents= -1,0,1,1,queries= \[0,2,3,2,2,5] - 输出:2,3,7
- 解释 :0,2 最大基因差的对应节点为 0,基因差为 2
XOR0 = 2;
3,2 对应节点 1,2XOR1 = 3;2,5 对应节点 2,5XOR2 = 7。
示例 2
- 输入 :
parents= 3,7,-1,2,0,7,0,2,queries= \[4,6,1,15,0,5] - 输出:6,14,7
- 解释 :4,6 对应节点 0,6
XOR0 = 6;1,15 对应节点 1,15XOR1 = 14;
0,5 对应节点 2,5XOR2 = 7。
提示: - 2 <= parents.length <= 10^5-
- 对于每个不是根节点的
i,有 0 <=parents[i]<=parents.length- 1
- 对于每个不是根节点的
- parents[root] == -1- 1 <= queries.length <= 10^5- 0 <= nodei <= parents.length - 1- 0 <= vali <= 2 * 10^5
解题思路
解法一:离线 DFS + 二进制字典树回溯,O((n+q) * 17),主提交版本
把查询按 node 挂在树节点上。从根 DFS,进入节点时把其编号(基因值)插入字典树,离开时删除。
这样 DFS 过程中字典树恰好维护"当前节点到根路径上的所有基因值"。遇到挂在该节点的查询
(node, val),就在字典树上贪心求 val XOR 任意路径基因值 的最大值(从高位到低位走相反分支)。
用 cnt[node] 记录字典树节点计数实现可删除;插入/删除都维护计数,查询时只走存在分支。
解法二:暴力枚举路径比对,对照
对每个查询,从 node 向上走到根,沿途枚举所有祖先编号求异或
最大值。O(q * depth) 仅适用于小数据,用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
private static final int MAX_BIT = 17; // val 与节点编号 <= 2*10^5 < 2^18,用 17..0 共 18 位足够
public int[] maxGeneticDifference(int[] parents, int[][] queries) {
int n = parents.length;
List<Integer>[] children = new ArrayList[n];
for (int i = 0; i < n; i++) {
children[i] = new ArrayList<>();
}
int root = -1;
for (int i = 0; i < n; i++) {
if (parents[i] == -1) {
root = i;
} else {
children[parents[i]].add(i);
}
}
// 查询按 node 分组
Map<Integer, List<int[]>> qByNode = new HashMap<>();
for (int qi = 0; qi < queries.length; qi++) {
qByNode.computeIfAbsent(queries[qi][0], k -> new ArrayList<>())
.add(new int[]{queries[qi][1], qi});
}
Trie trie = new Trie();
int[] ans = new int[queries.length];
dfs(root, children, qByNode, trie, ans);
return ans;
}
private void dfs(int u, List<Integer>[] children, Map<Integer, List<int[]>> qByNode,
Trie trie, int[] ans) {
trie.insert(u);
List<int[]> qs = qByNode.get(u);
if (qs != null) {
for (int[] q : qs) {
ans[q[1]] = trie.maxXor(q[0]);
}
}
for (int v : children[u]) {
dfs(v, children, qByNode, trie, ans);
}
trie.remove(u); // 回溯:离开子树时删除自己
}
static class Trie {
private static class Node {
Node[] next = new Node[2];
int cnt = 0;
}
private final Node root = new Node();
void insert(int x) {
Node cur = root;
for (int b = MAX_BIT; b >= 0; b--) {
int bit = (x >> b) & 1;
if (cur.next[bit] == null) {
cur.next[bit] = new Node();
}
cur = cur.next[bit];
cur.cnt++;
}
}
void remove(int x) {
Node cur = root;
for (int b = MAX_BIT; b >= 0; b--) {
int bit = (x >> b) & 1;
cur = cur.next[bit];
cur.cnt--;
}
}
int maxXor(int x) {
Node cur = root;
int res = 0;
for (int b = MAX_BIT; b >= 0; b--) {
int bit = (x >> b) & 1;
int want = 1 - bit; // 优先走相反位
if (cur.next[want] != null && cur.next[want].cnt > 0) {
res |= (1 << b);
cur = cur.next[want];
} else {
cur = cur.next[bit];
}
}
return res;
}
}
public int[] maxGeneticDifferenceBrute(int[] parents, int[][] queries) {
int n = parents.length;
int[] ans = new int[queries.length];
for (int qi = 0; qi < queries.length; qi++) {
int node = queries[qi][0], val = queries[qi][1];
int best = 0;
int cur = node;
while (cur != -1) {
best = Math.max(best, val ^ cur);
cur = parents[cur];
}
ans[qi] = best;
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] p1 = {-1, 0, 1, 1};
int[][] q1 = {{0, 2}, {3, 2}, {2, 5}};
int[] e1 = {2, 3, 7};
// p2: parents[0]=3, parents[1]=7, parents[2]=-1, parents[3]=2, parents[4]=0, parents[5]=7, parents[6]=0, parents[7]=2
int[] p2 = {3, 7, -1, 2, 0, 7, 0, 2};
int[][] q2 = {{4, 6}, {1, 15}, {0, 5}};
int[] e2 = {6, 14, 7};
boolean ok = java.util.Arrays.equals(s.maxGeneticDifference(p1, q1), e1)
&& java.util.Arrays.equals(s.maxGeneticDifferenceBrute(p1, q1), e1)
&& java.util.Arrays.equals(s.maxGeneticDifference(p2, q2), e2)
&& java.util.Arrays.equals(s.maxGeneticDifferenceBrute(p2, q2), e2);
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + java.util.Arrays.toString(s.maxGeneticDifference(p1, q1)));
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + java.util.Arrays.toString(s.maxGeneticDifference(p2, q2)));
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1939. Users That Actively Request Confirmation Messages(主动请求确认消息的用户)
题目类型:数据库(SQL)
题目描述
表:Signups
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
+----------------+----------+
user_id 是该表的主键。
表:Confirmations
+----------------+----------+
| Column Name | Type |
+----------------+----------+
| user_id | int |
| time_stamp | datetime |
| action | ENUM |
+----------------+----------+
(user_id, time_stamp) 是该表的主键。action 是类型为 ('confirmed', 'timeout') 的 ENUM。
编写 SQL 查询以查找在 24 小时窗口内 两次请求确认消息的用户的 ID。两个正好相隔 24 小时的消息被认为是在窗口内(即时间差 <= 24 小时,包含端点)。
示例:
输入:
Confirmations table:
+---------+---------------------+-----------+
| user_id | time_stamp | action |
+---------+---------------------+-----------+
| 3 | 2021-01-06 03:30:46 | timeout |
| 3 | 2021-01-06 03:37:45 | timeout |
| 7 | 2021-06-12 11:57:29 | confirmed |
| 7 | 2021-06-13 11:57:30 | confirmed |
| 2 | 2021-01-22 00:00:00 | confirmed |
| 2 | 2021-01-23 00:00:00 | timeout |
| 6 | 2021-10-23 14:14:14 | confirmed |
| 6 | 2021-10-24 14:14:13 | timeout |
+---------+---------------------+-----------+
输出:
+---------+
| user_id |
+---------+
| 2 |
| 3 |
| 6 |
+---------+
解释:用户 2 恰好相隔 24 小时、用户 3 相隔 6 分 59 秒、用户 6 相隔 23 小时 59 分 59 秒,均在窗口内;用户 7 相隔 24 小时 1 秒,不在窗口内。
解题思路
表自连接:同一 user_id 的两条消息,用 TIMESTAMPDIFF(HOUR, ...)(或 TIME_TO_SEC 差值)判断
两条消息时间差 <= 24 小时即可,DISTINCT 去重后输出。
代码实现
sql
SELECT DISTINCT a.user_id
FROM Confirmations a
JOIN Confirmations b
ON a.user_id = b.user_id
AND a.time_stamp < b.time_stamp
WHERE TIMESTAMPDIFF(HOUR, a.time_stamp, b.time_stamp) <= 24
ORDER BY a.user_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1940. Longest Common Subsequence Between Sorted Arrays(排序数组之间的最长公共子序列)
题目描述
给定一个由整数数组组成的数组 arrays,其中 arrays[i] 是严格递增排序的,返回一个所有数组均包含的
最长公共子序列的整数数组。
子序列是从另一个序列派生出来的序列,删除一些元素或不删除任何元素,而不改变其余元素的顺序。
示例 1
- 输入 :
arrays= \[1,3,4,1,4,7,9] - 输出:1,4
- 解释:这两个数组中的最长子序列是 1,4。
示例 2
- 输入 :
arrays= \[2,3,6,8,1,2,3,5,6,7,10,2,3,4,6,9] - 输出:2,3,6
- 解释:这三个数组中的最长公共子序列是 2,3,6。
示例 3
- 输入 :
arrays= \[1,2,3,4,5,6,7,8] - 输出:\[\]
- 解释 :这两个数组之间没有公共子序列。
限制条件: 2 <= arrays.length <= 1001 <= arrays[i].length <= 1001 <= arrays[i][j] <= 100arrays[i]是严格递增排序
解题思路
解法一:计数 + 频次筛选,O(total * len),主提交版本
因为每个数组严格递增且值域只有 1...100,
所以公共子序列等价于"每个数组都包含的元素集合"(严格递增有序数组中的公共子序列就是公共元素
的交集按原顺序排列)。用计数数组统计每个值在多少数组中出现,出现次数 == arrays.length 的值
即公共元素,按数值升序即为最长公共子序列(且唯一)。
解法二:哈希集合求交,对照
第一个数组元素放入集合,逐个与后续数组取交集(Intersection),
最后排序输出。逻辑与解法一同源,仅实现方式不同,用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<Integer> longestCommonSubsequence(int[][] arrays) {
int k = arrays.length;
int[] cnt = new int[101];
for (int[] arr : arrays) {
boolean[] seen = new boolean[101];
for (int x : arr) {
if (!seen[x]) {
seen[x] = true;
cnt[x]++;
}
}
}
List<Integer> res = new ArrayList<>();
for (int v = 1; v <= 100; v++) {
if (cnt[v] == k) {
res.add(v);
}
}
return res;
}
public List<Integer> longestCommonSubsequenceSet(int[][] arrays) {
boolean[] common = new boolean[101];
for (int x : arrays[0]) {
common[x] = true;
}
for (int i = 1; i < arrays.length; i++) {
boolean[] cur = new boolean[101];
for (int x : arrays[i]) {
cur[x] = true;
}
for (int v = 1; v <= 100; v++) {
common[v] = common[v] && cur[v];
}
}
List<Integer> res = new ArrayList<>();
for (int v = 1; v <= 100; v++) {
if (common[v]) {
res.add(v);
}
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] a1 = {{1, 3, 4}, {1, 4, 7, 9}};
int[][] a2 = {{2, 3, 6, 8}, {1, 2, 3, 5, 6, 7, 10}, {2, 3, 4, 6, 9}};
int[][] a3 = {{1, 2, 3, 4, 5}, {6, 7, 8}};
List<Integer> e1 = Arrays.asList(1, 4);
List<Integer> e2 = Arrays.asList(2, 3, 6);
List<Integer> e3 = new ArrayList<>();
boolean ok = s.longestCommonSubsequence(a1).equals(e1)
&& s.longestCommonSubsequenceSet(a1).equals(e1)
&& s.longestCommonSubsequence(a2).equals(e2)
&& s.longestCommonSubsequenceSet(a2).equals(e2)
&& s.longestCommonSubsequence(a3).equals(e3)
&& s.longestCommonSubsequenceSet(a3).equals(e3);
System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + s.longestCommonSubsequence(a1));
System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + s.longestCommonSubsequence(a2));
System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + s.longestCommonSubsequence(a3));
System.out.println(ok ? "ALL PASS" : "SOME FAIL");
}
}