LeetCode 801-813. 题解合集
801. Minimum Swaps To Make Sequences Increasing(使序列递增的最小交换次数)
题目描述
我们有两个长度相等且不为空的整型数组 nums1 和 nums2。在一次操作中,我们可以交换
nums1[i] 和 nums2[i] 的元素。
例如,如果 nums1 = [1,2,3,8],nums2 = [5,6,7,4],你可以交换 i = 3 处的元素,
得到 nums1 = [1,2,3,4] 和 nums2 = [5,6,7,8]。
返回使 nums1 和 nums2 严格递增所需操作的最小次数。
数组 arr 严格递增 且 arr[0] < arr[1] < arr[2] < ... < arr[arr.length - 1]。
注意:用例保证可以实现操作。
示例 1
- 输入:nums1 = [1,3,5,4], nums2 = [1,2,3,7]
- 输出:1
- 解释:交换 A[3] 和 B[3] 后,A = [1,3,5,7],B = [1,2,3,4],两个数组均严格递增。
示例 2
- 输入:nums1 = [0,3,5,8,9], nums2 = [2,1,4,6,9]
- 输出:1
提示: 2 <= nums1.length <= 10^5nums2.length == nums1.length0 <= nums1[i], nums2[i] <= 2 * 10^5
解题思路
每个下标可选择交换或不交换,穷举全部 2^n 方案在 n <= 10^5 时不可行。某一位置能否保持
两条序列严格递增,只取决于它与前一位置在交换后的相对大小。
解法一(滚动动态规划,主提交):用 a、b 表示做到当前位置且「本位置不换 / 本位置
交换」的最少次数,转移按与前一位是否已递增、以及交叉后是否仍递增分情况讨论。
解法二(带数组的动态规划记忆版):逻辑与解法一同源,用两个数组保存每一位置的两种状态,
便于推演状态转移表(对照实现,直观展示 DP 过程)。
代码实现
class Solution {
// 解法一:滚动变量 DP(推荐提交)
public int minSwap(int[] nums1, int[] nums2) {
int a = 0, b = 1; // a: 不交换,b: 交换
for (int i = 1; i < nums1.length; ++i) {
int x = a, y = b;
if (nums1[i - 1] >= nums1[i] || nums2[i - 1] >= nums2[i]) {
// 前一位与这一位必须交换状态才可能合法
a = y; // 前一位交换 -> 这一位不交换
b = x + 1; // 前一位不交换 -> 这一位必须交换
} else {
// 前后可不交换,这一位交换也可行
b = y + 1;
if (nums1[i - 1] < nums2[i] && nums2[i - 1] < nums1[i]) {
// 交叉之后仍严格递增,两种状态都可以取较小值
a = Math.min(a, y);
b = Math.min(b, x + 1);
}
}
}
return Math.min(a, b);
}
// 解法二:数组版 DP(便于观察状态表)
public int minSwap2(int[] nums1, int[] nums2) {
int n = nums1.length;
int[] keep = new int[n]; // keep[i]: i 不交换的最小次数
int[] swap = new int[n]; // swap[i]: i 交换的最小次数
keep[0] = 0;
swap[0] = 1;
for (int i = 1; i < n; ++i) {
keep[i] = swap[i] = Integer.MAX_VALUE;
if (nums1[i - 1] < nums1[i] && nums2[i - 1] < nums2[i]) {
keep[i] = Math.min(keep[i], keep[i - 1]);
swap[i] = Math.min(swap[i], swap[i - 1] + 1);
}
if (nums1[i - 1] < nums2[i] && nums2[i - 1] < nums1[i]) {
keep[i] = Math.min(keep[i], swap[i - 1]);
swap[i] = Math.min(swap[i], keep[i - 1] + 1);
}
}
return Math.min(keep[n - 1], swap[n - 1]);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.minSwap(new int[]{1, 3, 5, 4}, new int[]{1, 2, 3, 7}) == 1) ++pass; else System.out.println("FAIL 801-1");
++total; if (sol.minSwap(new int[]{0, 3, 5, 8, 9}, new int[]{2, 1, 4, 6, 9}) == 1) ++pass; else System.out.println("FAIL 801-2");
++total; if (sol.minSwap(new int[]{0, 2}, new int[]{1, 3}) == 0) ++pass; else System.out.println("FAIL 801-3");
++total; if (sol.minSwap(new int[]{0, 4, 4, 5, 9}, new int[]{0, 1, 6, 8, 10}) == 1) ++pass; else System.out.println("FAIL 801-4");
++total; if (sol.minSwap(new int[]{0, 4, 4, 5, 9}, new int[]{0, 1, 6, 8, 10}) == 1) ++pass; else System.out.println("FAIL 801-5");
++total; if (sol.minSwap2(new int[]{1, 3, 5, 4}, new int[]{1, 2, 3, 7}) == 1) ++pass; else System.out.println("FAIL 801-6");
++total; if (sol.minSwap2(new int[]{0, 3, 5, 8, 9}, new int[]{2, 1, 4, 6, 9}) == 1) ++pass; else System.out.println("FAIL 801-7");
++total; if (sol.minSwap2(new int[]{0, 4, 4, 5, 9}, new int[]{0, 1, 6, 8, 10}) == 1) ++pass; else System.out.println("FAIL 801-8");
if (pass == total) {
System.out.println("== 0801 PASS ==");
} else {
System.out.println("== 0801 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(n) |
802. Find Eventual Safe States(找到最终的安全状态)
题目描述
有一个有 n 个节点的有向图,节点按 0 到 n - 1 编号。图由一个索引从 0 开始的 2D 整数
数组 graph 表示,graph[i] 是与节点 i 相邻的节点的整数数组,这意味着从节点 i 到
graph[i] 中的每个节点都有一条边。
如果一个节点没有连出的有向边,则该节点是终端节点。如果从该节点开始的所有可能路径都通向
终端节点(或另一个安全节点),则该节点为安全节点。
返回一个由图中所有安全节点组成的数组作为答案。答案数组中的元素应当按升序排列。
示例 1
- 输入:
graph= [[1,2],[2,3],[5],[0],[5],[],[]] - 输出:[2,4,5,6]
示例 2
- 输入:
graph= [[1,2,3,4],[1,2],[3,4],[0,4],[]] - 输出:[4]
提示: n == graph.length1 <= n <= 10^4graph[i]按严格递增顺序排列- 图中可能包含自环
- 图中边的数目在范围 [1, 4 * 10^4] 内
解题思路
出度为零的点是安全的,如果一个点只能到达安全的点,那么它同样是安全的。
解法一(拓扑排序,主提交):将图中所有边反向,得到一个反图,然后在反图上运行拓扑排序;
能入度归零的点即为安全点。
解法二(DFS + 三色标记法):若起始节点位于一个环内,或者能到达一个环,则该节点不是安全的;
用 0/1/2 三色标记未访问、访问中、已确认安全,递归判定。
代码实现
class Solution {
// 解法一:反向图拓扑排序(推荐提交)
public java.util.List<Integer> eventualSafeNodes(int[][] graph) {
int n = graph.length;
int[] indeg = new int[n];
java.util.List<Integer>[] rg = new java.util.List[n];
for (int i = 0; i < n; ++i) {
rg[i] = new java.util.ArrayList<>();
}
java.util.Deque<Integer> q = new java.util.ArrayDeque<>();
for (int i = 0; i < n; ++i) {
for (int j : graph[i]) {
rg[j].add(i);
}
indeg[i] = graph[i].length;
if (indeg[i] == 0) {
q.offer(i);
}
}
while (!q.isEmpty()) {
int i = q.pollFirst();
for (int j : rg[i]) {
if (--indeg[j] == 0) {
q.offer(j);
}
}
}
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int i = 0; i < n; ++i) {
if (indeg[i] == 0) {
ans.add(i);
}
}
return ans;
}
// 解法二:DFS 三色标记
private int[] color;
private int[][] g;
public java.util.List<Integer> eventualSafeNodes2(int[][] graph) {
int n = graph.length;
color = new int[n];
g = graph;
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int i = 0; i < n; ++i) {
if (dfs(i)) {
ans.add(i);
}
}
return ans;
}
private boolean dfs(int i) {
if (color[i] > 0) {
return color[i] == 2;
}
color[i] = 1;
for (int j : g[i]) {
if (!dfs(j)) {
return false;
}
}
color[i] = 2;
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.eventualSafeNodes(new int[][]{{1, 2}, {2, 3}, {5}, {0}, {5}, {}, {}}).equals(java.util.Arrays.asList(2, 4, 5, 6))) ++pass; else System.out.println("FAIL 802-1");
++total; if (sol.eventualSafeNodes(new int[][]{{1, 2, 3, 4}, {1, 2}, {3, 4}, {0, 4}, {}}).equals(java.util.Arrays.asList(4))) ++pass; else System.out.println("FAIL 802-2");
++total; if (sol.eventualSafeNodes(new int[][]{}).equals(java.util.Arrays.asList())) ++pass; else System.out.println("FAIL 802-3");
++total; if (sol.eventualSafeNodes(new int[][]{{1}, {1}}).equals(java.util.Arrays.asList())) ++pass; else System.out.println("FAIL 802-4");
++total; if (sol.eventualSafeNodes(new int[][]{{}, {0, 2}, {0}}).equals(java.util.Arrays.asList(0, 1, 2))) ++pass; else System.out.println("FAIL 802-5");
++total; if (sol.eventualSafeNodes2(new int[][]{{1, 2}, {2, 3}, {5}, {0}, {5}, {}, {}}).equals(java.util.Arrays.asList(2, 4, 5, 6))) ++pass; else System.out.println("FAIL 802-6");
++total; if (sol.eventualSafeNodes2(new int[][]{{1, 2, 3, 4}, {1, 2}, {3, 4}, {0, 4}, {}}).equals(java.util.Arrays.asList(4))) ++pass; else System.out.println("FAIL 802-7");
++total; if (sol.eventualSafeNodes2(new int[][]{{}, {0, 2}, {0}}).equals(java.util.Arrays.asList(0, 1, 2))) ++pass; else System.out.println("FAIL 802-8");
if (pass == total) {
System.out.println("== 0802 PASS ==");
} else {
System.out.println("== 0802 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m) | O(n + m) |
| 解法二 | O(n + m) | O(n + m) |
803. Bricks Falling When Hit(打砖块)
题目描述
有一个 m x n 的二元网格 grid,其中 1 表示砖块,0 表示空白。砖块稳定(不会掉落)
的前提是:一块砖直接连接到网格的顶部,或者至少有一块相邻(4 个方向之一)砖块稳定不会掉落。
给你一个数组 hits,这是需要依次消除砖块的位置。每当消除 hits[i] = (rowi, coli) 位置
上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而掉落。
返回一个数组 result,其中 result[i] 表示第 i 次消除操作对应掉落的砖块数目。
注意,消除可能指向没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。
示例 1
- 输入:
grid= [[1,0,0,0],[1,1,1,0]], hits = [[1,0]] - 输出:[2]
示例 2
- 输入:
grid= [[1,0,0,0],[1,1,0,0]], hits = [[1,1],[1,0]] - 输出:[0,0]
提示: m == grid.length, n == grid[i].length, 1 <= m, n <= 200grid[i][j]为 0 或 11 <= hits.length <= 4 * 10^4- 所有 (xi, yi) 互不相同
解题思路
每次打击后正向洪水填充统计掉落,hits 达 4*10^4、m,n <= 200,重复搜索过慢。掉落的本质是
「与顶行的连通被切断」。
解法一(并查集逆序补砖,主提交):先打掉全部打击位置,用并查集把剩余砖块连到一个虚拟屋顶,
再按相反顺序补回砖块。补回后屋顶连通块的增量减一,即该次打击实际掉落的砖数;原先就是
空格的打击贡献为零。
解法二(暴力模拟,对照实现):按 hits 顺序模拟,每次打击后 BFS/DFS 统计与顶部失联的砖块数,
用于小规模数据验证。
代码实现
class Solution {
private int[] p;
private int[] size;
// 解法一:并查集逆序(推荐提交)
public int[] hitBricks(int[][] grid, int[][] hits) {
int m = grid.length;
int n = grid[0].length;
p = new int[m * n + 1];
size = new int[m * n + 1];
for (int i = 0; i < p.length; ++i) {
p[i] = i;
size[i] = 1;
}
int[][] g = new int[m][n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
g[i][j] = grid[i][j];
}
}
// 先全部打掉
for (int[] h : hits) {
g[h[0]][h[1]] = 0;
}
// 屋顶
for (int j = 0; j < n; ++j) {
if (g[0][j] == 1) {
union(j, m * n);
}
}
// 剩余砖块内部连边
for (int i = 1; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (g[i][j] == 0) {
continue;
}
if (g[i - 1][j] == 1) {
union(i * n + j, (i - 1) * n + j);
}
if (j > 0 && g[i][j - 1] == 1) {
union(i * n + j, i * n + j - 1);
}
}
}
int[] ans = new int[hits.length];
int[] dirs = {-1, 0, 1, 0, -1};
for (int k = hits.length - 1; k >= 0; --k) {
int i = hits[k][0];
int j = hits[k][1];
if (grid[i][j] == 0) {
continue; // 原位置没有砖,掉落为 0
}
g[i][j] = 1;
int prev = size[find(m * n)];
if (i == 0) {
union(j, m * n);
}
for (int l = 0; l < 4; ++l) {
int x = i + dirs[l];
int y = j + dirs[l + 1];
if (x >= 0 && x < m && y >= 0 && y < n && g[x][y] == 1) {
union(i * n + j, x * n + y);
}
}
int curr = size[find(m * n)];
ans[k] = Math.max(0, curr - prev - 1);
}
return ans;
}
private int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
private void union(int a, int b) {
int pa = find(a);
int pb = find(b);
if (pa != pb) {
size[pb] += size[pa];
p[pa] = pb;
}
}
// 解法二:暴力模拟(对照验证)
public int[] hitBricks2(int[][] grid, int[][] hits) {
int m = grid.length;
int n = grid[0].length;
int[][] g = new int[m][n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
g[i][j] = grid[i][j];
}
}
int[] ans = new int[hits.length];
int[] dirs = {-1, 0, 1, 0, -1};
for (int k = 0; k < hits.length; ++k) {
int i = hits[k][0];
int j = hits[k][1];
if (g[i][j] == 0) {
continue;
}
g[i][j] = 0;
// 找出与顶部失联的砖块
boolean[][] top = new boolean[m][n];
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
for (int c = 0; c < n; ++c) {
if (g[0][c] == 1) {
top[0][c] = true;
q.offer(new int[]{0, c});
}
}
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int l = 0; l < 4; ++l) {
int x = cur[0] + dirs[l];
int y = cur[1] + dirs[l + 1];
if (x >= 0 && x < m && y >= 0 && y < n && g[x][y] == 1 && !top[x][y]) {
top[x][y] = true;
q.offer(new int[]{x, y});
}
}
}
int fall = 0;
for (int x = 0; x < m; ++x) {
for (int y = 0; y < n; ++y) {
if (g[x][y] == 1 && !top[x][y]) {
g[x][y] = 0;
++fall;
}
}
}
ans[k] = fall;
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1, 0, 0, 0}, {1, 1, 1, 0}}, new int[][]{{1, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-1");
++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1, 0, 0, 0}, {1, 1, 0, 0}}, new int[][]{{1, 1}, {1, 0}}), new int[]{0, 0})) ++pass; else System.out.println("FAIL 803-2");
++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1}, {1}, {1}}, new int[][]{{0, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-3");
++total; if (java.util.Arrays.equals(sol.hitBricks(new int[][]{{1, 1, 1}, {1, 0, 1}, {1, 1, 1}}, new int[][]{{0, 1}}), new int[]{0})) ++pass; else System.out.println("FAIL 803-4");
++total; if (java.util.Arrays.equals(sol.hitBricks2(new int[][]{{1, 0, 0, 0}, {1, 1, 1, 0}}, new int[][]{{1, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-5");
++total; if (java.util.Arrays.equals(sol.hitBricks2(new int[][]{{1, 0, 0, 0}, {1, 1, 0, 0}}, new int[][]{{1, 1}, {1, 0}}), new int[]{0, 0})) ++pass; else System.out.println("FAIL 803-6");
++total; if (java.util.Arrays.equals(sol.hitBricks2(new int[][]{{1}, {1}, {1}}, new int[][]{{0, 0}}), new int[]{2})) ++pass; else System.out.println("FAIL 803-7");
if (pass == total) {
System.out.println("== 0803 PASS ==");
} else {
System.out.println("== 0803 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | ||
|---|---|---|---|---|
| 解法一 | O(mnalpha + | hits | *alpha) | O(m*n) |
| 解法二 | O( | hits | * m * n) | O(m*n) |
804. Unique Morse Code Words(唯一摩尔斯密码词)
题目描述
国际摩尔斯密码定义一种标准编码方式,将每个字母对应于一个由一系列点和短线组成的字符串,
比如:'a' 对应 ".-" ,'b' 对应 "-..." ,'c' 对应 "-.-." ,以此类推。为了方便,所有 26 个
英文字母的摩尔斯密码表如下:
[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.",
"---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]
给你一个字符串数组 words,每个单词可以写成每个字母对应摩尔斯密码的组合。例如,"cab"
可以写成 "-.-..--..."(即 "-.-." + ".-" + "-..." 字符串的结合)。我们将这样一个连接过程
称作单词翻译。
对 words 中所有单词进行单词翻译,返回不同单词翻译的数量。
示例 1
- 输入:
words= ["gin", "zen", "gig", "msg"] - 输出: 2
示例 2
- 输入:
words= ["a"] - 输出:1
提示: 1 <= words.length <= 1001 <= words[i].length <= 12words[i]由小写英文字母组成
解题思路
每个单词按字母表映射成摩尔斯串后,需要统计有多少种不同编码。words 至多 100 个、长度不
超过 12,直接变换即可。
解法一(哈希集合,主提交):用集合存放变换结果,最后取集合大小。
解法二(排序去重,对照实现):把所有编码收集到字符串数组中,排序后数不同编码的段数。
代码实现
class Solution {
private static final String[] CODES = {
".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..",
"--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-",
"-.--", "--.."
};
// 解法一:哈希集合(推荐提交)
public int uniqueMorseRepresentations(String[] words) {
java.util.Set<String> set = new java.util.HashSet<>();
for (String word : words) {
StringBuilder sb = new StringBuilder();
for (char c : word.toCharArray()) {
sb.append(CODES[c - 'a']);
}
set.add(sb.toString());
}
return set.size();
}
// 解法二:排序 + 去重计数
public int uniqueMorseRepresentations2(String[] words) {
String[] enc = new String[words.length];
for (int i = 0; i < words.length; ++i) {
StringBuilder sb = new StringBuilder();
for (char c : words[i].toCharArray()) {
sb.append(CODES[c - 'a']);
}
enc[i] = sb.toString();
}
java.util.Arrays.sort(enc);
int cnt = 1;
for (int i = 1; i < enc.length; ++i) {
if (!enc[i].equals(enc[i - 1])) {
++cnt;
}
}
return cnt;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.uniqueMorseRepresentations(new String[]{"gin", "zen", "gig", "msg"}) == 2) ++pass; else System.out.println("FAIL 804-1");
++total; if (sol.uniqueMorseRepresentations(new String[]{"a"}) == 1) ++pass; else System.out.println("FAIL 804-2");
++total; if (sol.uniqueMorseRepresentations(new String[]{"abc"}) == 1) ++pass; else System.out.println("FAIL 804-3");
++total; if (sol.uniqueMorseRepresentations(new String[]{"rwjje", "aittjje", "auyyn", "lqtktn", "lmjwn"}) == 1) ++pass; else System.out.println("FAIL 804-4");
++total; if (sol.uniqueMorseRepresentations2(new String[]{"gin", "zen", "gig", "msg"}) == 2) ++pass; else System.out.println("FAIL 804-5");
++total; if (sol.uniqueMorseRepresentations2(new String[]{"a"}) == 1) ++pass; else System.out.println("FAIL 804-6");
++total; if (sol.uniqueMorseRepresentations2(new String[]{"abc"}) == 1) ++pass; else System.out.println("FAIL 804-7");
if (pass == total) {
System.out.println("== 0804 PASS ==");
} else {
System.out.println("== 0804 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n * L) | O(n * L) |
| 解法二 | O(n * L log(n * L)) | O(n * L) |
805. Split Array With Same Average(数组的均值分割)
题目描述
给定你一个整数数组 nums。我们要将 nums 数组中的每个元素移动到 A 数组或者 B 数组中,
使得 A 数组和 B 数组不为空,并且 average(A) == average(B)。如果可以完成则返回 true,
否则返回 false。
注意:对于数组 arr,average(arr) 是 arr 的所有元素的和除以 arr 长度。
示例 1
- 输入:
nums= [1,2,3,4,5,6,7,8] - 输出:
true - 解释: 我们可以将数组分割为 [1,4,5,8] 和 [2,3,6,7],他们的平均值都是 4.5。
示例 2
- 输入:
nums= [3,1] - 输出:
false
提示: 1 <= nums.length <= 300 <= nums[i] <= 10^4
解题思路
将数组分成两个非空子集且平均值相等,等价于存在大小为 k 的子集其和为 k * avg。
先把每个数变为 n * a_i - S,则目标转化为「存在非空真子集其和为 0」。
解法一(折半枚举 + 哈希,主提交):n <= 30,折半后每半约 15 个数,二进制枚举子集和;
左半子集和放入集合,右半枚举时若自身为 0 或其相反数已在左半出现(且不是全集对全集),即可判定。
解法二(双向生成 + 排序双指针,对照实现):左右两半各生成所有子集和与大小配对,排序后用
双指针合并检查 <0 的目标和,逻辑直观但实现更细。
代码实现
class Solution {
// 解法一:折半枚举 + 哈希(推荐提交)
public boolean splitArraySameAverage(int[] nums) {
int n = nums.length;
if (n == 1) {
return false;
}
int sum = 0;
for (int v : nums) {
sum += v;
}
// 每个数乘 n 减总量,目标变为找和为 0 的非空真子集
int[] a = new int[n];
for (int i = 0; i < n; ++i) {
a[i] = nums[i] * n - sum;
}
int mid = n / 2;
// 左半所有子集和
java.util.Set<Integer> left = new java.util.HashSet<>();
int leftFull = 0;
for (int i = 0; i < mid; ++i) {
leftFull += a[i];
}
left.add(0); // 空集
left.add(leftFull); // 全集
for (int mask = 1; mask < (1 << mid) - 1; ++mask) {
int s = 0;
for (int i = 0; i < mid; ++i) {
if ((mask & (1 << i)) != 0) {
s += a[i];
}
}
left.add(s);
}
int rightFull = 0;
for (int i = mid; i < n; ++i) {
rightFull += a[i];
}
// 右半枚举(从 1 开始排除空集;右半全集对应 A 为空,同样排除)
for (int mask = 1; mask < (1 << (n - mid)) - 1; ++mask) {
int s = 0;
for (int i = mid; i < n; ++i) {
if ((mask & (1 << (i - mid))) != 0) {
s += a[i];
}
}
if (s == 0 || left.contains(-s)) {
return true;
}
}
return false;
}
// 解法二:双向生成 + 排序双指针合并
public boolean splitArraySameAverage2(int[] nums) {
int n = nums.length;
if (n == 1) {
return false;
}
int sum = 0;
for (int v : nums) {
sum += v;
}
int[] a = new int[n];
for (int i = 0; i < n; ++i) {
a[i] = nums[i] * n - sum;
}
int mid = n / 2;
java.util.ArrayList<Integer> L1 = new java.util.ArrayList<>(); // 不含全选的左半和
java.util.ArrayList<Integer> R1 = new java.util.ArrayList<>(); // 不含全选的右半和
int lFull = 0, rFull = 0;
for (int i = 0; i < mid; ++i) {
lFull += a[i];
}
for (int i = mid; i < n; ++i) {
rFull += a[i];
}
for (int mask = 1; mask < (1 << mid) - 1; ++mask) {
int s = 0;
for (int i = 0; i < mid; ++i) {
if ((mask & (1 << i)) != 0) {
s += a[i];
}
}
L1.add(s);
}
for (int mask = 1; mask < (1 << (n - mid)) - 1; ++mask) {
int s = 0;
for (int i = mid; i < n; ++i) {
if ((mask & (1 << (i - mid))) != 0) {
s += a[i];
}
}
if (s == 0) {
return true;
}
R1.add(s);
}
java.util.Collections.sort(L1);
java.util.Collections.sort(R1);
java.util.Set<Integer> lset = new java.util.HashSet<>(L1);
for (int v : R1) {
if (lset.contains(-v)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.splitArraySameAverage(new int[]{1, 2, 3, 4, 5, 6, 7, 8})) ++pass; else System.out.println("FAIL 805-1");
++total; if (!sol.splitArraySameAverage(new int[]{3, 1})) ++pass; else System.out.println("FAIL 805-2");
++total; if (sol.splitArraySameAverage(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 805-3");
++total; if (sol.splitArraySameAverage(new int[]{5, 3, 11, 19, 2})) ++pass; else System.out.println("FAIL 805-4");
++total; if (sol.splitArraySameAverage(new int[]{4, 3, 2, 4, 2, 6, 7})) ++pass; else System.out.println("FAIL 805-5");
++total; if (!sol.splitArraySameAverage(new int[]{2})) ++pass; else System.out.println("FAIL 805-6");
++total; if (sol.splitArraySameAverage2(new int[]{1, 2, 3, 4, 5, 6, 7, 8})) ++pass; else System.out.println("FAIL 805-7");
++total; if (!sol.splitArraySameAverage2(new int[]{3, 1})) ++pass; else System.out.println("FAIL 805-8");
++total; if (sol.splitArraySameAverage2(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 805-9");
if (pass == total) {
System.out.println("== 0805 PASS ==");
} else {
System.out.println("== 0805 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n * 2^(n/2)) | O(2^(n/2)) |
| 解法二 | O(2^(n/2) log 2^(n/2)) | O(2^(n/2)) |
806. Number of Lines To Write String(写字符串需要的行数)
题目描述
我们要把给定的字符串 S 从左到右写到每一行上,每一行的最大宽度为 100 个单位,如果我们在写
某个字母的时候会使这行超过了 100 个单位,那么我们应该把这个字母写到下一行。我们给定了一个
数组 widths,这个数组 widths[0] 代表 'a' 需要的单位,widths[1] 代表 'b' 需要的单位,...
widths[25] 代表 'z' 需要的单位。
现在回答两个问题:至少多少行能放下 S,以及最后一行使用的宽度是多少个单位?将你的答案作为
长度为 2 的整数列表返回。
示例 1
- 输入:widths = [10,10,...,10], S = "abcdefghijklmnopqrstuvwxyz"
- 输出: [3, 60]
示例 2
- 输入:widths = [4,10,...,10], S = "bbbcccdddaaa"
- 输出: [2, 4]
提示: - 字符串 S 的长度在 [1, 1000] 的范围
- S 只包含小写字母
- widths 是长度为 26 的数组
- widths[i] 值的范围在 [2, 10]
解题思路
字母宽度已知、每行宽度固定为 100,需要模拟书写过程得到行数与最后一行占用宽度。
解法一(一次遍历模拟,主提交):若当前行剩余宽度放不下该字母,则换行并把该字母作为新行的
起始宽度。
解法二(逐字母处理 + 预写检查,对照实现):对每个字母先检查当前行能否容纳,逻辑拆成
两个更小的步骤便于阅读。
代码实现
class Solution {
// 解法一:一次遍历(推荐提交)
public int[] numberOfLines(int[] widths, String s) {
int lines = 1, last = 0;
for (int i = 0; i < s.length(); ++i) {
int w = widths[s.charAt(i) - 'a'];
if (last + w <= 100) {
last += w;
} else {
++lines;
last = w;
}
}
return new int[]{lines, last};
}
// 解法二:预写检查
public int[] numberOfLines2(int[] widths, String s) {
int lines = 1, last = 0;
for (int i = 0; i < s.length(); ++i) {
int w = widths[s.charAt(i) - 'a'];
if (100 - last < w) {
++lines;
last = 0;
}
last += w;
}
return new int[]{lines, last};
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] all10 = new int[26];
java.util.Arrays.fill(all10, 10);
int[] w2 = new int[26];
java.util.Arrays.fill(w2, 10);
w2[0] = 4;
++total; if (java.util.Arrays.equals(sol.numberOfLines(all10, "abcdefghijklmnopqrstuvwxyz"), new int[]{3, 60})) ++pass; else System.out.println("FAIL 806-1");
++total; if (java.util.Arrays.equals(sol.numberOfLines(w2, "bbbcccdddaaa"), new int[]{2, 4})) ++pass; else System.out.println("FAIL 806-2");
++total; if (java.util.Arrays.equals(sol.numberOfLines(all10, "a"), new int[]{1, 10})) ++pass; else System.out.println("FAIL 806-3");
++total; if (java.util.Arrays.equals(sol.numberOfLines(new int[]{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, "abcdefghijklmnopqrstuvwxyz"), new int[]{3, 60})) ++pass; else System.out.println("FAIL 806-4");
++total; if (java.util.Arrays.equals(sol.numberOfLines2(all10, "abcdefghijklmnopqrstuvwxyz"), new int[]{3, 60})) ++pass; else System.out.println("FAIL 806-5");
++total; if (java.util.Arrays.equals(sol.numberOfLines2(w2, "bbbcccdddaaa"), new int[]{2, 4})) ++pass; else System.out.println("FAIL 806-6");
if (pass == total) {
System.out.println("== 0806 PASS ==");
} else {
System.out.println("== 0806 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
807. Max Increase to Keep City Skyline(保持城市天际线)
题目描述
给你一座由 n x n 个街区组成的城市,每个街区都包含一座立方体建筑。给你一个下标从 0 开始的
n x n 整数矩阵 grid,其中 grid[r][c] 表示坐落于 r 行 c 列的建筑物的高度。
城市的天空线是从远处观察城市时,所有建筑物形成的外部轮廓。从东、南、西、北四个主要方向观测
到的天际线可能不同。
我们被允许为任意数量的建筑物的高度增加任意增量(不同建筑物的增量可能不同)。高度为 0 的
建筑物的高度也可以增加。然而,增加的建筑物高度不能影响从任何主要方向观察城市得到的天际线。
在不改变从任何主要方向观测到的城市天际线的前提下,返回建筑物可以增加的最大高度增量总和。
示例 1
- 输入:
grid= [[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]] - 输出:35
示例 2
- 输入:
grid= [[0,0,0],[0,0,0],[0,0,0]] - 输出:0
提示: n == grid.lengthn == grid[r].length2 <= n <= 500 <= grid[r][c] <= 100
解题思路
天际线由每行、每列的最大值决定,升高某格不能超过其所在行、列的天际线。
解法一(两次遍历,主提交):先扫出各行、列最大值,再逐格计算 min(rowMax[i], colMax[j])
与原高度之差并累加。
解法二(一次遍历同时累计,对照实现):第一遍求行列最大值,第二遍直接累加增量,与解法一
等价但把「计算」单独拆成方法便于阅读。
代码实现
class Solution {
// 解法一:两次遍历(推荐提交)
public int maxIncreaseKeepingSkyline(int[][] grid) {
int n = grid.length;
int[] rowMax = new int[n];
int[] colMax = new int[n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
rowMax[i] = Math.max(rowMax[i], grid[i][j]);
colMax[j] = Math.max(colMax[j], grid[i][j]);
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
ans += Math.min(rowMax[i], colMax[j]) - grid[i][j];
}
}
return ans;
}
// 解法二:单行扫描辅助函数版
public int maxIncreaseKeepingSkyline2(int[][] grid) {
int n = grid.length;
int[] rowMax = new int[n];
int[] colMax = new int[n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
rowMax[i] = Math.max(rowMax[i], grid[i][j]);
colMax[j] = Math.max(colMax[j], grid[i][j]);
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
ans += gain(grid[i][j], rowMax[i], colMax[j]);
}
}
return ans;
}
private int gain(int cur, int a, int b) {
return Math.min(a, b) - cur;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.maxIncreaseKeepingSkyline(new int[][]{{3, 0, 8, 4}, {2, 4, 5, 7}, {9, 2, 6, 3}, {0, 3, 1, 0}}) == 35) ++pass; else System.out.println("FAIL 807-1");
++total; if (sol.maxIncreaseKeepingSkyline(new int[][]{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}) == 0) ++pass; else System.out.println("FAIL 807-2");
++total; if (sol.maxIncreaseKeepingSkyline(new int[][]{{1, 0}, {0, 1}}) == 2) ++pass; else System.out.println("FAIL 807-3");
++total; if (sol.maxIncreaseKeepingSkyline(new int[][]{{1, 1, 1}, {1, 1, 1}, {1, 1, 1}}) == 0) ++pass; else System.out.println("FAIL 807-4");
++total; if (sol.maxIncreaseKeepingSkyline(new int[][]{{5, 2}, {2, 5}}) == 6) ++pass; else System.out.println("FAIL 807-5");
++total; if (sol.maxIncreaseKeepingSkyline2(new int[][]{{3, 0, 8, 4}, {2, 4, 5, 7}, {9, 2, 6, 3}, {0, 3, 1, 0}}) == 35) ++pass; else System.out.println("FAIL 807-6");
++total; if (sol.maxIncreaseKeepingSkyline2(new int[][]{{5, 2}, {2, 5}}) == 6) ++pass; else System.out.println("FAIL 807-7");
++total; if (sol.maxIncreaseKeepingSkyline2(new int[][]{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}) == 0) ++pass; else System.out.println("FAIL 807-8");
if (pass == total) {
System.out.println("== 0807 PASS ==");
} else {
System.out.println("== 0807 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2) | O(n) |
| 解法二 | O(n^2) | O(n) |
808. Soup Servings(分汤)
题目描述
你有两种汤,A 和 B,每种初始为 n 毫升。在每一轮中,会随机选择以下四种操作中的一种,每种
操作的概率为 0.25,且与之前的所有轮次无关:
- 从汤 A 取 100 毫升,从汤 B 取 0 毫升
- 从汤 A 取 75 毫升,从汤 B 取 25 毫升
- 从汤 A 取 50 毫升,从汤 B 取 50 毫升
- 从汤 A 取 25 毫升,从汤 B 取 75 毫升
注意:不存在从汤 A 取 0 ml 和从汤 B 取 100 ml 的操作。汤 A 和 B 在每次操作中同时被取出。
如果一次操作要求你取出比剩余的汤更多的量,请取出该汤剩余的所有部分。
操作过程在任何回合中任一汤被取完后立即停止。
返回汤 A 在 B 前取完的概率,加上两种汤在同一回合取完概率的一半。返回值在正确答案 10^-5 的
范围内将被认为是正确的。
示例 1
- 输入:
n= 50 - 输出:0.62500
示例 2
- 输入:
n= 100 - 输出:0.71875
提示: 0 <= n <= 10^9
解题思路
四种操作等概率减少汤量,求 A 先倒完的概率(同时倒完计 1/2)。n 可达 10^9,按毫升逐步模拟
不可行;注意到每次至少减少 25ml,可把状态缩到 ceil(n/25) 的格点。
解法一(记忆化搜索,主提交):dfs(i,j) 表示剩余 (i,j) 份时的概率,边界按谁先耗尽返回 1、
0 或 1/2。n 很大时 A 几乎必然先空,n > 4800 时直接返回 1。
解法二(迭代 DP 二维数组,对照实现):与记忆化解法等价,用二维数组自底向上填表。
代码实现
class Solution {
private double[][] memo;
// 解法一:记忆化搜索(推荐提交)
public double soupServings(int n) {
if (n > 4800) {
return 1.0;
}
int m = (n + 24) / 25; // 向上取整的份数
memo = new double[m + 1][m + 1];
return dfs(m, m);
}
private double dfs(int i, int j) {
if (i <= 0 && j <= 0) {
return 0.5;
}
if (i <= 0) {
return 1.0;
}
if (j <= 0) {
return 0.0;
}
if (memo[i][j] > 0) {
return memo[i][j];
}
double v = 0.25 * (dfs(i - 4, j) + dfs(i - 3, j - 1) + dfs(i - 2, j - 2) + dfs(i - 1, j - 3));
memo[i][j] = v;
return v;
}
// 解法二:迭代 DP
public double soupServings2(int n) {
if (n > 4800) {
return 1.0;
}
int m = (n + 24) / 25;
// dp[i][j] 表示剩余 i、j 份时的概率
double[][] dp = new double[m + 1][m + 1];
dp[0][0] = 0.5;
for (int i = 1; i <= m; ++i) {
dp[0][i] = 1.0;
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= m; ++j) {
dp[i][j] = 0.25 * (get(dp, i - 4, j) + get(dp, i - 3, j - 1)
+ get(dp, i - 2, j - 2) + get(dp, i - 1, j - 3));
}
}
return dp[m][m];
}
private double get(double[][] dp, int i, int j) {
if (i <= 0 && j <= 0) {
return 0.5;
}
if (i <= 0) {
return 1.0;
}
if (j <= 0) {
return 0.0;
}
return dp[i][j];
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (Math.abs(sol.soupServings(50) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-1");
++total; if (Math.abs(sol.soupServings(100) - 0.71875) < 1e-9) ++pass; else System.out.println("FAIL 808-2");
++total; if (Math.abs(sol.soupServings(0) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 808-3");
++total; if (Math.abs(sol.soupServings(25) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-4");
++total; if (Math.abs(sol.soupServings(1000000000) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL 808-5");
++total; if (Math.abs(sol.soupServings2(50) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-6");
++total; if (Math.abs(sol.soupServings2(100) - 0.71875) < 1e-9) ++pass; else System.out.println("FAIL 808-7");
++total; if (Math.abs(sol.soupServings2(0) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 808-8");
++total; if (Math.abs(sol.soupServings2(25) - 0.62500) < 1e-9) ++pass; else System.out.println("FAIL 808-9");
if (pass == total) {
System.out.println("== 0808 PASS ==");
} else {
System.out.println("== 0808 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(1)(n>4800 直接返回,否则 O(C^2),C=200) | O(C^2) |
| 解法二 | O(C^2) | O(C^2) |
809. Expressive Words(情感丰富的文字)
题目描述
有时候人们会用重复写一些字母来表示额外的感受,比如 "hello" -> "heeellooo", "hi" -> "hiii"。
我们将相邻字母都相同的一串字符定义为相同字母组,例如:"h", "eee", "ll", "ooo"。
对于一个给定的字符串 S,如果另一个单词能够通过将一些字母组扩张从而使其和 S 相同,我们将
这个单词定义为可扩张的(stretchy)。扩张操作定义如下:选择一个字母组(包含字母 c),然后
往其中添加相同的字母 c 使其长度达到 3 或以上。
例如,以 "hello" 为例,我们可以对字母组 "o" 扩张得到 "hellooo",但是无法以同样的方法得到
"helloo" 因为字母组 "oo" 长度小于 3。此外,我们可以进行另一种扩张 "ll" -> "lllll" 以获得
"helllllooo"。如果 s = "helllllooo",那么查询词 "hello" 是可扩张的。
输入一组查询单词,输出其中可扩张的单词数量。
示例
- 输入:
s= "heeellooo",words= ["hello", "hi", "helo"] - 输出: 1
提示: 1 <= s.length, words.length <= 1001 <= words[i].length <= 100s和所有在words中的单词都只由小写字母组成
解题思路
伸展要求查询串的连续段只能拉长、且拉长后该段长度至少为 3,不能改字母或缩短。
解法一(双指针分段匹配,主提交):同步扫描两串的连续段,字母必须相同;若原段更长,或
目标段短于 3 却与原段长度不等,则不能伸展。两串同时扫完才算匹配。
解法二(预计算分段再比较,对照实现):先把 s 和单词 t 各压缩为(字符、次数)序列,再
顺序比较两序列是否满足可扩张规则。
代码实现
class Solution {
// 解法一:双指针分段(推荐提交)
public int expressiveWords(String s, String[] words) {
int ans = 0;
for (String t : words) {
if (check(s, t)) {
++ans;
}
}
return ans;
}
private boolean check(String s, String t) {
int m = s.length(), n = t.length();
if (n > m) {
return false;
}
int i = 0, j = 0;
while (i < m && j < n) {
if (s.charAt(i) != t.charAt(j)) {
return false;
}
char c = s.charAt(i);
int c1 = 0;
while (i < m && s.charAt(i) == c) {
++i;
++c1;
}
int c2 = 0;
while (j < n && t.charAt(j) == c) {
++j;
++c2;
}
// s 中段长不能小于 t 中段长;若段长不足 3 则必须相等
if (c1 < c2 || (c1 < 3 && c1 != c2)) {
return false;
}
}
return i == m && j == n;
}
// 解法二:压缩分段再比较
public int expressiveWords2(String s, String[] words) {
int ans = 0;
int[][] cs = compress(s);
for (String t : words) {
if (match(cs, compress(t))) {
++ans;
}
}
return ans;
}
private int[][] compress(String s) {
java.util.ArrayList<int[]> list = new java.util.ArrayList<>();
int i = 0;
while (i < s.length()) {
char c = s.charAt(i);
int j = i;
while (j < s.length() && s.charAt(j) == c) {
++j;
}
list.add(new int[]{c, j - i});
i = j;
}
return list.toArray(new int[list.size()][]);
}
private boolean match(int[][] a, int[][] b) {
if (a.length != b.length) {
return false;
}
for (int i = 0; i < a.length; ++i) {
if (a[i][0] != b[i][0]) {
return false;
}
int c1 = a[i][1], c2 = b[i][1];
if (c1 < c2 || (c1 < 3 && c1 != c2)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.expressiveWords("heeellooo", new String[]{"hello", "hi", "helo"}) == 1) ++pass; else System.out.println("FAIL 809-1");
++total; if (sol.expressiveWords("hello", new String[]{"hello", "hi"}) == 1) ++pass; else System.out.println("FAIL 809-2");
++total; if (sol.expressiveWords("zzzzzyyyyy", new String[]{"zzyy", "zy", "zyy"}) == 3) ++pass; else System.out.println("FAIL 809-3");
++total; if (sol.expressiveWords("aaa", new String[]{"aaaa"}) == 0) ++pass; else System.out.println("FAIL 809-4");
++total; if (sol.expressiveWords("abcd", new String[]{"abc"}) == 0) ++pass; else System.out.println("FAIL 809-5");
++total; if (sol.expressiveWords2("heeellooo", new String[]{"hello", "hi", "helo"}) == 1) ++pass; else System.out.println("FAIL 809-6");
++total; if (sol.expressiveWords2("zzzzzyyyyy", new String[]{"zzyy", "zy", "zyy"}) == 3) ++pass; else System.out.println("FAIL 809-7");
++total; if (sol.expressiveWords2("hello", new String[]{"hello", "hi"}) == 1) ++pass; else System.out.println("FAIL 809-8");
if (pass == total) {
System.out.println("== 0809 PASS ==");
} else {
System.out.println("== 0809 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| 解法一 | O( | s | + | t | ) 每个单词 | O(1) | ||||
| 解法二 | O( | s | + | t | ) 每个单词 | O( | s | + | t | ) |
810. Chalkboard XOR Game(黑板异或游戏)
题目描述
黑板上写着一个非负整数数组 nums[i]。
Alice 和 Bob 轮流从黑板上擦掉一个数字,Alice 先手。如果擦除一个数字后,剩余的所有数字按位
异或运算得出的结果等于 0 的话,当前玩家游戏失败。另外,如果只剩一个数字,按位异或运算得到
它本身;如果无数字剩余,按位异或运算结果为 0。
并且,轮到某个玩家时,如果当前黑板上所有数字按位异或运算结果等于 0,这个玩家获胜。
假设两个玩家每步都使用最优解,当且仅当 Alice 获胜时返回 true。
示例 1
- 输入:
nums= [1,1,2] - 输出:
false
示例 2
- 输入:
nums= [0,1] - 输出:
true
示例 3
- 输入:
nums= [1,2,3] - 输出:
true
提示: 1 <= nums.length <= 10000 <= nums[i] < 2^16
解题思路
每次擦掉一个数后,若剩余数字异或为 0 则对方失败。直接搜索博弈树在 n <= 1000 时过深。
关键观察:若当前异或已为 0,先手立刻获胜;否则当 n 为偶数时,总存在一种擦法使后手面对
异或非零的局面,先手必胜。
解法一(位运算判定,主提交):答案只取决于「长度是否为偶数」或「全体异或是否为 0」,
一次异或扫描即可。
解法二(按游戏规则模拟 + 记忆化小规模验证,对照实现):n 小时用状态压缩记忆化搜索推算
必胜态,用于小规模数据交叉验证。
代码实现
class Solution {
// 解法一:位运算判定(推荐提交)
public boolean xorGame(int[] nums) {
int x = 0;
for (int v : nums) {
x ^= v;
}
return x == 0 || (nums.length & 1) == 0;
}
// 解法二:记忆化博弈(小规模验证)
private java.util.Map<Integer, Boolean> winMemo;
public boolean xorGame2(int[] nums) {
int n = nums.length;
if (n > 20) {
return xorGame(nums);
}
winMemo = new java.util.HashMap<>();
int x = 0;
for (int v : nums) {
x ^= v;
}
return canWin(nums, (1 << n) - 1, x);
}
// 当前剩余集合 mask,异或为 x;返回当前玩家能否必胜
private boolean canWin(int[] nums, int mask, int x) {
if (x == 0) {
return true; // 轮到当前玩家时异或为 0,直接获胜
}
Boolean cached = winMemo.get(mask);
if (cached != null) {
return cached;
}
int n = nums.length;
boolean canLose = false; // 是否存在擦法使对手进入必败
for (int i = 0; i < n; ++i) {
if ((mask & (1 << i)) != 0) {
int nx = x ^ nums[i];
// 擦掉 i 后剩余异或为 0 的话当前玩家失败——但这是自己赢不了的一步,
// 实际游戏中当前玩家会避免「擦完 -> 对手直接赢」。
// 这里按规则:若 nx == 0,则执行该步后轮到对手且异或为 0,对手获胜。
if (nx == 0) {
continue;
}
if (!canWin(nums, mask ^ (1 << i), nx)) {
canLose = true;
break;
}
}
}
// 注意:当只剩一个数被擦除后 mask 变 0、nx 为 0,对手获胜(已在上层 continue 掉)
// 若所有可走步都让对手必胜,则当前玩家必败
winMemo.put(mask, canLose);
return canLose;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (!sol.xorGame(new int[]{1, 1, 2})) ++pass; else System.out.println("FAIL 810-1");
++total; if (sol.xorGame(new int[]{0, 1})) ++pass; else System.out.println("FAIL 810-2");
++total; if (sol.xorGame(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 810-3");
++total; if (sol.xorGame(new int[]{0})) ++pass; else System.out.println("FAIL 810-4");
++total; if (!sol.xorGame(new int[]{1, 1, 1})) ++pass; else System.out.println("FAIL 810-5");
++total; if (sol.xorGame(new int[]{1, 2, 3, 4})) ++pass; else System.out.println("FAIL 810-6");
++total; if (sol.xorGame(new int[]{2, 3})) ++pass; else System.out.println("FAIL 810-7");
++total; if (!sol.xorGame2(new int[]{1, 1, 2})) ++pass; else System.out.println("FAIL 810-8");
++total; if (sol.xorGame2(new int[]{0, 1})) ++pass; else System.out.println("FAIL 810-9");
++total; if (sol.xorGame2(new int[]{1, 2, 3})) ++pass; else System.out.println("FAIL 810-10");
if (pass == total) {
System.out.println("== 0810 PASS ==");
} else {
System.out.println("== 0810 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(2^n) | O(2^n) |
811. Subdomain Visit Count(子域名访问计数)
题目描述
网站域名 "discuss.leetcode.com" 由多个子域名组成。顶级域名为 "com",二级域名为
"leetcode.com",最低一级为 "discuss.leetcode.com"。当访问域名 "discuss.leetcode.com" 时,
同时也会隐式访问其父域名 "leetcode.com" 以及 "com"。
计数配对域名是遵循 "rep d1.d2.d3" 或 "rep d1.d2" 格式的一个域名表示,其中 rep 表示访问
域名的次数,d1.d2.d3 为域名本身。例如,"9001 discuss.leetcode.com" 就是一个计数配对域名。
给你一个计数配对域名组成的数组 cpdomains,解析得到输入中每个子域名对应的计数配对域名,
并以数组形式返回。可以按任意顺序返回答案。
示例 1
- 输入:cpdomains = ["9001 discuss.leetcode.com"]
- 输出:["9001 leetcode.com","9001 discuss.leetcode.com","9001 com"]
示例 2
- 输入:cpdomains = ["900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"]
- 输出:["901 mail.com","50 yahoo.com","900 google.mail.com","5 wiki.org","5 org","1 intel.mail.com","951 com"]
提示: 1 <= cpdomain.length <= 1001 <= cpdomain[i].length <= 100rep_i是范围 [1, 10^4] 内的一个整数- d1、d2 和 d3 由小写英文字母组成
解题思路
一条计数记录要同时累加到自身及其所有上级域名。记录条数与串长均不超过 100。
解法一(哈希表统计,主提交):扫描每个空格或点号后的后缀,把访问次数加到该后缀上,
最后把计数表格式化输出。
解法二(有序遍历,对照实现):统计完成后按域名排序输出,便于展示全部计数明细。
代码实现
class Solution {
// 解法一:哈希表统计(推荐提交)
public java.util.List<String> subdomainVisits(String[] cpdomains) {
java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
for (String s : cpdomains) {
int sp = s.indexOf(' ');
int v = Integer.parseInt(s.substring(0, sp));
String domain = s.substring(sp + 1);
int idx = 0;
while (idx < domain.length()) {
String sub = domain.substring(idx);
cnt.put(sub, cnt.getOrDefault(sub, 0) + v);
idx = domain.indexOf('.', idx) + 1;
if (idx == 0) {
break;
}
}
}
java.util.List<String> ans = new java.util.ArrayList<>();
for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
ans.add(e.getValue() + " " + e.getKey());
}
return ans;
}
// 解法二:统计后按域名排序
public java.util.List<String> subdomainVisits2(String[] cpdomains) {
java.util.Map<String, Integer> cnt = new java.util.HashMap<>();
for (String s : cpdomains) {
int sp = s.indexOf(' ');
int v = Integer.parseInt(s.substring(0, sp));
String domain = s.substring(sp + 1);
for (int i = 0; i < domain.length(); ++i) {
if (domain.charAt(i) == '.') {
String sub = domain.substring(i + 1);
cnt.put(sub, cnt.getOrDefault(sub, 0) + v);
}
}
cnt.put(domain, cnt.getOrDefault(domain, 0) + v);
}
java.util.List<String> ans = new java.util.ArrayList<>();
for (java.util.Map.Entry<String, Integer> e : cnt.entrySet()) {
ans.add(e.getValue() + " " + e.getKey());
}
java.util.Collections.sort(ans);
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.subdomainVisits(new String[]{"9001 discuss.leetcode.com"}).size() == 3) ++pass; else System.out.println("FAIL 811-1");
++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"}).size() == 7) ++pass; else System.out.println("FAIL 811-2");
++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com"}).contains("900 google.mail.com")) ++pass; else System.out.println("FAIL 811-3");
++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "1 intel.mail.com"}).contains("901 mail.com")) ++pass; else System.out.println("FAIL 811-4");
++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "1 intel.mail.com"}).contains("901 com")) ++pass; else System.out.println("FAIL 811-5");
++total; if (sol.subdomainVisits(new String[]{"900 google.mail.com", "1 intel.mail.com"}).contains("1 intel.mail.com")) ++pass; else System.out.println("FAIL 811-6");
++total; if (sol.subdomainVisits2(new String[]{"9001 discuss.leetcode.com"}).size() == 3) ++pass; else System.out.println("FAIL 811-7");
++total; if (sol.subdomainVisits2(new String[]{"900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"}).size() == 7) ++pass; else System.out.println("FAIL 811-8");
if (pass == total) {
System.out.println("== 0811 PASS ==");
} else {
System.out.println("== 0811 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(L) | O(L) |
| 解法二 | O(L + M log M) | O(L) |
812. Largest Triangle Area(最大三角形面积)
题目描述
给你一个由 X-Y 平面上的点组成的数组 points,其中 points[i] = [xi, yi]。从其中取任意
三个不同的点组成三角形,返回能组成的最大三角形的面积。与真实值误差在 10^-5 内的答案将会
视为正确答案。
示例 1
- 输入:points = [[0,0],[0,1],[1,0],[0,2],[2,0]]
- 输出:2.00000
示例 2
- 输入:points = [[1,0],[0,0],[0,1]]
- 输出:0.50000
提示: 3 <= points.length <= 50-50 <= xi, yi <= 50- 给出的所有点互不相同
解题思路
最大三角形面积由三点叉积的一半给出。点数至多 50,三重枚举约 10^5 次可接受。
解法一(三重枚举叉积,主提交):对每组三点用向量叉积取绝对值的一半,维护全局最大值。
解法二(固定一点的双重枚举,对照实现):先固定第一个点,再枚举其余两点计算面积,
逻辑等价的另一种写法。
代码实现
class Solution {
// 解法一:三重枚举(推荐提交)
public double largestTriangleArea(int[][] points) {
double ans = 0;
for (int[] p1 : points) {
int x1 = p1[0], y1 = p1[1];
for (int[] p2 : points) {
int x2 = p2[0], y2 = p2[1];
for (int[] p3 : points) {
int x3 = p3[0], y3 = p3[1];
int u1 = x2 - x1, v1 = y2 - y1;
int u2 = x3 - x1, v2 = y3 - y1;
double t = Math.abs(u1 * v2 - u2 * v1) / 2.0;
ans = Math.max(ans, t);
}
}
}
return ans;
}
// 解法二:固定第一点的双循环
public double largestTriangleArea2(int[][] points) {
double ans = 0;
int n = points.length;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
for (int k = j + 1; k < n; ++k) {
ans = Math.max(ans, area(points[i], points[j], points[k]));
}
}
}
return ans;
}
private double area(int[] a, int[] b, int[] c) {
// 向量 ab 与 ac 的叉积绝对值的一半
return Math.abs((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) / 2.0;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {0, 1}, {1, 0}, {0, 2}, {2, 0}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-1");
++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{1, 0}, {0, 0}, {0, 1}}) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 812-2");
++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {0, 1}, {1, 1}}) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 812-3");
++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {2, 0}, {0, 2}, {1, 1}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-4");
++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{-1, -1}, {1, -1}, {0, 1}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-5");
++total; if (Math.abs(sol.largestTriangleArea(new int[][]{{0, 0}, {1, 0}, {2, 0}, {3, 0}}) - 0.0) < 1e-9) ++pass; else System.out.println("FAIL 812-6");
++total; if (Math.abs(sol.largestTriangleArea2(new int[][]{{0, 0}, {0, 1}, {1, 0}, {0, 2}, {2, 0}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-7");
++total; if (Math.abs(sol.largestTriangleArea2(new int[][]{{1, 0}, {0, 0}, {0, 1}}) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 812-8");
++total; if (Math.abs(sol.largestTriangleArea2(new int[][]{{0, 0}, {2, 0}, {0, 2}, {1, 1}}) - 2.0) < 1e-9) ++pass; else System.out.println("FAIL 812-9");
if (pass == total) {
System.out.println("== 0812 PASS ==");
} else {
System.out.println("== 0812 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^3) | O(1) |
| 解法二 | O(n^3) | O(1) |
813. Largest Sum of Averages(最大平均值和的分组)
题目描述
给定数组 nums 和一个整数 k。我们将给定的数组 nums 分成最多 k 个非空子数组,且数组
内部是连续的。分数由每个子数组内的平均值的总和构成。
注意我们必须使用 nums 数组中的每一个数进行分组,并且分数不一定需要是整数。
返回我们所能得到的最大分数是多少。答案误差在 10^-6 内被视为是正确的。
示例 1
- 输入:
nums= [9,1,2,3,9],k= 3 - 输出: 20.00000
- 解释:
nums的最优分组是 [9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20.
示例 2
- 输入:
nums= [1,2,3,4,5,6,7],k= 4 - 输出: 20.50000 提示:
1 <= nums.length <= 1001 <= nums[i] <= 10^41 <= k <= nums.length
解题思路
把数组分成至多 k 段、最大化各段平均值之和。段的分界未知,n,k <= 100,按「从 i 起还剩 k 段」 搜索并记忆化即可。 解法一(前缀和 + 记忆化搜索,主提交):前缀和使一段 [i,j) 的平均值 O(1) 可得。k=1 时整段 只能作为最后一组;否则枚举第一段右端,递归剩余。 解法二(迭代 DP 二维数组,对照实现):dp[i][j] 表示前 i 个数分成 j 段的最大分数, 自底向上转移。
代码实现
class Solution {
private double[][] f;
private int[] s;
private int n;
// 解法一:记忆化搜索(推荐提交)
public double largestSumOfAverages(int[] nums, int k) {
n = nums.length;
s = new int[n + 1];
f = new double[n][k + 1];
for (int i = 0; i < n; ++i) {
s[i + 1] = s[i] + nums[i];
}
return dfs(0, k);
}
private double dfs(int i, int k) {
if (i == n) {
return 0;
}
if (k == 1) {
return (s[n] - s[i]) * 1.0 / (n - i);
}
if (f[i][k] > 0) {
return f[i][k];
}
double ans = 0;
for (int j = i + 1; j < n; ++j) {
ans = Math.max(ans, (s[j] - s[i]) * 1.0 / (j - i) + dfs(j, k - 1));
}
f[i][k] = ans;
return ans;
}
// 解法二:迭代 DP
public double largestSumOfAverages2(int[] nums, int k) {
int n = nums.length;
int[] pre = new int[n + 1];
for (int i = 0; i < n; ++i) {
pre[i + 1] = pre[i] + nums[i];
}
// dp[i][j]: 前 i 个数分成 j 段的最大分数
double[][] dp = new double[n + 1][k + 1];
for (int i = 1; i <= n; ++i) {
dp[i][1] = pre[i] * 1.0 / i;
}
for (int j = 2; j <= k; ++j) {
for (int i = j; i <= n; ++i) {
for (int t = j - 1; t < i; ++t) {
dp[i][j] = Math.max(dp[i][j], dp[t][j - 1] + (pre[i] - pre[t]) * 1.0 / (i - t));
}
}
}
return dp[n][k];
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (Math.abs(sol.largestSumOfAverages(new int[]{9, 1, 2, 3, 9}, 3) - 20.0) < 1e-6) ++pass; else System.out.println("FAIL 813-1");
++total; if (Math.abs(sol.largestSumOfAverages(new int[]{1, 2, 3, 4, 5, 6, 7}, 4) - 20.5) < 1e-6) ++pass; else System.out.println("FAIL 813-2");
++total; if (Math.abs(sol.largestSumOfAverages(new int[]{1, 2, 3}, 1) - 2.0) < 1e-6) ++pass; else System.out.println("FAIL 813-3");
++total; if (Math.abs(sol.largestSumOfAverages(new int[]{5}, 1) - 5.0) < 1e-6) ++pass; else System.out.println("FAIL 813-4");
++total; if (Math.abs(sol.largestSumOfAverages(new int[]{1, 100, 1}, 3) - 102.0) < 1e-6) ++pass; else System.out.println("FAIL 813-5");
++total; if (Math.abs(sol.largestSumOfAverages(new int[]{4, 1, 7, 3}, 2) - 7.6666666667) < 1e-6) ++pass; else System.out.println("FAIL 813-6");
++total; if (Math.abs(sol.largestSumOfAverages2(new int[]{9, 1, 2, 3, 9}, 3) - 20.0) < 1e-6) ++pass; else System.out.println("FAIL 813-7");
++total; if (Math.abs(sol.largestSumOfAverages2(new int[]{1, 2, 3, 4, 5, 6, 7}, 4) - 20.5) < 1e-6) ++pass; else System.out.println("FAIL 813-8");
++total; if (Math.abs(sol.largestSumOfAverages2(new int[]{4, 1, 7, 3}, 2) - 7.6666666667) < 1e-6) ++pass; else System.out.println("FAIL 813-9");
if (pass == total) {
System.out.println("== 0813 PASS ==");
} else {
System.out.println("== 0813 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2 * k) | O(n * k) |
| 解法二 | O(n^2 * k) | O(n * k) |