LeetCode 1721-1740. 题解合集
文章目录
- [LeetCode 1721-1740. 题解合集](#LeetCode 1721-1740. 题解合集)
-
- [1721. Swapping Nodes in a Linked List(交换链表中的节点)](#1721. Swapping Nodes in a Linked List(交换链表中的节点))
- [1722. Minimize Hamming Distance After Swap Operations(执行交换操作后的最小汉明距离)](#1722. Minimize Hamming Distance After Swap Operations(执行交换操作后的最小汉明距离))
- [1723. Find Minimum Time to Finish All Jobs(完成所有工作的最短时间)](#1723. Find Minimum Time to Finish All Jobs(完成所有工作的最短时间))
- [1724. Checking Existence of Edge Length Limited Paths II(检查边长度限制的路径是否存在 II)](#1724. Checking Existence of Edge Length Limited Paths II(检查边长度限制的路径是否存在 II))
- [1725. Number Of Rectangles That Can Form The Largest Square(可以形成最大正方形的矩形数目)](#1725. Number Of Rectangles That Can Form The Largest Square(可以形成最大正方形的矩形数目))
- [1726. Tuple with Same Product(同积元组)](#1726. Tuple with Same Product(同积元组))
- [1727. Largest Submatrix With Rearrangements(重新排列后的最大子矩阵)](#1727. Largest Submatrix With Rearrangements(重新排列后的最大子矩阵))
- [1728. Cat and Mouse II(猫和老鼠 II)](#1728. Cat and Mouse II(猫和老鼠 II))
- [1729. Find Followers Count(求关注者的数量)](#1729. Find Followers Count(求关注者的数量))
- [1730. Shortest Path to Get Food(获取食物的最短路径)](#1730. Shortest Path to Get Food(获取食物的最短路径))
- [1731. The Number of Employees Which Report to Each Employee(每位经理的下属员工数量)](#1731. The Number of Employees Which Report to Each Employee(每位经理的下属员工数量))
- [1732. Find the Highest Altitude(找到最高海拔)](#1732. Find the Highest Altitude(找到最高海拔))
- [1733. Minimum Number of People to Teach(需要教语言的最少人数)](#1733. Minimum Number of People to Teach(需要教语言的最少人数))
- [1734. Decode XORed Permutation(解码异或后的排列)](#1734. Decode XORed Permutation(解码异或后的排列))
- [1735. Count Ways to Make Array With Product(生成乘积数组的方案数)](#1735. Count Ways to Make Array With Product(生成乘积数组的方案数))
- [1736. Latest Time by Replacing Hidden Digits(替换隐藏数字得到的最晚时间)](#1736. Latest Time by Replacing Hidden Digits(替换隐藏数字得到的最晚时间))
- [1737. Change Minimum Characters to Satisfy One of Three Conditions(满足三条件之一需改变的最少字符数)](#1737. Change Minimum Characters to Satisfy One of Three Conditions(满足三条件之一需改变的最少字符数))
- [1738. Find Kth Largest XOR Coordinate Value(找出第 K 大的异或坐标值)](#1738. Find Kth Largest XOR Coordinate Value(找出第 K 大的异或坐标值))
- [1739. Building Boxes(放置盒子)](#1739. Building Boxes(放置盒子))
- [1740. Find Distance in a Binary Tree(找到二叉树中的距离)](#1740. Find Distance in a Binary Tree(找到二叉树中的距离))
1721. Swapping Nodes in a Linked List(交换链表中的节点)
题目描述
给你链表的头节点 head 和一个整数 k。
交换链表正数第 k 个节点和倒数第 k 个节点的值后,返回链表的头节点(链表从 1 开始索引)。
示例
head = [1,2,3,4,5], k = 2,输出 [1,4,3,2,5]。
解题思路
解法一:双指针找两节点(主提交版本)
- 第一遍快指针先走 k 步到达正数第 k 个节点 nodeK;
- 之后慢指针从头出发,与快指针同步走,快指针走到末尾时慢指针正好在倒数第 k 个节点;
- 交换两个节点的 val。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二:先求长度再定位(对照) - 先遍历一次求链表长度 len,再分别找到正数第 k 个与倒数第 k 个(正数第 len-k+1 个)节点交换。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
代码实现
java
public class Solution {
private static class ListNode {
int val;
ListNode next;
ListNode(int v) { val = v; }
}
// 解法一:双指针找两节点(主提交版本)
public ListNode swapNodes(ListNode head, int k) {
ListNode fast = head;
for (int i = 1; i < k; i++) {
fast = fast.next;
}
ListNode nodeK = fast; // 正数第 k 个
ListNode slow = head;
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 此时 slow 为倒数第 k 个
int tmp = nodeK.val;
nodeK.val = slow.val;
slow.val = tmp;
return head;
}
// 解法二:先求长度再定位(对照)
public ListNode swapNodesLen(ListNode head, int k) {
int len = 0;
ListNode p = head;
while (p != null) {
len++;
p = p.next;
}
ListNode a = head, b = head;
for (int i = 1; i < k; i++) {
a = a.next;
}
for (int i = 1; i < len - k + 1; i++) {
b = b.next;
}
int tmp = a.val;
a.val = b.val;
b.val = tmp;
return head;
}
private static ListNode of(int... vals) {
ListNode dummy = new ListNode(0), t = dummy;
for (int v : vals) {
t.next = new ListNode(v);
t = t.next;
}
return dummy.next;
}
private static String show(ListNode h) {
StringBuilder sb = new StringBuilder();
while (h != null) {
sb.append(h.val).append(' ');
h = h.next;
}
return sb.toString().trim();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(show(sol.swapNodes(of(1, 2, 3, 4, 5), 2))); // 1 4 3 2 5
System.out.println(show(sol.swapNodes(of(1), 1))); // 1
System.out.println(show(sol.swapNodesLen(of(1, 2, 3, 4, 5), 2))); // 1 4 3 2 5
System.out.println(show(sol.swapNodesLen(of(1), 1))); // 1
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
1722. Minimize Hamming Distance After Swap Operations(执行交换操作后的最小汉明距离)
题目描述
给你两个整数数组 source 和 target,长度都是 n。还有一个数组 allowedSwaps,
其中每个 allowedSwaps[i] = `ai`, `bi` 表示你可以交换数组 source 中下标为 ai 和 bi 的两个元素
(下标从 0 开始)。注意,你可以按任意顺序多次交换一对特定下标指向的元素。
相同长度的两个数组 source 和 target 间的汉明距离是元素不同的下标数量。
在使用若干次 allowedSwaps 中的交换之后,请你返回 source 和 target 间的最小汉明距离。
示例 1
source = [1,2,3,4], target = [2,1,4,5], allowedSwaps = [[0,1],[2,3]],输出 1。
示例 2
source = [1,2,3,4], target = [1,3,2,4], allowedSwaps = [],输出 2。
解题思路
解法一:并查集 + 连通分量内计数(主提交版本)
- 可以通过 allowedSwaps 交换等价于:同一连通分量(并查集合并)内的下标可以随意重排。
- 对每个连通分量,统计 source 中的值频次,然后与同分量 target 中的值匹配消费:
差值部分即为该分量无法匹配的个数,累加所有分量即最小汉明距离。
复杂度:时间复杂度 O(n α(n)),空间复杂度 O(n)。
解法二:BFS 找连通块(对照) - 用邻接表 + BFS 求连通分量,其余逻辑相同。
复杂度:时间复杂度 O(n + m),空间复杂度 O(n + m)。
代码实现
java
public class Solution {
// 解法一:并查集 + 连通分量内计数(主提交版本)
private static class DSU {
int[] p;
DSU(int n) {
p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
}
int find(int x) {
while (p[x] != x) {
p[x] = p[p[x]];
x = p[x];
}
return x;
}
void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) p[ra] = rb;
}
}
public int minimumHammingDistance(int[] source, int[] target, int[][] allowedSwaps) {
int n = source.length;
DSU dsu = new DSU(n);
for (int[] e : allowedSwaps) {
dsu.union(e[0], e[1]);
}
java.util.Map<Integer, java.util.List<Integer>> groups = new java.util.HashMap<>();
for (int i = 0; i < n; i++) {
int r = dsu.find(i);
groups.computeIfAbsent(r, k -> new java.util.ArrayList<>()).add(i);
}
int ans = 0;
for (java.util.List<Integer> list : groups.values()) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int idx : list) {
cnt.put(source[idx], cnt.getOrDefault(source[idx], 0) + 1);
}
for (int idx : list) {
int v = target[idx];
int c = cnt.getOrDefault(v, 0);
if (c > 0) {
cnt.put(v, c - 1);
} else {
ans++; // 该分量中 target 的这个值无法与 source 匹配
}
}
}
return ans;
}
// 解法二:BFS 求连通块(对照)
public int minimumHammingDistanceBfs(int[] source, int[] target, int[][] allowedSwaps) {
int n = source.length;
java.util.List<Integer>[] g = new java.util.ArrayList[n];
for (int i = 0; i < n; i++) {
g[i] = new java.util.ArrayList<>();
}
for (int[] e : allowedSwaps) {
g[e[0]].add(e[1]);
g[e[1]].add(e[0]);
}
boolean[] vis = new boolean[n];
int ans = 0;
for (int i = 0; i < n; i++) {
if (vis[i]) {
continue;
}
java.util.List<Integer> comp = new java.util.ArrayList<>();
java.util.ArrayDeque<Integer> dq = new java.util.ArrayDeque<>();
dq.add(i);
vis[i] = true;
while (!dq.isEmpty()) {
int u = dq.poll();
comp.add(u);
for (int v : g[u]) {
if (!vis[v]) {
vis[v] = true;
dq.add(v);
}
}
}
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int idx : comp) {
cnt.put(source[idx], cnt.getOrDefault(source[idx], 0) + 1);
}
for (int idx : comp) {
int v = target[idx];
int c = cnt.getOrDefault(v, 0);
if (c > 0) {
cnt.put(v, c - 1);
} else {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumHammingDistance(new int[]{1, 2, 3, 4}, new int[]{2, 1, 4, 5},
new int[][]{{0, 1}, {2, 3}})); // 1
System.out.println(sol.minimumHammingDistance(new int[]{1, 2, 3, 4}, new int[]{1, 3, 2, 4},
new int[][]{})); // 2
System.out.println(sol.minimumHammingDistanceBfs(new int[]{1, 2, 3, 4}, new int[]{2, 1, 4, 5},
new int[][]{{0, 1}, {2, 3}})); // 1
System.out.println(sol.minimumHammingDistanceBfs(new int[]{1, 2, 3, 4}, new int[]{1, 3, 2, 4},
new int[][]{})); // 2
}
}
复杂度分析
复杂度:时间复杂度 O(n α(n)),空间复杂度 O(n)。
复杂度:时间复杂度 O(n + m),空间复杂度 O(n + m)。
1723. Find Minimum Time to Finish All Jobs(完成所有工作的最短时间)
题目描述
给你一个整数数组 jobs,其中 jobs[i] 是完成第 i 项工作要花费的时间。
请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。
工人的工作时间是完成分配给他们的所有工作花费时间的总和。
请你设计一套最佳的工作分配方案,使工人的最大工作时间得以最小化,返回尽可能小的最大工作时间。
示例 1
jobs = [3,2,3], k = 3,输出 3。
示例 2
jobs = [1,2,4,7,8], k = 2,输出 11。
解题思路
解法一:回溯 + 剪枝(主提交版本)
- 降序排序 jobs(先分配大任务更易剪枝)。
- 回溯尝试把每个任务分配给某个工人,用"当前最大值"剪枝:若某方案最大值已 >= 已知最优则放弃。
- 典型优化:若 workeri == workerj(负载相同),跳过重复分配。
复杂度:时间复杂度 O(k^n) 最坏(剪枝后实际远小于),空间复杂度 O(k)。
解法二:状态压缩 DP 二分答案(对照) - 二分答案 limit,用 dpmask 表示分配 mask 内工作所需的"最少工人越界" -> 直接判定
(mask 中工作时长总和 / limit) 是否可行:dpmask = 是否能把 mask 分成若干组,每组和 <= limit。 - 采用子集枚举 DP:canmask 表示 mask 是否能被合法分成若干 <= limit 的组。
由于 k 较小,也可用"最小分组数" dpmask 判定 dpmask <= k。
复杂度:时间复杂度 O(3^n)(子集枚举),空间复杂度 O(2^n)。
代码实现
java
public class Solution {
// 解法一:回溯 + 剪枝(主提交版本)
public int minimumTimeRequired(int[] jobs, int k) {
int n = jobs.length;
// 从大到小排序
int[] sorted = jobs.clone();
java.util.Arrays.sort(sorted);
for (int i = 0; i < n / 2; i++) {
int t = sorted[i];
sorted[i] = sorted[n - 1 - i];
sorted[n - 1 - i] = t;
}
int[] load = new int[k];
int[] best = {Integer.MAX_VALUE};
backtrack(sorted, 0, k, load, 0, best);
return best[0];
}
private void backtrack(int[] jobs, int idx, int k, int[] load, int curMax, int[] best) {
if (curMax >= best[0]) {
return; // 剪枝:当前最大已不小于已知最优
}
if (idx == jobs.length) {
best[0] = curMax;
return;
}
int v = jobs[idx];
java.util.Set<Integer> seen = new java.util.HashSet<>();
for (int i = 0; i < k; i++) {
if (seen.contains(load[i])) {
continue; // 相同负载的工人等价,避免重复
}
seen.add(load[i]);
load[i] += v;
backtrack(jobs, idx + 1, k, load, Math.max(curMax, load[i]), best);
load[i] -= v;
}
}
// 解法二:二分答案 + 状态压缩 DP(对照)
public int minimumTimeRequiredDp(int[] jobs, int k) {
int n = jobs.length;
int full = 1 << n;
// sum[mask]
int[] sum = new int[full];
for (int mask = 1; mask < full; mask++) {
int low = Integer.numberOfTrailingZeros(mask);
sum[mask] = sum[mask ^ (1 << low)] + jobs[low];
}
int lo = 0, hi = sum[full - 1];
for (int s : jobs) {
lo = Math.max(lo, s);
}
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (can(jobs, k, mid, sum, full)) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
private boolean can(int[] jobs, int k, int limit, int[] sum, int full) {
// dp[mask] = mask 所需最少组数,转移时拆出一个可行子集
int[] dp = new int[full];
java.util.Arrays.fill(dp, k + 1);
dp[0] = 0;
for (int mask = 1; mask < full; mask++) {
// 枚举 mask 的子集 sub(不含 mask 本身),且 sum[sub] <= limit
for (int sub = mask; sub > 0; sub = (sub - 1) & mask) {
if (sum[sub] <= limit && dp[mask ^ sub] + 1 < dp[mask]) {
dp[mask] = dp[mask ^ sub] + 1;
}
}
}
return dp[full - 1] <= k;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumTimeRequired(new int[]{3, 2, 3}, 3)); // 3
System.out.println(sol.minimumTimeRequired(new int[]{1, 2, 4, 7, 8}, 2)); // 11
System.out.println(sol.minimumTimeRequiredDp(new int[]{3, 2, 3}, 3)); // 3
System.out.println(sol.minimumTimeRequiredDp(new int[]{1, 2, 4, 7, 8}, 2)); // 11
}
}
复杂度分析
复杂度:时间复杂度 O(k^n) 最坏(剪枝后实际远小于),空间复杂度 O(k)。
复杂度:时间复杂度 O(3^n)(子集枚举),空间复杂度 O(2^n)。
1724. Checking Existence of Edge Length Limited Paths II(检查边长度限制的路径是否存在 II)
题目描述
一张有 n 个节点的无向图以边的列表 edgeList 的形式定义,其中 edgeList[i] = `ui`, `vi`, `disi`
表示一条连接 ui 和 vi,距离为 disi 的边。同一对节点间可能有多条边,且图可能不是连通的。
实现 DistanceLimitedPathsExist 类:
- DistanceLimitedPathsExist(
intn,int[][]edgeList) 以给定的无向图初始化对象。 booleanquery(intp,intq,intlimit) 当存在一条从p到q的路径,且路径中每条边的距离都
严格小于limit时,返回true,否则返回false。
示例
n=6, edges=[[0,2,4],[0,3,2],[1,2,3],[2,3,1],[4,5,5]];
query(2,3,2)=true, query(1,3,3)=false, query(2,0,3)=true, query(0,5,6)=false。
解题思路
解法一:边权递增并查集(主提交版本)
- 与"路径上最大边权尽量小"等价:p 与 q 能在 limit 内连通 ⇔ 把所有边权 < limit 的边加入并查集后
p 与 q 连通。 - 将边按权排序,维护并查集按时间戳逐步加边:query 时把所有权 < limit 的边合并,再判断连通性。
- 为支持多次 query 不重复扫描,预处理每个 limit 的"并查集版本":借助持久化思路不可行时,
采用拆分为"每当 limit 变大时把新边并入",查询间只加边不删边。
复杂度:预处理排序 O(m log m),每次 query 增量加边均摊 O((m + q) α(n)),空间 O(n)。
解法二:离线排序查询(对照) - 把所有 query 按 limit 升序离线排序,用同一个并查集从小到大加边回答,结果写回原序。
复杂度:时间复杂度 O((m+q) log (m+q) + (m+q) α(n)),空间 O(n + q)。
代码实现
java
public class Solution {
private int[] p;
private int find(int x) {
while (p[x] != x) {
p[x] = p[p[x]];
x = p[x];
}
return x;
}
private void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) p[ra] = rb;
}
private boolean connected(int a, int b) {
return find(a) == find(b);
}
// 解法一:边权递增并查集(主提交版本:逐步加边缓存版本)
// 为简洁可读,本实现采用"排序边 + 二分每个查询对应加边进度"(find & union 单调性)
private java.util.List<int[]> sortedEdges;
private java.util.Map<Integer, Integer> progressCache; // 已加到的边下标(闭区间)
public Solution(int n, int[][] edgeList) {
p = new int[n];
java.util.Arrays.sort(edgeList, (a, b) -> a[2] - b[2]);
sortedEdges = new java.util.ArrayList<>();
for (int[] e : edgeList) {
sortedEdges.add(e);
}
progressCache = new java.util.HashMap<>();
}
public boolean query(int p1, int q1, int limit) {
// 重置并查集
for (int i = 0; i < p.length; i++) {
p[i] = i;
}
int global = -1;
for (int i = 0; i < sortedEdges.size(); i++) {
if (sortedEdges.get(i)[2] < limit) {
union(sortedEdges.get(i)[0], sortedEdges.get(i)[1]);
global = i;
}
}
progressCache.put(limit, global);
return connected(p1, q1);
}
// 解法二:离线排序查询(对照)------静态方法,直接在解法内模拟
public static boolean[] offlineQuery(int n, int[][] edgeList, int[][] queries) {
int q = queries.length;
// queries[i] = [p, qnode, limit],构造 (limit, p, qnode, idx)
java.util.List<int[]> qs = new java.util.ArrayList<>();
for (int i = 0; i < q; i++) {
qs.add(new int[]{queries[i][2], queries[i][0], queries[i][1], i});
}
qs.sort((a, b) -> a[0] - b[0]);
java.util.Arrays.sort(edgeList, (a, b) -> a[2] - b[2]);
int[] par = new int[n];
for (int i = 0; i < n; i++) par[i] = i;
class D {
int find(int x) {
while (par[x] != x) {
par[x] = par[par[x]];
x = par[x];
}
return x;
}
void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) par[ra] = rb;
}
boolean conn(int a, int b) {
return find(a) == find(b);
}
}
D d = new D();
boolean[] ans = new boolean[q];
int e = 0;
for (int[] qu : qs) {
int limit = qu[0];
while (e < edgeList.length && edgeList[e][2] < limit) {
d.union(edgeList[e][0], edgeList[e][1]);
e++;
}
ans[qu[3]] = d.conn(qu[1], qu[2]);
}
return ans;
}
public static void main(String[] args) {
int[][] edges = {{0, 2, 4}, {0, 3, 2}, {1, 2, 3}, {2, 3, 1}, {4, 5, 5}};
Solution sol = new Solution(6, edges);
System.out.println(sol.query(2, 3, 2)); // true
System.out.println(sol.query(1, 3, 3)); // false
System.out.println(sol.query(2, 0, 3)); // true
System.out.println(sol.query(0, 5, 6)); // false
boolean[] off = offlineQuery(6, new int[][]{{0, 2, 4}, {0, 3, 2}, {1, 2, 3}, {2, 3, 1}, {4, 5, 5}},
new int[][]{{2, 3, 2}, {1, 3, 3}, {2, 0, 3}, {0, 5, 6}});
System.out.println(java.util.Arrays.toString(off)); // [true, false, true, false]
}
}
复杂度分析
复杂度:预处理排序 O(m log m),每次 query 增量加边均摊 O((m + q) α(n)),空间 O(n)。
复杂度:时间复杂度 O((m+q) log (m+q) + (m+q) α(n)),空间 O(n + q)。
1725. Number Of Rectangles That Can Form The Largest Square(可以形成最大正方形的矩形数目)
题目描述
给你一个数组 rectangles,其中 rectangles[i] = `li`, `wi` 表示第 i 个矩形的长度为 li、宽度为 wi。
如果存在 k 同时满足 k <= li 和 k <= wi,就可以将第 i 个矩形切成边长为 k 的正方形。
设 maxLen 为可以从矩形数组 rectangles 切分得到的最大正方形的边长。
请你统计有多少个矩形能够切出边长为 maxLen 的正方形,并返回矩形数目。
示例 1
rectangles = [[5,8],[3,9],[5,12],[16,5]],输出 3。
示例 2
rectangles = [[2,3],[3,7],[4,3],[3,7]],输出 3。
解题思路
解法一:求最大边长并计数(主提交版本)
- 每个矩形最大正方形边长 = min(l, w);第一遍求所有这些最小值的最大值 maxLen;
第二遍统计多少个矩形的 min(l, w) == maxLen。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二:一次遍历维护(对照) - 单次遍历同时维护 maxLen 与计数:遇到更大则计数重置为 1,相等则计数 +1。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:求最大边长并计数(主提交版本)
public int countGoodRectangles(int[][] rectangles) {
int maxLen = 0;
for (int[] r : rectangles) {
maxLen = Math.max(maxLen, Math.min(r[0], r[1]));
}
int cnt = 0;
for (int[] r : rectangles) {
if (Math.min(r[0], r[1]) == maxLen) {
cnt++;
}
}
return cnt;
}
// 解法二:一次遍历维护(对照)
public int countGoodRectanglesOnePass(int[][] rectangles) {
int maxLen = 0, cnt = 0;
for (int[] r : rectangles) {
int side = Math.min(r[0], r[1]);
if (side > maxLen) {
maxLen = side;
cnt = 1;
} else if (side == maxLen) {
cnt++;
}
}
return cnt;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countGoodRectangles(new int[][]{{5, 8}, {3, 9}, {5, 12}, {16, 5}})); // 3
System.out.println(sol.countGoodRectangles(new int[][]{{2, 3}, {3, 7}, {4, 3}, {3, 7}})); // 3
System.out.println(sol.countGoodRectanglesOnePass(new int[][]{{5, 8}, {3, 9}, {5, 12}, {16, 5}})); // 3
System.out.println(sol.countGoodRectanglesOnePass(new int[][]{{2, 3}, {3, 7}, {4, 3}, {3, 7}})); // 3
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
1726. Tuple with Same Product(同积元组)
题目描述
给你一个由不同正整数组成的数组 nums,请你返回满足 a * b = c * d 的元组 (a, b, c, d) 的数量。
其中 a、b、c 和 d 都是 nums 中的元素,且 a、b、c、d 互不相同。
(相当于选择两个乘积相等的数对,每个数对可形成 8 个有序元组。)
示例 1
nums = [2,3,4,6],输出 8。
示例 2
nums = [1,2,4,5,10],输出 16。
解题思路
解法一:哈希表统计乘积出现次数(主提交版本)
- 两两枚举所有数对 (i<j),统计每种乘积 p 的出现次数 cntp。
- 乘积为 p 的不同数对有 C(cntp, 2) 组,每组数对可组成 (p 对一 q 对二) 4 种有序(a,b)内序
× 2 种数对排列 = 8 个元组,因此贡献 cntp*(cntp-1)/2 * 8。
复杂度:时间复杂度 O(n^2),空间复杂度 O(n^2)。
解法二:边枚举边累加(对照) - 遍历过程中,对当前数对 (numsi, numsj) 的乘积 p,直接累加前面出现过的相同乘积的
数对个数 * 8,再更新 cntp。
复杂度:时间复杂度 O(n^2),空间复杂度 O(n^2)。
代码实现
java
public class Solution {
// 解法一:哈希表统计乘积出现次数(主提交版本)
public int tupleSameProduct(int[] nums) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int prod = nums[i] * nums[j];
cnt.put(prod, cnt.getOrDefault(prod, 0) + 1);
}
}
int ans = 0;
for (int c : cnt.values()) {
ans += c * (c - 1) / 2 * 8;
}
return ans;
}
// 解法二:边枚举边累加(对照)
public int tupleSameProductOnline(int[] nums) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int prod = nums[i] * nums[j];
ans += cnt.getOrDefault(prod, 0) * 8;
cnt.put(prod, cnt.getOrDefault(prod, 0) + 1);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.tupleSameProduct(new int[]{2, 3, 4, 6})); // 8
System.out.println(sol.tupleSameProduct(new int[]{1, 2, 4, 5, 10})); // 16
System.out.println(sol.tupleSameProductOnline(new int[]{2, 3, 4, 6})); // 8
System.out.println(sol.tupleSameProductOnline(new int[]{1, 2, 4, 5, 10})); // 16
}
}
复杂度分析
复杂度:时间复杂度 O(n^2),空间复杂度 O(n^2)。
1727. Largest Submatrix With Rearrangements(重新排列后的最大子矩阵)
题目描述
给你一个二进制矩阵 matrix,它的大小为 m x n,你可以将 matrix 中的列按任意顺序重新排列。
请你返回最优方案下将 matrix 重新排列后,全是 1 的最大子矩阵面积。
示例 1
matrix = [[0,0,1],[1,1,1],[1,0,1]],输出 4。
示例 2
matrix = [[1,0,1,0,1]],输出 3。
示例 3
matrix = [[1,1,0],[1,0,1]],输出 2。
解题思路
解法一:统计连续 1 高度 + 排序(主提交版本)
- 先对每列做"连续 1 高度":upij 表示从 (i,j) 向上连续 1 的个数。
- 对每一行 i,取出 upi\* 列高度数组并按降序排序:
若将列重排,选 k 列组成子矩阵,其高度下界为第 k 大的高度 h_k,面积 = h_k * k。
取 max(h_k * k) 即为该行作为底边时的最大全 1 子矩阵面积。 - 对所有行取最大值。
复杂度:时间复杂度 O(m*n log n),空间复杂度 O(n)。
解法二:直接对每行高度排序(对照) - 思路同解法一,但记录整个 up 矩阵,代码结构更直观(便于与解法一交叉验证)。
复杂度:时间复杂度 O(mn log n),空间复杂度 O(mn)。
代码实现
java
public class Solution {
// 解法一:统计连续 1 高度 + 排序(主提交版本)
public int largestSubmatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int ans = 0;
int[] height = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 1) {
height[j]++;
} else {
height[j] = 0;
}
}
int[] h = height.clone();
java.util.Arrays.sort(h); // 升序
for (int j = 0; j < n; j++) {
// 取 h[j] 为高度,有 n - j 列高度 >= h[j]
ans = Math.max(ans, h[j] * (n - j));
}
}
return ans;
}
// 解法二:up 矩阵 + 排序(对照)
public int largestSubmatrixUp(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
int[][] up = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 1) {
up[i][j] = (i == 0 ? 0 : up[i - 1][j]) + 1;
}
}
}
int ans = 0;
for (int i = 0; i < m; i++) {
int[] h = up[i].clone();
java.util.Arrays.sort(h);
for (int j = 0; j < n; j++) {
ans = Math.max(ans, h[j] * (n - j));
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.largestSubmatrix(new int[][]{{0, 0, 1}, {1, 1, 1}, {1, 0, 1}})); // 4
System.out.println(sol.largestSubmatrix(new int[][]{{1, 0, 1, 0, 1}})); // 3
System.out.println(sol.largestSubmatrix(new int[][]{{1, 1, 0}, {1, 0, 1}})); // 2
System.out.println(sol.largestSubmatrixUp(new int[][]{{0, 0, 1}, {1, 1, 1}, {1, 0, 1}})); // 4
System.out.println(sol.largestSubmatrixUp(new int[][]{{1, 0, 1, 0, 1}})); // 3
System.out.println(sol.largestSubmatrixUp(new int[][]{{1, 1, 0}, {1, 0, 1}})); // 2
}
}
复杂度分析
复杂度:时间复杂度 O(mn log n),空间复杂度 O(n)。
复杂度:时间复杂度 O(mn log n),空间复杂度 O(m*n)。
1728. Cat and Mouse II(猫和老鼠 II)
题目描述
一只猫和一只老鼠在玩一个叫做猫和老鼠的游戏。环境是一个 rows x cols 的方格 grid:
- '
C' 代表猫,'M' 代表老鼠,'F' 代表食物,'.' 是地板,'#' 是墙。 - 老鼠先移动,然后轮流移动。每次可以上下左右跳 1...
jump步(不能穿墙、不能出界),也可停留原地。 - 老鼠可以跳过猫的位置。
游戏结束:猫与老鼠同格则猫胜;猫先到食物则猫胜;老鼠先到食物则老鼠胜;
1000 步内老鼠没到食物则猫胜(实际是超过行数列数 状态数即认为猫胜)。
双方最优,若老鼠获胜返回true,否则false。
示例 1
grid = ["####F","#C...","M...."], catJump=1, mouseJump=2,输出 true。
示例 2
grid = ["M.C...F"], catJump=1, mouseJump=4,输出 true。
示例 3
grid = ["M.C...F"], catJump=1, mouseJump=3,输出 false。
解题思路
解法一:记忆化搜索博弈(主提交版本)
- 状态 (turn, mc, cc, step):turn=0 老鼠走、1 猫走;mc/cc 分别是老鼠/猫位置(一维化)。
- 深度优先搜索判定当前状态是否老鼠胜(turn=0 时存在一站胜、turn=1 时所有猫走法都使老鼠胜)。
- 为防止环导致无限递归,用 step 限制深度:超过 rowscols 2(状态空间上界)视为猫胜
(在 1000 步内到达食物的剪枝)。
复杂度:最坏状态数 O(rows*cols)^2 * step,空间 O(状态数),实际剪枝后可行。
解法二:限制步数的博弈 DFS(对照) - 与解法一逻辑完全一致,但把"超步数判负"明确为超过 catJump+mouseJump 步内的跳跃空间上界,
通过外部传入 maxSteps 参数,便于对比不同上界。
复杂度:同解法一。
代码实现
java
public class Solution {
private int rows, cols;
private char[][] g;
private int food;
private int catJ, mouseJ;
private String[] gridArr;
private int maxSteps;
// 状态:step * 2 * total * total 太大,用 HashMap 记忆
private java.util.Map<Long, Boolean> memo;
private int pos(int r, int c) {
return r * cols + c;
}
private int[] rc(int p) {
return new int[]{p / cols, p % cols};
}
private java.util.List<Integer> moves(int p, int dist, boolean cat) {
java.util.List<Integer> res = new java.util.ArrayList<>();
int r = p / cols, c = p % cols;
// 上下左右 1..dist 步
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] d : dirs) {
int nr = r, nc = c;
for (int s = 0; s < dist; s++) {
nr += d[0];
nc += d[1];
if (nr < 0 || nr >= rows || nc < 0 || nc >= cols || g[nr][nc] == '#') {
break;
}
res.add(pos(nr, nc));
}
}
res.add(p); // 停留原地
return res;
}
private boolean win(int step, int who, int m, int c) {
if (m == c) {
return false; // 猫在同格,老鼠输
}
if (m == food) {
return true; // 老鼠到食物(且猫不在同格)
}
if (c == food) {
return false; // 猫先到食物
}
if (step >= maxSteps) {
return false; // 步数耗尽,猫胜
}
Long key = ((long) step) << 32 | (long) who << 31 | (long) m << 15 | c;
Boolean cached = memo.get(key);
if (cached != null) {
return cached;
}
boolean res;
if (who == 0) { // 老鼠走:任一可行走法可赢即可
res = false;
for (int nm : moves(m, mouseJ, false)) {
if (win(step + 1, 1, nm, c)) {
res = true;
break;
}
}
} else { // 猫走:猫的所有走法中必须全部老鼠仍赢
res = true;
for (int nc : moves(c, catJ, true)) {
if (!win(step + 1, 0, m, nc)) {
res = false;
break;
}
}
}
memo.put(key, res);
return res;
}
// 解法一:记忆化搜索博弈(主提交版本)
public boolean canMouseWin(String[] grid, int catJump, int mouseJump) {
this.gridArr = grid;
rows = grid.length;
cols = grid[0].length();
this.catJ = catJump;
this.mouseJ = mouseJump;
g = new char[rows][cols];
int cat = -1, mouse = -1;
food = -1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
char ch = grid[i].charAt(j);
g[i][j] = ch;
if (ch == 'C') {
cat = pos(i, j);
} else if (ch == 'M') {
mouse = pos(i, j);
} else if (ch == 'F') {
food = pos(i, j);
}
}
}
// 状态空间上界:所有 (m, c) 组合的两倍,防止无限递归
maxSteps = rows * cols * cols * 2;
memo = new java.util.HashMap<>();
return win(0, 0, mouse, cat);
}
// 解法二:限制步数的博弈 DFS(对照,显式步数上限)
public boolean canMouseWinSteps(String[] grid, int catJump, int mouseJump) {
this.gridArr = grid;
rows = grid.length;
cols = grid[0].length();
this.catJ = catJump;
this.mouseJ = mouseJump;
g = new char[rows][cols];
int cat = -1, mouse = -1;
food = -1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
char ch = grid[i].charAt(j);
g[i][j] = ch;
if (ch == 'C') {
cat = pos(i, j);
} else if (ch == 'M') {
mouse = pos(i, j);
} else if (ch == 'F') {
food = pos(i, j);
}
}
}
maxSteps = 1000; // 题意:老鼠 1000 次操作内获胜
memo = new java.util.HashMap<>();
return win(0, 0, mouse, cat);
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.canMouseWin(new String[]{"####F", "#C...", "M...."}, 1, 2)); // true
System.out.println(sol.canMouseWin(new String[]{"M.C...F"}, 1, 4)); // true
System.out.println(sol.canMouseWin(new String[]{"M.C...F"}, 1, 3)); // false
System.out.println(sol.canMouseWinSteps(new String[]{"####F", "#C...", "M...."}, 1, 2)); // true
System.out.println(sol.canMouseWinSteps(new String[]{"M.C...F"}, 1, 4)); // true
System.out.println(sol.canMouseWinSteps(new String[]{"M.C...F"}, 1, 3)); // false
}
}
复杂度分析
复杂度:最坏状态数 O(rows*cols)^2 * step,空间 O(状态数),实际剪枝后可行。
1729. Find Followers Count(求关注者的数量)
题目类型:数据库(SQL)
题目描述
表:Followers
+-------------+------+
| Column Name | Type |
+-------------+------+
| user_id | int |
| follower_id | int |
+-------------+------+
(user_id, follower_id) 是这个表的主键。该表包含一个关注关系中关注者和用户的编号,其中关注者关注用户。
编写解决方案,对于每一个用户,返回该用户的关注者数量。按 user_id 的顺序返回结果表。
示例 1:
输入:
Followers 表:
+---------+-------------+
| user_id | follower_id |
+---------+-------------+
| 0 | 1 |
| 1 | 0 |
| 2 | 0 |
| 2 | 1 |
+---------+-------------+
输出:
+---------+----------------+
| user_id | followers_count|
+---------+----------------+
| 0 | 1 |
| 1 | 1 |
| 2 | 2 |
+---------+----------------+
解题思路
直接按 user_id 分组,数每组行数(即关注者人数),输出前按 user_id 升序排序。
代码实现
sql
SELECT user_id, COUNT(*) AS followers_count
FROM Followers
GROUP BY user_id
ORDER BY user_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1730. Shortest Path to Get Food(获取食物的最短路径)
题目描述
你现在很饿,想要尽快找东西吃。你需要找到最短的路径到达一个食物所在的格子。
给定一个 m x n 的字符矩阵 grid,包含下列不同类型的格子:
- '*' 是你的位置(有且只有一个)
- '#' 是食物(有且只有一个)
- '
O' 是空地,你可以通过 - '
X' 是障碍物,无法通过
返回你到食物所在格子的最短路径的长度(边的数量);如果你无法到达食物,返回 -1。
示例 1
grid = [["X","X","X","X","X","X"],["X","*","O","O","O","X"],["X","O","O","#","O","X"]],
输出 3。
示例 2
grid = [["X","X","X","X","X"],["X","*","X","O","X"],["X","O","X","#","X"],
["X","X","X","X","X"]],输出 -1。
解题思路
解法一:BFS 最短路(主提交版本)
- 从 '*' 出发做四方向 BFS,'X' 不能走,'O' 与 '#' 可走,第一次遇到 '#' 时的层数即最短路径长度。
复杂度:时间复杂度 O(mn),空间复杂度 O(m n)。
解法二:DFS 带剪枝(对照) - 用 DFS + visited 剪枝搜索,维护当前路径长度,到 '#' 时更新答案;可能超时但作为小数据验证。
复杂度:时间复杂度 O(4^(mn)) 最坏,空间复杂度 O(mn)。
代码实现
java
public class Solution {
// 解法一:BFS 最短路(主提交版本)
public int getFood(char[][] grid) {
int m = grid.length, n = grid[0].length;
int sr = -1, sc = -1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '*') {
sr = i;
sc = j;
}
}
}
java.util.ArrayDeque<int[]> dq = new java.util.ArrayDeque<>();
boolean[][] vis = new boolean[m][n];
dq.add(new int[]{sr, sc});
vis[sr][sc] = true;
int steps = 0;
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!dq.isEmpty()) {
int sz = dq.size();
for (int k = 0; k < sz; k++) {
int[] cur = dq.poll();
for (int[] d : dirs) {
int nr = cur[0] + d[0], nc = cur[1] + d[1];
if (nr < 0 || nr >= m || nc < 0 || nc >= n || vis[nr][nc]
|| grid[nr][nc] == 'X') {
continue;
}
if (grid[nr][nc] == '#') {
return steps + 1;
}
vis[nr][nc] = true;
dq.add(new int[]{nr, nc});
}
}
steps++;
}
return -1;
}
// 解法二:DFS 带剪枝(对照)
private int best;
private char[][] grid;
private int m, n;
public int getFoodDfs(char[][] grid) {
this.grid = grid;
m = grid.length;
n = grid[0].length;
best = Integer.MAX_VALUE;
int sr = -1, sc = -1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '*') {
sr = i;
sc = j;
}
}
}
boolean[][] vis = new boolean[m][n];
vis[sr][sc] = true;
dfs(sr, sc, 0, vis);
return best == Integer.MAX_VALUE ? -1 : best;
}
private void dfs(int r, int c, int dist, boolean[][] vis) {
if (dist >= best) {
return;
}
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr < 0 || nr >= m || nc < 0 || nc >= n || vis[nr][nc] || grid[nr][nc] == 'X') {
continue;
}
if (grid[nr][nc] == '#') {
best = Math.min(best, dist + 1);
continue;
}
vis[nr][nc] = true;
dfs(nr, nc, dist + 1, vis);
vis[nr][nc] = false;
}
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.getFood(new char[][]{
{'X','X','X','X','X','X'}, {'X','*','O','O','O','X'},
{'X','O','O','#','O','X'}})); // 3
System.out.println(sol.getFood(new char[][]{
{'X','X','X','X','X'}, {'X','*','X','O','X'},
{'X','O','X','#','X'}, {'X','X','X','X','X'}})); // -1
System.out.println(sol.getFoodDfs(new char[][]{
{'X','X','X','X','X','X'}, {'X','*','O','O','O','X'},
{'X','O','O','#','O','X'}})); // 3
System.out.println(sol.getFoodDfs(new char[][]{
{'X','X','X','X','X'}, {'X','*','X','O','X'},
{'X','O','X','#','X'}, {'X','X','X','X','X'}})); // -1
}
}
复杂度分析
复杂度:时间复杂度 O(mn),空间复杂度 O(m n)。
复杂度:时间复杂度 O(4^(mn)) 最坏,空间复杂度 O(mn)。
1731. The Number of Employees Which Report to Each Employee(每位经理的下属员工数量)
题目类型:数据库(SQL)
题目描述
表:Employees
+-------------+----------+
| Column Name | Type |
+-------------+----------+
| employee_id | int |
| name | varchar |
| reports_to | int |
| age | int |
+-------------+----------+
employee_id 是这个表中具有不同值的列。该表包含员工以及需要听取他们汇报的上级经理的 ID 的信息。有些员工不需要向任何人汇报(reports_to 为空)。
对于此问题,我们将至少有一个其他员工需要向他汇报的员工,视为一个经理。编写一个解决方案来返回需要听取汇报的所有经理的 ID、名称、直接向该经理汇报的员工人数,以及这些员工的平均年龄(四舍五入到最接近的整数)。返回的结果集需要按照 employee_id 进行排序。
示例 1:
输入:
Employees 表:
+-------------+---------+------------+-----+
| employee_id | name | reports_to | age |
+-------------+---------+------------+-----+
| 9 | Hercy | null | 43 |
| 6 | Alice | 9 | 41 |
| 4 | Bob | 9 | 36 |
| 2 | Winston | null | 37 |
+-------------+---------+------------+-----+
输出:
+-------------+-------+---------------+-------------+
| employee_id | name | reports_count | average_age |
+-------------+-------+---------------+-------------+
| 9 | Hercy | 2 | 39 |
+-------------+-------+---------------+-------------+
解释:Hercy 有两个直接下属 Alice 和 Bob,平均年龄 (41+36)/2 = 38.5,四舍五入为 39。
解题思路
经理与下属都在同一张表里,用表自连接:Employees m 作为经理、Employees e 作为下属,
连接条件 m.employee_id = e.reports_to;按经理分组统计 COUNT(e.employee_id) 与 ROUND(AVG(e.age)),
最后按 employee_id 排序。
代码实现
sql
SELECT m.employee_id, m.name,
COUNT(e.employee_id) AS reports_count,
ROUND(AVG(e.age)) AS average_age
FROM Employees m
JOIN Employees e ON m.employee_id = e.reports_to
GROUP BY m.employee_id, m.name
ORDER BY m.employee_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1732. Find the Highest Altitude(找到最高海拔)
题目描述
有一个自行车手打算进行一场公路骑行,这条路线总共由 n + 1 个不同海拔的点组成。
自行车手从海拔为 0 的点 0 开始骑行。
给你一个长度为 n 的整数数组 gain,其中 gain[i] 是点 i 和点 i + 1 的净海拔高度差
(0 <= i < n)。请你返回最高点的海拔。
示例 1
gain = [-5,1,5,0,-7],海拔依次为 [0,-5,-4,1,1,-6],最高海拔为 1。
示例 2
gain = [-4,-3,-2,-1,4,3,2],海拔依次为 [0,-4,-7,-9,-10,-6,-3,-1],最高海拔为 0。
提示:
n== gain.length,1 <=n<= 100,-100 <= gaini <= 100。
解题思路
解法一:前缀和遍历(主提交版本)
- cur 记录当前海拔(起点 0),每累加 gaini 后与 ans 取最大值即可。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二:前缀和高低峰区间观察(对照) - 由于 n 很小,可先构造完整前缀和数组,再对数组整体求最大值。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:前缀和遍历(主提交版本)
public int largestAltitude(int[] gain) {
int cur = 0, ans = 0;
for (int g : gain) {
cur += g;
ans = Math.max(ans, cur);
}
return ans;
}
// 解法二:前缀和数组求最大(对照)
public int largestAltitudeArray(int[] gain) {
int n = gain.length;
int[] pre = new int[n + 1];
pre[0] = 0;
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i] + gain[i];
}
int ans = pre[0];
for (int x : pre) {
ans = Math.max(ans, x);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.largestAltitude(new int[]{-5, 1, 5, 0, -7})); // 1
System.out.println(sol.largestAltitude(new int[]{-4, -3, -2, -1, 4, 3, 2})); // 0
System.out.println(sol.largestAltitudeArray(new int[]{-5, 1, 5, 0, -7})); // 1
System.out.println(sol.largestAltitudeArray(new int[]{-4, -3, -2, -1, 4, 3, 2})); // 0
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
1733. Minimum Number of People to Teach(需要教语言的最少人数)
题目描述
在一个由 m 个用户组成的社交网络里,我们获取到一些用户之间的好友关系。
两个用户之间可以相互沟通的条件是他们都掌握同一门语言。
给你一个整数 n,数组 languages 和数组 friendships,它们的含义如下:
- 总共有
n种语言,编号从 1 到n。 languages[i]是第i位用户掌握的语言集合。friendships[i]= `ui`, `vi` 表示ui和vi为好友关系。
你可以选择一门语言并教会一些用户,使得所有好友之间都可以相互沟通。
请返回你最少需要教会多少名用户。
请注意,好友关系没有传递性,也就是说如果x和y是好友,且y和z是好友,
x和z不一定是好友。
示例 1
n = 2, languages = [[1],[2],[1,2]], friendships = [[1,2],[1,3],[2,3]]
输出 1(可以选择教用户 1 第二门语言,也可以选择教用户 2 第一门语言)。
示例 2
n = 3, languages = [[2],[1,3],[1,2],[3]], friendships = [[1,4],[1,2],[3,4],[2,3]]
输出 2(教用户 1 和用户 3 第三门语言)。
提示:
- 2 <=
n<= 500,1 <=m<= 500,languages[i]中包含的值互不相同。
解题思路
解法一:候选语言计数(主提交版本)
- 先过滤出所有还不能沟通的好友对,这些对中的用户必须至少有一方学新语言。
- 对每种候选语言 1...n,统计这些"必须沟通"用户中不掌握该语言的人数,
取最小值即为答案(同一用户只会被统计一次,用 seen 集合去重)。
复杂度:时间复杂度 O(n * (m + 总语言数)),空间复杂度 O(m * 语言数)。
解法二:按好友对逐一对齐(对照) - 记录每对好友"缺哪些语言",然后枚举教哪门课使最多"缺口"被补上。
复杂度:时间复杂度 O(n * #好友对),空间复杂度 O(#好友对)。
代码实现
java
public class Solution {
// 解法一:候选语言计数(主提交版本)
public int minimumTeachings(int n, int[][] languages, int[][] friendships) {
int m = languages.length;
boolean[][] has = new boolean[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int lang : languages[i]) {
has[i + 1][lang] = true;
}
}
// 需要沟通但还没共同语言的用户集合
java.util.Set<Integer> need = new java.util.HashSet<>();
for (int[] f : friendships) {
int u = f[0], v = f[1];
if (!canTalk(has, u, v, n)) {
need.add(u);
need.add(v);
}
}
int ans = m;
for (int lang = 1; lang <= n; lang++) {
int cnt = 0;
for (int uid : need) {
if (!has[uid][lang]) {
cnt++;
}
}
ans = Math.min(ans, cnt);
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
private boolean canTalk(boolean[][] has, int u, int v, int n) {
for (int lang = 1; lang <= n; lang++) {
if (has[u][lang] && has[v][lang]) {
return true;
}
}
return false;
}
// 解法二:按好友对逐一统计缺口(对照)
public int minimumTeachingsBrute(int n, int[][] languages, int[][] friendships) {
int m = languages.length;
boolean[][] has = new boolean[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int lang : languages[i]) {
has[i + 1][lang] = true;
}
}
// 需要沟通且没有共同语言的用户集合(去重)
java.util.Set<Integer> need2 = new java.util.HashSet<>();
for (int[] f : friendships) {
int u = f[0], v = f[1];
if (!canTalk(has, u, v, n)) {
need2.add(u);
need2.add(v);
}
}
int[] miss = new int[n + 1];
for (int uid : need2) {
for (int lang = 1; lang <= n; lang++) {
if (!has[uid][lang]) {
miss[lang]++;
}
}
}
int ans = Integer.MAX_VALUE;
for (int lang = 1; lang <= n; lang++) {
ans = Math.min(ans, miss[lang]);
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumTeachings(2,
new int[][]{{1}, {2}, {1, 2}},
new int[][]{{1, 2}, {1, 3}, {2, 3}})); // 1
System.out.println(sol.minimumTeachings(3,
new int[][]{{2}, {1, 3}, {1, 2}, {3}},
new int[][]{{1, 4}, {1, 2}, {3, 4}, {2, 3}})); // 2
System.out.println(sol.minimumTeachingsBrute(2,
new int[][]{{1}, {2}, {1, 2}},
new int[][]{{1, 2}, {1, 3}, {2, 3}})); // 1
System.out.println(sol.minimumTeachingsBrute(3,
new int[][]{{2}, {1, 3}, {1, 2}, {3}},
new int[][]{{1, 4}, {1, 2}, {3, 4}, {2, 3}})); // 2
}
}
复杂度分析
复杂度:时间复杂度 O(n * (m + 总语言数)),空间复杂度 O(m * 语言数)。
复杂度:时间复杂度 O(n * #好友对),空间复杂度 O(#好友对)。
1734. Decode XORed Permutation(解码异或后的排列)
题目描述
给你一个整数数组 perm,它是前 n 个正整数的排列,且 n 是个奇数。
它被加密成另一个长度为 n - 1 的整数数组 encoded,满足
encoded[i] = perm[i] XOR perm[i + 1]。比方说,如果 perm = 1,3,2,
那么 encoded = 2,1。
给你 encoded 数组,请你返回原始数组 perm。题目保证答案存在且唯一。
示例 1
encoded = [3,1],输出 [1,2,3]。
示例 2
encoded = [6,5,4,6],输出 [2,4,1,5,3]。
提示:
- 3 <=
n< 10^5,n是奇数,encoded.length==n- 1。
解题思路
解法一:异或前缀求首元素(主提交版本)
- 记全部 1...n 的异或为 total;encoded1encoded[3]...^encodedn-2
恰好等于 perm1perm[2]...^permn-1(因为 perm 长度为奇数时,
相隔一个的 encoded 下标会覆盖 perm 除首元素外的全部下标)。
因此 perm0 = total ^ (encoded1encoded[3]...),
再按 permi+1 = permi ^ encodedi 递推即可。
复杂度:时间复杂度 O(n),空间复杂度 O(n)(返回数组)。
解法二:暴力枚举恢复(对照) - 枚举 perm0 的可能值(1...n),按递推重建整个数组,校验是否为 1...n 的排列。
复杂度:时间复杂度 O(n^2),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:异或前缀求首元素(主提交版本)
public int[] decode(int[] encoded) {
int n = encoded.length + 1;
int total = 0;
for (int i = 1; i <= n; i++) {
total ^= i;
}
int first0 = 0;
for (int i = 1; i < encoded.length; i += 2) {
first0 ^= encoded[i];
}
int[] perm = new int[n];
perm[0] = total ^ first0;
for (int i = 1; i < n; i++) {
perm[i] = perm[i - 1] ^ encoded[i - 1];
}
return perm;
}
// 解法二:暴力枚举 perm[0](对照)
public int[] decodeBrute(int[] encoded) {
int n = encoded.length + 1;
for (int first = 1; first <= n; first++) {
int[] perm = new int[n];
perm[0] = first;
boolean[] seen = new boolean[n + 1];
seen[first] = true;
boolean ok = true;
for (int i = 1; i < n; i++) {
perm[i] = perm[i - 1] ^ encoded[i - 1];
if (perm[i] < 1 || perm[i] > n || seen[perm[i]]) {
ok = false;
break;
}
seen[perm[i]] = true;
}
if (ok) {
return perm;
}
}
return new int[0];
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(java.util.Arrays.toString(sol.decode(new int[]{3, 1}))); // [1, 2, 3]
System.out.println(java.util.Arrays.toString(sol.decode(new int[]{6, 5, 4, 6}))); // [2, 4, 1, 5, 3]
System.out.println(java.util.Arrays.toString(sol.decodeBrute(new int[]{3, 1}))); // [1, 2, 3]
System.out.println(java.util.Arrays.toString(sol.decodeBrute(new int[]{6, 5, 4, 6}))); // [2, 4, 1, 5, 3]
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(n)(返回数组)。
复杂度:时间复杂度 O(n^2),空间复杂度 O(n)。
1735. Count Ways to Make Array With Product(生成乘积数组的方案数)
题目描述
给你一个二维整数数组 queries,其中 queries[i] = `ni`, `ki`。第 i 个查询要求
构造长度为 ni、每个元素都是正整数的数组,且满足所有元素的乘积为 ki,
请你找出有多少种可行的方案。由于答案可能会很大,方案数需要对 10^9 + 7 取余。
返回一个整数数组 answer,其中 answer[i] 是第 i 个查询的结果。
示例 1
queries = [[2,6],[5,1],[73,660]],输出 [4,1,50734910]。
示例 2
queries = [[1,1],[2,2],[3,3],[4,4],[5,5]],输出 [1,2,3,10,5]。
解题思路
解法一:质因数分解 + 组合计数(主提交版本)
- 将 ki 分解质因数:ki = p1^e1 * p2^e2 * ... * pt^et。
- 长度为 ni 的数组,每个位置分配若干质因子,问题转化为:每个质因子的 e
个幂分配(可重复)到 ni 个盒子,方案数为 C(e + ni - 1, ni - 1)(隔板法)。 - 不同质因子之间互相独立,总方案数为各质因子方案数之积。
- 预处理组合数 C(i, j)(i 最大 e + ni - 1 <= 15 + 10^4),模 10^9+7。
复杂度:时间复杂度 O(maxN + 查询数 * sqrt(ki)),空间复杂度 O(maxN)。
解法二:因数枚举 + 逐位 DP(对照) - 对单个查询,先枚举 ki 的所有因数,再用 Map 递推 dpv:
已填 t 个位置、当前乘积为 v 的方案数;每个位置可以乘上任意因数。
复杂度:时间复杂度 O(n * f^2)(f 为因数个数),空间复杂度 O(f),仅适合小数据验证。
代码实现
java
public class Solution {
private static final long MOD = 1_000_000_007L;
private long[][] c;
// 解法一:质因数分解 + 隔板法(主提交版本)
public int[] waysToFillArray(int[][] queries) {
// 组合数第二维上限:单质因子最多出现 30 次(2^30 > 1e9),取 32 足够,
// 但为简单统一取 ni 最大 + 30。
int maxN = 0;
for (int[] q : queries) {
maxN = Math.max(maxN, q[0]);
}
int lim = maxN + 40;
c = new long[lim + 1][41];
for (int i = 0; i <= lim; i++) {
c[i][0] = 1;
for (int j = 1; j <= Math.min(i, 40); j++) {
c[i][j] = (c[i - 1][j - 1] + (j <= i - 1 ? c[i - 1][j] : 0)) % MOD;
}
}
int[] ans = new int[queries.length];
for (int i = 0; i < queries.length; i++) {
int n = queries[i][0], k = queries[i][1];
long res = 1;
for (int p = 2; (long) p * p <= k; p++) {
if (k % p == 0) {
int e = 0;
while (k % p == 0) {
k /= p;
e++;
}
// 把 e 个 p 分摊到 n 个位置:C(e + n - 1, n - 1)
res = res * comb(e + n - 1, e) % MOD;
}
}
if (k > 1) {
// 剩余一个大于 sqrt 的大质因子,指数为 1
res = res * comb(n, 1) % MOD;
}
ans[i] = (int) res;
}
return ans;
}
private long comb(int n, int k) {
if (k < 0 || k > n) {
return 0;
}
return c[n][k];
}
// 解法二:因数枚举 + 逐位 DP(对照,小数据验证用)
public int waysToFillArraySingle(int n, int k) {
java.util.List<Integer> factors = new java.util.ArrayList<>();
for (int d = 1; (long) d * d <= k; d++) {
if (k % d == 0) {
factors.add(d);
if (d * d != k) {
factors.add(k / d);
}
}
}
java.util.Collections.sort(factors);
java.util.Map<Integer, Long> dp = new java.util.HashMap<>();
dp.put(1, 1L);
for (int t = 0; t < n; t++) {
java.util.Map<Integer, Long> ndp = new java.util.HashMap<>();
for (java.util.Map.Entry<Integer, Long> en : dp.entrySet()) {
long cur = en.getValue();
int v = en.getKey();
for (int f : factors) {
long nv = (long) v * f;
if (nv > k || k % nv != 0) {
continue;
}
ndp.merge((int) nv, cur, (a, b) -> (a + b) % MOD);
}
}
dp = ndp;
}
return (int) (dp.getOrDefault(k, 0L) % MOD);
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(java.util.Arrays.toString(sol.waysToFillArray(
new int[][]{{2, 6}, {5, 1}, {73, 660}}))); // [4, 1, 50734910]
System.out.println(java.util.Arrays.toString(sol.waysToFillArray(
new int[][]{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}))); // [1, 2, 3, 10, 5]
// 解法二小数据对照
System.out.println(sol.waysToFillArraySingle(2, 6)); // 4
System.out.println(sol.waysToFillArraySingle(5, 1)); // 1
System.out.println(sol.waysToFillArraySingle(1, 1)); // 1
System.out.println(sol.waysToFillArraySingle(2, 2)); // 2
System.out.println(sol.waysToFillArraySingle(5, 5)); // 5
}
}
复杂度分析
复杂度:时间复杂度 O(maxN + 查询数 * sqrt(ki)),空间复杂度 O(maxN)。
复杂度:时间复杂度 O(n * f^2)(f 为因数个数),空间复杂度 O(f),仅适合小数据验证。
1736. Latest Time by Replacing Hidden Digits(替换隐藏数字得到的最晚时间)
题目描述
给你一个字符串 time,格式为 hh:mm(小时:分钟),其中某几位数字被隐藏(用 '?' 表示)。
有效的时间为 00:00 到 23:59 之间的所有时间,包括 00:00 和 23:59。
替换 time 中隐藏的数字,返回你可以得到的最晚有效时间。
示例 1
time = "2?:?0",输出 "23:50"。
示例 2
time = "0?:3?",输出 "09:39"。
示例 3
time = "1?:22",输出 "19:22"。
提示:
time的格式为hh:mm,题目数据保证你可以由输入的字符串生成有效的时间。
解题思路
解法一:按位置贪心(主提交版本)
- 时、分位逐个判断:十位为 '?' 时依据个位决定填 2/1;个位为 '?' 时依据十位决定。
- 分同理:十位最大 5,个位最大 9。
复杂度:时间复杂度 O(1),空间复杂度 O(1)。
解法二:枚举补全(对照) - 将 '?' 视为通配,枚举 2460 全部合法时间,取与模板匹配的最晚一个。
复杂度:时间复杂度 O(2460),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:按位置贪心(主提交版本)
public String maximumTime(String time) {
char[] t = time.toCharArray();
// 小时十位
if (t[0] == '?') {
t[0] = (t[1] == '?' || t[1] <= '3') ? '2' : '1';
}
// 小时个位
if (t[1] == '?') {
t[1] = (t[0] == '2') ? '3' : '9';
}
// 分钟十位
if (t[3] == '?') {
t[3] = '5';
}
// 分钟个位
if (t[4] == '?') {
t[4] = '9';
}
return new String(t);
}
// 解法二:枚举 24*60(对照)
public String maximumTimeEnum(String time) {
for (int h = 23; h >= 0; h--) {
for (int m = 59; m >= 0; m--) {
String cand = String.format("%02d:%02d", h, m);
if (matches(time, cand)) {
return cand;
}
}
}
return "";
}
private boolean matches(String mask, String cand) {
for (int i = 0; i < 5; i++) {
if (mask.charAt(i) != '?' && mask.charAt(i) != cand.charAt(i)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maximumTime("2?:?0")); // 23:50
System.out.println(sol.maximumTime("0?:3?")); // 09:39
System.out.println(sol.maximumTime("1?:22")); // 19:22
System.out.println(sol.maximumTimeEnum("2?:?0")); // 23:50
System.out.println(sol.maximumTimeEnum("0?:3?")); // 09:39
System.out.println(sol.maximumTimeEnum("1?:22")); // 19:22
}
}
复杂度分析
复杂度:时间复杂度 O(1),空间复杂度 O(1)。
复杂度:时间复杂度 O(24*60),空间复杂度 O(1)。
1737. Change Minimum Characters to Satisfy One of Three Conditions(满足三条件之一需改变的最少字符数)
题目描述
给你两个字符串 a 和 b,二者均由小写字母组成。一步操作中,你可以将 a 或 b 中的
任一字符改变为任一小写字母。操作的最终目标是满足下列三个条件之一:
a中的每个字母在字母表中严格小于b中的每个字母。b中的每个字母在字母表中严格小于a中的每个字母。a和b都由同一个字母组成。
返回达成目标所需的最少操作数。
示例 1
a = "aba", b = "caa",输出 2。
示例 2
a = "dabadd", b = "cda",输出 3。
解题思路
解法一:26 字母分界点枚举(主提交版本)
- 统计 a、b 各字母频次及前缀和。
- 条件 3:把 a 全部改为 c、b 全部改为 c(同一个字母),操作数 = len(a)-cntAc + len(b)-cntBc。
- 条件 1:存在分界字母 x,a 中所有字母 <= x、b 中所有字母 > x,
操作数 = 把 a 中 > x 的改为小 + 把 b 中 <= x 的改为大。 - 条件 2 对称。枚举 x 从 0...25 取最小。
复杂度:时间复杂度 O(26 + |a| + |b|),空间复杂度 O(26)。
解法二:逐分界点暴力计数(对照) - 不建前缀和,直接对每个分界 x 重新统计两次字符串中需要改的字符数。
- 逻辑与解法一完全一致,仅用原始字符数组遍历,作为对照实现。
复杂度:时间复杂度 O(26 * (|a|+|b|)),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:26 字母分界点枚举 + 前缀和(主提交版本)
public int minCharacters(String a, String b) {
int[] ca = count(a);
int[] cb = count(b);
int na = a.length(), nb = b.length();
int[] pa = new int[27], pb = new int[27];
for (int i = 0; i < 26; i++) {
pa[i + 1] = pa[i] + ca[i];
pb[i + 1] = pb[i] + cb[i];
}
int ans = Integer.MAX_VALUE;
// 条件 3:a、b 都改成同一个字母 c
for (int c = 0; c < 26; c++) {
ans = Math.min(ans, na - ca[c] + nb - cb[c]);
}
// 条件 1:a 中每个字母严格小于 b 中每个字母
// => 存在 x,a 全 <= x 且 b 全 > x
for (int x = 0; x < 25; x++) {
int cost = (na - pa[x + 1]) + pb[x + 1];
ans = Math.min(ans, cost);
}
// 条件 2:b 中每个字母严格小于 a 中每个字母
for (int x = 0; x < 25; x++) {
int cost = (nb - pb[x + 1]) + pa[x + 1];
ans = Math.min(ans, cost);
}
return ans;
}
private int[] count(String s) {
int[] c = new int[26];
for (char ch : s.toCharArray()) {
c[ch - 'a']++;
}
return c;
}
// 解法二:逐分界点暴力计数(对照)
public int minCharactersBrute(String a, String b) {
int best = Integer.MAX_VALUE;
// 条件 3
for (int c = 0; c < 26; c++) {
int cost = 0;
for (char ch : a.toCharArray()) {
if (ch - 'a' != c) {
cost++;
}
}
for (char ch : b.toCharArray()) {
if (ch - 'a' != c) {
cost++;
}
}
best = Math.min(best, cost);
}
// 条件 1:a 全 <= x 且 b 全 > x
for (int x = 0; x < 25; x++) {
int cost = 0;
for (char ch : a.toCharArray()) {
if (ch - 'a' > x) {
cost++;
}
}
for (char ch : b.toCharArray()) {
if (ch - 'a' <= x) {
cost++;
}
}
best = Math.min(best, cost);
}
// 条件 2:b 全 <= x 且 a 全 > x
for (int x = 0; x < 25; x++) {
int cost = 0;
for (char ch : b.toCharArray()) {
if (ch - 'a' > x) {
cost++;
}
}
for (char ch : a.toCharArray()) {
if (ch - 'a' <= x) {
cost++;
}
}
best = Math.min(best, cost);
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minCharacters("aba", "caa")); // 2
System.out.println(sol.minCharacters("dabadd", "cda")); // 3
System.out.println(sol.minCharactersBrute("aba", "caa")); // 2
System.out.println(sol.minCharactersBrute("dabadd", "cda")); // 3
}
}
复杂度分析
复杂度:时间复杂度 O(26 + |a| + |b|),空间复杂度 O(26)。
复杂度:时间复杂度 O(26 * (|a|+|b|)),空间复杂度 O(1)。
1738. Find Kth Largest XOR Coordinate Value(找出第 K 大的异或坐标值)
题目描述
给你一个二维矩阵 matrix 和一个整数 k,矩阵大小为 m x n 由非负整数组成。
矩阵中坐标 (a, b) 的目标值可以通过对所有元素 matrix[i][j] 执行异或运算得到,
其中 i 和 j 满足 0 <= i <= a < m 且 0 <= j <= b < n。
请你找出 matrix 的所有坐标中第 k 大的目标值(k 的值从 1 开始计数)。
示例 1
matrix = [[5,2],[1,6]], k = 1,输出 7。
示例 2
matrix = [[5,2],[1,6]], k = 2,输出 5。
示例 3
matrix = [[3,10,1],[9,3,7],[10,4,4]], k = 3,输出 5。
解题思路
解法一:二维前缀异或 + 全部值排序取第 k 大(主提交版本)
- preij = prei-1j ^ preij-1 ^ prei-1j-1 ^ matrixi-1j-1。
- 收集所有 pre 值,降序排序后取第 k-1 个。
复杂度:时间复杂度 O(mn log(mn)),空间复杂度 O(m n)。
解法二:小根堆维护前 k 大(对照) - 同样生成全部异或坐标值,用容量为 k 的小根堆过滤,堆顶即第 k 大。
复杂度:时间复杂度 O(mnlog k),空间复杂度 O(k)。
代码实现
java
public class Solution {
// 解法一:前缀异或 + 排序(主提交版本)
public int kthLargestValue(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
int[][] pre = new int[m + 1][n + 1];
java.util.List<Integer> all = new java.util.ArrayList<>();
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
pre[i][j] = pre[i - 1][j] ^ pre[i][j - 1]
^ pre[i - 1][j - 1] ^ matrix[i - 1][j - 1];
all.add(pre[i][j]);
}
}
java.util.Collections.sort(all, java.util.Collections.reverseOrder());
return all.get(k - 1);
}
// 解法二:小根堆维护前 k 大(对照)
public int kthLargestValueHeap(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
int[][] pre = new int[m + 1][n + 1];
java.util.PriorityQueue<Integer> pq = new java.util.PriorityQueue<>();
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
pre[i][j] = pre[i - 1][j] ^ pre[i][j - 1]
^ pre[i - 1][j - 1] ^ matrix[i - 1][j - 1];
pq.offer(pre[i][j]);
if (pq.size() > k) {
pq.poll();
}
}
}
return pq.peek();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.kthLargestValue(new int[][]{{5, 2}, {1, 6}}, 1)); // 7
System.out.println(sol.kthLargestValue(new int[][]{{5, 2}, {1, 6}}, 2)); // 5
System.out.println(sol.kthLargestValue(new int[][]{{3, 10, 1}, {9, 3, 7}, {10, 4, 4}}, 3)); // 5
System.out.println(sol.kthLargestValueHeap(new int[][]{{5, 2}, {1, 6}}, 1)); // 7
System.out.println(sol.kthLargestValueHeap(new int[][]{{5, 2}, {1, 6}}, 2)); // 5
System.out.println(sol.kthLargestValueHeap(new int[][]{{3, 10, 1}, {9, 3, 7}, {10, 4, 4}}, 3)); // 5
}
}
复杂度分析
复杂度:时间复杂度 O(mn log(mn)),空间复杂度 O(m n)。
复杂度:时间复杂度 O(mnlog k),空间复杂度 O(k)。
1739. Building Boxes(放置盒子)
题目描述
有一个立方体房间,其长度、宽度和高度都等于 n 个单位。请你在房间里放置 n 个盒子,
每个盒子都是一个单位边长的立方体。放置规则如下:
- 你可以把盒子放在地板上的任何地方。
- 如果盒子
x需要放置在盒子y的顶部,那么盒子y竖直的四个侧面都必须与另一个盒子或墙相邻。
给你一个整数n,返回接触地面的盒子的最少可能数量。
示例 1
n = 3,输出 3。
示例 2
n = 4,输出 3。
示例 3
n = 10,输出 6。
解题思路
解法一:数学贪心(主提交版本)
- 为了用最少的"底面盒子"支持尽可能多的总盒子,应把盒子堆成"完美塔堆":
最底层是 kk 方格(k 层依次递减)。满 k 层塔共能放 k(k+1)*(k+2)/6 个盒子。 - 先找最大的 k 使 sum_{t=1...k} t(t+1)/2 <= n,即 k 层完美塔。
- 剩余盒子再沿最底层的一条边向上堆(每加一层需要 floor 新增一层:1、2、3...)。
设剩余 r 个盒子,需要新增 x 层(1+2+...+x >= r)。
复杂度:时间复杂度 O(cuberoot(n)),空间复杂度 O(1)。
解法二:二分找层数(对照) - 同思路,把"找最大 k"改为在 0, 2000 上二分。
复杂度:时间复杂度 O(log n),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:数学贪心(主提交版本)
public int minimumBoxes(int n) {
// 找最大 k:1 + (1+2) + (1+2+3) + ... + (1+...+k) <= n
// 即 sum_{i=1..k} i*(i+1)/2 <= n
int k = 1;
while (k * (k + 1) * (k + 2) / 6 <= n) {
k++;
}
k--; // 完美塔层数
long used = (long) k * (k + 1) * (k + 2) / 6;
long ground = (long) k * (k + 1) / 2; // 完美塔接触地面的盒子数
int rest = n - (int) used;
// 剩余 rest 个盒子沿一条边堆:加第 1 层放 1 个、第 2 层放 2 个......
// 每放 1 层,ground 增加对应数量的盒子
int layer = 1;
while (rest > 0) {
rest -= layer;
ground += layer;
layer++;
}
return (int) ground;
}
// 解法二:二分(对照)
public int minimumBoxesBinary(int n) {
int lo = 0, hi = 2000, k = 0;
// 找最大的 k:sum_{i=1..k} i*(i+1)/2 <= n
while (lo <= hi) {
int mid = (lo + hi) >> 1;
long total = (long) mid * (mid + 1) * (mid + 2) / 6;
if (total <= n) {
k = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
long used = (long) k * (k + 1) * (k + 2) / 6;
long ground = (long) k * (k + 1) / 2;
int rest = n - (int) used;
int layer = 1;
while (rest > 0) {
rest -= layer;
ground += layer;
layer++;
}
return (int) ground;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumBoxes(3)); // 3
System.out.println(sol.minimumBoxes(4)); // 3
System.out.println(sol.minimumBoxes(10)); // 6
System.out.println(sol.minimumBoxesBinary(3)); // 3
System.out.println(sol.minimumBoxesBinary(4)); // 3
System.out.println(sol.minimumBoxesBinary(10)); // 6
}
}
复杂度分析
复杂度:时间复杂度 O(cuberoot(n)),空间复杂度 O(1)。
复杂度:时间复杂度 O(log n),空间复杂度 O(1)。
1740. Find Distance in a Binary Tree(找到二叉树中的距离)
题目描述
给定一棵二叉树的根节点 root 以及两个整数 p 和 q,返回该二叉树中值为 p 的结点
与值为 q 的结点间的距离。两个结点间的距离就是从一个结点到另一个结点的路径上边的数目。
示例 1
root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 0,输出 3。
示例 2
同树,p = 5, q = 7,输出 2。
示例 3
同树,p = 5, q = 5,输出 0。
提示:
- 树中结点个数的范围在 1, 10\^4,0 <=
Node.val<= 10^9, - 树中所有结点的值都是唯一的,
p和q是树中结点的值。
解题思路
解法一:最近公共祖先 + 深度差(主提交版本)
- 先求出 p、q 的最近公共祖先 anc,再分别求 p、q 到 anc 的深度(边数),
距离 = d(p, anc) + d(q, anc)(当 anc 为 p 或 q 时公式同样成立)。 - 求 LCA 用递归:根为 null 返回 null;根值等于 p 或 q 返回根;
分别递归左、右子树,两边都有则当前根是 LCA。
复杂度:时间复杂度 O(n),空间复杂度 O(h)(递归栈)。
解法二:记录父指针向上爬(对照) - 一次 DFS 建立每个结点到父结点的映射,并记录 p、q 结点。
- 从 p 向上爬并标记路径,再从 q 向上爬到第一个被标记的结点即 LCA,统计边数。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
代码实现
java
// 树结点定义(LeetCode 内置)
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class Solution {
// 解法一:LCA + 深度(主提交版本)
public int findDistance(TreeNode root, int p, int q) {
TreeNode anc = lca(root, p, q);
return depth(anc, p) + depth(anc, q);
}
private TreeNode lca(TreeNode root, int p, int q) {
if (root == null || root.val == p || root.val == q) {
return root;
}
TreeNode left = lca(root.left, p, q);
TreeNode right = lca(root.right, p, q);
if (left != null && right != null) {
return root;
}
return left != null ? left : right;
}
private int depth(TreeNode node, int target) {
if (node == null) {
return -100000; // 找不到(不应发生)
}
if (node.val == target) {
return 0;
}
int d = depth(node.left, target);
if (d >= 0) {
return d + 1;
}
d = depth(node.right, target);
return d >= 0 ? d + 1 : -100000;
}
// 解法二:父指针路径标记(对照)
public int findDistanceParent(TreeNode root, int p, int q) {
java.util.Map<TreeNode, TreeNode> parent = new java.util.HashMap<>();
java.util.Map<Integer, TreeNode> byVal = new java.util.HashMap<>();
dfs(root, null, parent, byVal);
TreeNode pNode = byVal.get(p);
TreeNode qNode = byVal.get(q);
java.util.Set<TreeNode> path = new java.util.HashSet<>();
for (TreeNode cur = pNode; cur != null; cur = parent.get(cur)) {
path.add(cur);
}
TreeNode anc = qNode;
int up = 0;
while (anc != null && !path.contains(anc)) {
anc = parent.get(anc);
up++;
}
int down = 0;
for (TreeNode cur = pNode; cur != anc; cur = parent.get(cur)) {
down++;
}
return up + down;
}
private void dfs(TreeNode node, TreeNode par,
java.util.Map<TreeNode, TreeNode> parent,
java.util.Map<Integer, TreeNode> byVal) {
if (node == null) {
return;
}
if (par != null) {
parent.put(node, par);
}
byVal.put(node.val, node);
dfs(node.left, node, parent, byVal);
dfs(node.right, node, parent, byVal);
}
public static void main(String[] args) {
Solution sol = new Solution();
TreeNode root = build(
new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
System.out.println(sol.findDistance(root, 5, 0)); // 3
System.out.println(sol.findDistance(root, 5, 7)); // 2
System.out.println(sol.findDistance(root, 5, 5)); // 0
System.out.println(sol.findDistanceParent(root, 5, 0)); // 3
System.out.println(sol.findDistanceParent(root, 5, 7)); // 2
System.out.println(sol.findDistanceParent(root, 5, 5)); // 0
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.offer(cur.left);
}
i++;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.offer(cur.right);
}
i++;
}
return root;
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(h)(递归栈)。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。