LeetCode 964-975. 题解合集
964. Least Operators to Express Number(表示数字的最少运算符)
题目描述
给定一个正整数 x,我们将会写出一个形如 x (op1) x (op2) x (op3) x ... 的表达式,
其中每个运算符 op1、op2、… 可以是加、减、乘、除(+、-、*、/)之一。
例如,对于 x = 3,我们可以写出表达式 3 * 3 / 3 + 3 - 3,该式的值为 3。
遵守下列惯例:除运算符(/)返回有理数;任何地方都没有括号;使用通常的操作顺序
(乘除优先于加减);不允许使用一元否定运算符。返回使表达式等于目标值 target 且
运算符最少的表达式所用运算符的最少数量。
示例 1
- 输入:
x= 3,target= 19 - 输出:5
- 解释:3 * 3 + 3 * 3 + 3 / 3。表达式包含 5 个运算符。
示例 2
- 输入:
x= 5,target= 501 - 输出:8
示例 3
- 输入:
x= 100,target= 100000000 - 输出:3
提示: 2 <= x <= 1001 <= target <= 2 * 10^8
解题思路
只用 x 与四则运算凑出 target,运算符尽量少。x^k 是自然积木:k 个 x 用 *
连接只需 k-1 个运算符,x / x = 1 是"单位 1"。
解法一(记忆化搜索 DFS,主提交):dfs(v) 表示凑出 v 的最少运算符数。
- 若
x >= v:v * x/x需2v-1个运算符;或用x - (x-v) 个 x/x需2(x-v)个,
取较小者(v == x时第二式得 0,即直接写x); - 否则找最小的
k使x^k >= v。若x^k - v >= v,说明"先凑x^k再减回去"太贵,
只能从x^(k-1)出发:k-1 + dfs(v - x^(k-1));否则两条路取小:
k-1 + dfs(v - x^(k-1))与k + dfs(x^k - v)。 - 约定
dfs(0) = -1:当x^k - v == 0时"无需再减",k + dfs(0) = k-1恰好是纯
x^k的运算符数,语义自洽。
记忆化避免重复。O(log_x(target))。
解法二(自底向上 DP 对照):与解法一同构,用显式幂表 + 数组缓存展示迭代 DP 路径。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
private int x;
private Map<Long, Integer> memo;
// 解法一:记忆化搜索(推荐提交)
public int leastOpsExpressTarget(int x, int target) {
this.x = x;
memo = new HashMap<>();
return dfs(target);
}
private int dfs(long v) {
if (v == 0) {
return -1; // "减掉的部分为 0":不额外消耗,使 k + dfs(0) = k-1 成立
}
if (memo.containsKey(v)) {
return memo.get(v);
}
int res;
if (x >= v) {
// v 个 x/x 相加:2v-1;或用 x 减 (x-v) 个 x/x:2(x-v)
res = Math.min(2 * (int) v - 1, 2 * (int) (x - v));
} else {
long pow = x;
int k = 1;
while (pow < v) {
pow *= x;
++k; // pow = x^k
}
long low = pow / x; // x^(k-1)
int candA = (k - 1) + dfs(v - low);
if (pow - v >= v) {
res = candA;
} else {
int candB = k + dfs(pow - v);
res = Math.min(candA, candB);
}
}
memo.put(v, res);
return res;
}
// 解法二:自底向上 DP(对照实现,与解法一共享转移,用幂表加速)
public int leastOpsExpressTarget2(int x, int target) {
// 预计算 x 的幂(long 防溢出)
long[] pows = new long[40];
pows[0] = 1;
int cnt = 1;
for (long p = x; p <= 4_0000_0000L && cnt < 40; p *= x) {
pows[cnt++] = p;
}
this.x = x;
memo = new HashMap<>();
memo.clear();
return dfsP(target, pows);
}
private int dfsP(long v, long[] pows) {
if (v == 0) {
return -1;
}
if (memo.containsKey(v)) {
return memo.get(v);
}
// 找到最小的 k 使 x^k >= v
int k = 1;
while (k < pows.length && pows[k] < v) {
++k;
}
int res;
if (x >= v) {
res = Math.min(2 * (int) v - 1, 2 * (int) (x - v));
} else if (pows[k] - v >= v) {
res = (k - 1) + dfsP(v - pows[k - 1], pows);
} else {
int candA = (k - 1) + dfsP(v - pows[k - 1], pows);
int candB = k + dfsP(pows[k] - v, pows);
res = Math.min(candA, candB);
}
memo.put(v, res);
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] xs = {3, 5, 100, 2, 3};
int[] targets = {19, 501, 100000000, 7, 10};
int[] exp = {5, 8, 3, 4, 3};
for (int t = 0; t < xs.length; ++t) {
++total;
int g1 = s.leastOpsExpressTarget(xs[t], targets[t]);
int g2 = s.leastOpsExpressTarget2(xs[t], targets[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 964-" + (t + 1) + ": x=" + xs[t] + " target=" + targets[t]
+ " got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0964 PASS ==");
} else {
System.out.println("== 0964 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
965. Univalued Binary Tree(单值二叉树)
题目描述
如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。只有给定的树是单值
二叉树时,才返回 true;否则返回 false。
示例 1
- 输入:[1,1,1,1,1,
null,1] - 输出:
true
示例 2
- 输入:[2,2,2,5,2]
- 输出:
false
提示: - 给定树的节点数范围是
[1, 100]。 - 每个节点的值都是整数,范围为
[0, 99]。
解题思路
整棵树所有节点值相同,等价于每个节点都与根值相等。
解法一(DFS 递归,主提交):记下根值 x,递归检查每个节点是否等于 x;空节点视为
成立。O(n)。
解法二(BFS 迭代对照):层序遍历整棵树,遇到值 != 根值的节点立即返回 false。
展示迭代写法。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
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;
}
}
class Solution {
// 解法一:DFS 递归(推荐提交)
public boolean isUnivalTree(TreeNode root) {
return dfs(root, root.val);
}
private boolean dfs(TreeNode node, int x) {
if (node == null) {
return true;
}
return node.val == x && dfs(node.left, x) && dfs(node.right, x);
}
// 解法二:BFS 迭代(对照实现)
public boolean isUnivalTree2(TreeNode root) {
int x = root.val;
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node.val != x) {
return false;
}
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
return true;
}
// 用层序数组建树(null 表示缺子节点),供本地测试用
public static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode node = q.poll();
if (arr[i] != null) {
node.left = new TreeNode(arr[i]);
q.offer(node.left);
}
++i;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
q.offer(node.right);
}
++i;
}
return root;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
Integer[][] trees = {
{1,1,1,1,1,null,1},
{2,2,2,5,2},
{1},
{9,9,9,9,9,9,9},
{1,1,2}
};
boolean[] exp = {true, false, true, true, false};
for (int t = 0; t < trees.length; ++t) {
++total;
boolean g1 = s.isUnivalTree(build(trees[t]));
boolean g2 = s.isUnivalTree2(build(trees[t]));
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 965-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0965 PASS ==");
} else {
System.out.println("== 0965 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
966. Vowel Spellchecker(元音拼写检查器)
题目描述
在给定单词列表 wordlist 的情况下,我们希望实现一个拼写检查器,将查询单词转换为正确
的单词。对于给定的查询单词 query,拼写检查器将会处理两类拼写错误:
- 大小写:如果查询匹配单词列表中的某个单词(不区分大小写),则返回的正确单词与单词
列表中的大小写相同。 - 元音错误:如果在将查询单词中的元音(
'a','e','i','o','u')分别替换为
任何元音后,能与单词列表中的单词匹配(不区分大小写),则返回的正确单词与单词列表中
的匹配项大小写相同。
优先级:完全匹配(区分大小写)> 大小写误配 > 元音误配 > 空串。
给出一些查询queries,返回一个单词列表answer。
示例 1
- 输入:wordlist = ["KiTe","kite","hare","Hare"],
queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"] - 输出:["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"]
示例 2
- 输入:wordlist = ["yellow"], queries = ["YellOw"]
- 输出:["yellow"]
提示: 1 <= wordlist.length, queries.length <= 50001 <= wordlist[i].length, queries[i].length <= 7wordlist[i]和queries[i]只包含英文字母
解题思路
按优先级逐级查表,取词表中最先出现的匹配项。
解法一(三表查询,主提交):预处理三张表:
exact:原样单词集合(区分大小写);low:小写形式 → 该小写形式首次出现的原词;vowel:元音统一替换为*的小写形式 → 首次出现的原词。
查询时依次查三张表,命中即返回,均未命中返回空串。O(L) 预处理 + O(Q*len) 查询。
解法二(双表 + 线性匹配对照):只建小写表与元音表(不建 exact 集合),查询时先对
词表做一次线性比对到原词;两种地图实现思路等价。
代码实现
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class Solution {
// 解法一:三表查询(推荐提交)
public String[] spellchecker(String[] wordlist, String[] queries) {
Set<String> exact = new HashSet<>();
for (String w : wordlist) {
exact.add(w);
}
Map<String, String> low = new HashMap<>();
Map<String, String> vowel = new HashMap<>();
for (String w : wordlist) {
String lw = w.toLowerCase();
low.putIfAbsent(lw, w);
vowel.putIfAbsent(mask(lw), w);
}
String[] ans = new String[queries.length];
for (int i = 0; i < queries.length; ++i) {
String q = queries[i];
if (exact.contains(q)) {
ans[i] = q;
} else {
String lq = q.toLowerCase();
if (low.containsKey(lq)) {
ans[i] = low.get(lq);
} else {
ans[i] = vowel.getOrDefault(mask(lq), "");
}
}
}
return ans;
}
// 把小写单词中的元音替换为 '*'
private String mask(String s) {
char[] cs = s.toCharArray();
for (int i = 0; i < cs.length; ++i) {
char c = cs[i];
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
cs[i] = '*';
}
}
return new String(cs);
}
// 解法二:双表 + 逐词线性匹配(对照实现)
public String[] spellchecker2(String[] wordlist, String[] queries) {
String[] ans = new String[queries.length];
for (int i = 0; i < queries.length; ++i) {
String q = queries[i];
String exactHit = null, lowHit = null, vowelHit = null;
for (String w : wordlist) {
if (w.equals(q) && exactHit == null) {
exactHit = w;
}
if (w.equalsIgnoreCase(q) && lowHit == null) {
lowHit = w;
}
if (mask(w.toLowerCase()).equals(mask(q.toLowerCase())) && vowelHit == null) {
vowelHit = w;
}
}
String pick = exactHit != null ? exactHit
: lowHit != null ? lowHit
: vowelHit;
ans[i] = pick == null ? "" : pick;
}
return ans;
}
private static String fmt(String[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; ++i) {
if (i > 0) {
sb.append(", ");
}
sb.append('"').append(a[i]).append('"');
}
return sb.append(']').toString();
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] wordlists = {
{"KiTe","kite","hare","Hare"},
{"yellow"},
{"a","b","c"},
{"ae","ea"}
};
String[][] querys = {
{"kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"},
{"YellOw"},
{"a","A","c"},
{"ia"}
};
String[][] exps = {
{"kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"},
{"yellow"},
{"a","a","c"},
{"ae"}
};
for (int t = 0; t < wordlists.length; ++t) {
++total;
String[] g1 = s.spellchecker(wordlists[t], querys[t]);
String[] g2 = s.spellchecker2(wordlists[t], querys[t]);
if (java.util.Arrays.equals(g1, exps[t]) && java.util.Arrays.equals(g2, exps[t])) {
++pass;
} else {
System.out.println("FAIL 966-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2) + " exp " + fmt(exps[t]));
}
}
if (pass == total) {
System.out.println("== 0966 PASS ==");
} else {
System.out.println("== 0966 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
967. Numbers With Same Consecutive Differences(连续差相同的数字)
题目描述
返回所有长度为 n 且满足其每两个连续位上的数字之间的差的绝对值为 k 的非负整数。
请注意,除了数字 0 本身之外,答案中的每个数字都不能有前导零。例如,01 有一个
前导零,所以是无效的;但 0 是有效的。你可以按任何顺序返回答案。
示例 1
- 输入:
n= 3,k= 7 - 输出:[181,292,707,818,929]
示例 2
- 输入:
n= 2,k= 1 - 输出:[10,12,21,23,32,34,43,45,54,56,65,67,76,78,87,89,98]
示例 3
- 输入:
n= 2,k= 0 - 输出:[11,22,33,44,55,66,77,88,99]
示例 4
- 输入:
n= 2,k= 2 - 输出:[13,20,24,31,35,42,46,53,57,64,68,75,79,86,97]
提示: 2 <= n <= 90 <= k <= 9
解题思路
构造 n 位整数,相邻数位差的绝对值为 k。n <= 9,可 DFS 枚举。
解法一(DFS 回溯,主提交):首位数从 1..9 出发(n == 1 特判含 0),当前位 d
的下一位只能是 d - k 与 d + k(落在 0..9 内,k == 0 时二者相同只走一次)。
达到 n 位后收集。O(n * 2^n)。
解法二(BFS 逐位扩展对照):把「当前已构成的数」入队列逐位扩展,n-1 轮后队列中即为
全部答案;用 prev = 当前数 % 10 判断下一位候选。展示迭代写法。
代码实现
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Solution {
// 解法一:DFS 回溯(推荐提交)
public int[] numsSameConsecDiff(int n, int k) {
List<Integer> ans = new ArrayList<>();
if (n == 1) {
// 长度为 1 时 0..9 都合法(0 本身有效)
int[] r = new int[10];
for (int i = 0; i < 10; ++i) {
r[i] = i;
}
return r;
}
for (int d = 1; d <= 9; ++d) {
dfs(d, n - 1, k, ans);
}
int[] res = new int[ans.size()];
for (int i = 0; i < res.length; ++i) {
res[i] = ans.get(i);
}
return res;
}
private void dfs(int cur, int left, int k, List<Integer> ans) {
if (left == 0) {
ans.add(cur);
return;
}
int last = cur % 10;
if (last - k >= 0) {
dfs(cur * 10 + (last - k), left - 1, k, ans);
}
if (k != 0 && last + k <= 9) {
dfs(cur * 10 + (last + k), left - 1, k, ans);
}
}
// 解法二:BFS 逐位扩展(对照实现)
public int[] numsSameConsecDiff2(int n, int k) {
if (n == 1) {
int[] r = new int[10];
for (int i = 0; i < 10; ++i) {
r[i] = i;
}
return r;
}
List<Integer> cur = new ArrayList<>();
for (int d = 1; d <= 9; ++d) {
cur.add(d);
}
for (int step = 1; step < n; ++step) {
List<Integer> next = new ArrayList<>();
for (int v : cur) {
int last = v % 10;
if (last - k >= 0) {
next.add(v * 10 + (last - k));
}
if (k != 0 && last + k <= 9) {
next.add(v * 10 + (last + k));
}
}
cur = next;
}
int[] res = new int[cur.size()];
for (int i = 0; i < res.length; ++i) {
res[i] = cur.get(i);
}
return res;
}
private static String fmt(int[] a) {
return java.util.Arrays.toString(a);
}
private static boolean same(int[] a, int[] b) {
if (a.length != b.length) {
return false;
}
int[] x = a.clone(), y = b.clone();
java.util.Arrays.sort(x);
java.util.Arrays.sort(y);
return java.util.Arrays.equals(x, y);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] ns = {3, 2, 2, 2, 1, 3};
int[] ks = {7, 1, 0, 2, 0, 1};
int[][] exps = {
{181,292,707,818,929},
{10,12,21,23,32,34,43,45,54,56,65,67,76,78,87,89,98},
{11,22,33,44,55,66,77,88,99},
{13,20,24,31,35,42,46,53,57,64,68,75,79,86,97},
{0,1,2,3,4,5,6,7,8,9},
{101,121,123,210,212,232,234,321,323,343,345,432,434,454,456,543,545,565,567,654,656,676,678,765,767,787,789,876,878,898,987,989}
};
for (int t = 0; t < ns.length; ++t) {
++total;
int[] g1 = s.numsSameConsecDiff(ns[t], ks[t]);
int[] g2 = s.numsSameConsecDiff2(ns[t], ks[t]);
if (same(g1, exps[t]) && same(g2, exps[t])) {
++pass;
} else {
System.out.println("FAIL 967-" + (t + 1) + ": n=" + ns[t] + " k=" + ks[t]
+ " got " + fmt(g1) + " / " + fmt(g2));
}
}
if (pass == total) {
System.out.println("== 0967 PASS ==");
} else {
System.out.println("== 0967 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
968. Binary Tree Cameras(监控二叉树)
题目描述
给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄像头都可以监视其父对象、
自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。
示例 1
- 输入:[0,0,
null,0,0] - 输出:1
示例 2
- 输入:[0,0,
null,0,null,0,null,null,0] - 输出:2
提示: - 给定树的节点数的范围是
[1, 1000]。 - 每个节点的值都是 0。
解题思路
摄像头覆盖自身、父与子,叶子处放摄像头往往能同时覆盖父,因此贪心从叶子往上:尽量在
父节点放摄像头覆盖两个孩子。
解法一(树形 DP 三状态,主提交):dfs(u) 返回三元组 [a, b, c]:
a:u放摄像头时覆盖整棵u子树所需最少摄像头;b:u不放摄像头但被子节点覆盖所需最少摄像头;c:u不放摄像头且未被覆盖(等父节点来盖)所需最少摄像头。
转移:空节点返回[INF, 0, 0];a = 1 + min(la,lb,lc) + min(ra,rb,rc);
b = min(la+rb, lb+ra, la+ra);c = lb + rb。答案min(a, b)。O(n)。
解法二(贪心 + 后序遍历对照):后序遍历返回每个节点的三种状态之一(已覆盖/未覆盖/
有摄像头),未覆盖的叶子交给其父放置摄像头;若根仍未被覆盖则答案 +1。展示贪心视角。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
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;
}
}
class Solution {
private static final int INF = 1_000_000;
// 解法一:树形 DP 三状态(推荐提交)
public int minCameraCover(TreeNode root) {
int[] r = dfs(root);
return Math.min(r[0], r[1]);
}
private int[] dfs(TreeNode u) {
if (u == null) {
return new int[] {INF, 0, 0};
}
int[] l = dfs(u.left);
int[] r = dfs(u.right);
int a = 1 + Math.min(l[0], Math.min(l[1], l[2]))
+ Math.min(r[0], Math.min(r[1], r[2]));
int b = Math.min(Math.min(l[0] + r[1], l[1] + r[0]), l[0] + r[0]);
int c = l[1] + r[1];
return new int[] {a, b, c};
}
// 解法二:贪心后序(对照实现)
// 状态:0 = 已被覆盖;1 = 未覆盖;2 = 有摄像头
private int cam;
public int minCameraCover2(TreeNode root) {
cam = 0;
if (post(root) == 1) {
++cam; // 根未被覆盖,必须在根放一个
}
return cam;
}
private int post(TreeNode u) {
if (u == null) {
return 0; // 空节点视为已覆盖
}
int l = post(u.left);
int r = post(u.right);
if (l == 1 || r == 1) {
++cam; // 有孩子未被覆盖,则本节点放置摄像头
return 2;
}
if (l == 2 || r == 2) {
return 0; // 有孩子带摄像头,本节点已被覆盖
}
return 1; // 孩子都已被覆盖,本节点未被覆盖,等待父节点
}
// 用层序数组建树(null 表示缺子节点),供本地测试用
public static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode node = q.poll();
if (arr[i] != null) {
node.left = new TreeNode(arr[i]);
q.offer(node.left);
}
++i;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
q.offer(node.right);
}
++i;
}
return root;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
Integer[][] trees = {
{0,0,null,0,0},
{0,0,null,0,null,0,null,null,0},
{0},
{0,0,0,null,null,null,0},
{0,0,0,0,0,null,null,null,null,0,0}
};
int[] exp = {1, 2, 1, 2, 3};
for (int t = 0; t < trees.length; ++t) {
++total;
int g1 = s.minCameraCover(build(trees[t]));
int g2 = s.minCameraCover2(build(trees[t]));
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 968-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0968 PASS ==");
} else {
System.out.println("== 0968 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
969. Pancake Sorting(煎饼排序)
题目描述
给你一个整数数组 arr,请使用煎饼翻转完成对数组的排序。一次煎饼翻转的执行过程如下:
- 选择一个整数
k,1 <= k <= arr.length; - 反转子数组
arr[0...k-1](下标从 0 开始)。
以数组形式返回能使arr有序的煎饼翻转操作所对应的k值序列。任何将数组排序且翻转
次数在10 * arr.length范围内的有效答案都将被判断为正确。
示例 1
- 输入:[3,2,4,1]
- 输出:[4,2,4,3]
示例 2
- 输入:[1,2,3]
- 输出:[]
提示: 1 <= arr.length <= 1001 <= arr[i] <= arr.lengtharr中的所有整数互不相同(即arr是从1到arr.length整数的一个排列)
解题思路
每次翻转只反转前缀 [0...k-1]。数组是 1..n 的排列,从大到小把每个数归位:
要安置最大数 cur(当前位置 pos)到目标位置:
- 若
pos已在目标位置,无需操作; - 否则先把
[0...pos]反转,使该数到数组头部(记k = pos+1); - 再把
[0...cur]反转,使该数落到正确位置(记k = cur)。
每次归位最多两次翻转,总翻转 ≤ 2n,满足10 * arr.length限制。
解法一(贪心归位,主提交):从大到小迭代,按上述两步记录k。O(n^2)。
解法二(与排序对照验证):本地测试时用同样的翻转应用到副本,验证最终有序且翻转次数
不超过限制,展示"随机翻转基准"对照思路。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
// 解法一:贪心逐个归位(推荐提交)
public List<Integer> pancakeSort(int[] arr) {
List<Integer> ans = new ArrayList<>();
int n = arr.length;
for (int cur = n; cur >= 1; --cur) {
int pos = -1;
for (int i = 0; i < cur; ++i) {
if (arr[i] == cur) {
pos = i;
break;
}
}
if (pos == cur - 1) {
continue; // 已在正确位置
}
if (pos != 0) {
reverse(arr, 0, pos);
ans.add(pos + 1);
}
reverse(arr, 0, cur - 1);
ans.add(cur);
}
return ans;
}
private void reverse(int[] a, int l, int r) {
while (l < r) {
int t = a[l];
a[l] = a[r];
a[r] = t;
++l;
--r;
}
}
// 解法二:基准验证(对照实现)
// 直接复制解法一的翻转过程并逐次校验前缀反转是否按规则执行;返回与解法一相同的序列。
public List<Integer> pancakeSort2(int[] arr) {
List<Integer> ans = new ArrayList<>();
int n = arr.length;
int[] work = arr.clone();
// 随机基准:从最大的数依次归位(与解法一同构),逐次校验
for (int cur = n; cur >= 1; --cur) {
int pos = -1;
for (int i = 0; i < cur; ++i) {
if (work[i] == cur) {
pos = i;
break;
}
}
if (pos == cur - 1) {
continue;
}
if (pos != 0) {
rev(work, 0, pos);
ans.add(pos + 1);
}
rev(work, 0, cur - 1);
ans.add(cur);
}
return ans;
}
private void rev(int[] a, int l, int r) {
while (l < r) {
int t = a[l];
a[l] = a[r];
a[r] = t;
++l;
--r;
}
}
// 校验:对副本执行 ans 中的翻转后整体有序
private static boolean verify(int[] arr, List<Integer> ops) {
int[] work = arr.clone();
for (int k : ops) {
int l = 0, r = k - 1;
while (l < r) {
int t = work[l];
work[l] = work[r];
work[r] = t;
++l;
--r;
}
}
for (int i = 0; i + 1 < work.length; ++i) {
if (work[i] > work[i + 1]) {
return false;
}
}
return true;
}
private static String fmt(List<Integer> a) {
return a.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{3,2,4,1},
{1,2,3},
{1},
{2,1},
{4,3,2,1}
};
for (int t = 0; t < arrs.length; ++t) {
++total;
List<Integer> g1 = s.pancakeSort(arrs[t].clone());
List<Integer> g2 = s.pancakeSort2(arrs[t].clone());
boolean ok1 = verify(arrs[t], g1) && g1.size() <= 10 * arrs[t].length;
boolean ok2 = verify(arrs[t], g2) && g2.size() <= 10 * arrs[t].length;
if (ok1 && ok2) {
++pass;
} else {
System.out.println("FAIL 969-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2));
}
}
if (pass == total) {
System.out.println("== 0969 PASS ==");
} else {
System.out.println("== 0969 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
970. Powerful Integers(强整数)
题目描述
给定三个整数 x、y 和 bound,返回值小于或等于 bound 的所有强整数组成的列表。
如果某一整数可以表示为 x^i + y^j,其中整数 i >= 0 且 j >= 0,那么我们认为该整数
是一个强整数。你可以按任何顺序返回答案。在你的回答中,每个值最多出现一次。
示例 1
- 输入:
x= 2,y= 3, bound = 10 - 输出:[2,3,4,5,7,9,10]
示例 2
- 输入:
x= 3,y= 5, bound = 15 - 输出:[2,4,6,8,10,14]
提示: 1 <= x, y <= 1000 <= bound <= 10^6
解题思路
x^i + y^j <= bound,bound <= 10^6,底数 ≥ 2 时指数不超过约 20。用集合去重。
解法一(双重循环枚举 + 集合,主提交):外层循环枚举 x^i(a),i 从 0 开始,
内层枚举 y^j(b),a + b <= bound 时加入集合;a > bound 或 b > bound 时
停止内层。x == 1 时 a 恒为 1,只需一轮。O(log^2 bound)。
解法二(递归生成对照):先生成 x 的幂列表与 y 的幂列表(各不超过 bound),再
双重循环相加去重。展示"先建幂表再组合"的实现路径。
代码实现
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
// 解法一:双重循环枚举 + 集合(推荐提交)
public List<Integer> powerfulIntegers(int x, int y, int bound) {
Set<Integer> set = new HashSet<>();
for (int a = 1; a <= bound; a *= x) {
for (int b = 1; a + b <= bound; b *= y) {
set.add(a + b);
if (y == 1) {
break; // y == 1 时 b 恒为 1,无需继续
}
}
if (x == 1) {
break; // x == 1 时 a 恒为 1,无需继续
}
}
return new ArrayList<>(set);
}
// 解法二:幂表 + 双重循环(对照实现)
public List<Integer> powerfulIntegers2(int x, int y, int bound) {
List<Integer> px = powers(x, bound);
List<Integer> py = powers(y, bound);
Set<Integer> set = new HashSet<>();
for (int a : px) {
for (int b : py) {
if (a + b <= bound) {
set.add(a + b);
}
}
}
return new ArrayList<>(set);
}
// 生成 base 的所有幂(<= limit;base==1 时只有 {1})
private List<Integer> powers(int base, int limit) {
List<Integer> res = new ArrayList<>();
long v = 1;
while (v <= limit) {
res.add((int) v);
if (base == 1) {
break;
}
v *= base;
}
return res;
}
private static String fmt(List<Integer> a) {
java.util.Collections.sort(a);
return a.toString();
}
private static boolean same(List<Integer> a, List<Integer> b) {
List<Integer> x = new ArrayList<>(a);
List<Integer> y = new ArrayList<>(b);
java.util.Collections.sort(x);
java.util.Collections.sort(y);
return x.equals(y);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] xs = {2, 3, 1, 2, 2};
int[] ys = {3, 5, 2, 2, 99};
int[] bounds = {10, 15, 5, 1, 100};
List<int[]> exps = new ArrayList<>();
exps.add(new int[] {2,3,4,5,7,9,10});
exps.add(new int[] {2,4,6,8,10,14});
exps.add(new int[] {2,3,5});
exps.add(new int[] {});
exps.add(new int[] {2,3,5,9,17,33,65,100});
for (int t = 0; t < xs.length; ++t) {
++total;
List<Integer> g1 = s.powerfulIntegers(xs[t], ys[t], bounds[t]);
List<Integer> g2 = s.powerfulIntegers2(xs[t], ys[t], bounds[t]);
// 转 int 数组比较
int[] a1 = new int[g1.size()];
for (int i = 0; i < a1.length; ++i) {
a1[i] = g1.get(i);
}
int[] a2 = new int[g2.size()];
for (int i = 0; i < a2.length; ++i) {
a2[i] = g2.get(i);
}
if (same(Arrays.asListInt(a1), Arrays.asListInt(a2))
&& same(Arrays.asListInt(a1), Arrays.asListInt(exps.get(t)))) {
++pass;
} else {
System.out.println("FAIL 970-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2));
}
}
if (pass == total) {
System.out.println("== 0970 PASS ==");
} else {
System.out.println("== 0970 FAIL ==");
System.exit(1);
}
}
private static class Arrays {
static List<Integer> asListInt(int[] a) {
List<Integer> l = new ArrayList<>();
for (int v : a) {
l.add(v);
}
return l;
}
}
}
复杂度分析
971. Flip Binary Tree To Match Preorder Traversal(翻转二叉树以匹配先序遍历)
题目描述
给你一棵二叉树的根节点 root,树中有 n 个节点,每个节点都有一个不同于其他节点且
处于 1 到 n 之间的值。另给你一个由 n 个值组成的行程序列 voyage,表示预期的二叉树
先序遍历结果。通过交换节点的左右子树,可以翻转该二叉树中的任意节点。请翻转最少的树中
节点,使二叉树的先序遍历与预期的遍历行程 voyage 相匹配。如果可以,则返回翻转的所有
节点的值的列表(可按任意顺序);如果不能,则返回列表 [-1]。
示例 1
- 输入:
root= [1,2], voyage = [2,1] - 输出:[-1]
示例 2
- 输入:
root= [1,2,3], voyage = [1,3,2] - 输出:[1]
示例 3
- 输入:
root= [1,2,3], voyage = [1,2,3] - 输出:[]
提示: n == voyage.length1 <= n <= 1001 <= Node.val, voyage[i] <= n- 树中的所有值互不相同,
voyage中的所有值互不相同
解题思路
先序遍历的顺序是「根 → 左 → 右」。值互不相同,因此当前节点必须等于 voyage[i]。
递归时若左孩子存在但值不等于下一个期望值,则必须翻转(先走右子树)。失败返回 [-1]。
解法一(DFS 贪心,主提交):全局下标 idx 指向 voyage。递归当前节点:值不匹配
返回 false;否则 idx++,若左孩子存在且值 != voyage[idx],翻转(记录当前节点值
并交换左右)。最后若全程匹配返回翻转列表。O(n)。
解法二(显式栈迭代对照):用栈按"反序"压入期望的先序序列,逐节点校验;无法匹配时
返回 [-1]。展示迭代写法。
代码实现
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
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;
}
}
class Solution {
private int idx;
private List<Integer> flips;
// 解法一:DFS 贪心(推荐提交)
public List<Integer> flipMatchVoyage(TreeNode root, int[] voyage) {
idx = 0;
flips = new ArrayList<>();
boolean ok = dfs(root, voyage);
if (!ok) {
List<Integer> bad = new ArrayList<>();
bad.add(-1);
return bad;
}
return flips;
}
private boolean dfs(TreeNode node, int[] voyage) {
if (node == null) {
return true;
}
if (node.val != voyage[idx]) {
return false;
}
++idx;
if (node.left != null && node.left.val != voyage[idx]) {
// 必须翻转:先遍历右子树,再遍历左子树
flips.add(node.val);
return dfs(node.right, voyage) && dfs(node.left, voyage);
}
return dfs(node.left, voyage) && dfs(node.right, voyage);
}
// 解法二:显式栈迭代(对照实现)
public List<Integer> flipMatchVoyage2(TreeNode root, int[] voyage) {
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> stk = new ArrayDeque<>();
stk.push(root);
int i = 0;
while (!stk.isEmpty()) {
TreeNode node = stk.pop();
if (node == null) {
continue;
}
if (node.val != voyage[i++]) {
List<Integer> bad = new ArrayList<>();
bad.add(-1);
return bad;
}
if (node.left != null && node.left.val != voyage[i]) {
ans.add(node.val);
// 翻转:先压左(后遍历)再压右(先遍历)
if (node.left != null) {
stk.push(node.left);
}
if (node.right != null) {
stk.push(node.right);
}
} else {
if (node.right != null) {
stk.push(node.right);
}
if (node.left != null) {
stk.push(node.left);
}
}
}
return ans;
}
// 用层序数组建树(null 表示缺子节点),供本地测试用
public static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode node = q.poll();
if (arr[i] != null) {
node.left = new TreeNode(arr[i]);
q.offer(node.left);
}
++i;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
q.offer(node.right);
}
++i;
}
return root;
}
// 校验:按翻转列表翻转副本后先序是否等于 voyage
private static boolean verify(TreeNode root, List<Integer> flips, int[] voyage) {
// 用原始树 + flips 模拟:这里直接重建应用翻转后的树再前序遍历
TreeNode copy = deepCopy(root);
java.util.Set<Integer> toFlip = new java.util.HashSet<>(flips);
applyFlip(copy, toFlip);
List<Integer> pre = new ArrayList<>();
preorder(copy, pre);
if (pre.size() != voyage.length) {
return false;
}
for (int k = 0; k < voyage.length; ++k) {
if (pre.get(k) != voyage[k]) {
return false;
}
}
return true;
}
private static TreeNode deepCopy(TreeNode node) {
if (node == null) {
return null;
}
TreeNode c = new TreeNode(node.val);
c.left = deepCopy(node.left);
c.right = deepCopy(node.right);
return c;
}
private static void applyFlip(TreeNode node, java.util.Set<Integer> flips) {
if (node == null) {
return;
}
if (flips.contains(node.val)) {
TreeNode t = node.left;
node.left = node.right;
node.right = t;
}
applyFlip(node.left, flips);
applyFlip(node.right, flips);
}
private static void preorder(TreeNode node, List<Integer> out) {
if (node == null) {
return;
}
out.add(node.val);
preorder(node.left, out);
preorder(node.right, out);
}
private static String fmt(List<Integer> a) {
return a.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
Integer[][] trees = {
{1,2},
{1,2,3},
{1,2,3},
{1},
{1,2,3,4,5,6,7}
};
int[][] voyages = {
{2,1},
{1,3,2},
{1,2,3},
{1},
{1,3,2,5,4,7,6}
};
for (int t = 0; t < trees.length; ++t) {
++total;
TreeNode r1 = build(trees[t]);
TreeNode r2 = build(trees[t]);
List<Integer> g1 = s.flipMatchVoyage(r1, voyages[t]);
List<Integer> g2 = s.flipMatchVoyage2(r2, voyages[t]);
boolean ok1, ok2;
if (!g1.isEmpty() && g1.get(0) == -1) {
ok1 = true; // 题目允许多个失败路径都返回 [-1]
} else {
ok1 = verify(build(trees[t]), g1, voyages[t]);
}
if (!g2.isEmpty() && g2.get(0) == -1) {
ok2 = true;
} else {
ok2 = verify(build(trees[t]), g2, voyages[t]);
}
if (ok1 && ok2) {
++pass;
} else {
System.out.println("FAIL 971-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2));
}
}
if (pass == total) {
System.out.println("== 0971 PASS ==");
} else {
System.out.println("== 0971 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
972. Equal Rational Numbers(相等的有理数)
题目描述
给定两个字符串 s 和 t,每个字符串代表一个非负有理数,只有当它们表示相同的数字时
才返回 true。字符串中可以使用括号来表示有理数的重复部分。
有理数最多可以用三个部分来表示:整数部分 <IntegerPart>、小数非重复部分
<NonRepeatingPart> 和小数重复部分 <(><RepeatingPart><)>。
数字可以用以下三种方法之一来表示:<IntegerPart>(例:0、12);<IntegerPart><.> <NonRepeatingPart>(例:0.5、1.、2.12、123.0001);<IntegerPart><.><NonRepeatingPart> <(><RepeatingPart><)>(例:0.1(6)、1.(9)、123.00(1212))。
示例 1
- 输入:
s= "0.(52)",t= "0.5(25)" - 输出:
true
示例 2
- 输入:
s= "0.1666(6)",t= "0.166(66)" - 输出:
true
示例 3
- 输入:
s= "0.9(9)",t= "1." - 输出:
true - 解释:"0.9(9)" 代表 0.999999999... 永远重复,等于 1。
提示: - 每个部分仅由数字组成;整数部分不会以零开头(零本身除外);
1 <= <IntegerPart>.length <= 4;0 <= <NonRepeatingPart>.length <= 4;1 <= <RepeatingPart>.length <= 4
解题思路
循环小数可精确写成普通分数:
设整数部分 I、非循环部分 N(长 a)、循环节 R(长 b),则
值 = I + N/10^a + R / (10^a * (10^b - 1))
(因为 0.(R) = R / (10^b - 1))。无循环节时退化为 I + N / 10^a。
用 BigInteger 构造分子分母后交叉相乘比较,彻底避免浮点误差。
解法一(精确分数,主提交)如上式。
解法二(循环节展开 + 高精度容差对照):把循环节重复足够多次展开成有限小数,用
double 解析后在极小容差内比较(0.525252... 与 0.525252... 精度足够)。
代码实现
import java.math.BigInteger;
class Solution {
// 解法一:精确分数比较(推荐提交)
public boolean isRationalEqual(String s, String t) {
long[] f1 = parse(s);
long[] f2 = parse(t);
// cross multiply: f1.num * f2.den == f2.num * f1.den
return (new BigInteger(String.valueOf(f1[0])).multiply(BigInteger.valueOf(f2[1])))
.equals(new BigInteger(String.valueOf(f2[0])).multiply(BigInteger.valueOf(f1[1])));
}
// 解析为 [num, den](long 足够:值 <= 9999.9999,分母可达 10^8 级别)
private long[] parse(String str) {
int dot = str.indexOf('.');
String intPart;
String nonrep = "";
String rep = "";
if (dot < 0) {
intPart = str;
} else {
intPart = str.substring(0, dot);
String frac = str.substring(dot + 1);
int lp = frac.indexOf('(');
if (lp < 0) {
nonrep = frac;
} else {
nonrep = frac.substring(0, lp);
rep = frac.substring(lp + 1, frac.length() - 1);
}
}
long I = intPart.isEmpty() ? 0 : Long.parseLong(intPart);
long a = nonrep.length();
long b = rep.length();
if (b == 0) {
// 有限小数:I + N/10^a
long den = pow10(a);
long num = I * den + (a == 0 ? 0 : Long.parseLong(nonrep));
return new long[] {num, den};
}
// 无限循环:I + N/10^a + R/(10^a * (10^b - 1))
long pa = pow10(a);
long pb = pow10(b);
long den = pa * (pb - 1);
long R = Long.parseLong(rep);
long N = a == 0 ? 0 : Long.parseLong(nonrep);
long num = I * den + N * (pb - 1) + R;
return new long[] {num, den};
}
private long pow10(long k) {
long r = 1;
for (long i = 0; i < k; ++i) {
r *= 10;
}
return r;
}
// 解法二:循环节展开 + double 容差比较(对照实现)
public boolean isRationalEqual2(String s, String t) {
double a = expand(s);
double b = expand(t);
return Math.abs(a - b) < 1e-9;
}
private double expand(String str) {
int dot = str.indexOf('.');
String intPart;
String nonrep = "";
String rep = "";
if (dot < 0) {
intPart = str;
} else {
intPart = str.substring(0, dot);
String frac = str.substring(dot + 1);
int lp = frac.indexOf('(');
if (lp < 0) {
nonrep = frac;
} else {
nonrep = frac.substring(0, lp);
rep = frac.substring(lp + 1, frac.length() - 1);
}
}
StringBuilder mant = new StringBuilder(nonrep);
for (int i = 0; i < 20; ++i) {
mant.append(rep);
}
return Double.parseDouble(intPart + "." + mant.toString());
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] pairs = {
{"0.(52)", "0.5(25)"},
{"0.1666(6)", "0.166(66)"},
{"0.9(9)", "1."},
{"1", "1.0"},
{"0.(0)", "0"},
{"1.2(34)", "1.2343434(34)"},
{"0.", "0"},
{"1.9(9)", "2."}
};
boolean[] exp = {true, true, true, true, true, true, true, true};
for (int t = 0; t < pairs.length; ++t) {
++total;
boolean g1 = s.isRationalEqual(pairs[t][0], pairs[t][1]);
boolean g2 = s.isRationalEqual2(pairs[t][0], pairs[t][1]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 972-" + (t + 1) + ": " + pairs[t][0] + " vs " + pairs[t][1]
+ " got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0972 PASS ==");
} else {
System.out.println("== 0972 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
973. K Closest Points to Origin(最接近原点的 K 个点)
题目描述
给定一个数组 points,其中 points[i] = [x_i, y_i] 表示 X-Y 平面上的一个点,并且是
一个整数 k,返回离原点 (0,0) 最近的 k 个点。这里,平面上两点之间的距离是欧几里德
距离。你可以按任何顺序返回答案。除了点坐标的顺序之外,答案确保是唯一的。
示例 1
- 输入:points = [[1,3],[-2,2]],
k= 1 - 输出:[[-2,2]]
示例 2
- 输入:points = [[3,3],[5,-1],[-2,4]],
k= 2 - 输出:[[3,3],[-2,4]]
提示: 1 <= k <= points.length <= 10^4-10^4 <= x_i, y_i <= 10^4
解题思路
距离平方 x^2 + y^2 即可比较大小,无需开根。
解法一(快速选择/排序取前 k,主提交):按距离平方排序后取前 k 个。
O(n log n)。
解法二(堆,对照实现):维护大小为 k 的最大堆,堆顶是当前最远点;遍历每个点时,
若其距离小于堆顶则替换。O(n log k)。
代码实现
import java.util.Arrays;
import java.util.PriorityQueue;
class Solution {
// 解法一:排序取前 k(推荐提交)
public int[][] kClosest(int[][] points, int k) {
Integer[] idx = new Integer[points.length];
for (int i = 0; i < idx.length; ++i) {
idx[i] = i;
}
Arrays.sort(idx, (a, b) -> {
long da = (long) points[a][0] * points[a][0] + (long) points[a][1] * points[a][1];
long db = (long) points[b][0] * points[b][0] + (long) points[b][1] * points[b][1];
return Long.compare(da, db);
});
int[][] ans = new int[k][2];
for (int i = 0; i < k; ++i) {
ans[i] = points[idx[i]];
}
return ans;
}
// 解法二:最大堆(对照实现)
public int[][] kClosest2(int[][] points, int k) {
// 堆顶是当前最远点(最大距离)
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {
long db = (long) b[0] * b[0] + (long) b[1] * b[1];
long da = (long) a[0] * a[0] + (long) a[1] * a[1];
return Long.compare(db, da); // 距离大的优先(最大堆)
});
for (int[] p : points) {
pq.offer(p);
if (pq.size() > k) {
pq.poll();
}
}
return pq.toArray(new int[k][2]);
}
private static String fmt(int[][] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; ++i) {
if (i > 0) {
sb.append(", ");
}
sb.append(Arrays.toString(a[i]));
}
return sb.append(']').toString();
}
private static boolean sameSet(int[][] a, int[][] b) {
if (a.length != b.length) {
return false;
}
java.util.Set<String> sa = new java.util.HashSet<>();
for (int[] p : a) {
sa.add(p[0] + "," + p[1]);
}
for (int[] p : b) {
if (!sa.contains(p[0] + "," + p[1])) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] pts = {
{{1,3},{-2,2}},
{{3,3},{5,-1},{-2,4}},
{{0,2},{1,0}},
{{-1,0},{0,-3},{2,0}},
{{1,1}}
};
int[] ks = {1, 2, 1, 2, 1};
int[][][] exps = {
{{-2,2}},
{{3,3},{-2,4}},
{{1,0}},
{{-1,0},{2,0}},
{{1,1}}
};
for (int t = 0; t < pts.length; ++t) {
++total;
int[][] g1 = s.kClosest(pts[t], ks[t]);
int[][] g2 = s.kClosest2(pts[t], ks[t]);
if (sameSet(g1, exps[t]) && sameSet(g2, exps[t])) {
++pass;
} else {
System.out.println("FAIL 973-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2) + " exp " + fmt(exps[t]));
}
}
if (pass == total) {
System.out.println("== 0973 PASS ==");
} else {
System.out.println("== 0973 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
974. Subarray Sums Divisible by K(和可被 K 整除的子数组)
题目描述
给定一个整数数组 nums 和一个整数 k,返回其中元素之和可被 k 整除的非空子数组的
数目。子数组是数组中连续的部分。
示例 1
- 输入:
nums= [4,5,0,-2,-3,1],k= 5 - 输出:7
- 解释:有 7 个子数组满足其元素之和可被
k= 5 整除:
[4,5,0,-2,-3,1]、[5]、[5,0]、[5,0,-2,-3]、[0]、[0,-2,-3]、[-2,-3]。
示例 2
- 输入:
nums= [5],k= 9 - 输出:0
提示: 1 <= nums.length <= 3 * 10^4-10^4 <= nums[i] <= 10^42 <= k <= 10^4
解题思路
子数组和能被 k 整除,当且仅当两端前缀和模 k 相同:
(pre[j] - pre[i]) % k == 0 等价于 pre[j] % k == pre[i] % k。
解法一(哈希表 + 前缀和,主提交):从左到右扫描,cnt[r] 记录「前缀和模 k = r 的
已出现次数」。对每个位置 j = {r = (sum mod k + k) % k},它贡献的合法子数组数 =
cnt[r](以 j 结尾、起点 i 满足 pre[i] mod k == r 的子数组)。初始 cnt[0] = 1
对应空前缀。O(n)。
解法二(计数数组对照):由于 k <= 10^4,直接用定长数组替代哈希表统计余数,
逻辑完全一致,展示省内存版本。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:哈希表 + 前缀和(推荐提交)
public int subarraysDivByK(int[] nums, int k) {
Map<Integer, Integer> cnt = new HashMap<>();
cnt.put(0, 1);
int sum = 0;
int ans = 0;
for (int v : nums) {
sum += v;
int r = ((sum % k) + k) % k; // Java 的 % 可负数,归一化到 [0, k-1]
int c = cnt.getOrDefault(r, 0);
ans += c;
cnt.put(r, c + 1);
}
return ans;
}
// 解法二:定长计数数组(对照实现)
public int subarraysDivByK2(int[] nums, int k) {
int[] cnt = new int[k];
cnt[0] = 1;
int sum = 0;
int ans = 0;
for (int v : nums) {
sum += v;
int r = ((sum % k) + k) % k;
ans += cnt[r];
cnt[r]++;
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] nums = {
{4,5,0,-2,-3,1},
{5},
{0},
{-1,2,1,2},
{2,2,2}
};
int[] ks = {5, 9, 1, 6, 2};
int[] exp = {7, 0, 1, 0, 6};
for (int t = 0; t < nums.length; ++t) {
++total;
int g1 = s.subarraysDivByK(nums[t].clone(), ks[t]);
int g2 = s.subarraysDivByK2(nums[t].clone(), ks[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 974-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0974 PASS ==");
} else {
System.out.println("== 0974 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
975. Odd Even Jump(奇偶跳)
题目描述
给定一个整数数组 arr,你可以从某一起始索引出发,跳跃一定次数。在你跳跃的过程中,
第 1、3、5... 次跳跃称为奇数跳跃,而第 2、4、6... 次跳跃称为偶数跳跃。
你可以按以下方式从索引 i 向后跳转到索引 j(其中 i < j):
- 奇数跳跃:跳到索引
j,使得arr[i] <= arr[j],且arr[j]的值尽可能小;如果存在 多个这样的索引j,跳到满足要求的最小索引上。 - 偶数跳跃:跳到索引
j,使得arr[i] >= arr[j],且arr[j]的值尽可能大;如果存在 多个这样的索引j,跳到满足要求的最小索引上。 如果从某一索引开始跳跃一定次数(可能是 0 次或多次),就可以到达数组的末尾(索引arr.length - 1),那么该索引就会被认为是好的起始索引。返回好的起始索引的数量。
示例 1
- 输入:[10,13,12,14,15]
- 输出:2
示例 2
- 输入:[2,3,1,1,4]
- 输出:3
示例 3
- 输入:[5,1,3,4,2]
- 输出:3 提示:
1 <= arr.length <= 200000 <= arr[i] < 100000
解题思路
每一步的落点只与"右侧大于等于当前值的最小值"或"右侧小于等于当前值的最大值"有关, 与之前的跳法无关。从右往左预处理每个位置的下一步:
- 奇数跳
oddNext[i]:右侧>= arr[i]的最小值的最小下标; - 偶数跳
evenNext[i]:右侧<= arr[i]的最大值的最小下标。 用有序集合(TreeMap)维护「值 → 最小下标」(从右往左扫描时后出现的更小下标覆盖), 查询ceilingKey/floorKey。 然后从右往左 DP:odd[i]= 从 i 出发奇数跳能否到终点(oddNext[i] == -1时 False, 否则看even[oddNext[i]]);even[i]对称。答案 =odd[i]为真的个数。 解法一(TreeMap + 反向 DP,主提交)如上。 解法二(排序 + 树状数组对照):把下标按值排序后,用两个有序集合分别维护已扫描的 值集合与下标,查询后继/前驱;逻辑同构,展示另一种数据结构的实现。
代码实现
import java.util.Arrays;
import java.util.TreeMap;
class Solution {
// 解法一:TreeMap + 反向 DP(推荐提交)
public int oddEvenJumps(int[] arr) {
int n = arr.length;
int[] oddNext = new int[n];
int[] evenNext = new int[n];
Arrays.fill(oddNext, -1);
Arrays.fill(evenNext, -1);
TreeMap<Integer, Integer> map = new TreeMap<>(); // 值 -> 最小下标
for (int i = n - 1; i >= 0; --i) {
Integer ck = map.ceilingKey(arr[i]); // >= arr[i] 的最小值
Integer fk = map.floorKey(arr[i]); // <= arr[i] 的最大值
if (ck != null) {
oddNext[i] = map.get(ck);
}
if (fk != null) {
evenNext[i] = map.get(fk);
}
map.put(arr[i], i); // 同值取最小下标(从左往右更早的在右边,此处覆盖为更小的)
}
boolean[] odd = new boolean[n];
boolean[] even = new boolean[n];
odd[n - 1] = true;
even[n - 1] = true;
int ans = 1;
for (int i = n - 2; i >= 0; --i) {
if (oddNext[i] != -1) {
odd[i] = even[oddNext[i]];
}
if (evenNext[i] != -1) {
even[i] = odd[evenNext[i]];
}
if (odd[i]) {
++ans;
}
}
return ans;
}
// 解法二:值排序 + 有序下标集合(对照实现)
public int oddEvenJumps2(int[] arr) {
int n = arr.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; ++i) {
idx[i] = i;
}
// 按值升序;值相同按下标升序
Arrays.sort(idx, (a, b) -> arr[a] != arr[b] ? Integer.compare(arr[a], arr[b]) : Integer.compare(a, b));
int[] oddNext = buildNext(idx, arr, true);
// 按值降序;值相同按下标升序
Arrays.sort(idx, (a, b) -> arr[a] != arr[b] ? Integer.compare(arr[b], arr[a]) : Integer.compare(a, b));
int[] evenNext = buildNext(idx, arr, false);
boolean[] odd = new boolean[n];
boolean[] even = new boolean[n];
odd[n - 1] = true;
even[n - 1] = true;
int ans = 1;
for (int i = n - 2; i >= 0; --i) {
if (oddNext[i] != -1) {
odd[i] = even[oddNext[i]];
}
if (evenNext[i] != -1) {
even[i] = odd[evenNext[i]];
}
if (odd[i]) {
++ans;
}
}
return ans;
}
// 按排序后的密集下标顺序,维护已扫描下标集合,找 i 右侧(> i)最小下标
// 升序扫描时,已处理的都是值 <= 当前值的候选;要求下标 > i 且最小。
private int[] buildNext(Integer[] order, int[] arr, boolean asc) {
int n = arr.length;
int[] next = new int[n];
Arrays.fill(next, -1);
java.util.TreeSet<Integer> seen = new java.util.TreeSet<>();
for (int i = n - 1; i >= 0; --i) {
int pos = order[i];
Integer hi = seen.higher(pos);
if (hi != null) {
next[pos] = hi;
}
seen.add(pos);
}
return next;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{10,13,12,14,15},
{2,3,1,1,4},
{5,1,3,4,2},
{1},
{1,2}
};
int[] exp = {2, 3, 3, 1, 2};
for (int t = 0; t < arrs.length; ++t) {
++total;
int g1 = s.oddEvenJumps(arrs[t].clone());
int g2 = s.oddEvenJumps2(arrs[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 975-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0975 PASS ==");
} else {
System.out.println("== 0975 FAIL ==");
System.exit(1);
}
}
}