From 425feff3c73da9d8f6bb1ae38ae3b8a0b1abb0d3 Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Sun, 28 Sep 2025 20:10:38 +0800 Subject: [PATCH] 9.28 --- src/main/java/All/DiffWaysToCompute.java | 87 ++++++++++++++++++++ src/main/java/All/LastRemaining.java | 100 +++++++++++++++++++++++ src/main/java/All/LongestSubstring.java | 65 +++++++++++++++ src/main/java/All/MaxCoins.java | 66 +++++++++++++++ src/main/java/tree/BuildTree.java | 13 +-- 5 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 src/main/java/All/DiffWaysToCompute.java create mode 100644 src/main/java/All/LastRemaining.java create mode 100644 src/main/java/All/LongestSubstring.java create mode 100644 src/main/java/All/MaxCoins.java diff --git a/src/main/java/All/DiffWaysToCompute.java b/src/main/java/All/DiffWaysToCompute.java new file mode 100644 index 0000000..0de0cb7 --- /dev/null +++ b/src/main/java/All/DiffWaysToCompute.java @@ -0,0 +1,87 @@ +package All; + +import java.util.ArrayList; +import java.util.List; + +/** + * 题目: 241. 为运算表达式设计优先级 + * 描述:给你一个由数字和运算符组成的字符串 expression ,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。 + * 生成的测试用例满足其对应输出值符合 32 位整数范围,不同结果的数量不超过 104 。 + + 示例 1: + 输入:expression = "2-1-1" + 输出:[0,2] + 解释: + ((2-1)-1) = 0 + (2-(1-1)) = 2 + + * 链接:https://leetcode.cn/problems/different-ways-to-add-parentheses/ + */ +//不会 +public class DiffWaysToCompute { + /* +核心思想 + +表达式由数字和运算符组成。 +我们要尝试给不同的运算符添加括号(改变计算顺序),得到不同结果。 +遇到运算符就把它当“分隔点”: +左边子串 → 所有可能结果 +右边子串 → 所有可能结果 +然后把左右结果组合在一起。 +递归过程 + +如果当前子串全是数字 → 直接转成整数,作为结果返回。 +否则,枚举子串中的运算符 + - *: +分别递归处理左右部分。 +把左右子结果两两组合,用当前运算符计算,放进结果集。 +结束条件 + +子串没有运算符,说明是纯数字,直接返回该数字。 + */ + char[] cs; // 存储输入字符串的字符数组,便于处理 + + public List diffWaysToCompute(String s) { + cs = s.toCharArray(); + // 从整个表达式 [0, n-1] 开始递归 + return dfs(0, cs.length - 1); + } + + /** + * 递归函数:计算表达式 s[l..r] 的所有可能结果 + */ + List dfs(int l, int r) { + List ans = new ArrayList<>(); + + // 枚举区间内的每一个字符 + for (int i = l; i <= r; i++) { + // 如果是数字,跳过;我们只在运算符处切分 + if (cs[i] >= '0' && cs[i] <= '9') continue; + + // 遇到运算符 cs[i],把表达式分为左右两部分 + List left = dfs(l, i - 1); + List right = dfs(i + 1, r); + + // 枚举左右子表达式的结果,两两组合 + for (int a : left) { + for (int b : right) { + int cur; + if (cs[i] == '+') cur = a + b; + else if (cs[i] == '-') cur = a - b; + else cur = a * b; // cs[i] == '*' + ans.add(cur); + } + } + } + + // 如果 ans 为空,说明整个区间没有运算符,是一个纯数字 + if (ans.isEmpty()) { + int cur = 0; + for (int i = l; i <= r; i++) { + cur = cur * 10 + (cs[i] - '0'); // 将字符转成数字 + } + ans.add(cur); + } + + return ans; + } +} diff --git a/src/main/java/All/LastRemaining.java b/src/main/java/All/LastRemaining.java new file mode 100644 index 0000000..a741084 --- /dev/null +++ b/src/main/java/All/LastRemaining.java @@ -0,0 +1,100 @@ +package All; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 题目: 390. 消除游戏 + * 描述:列表 arr 由在范围 [1, n] 中的所有整数组成,并按严格递增排序。请你对 arr 应用下述算法: + * 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。 + * 重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。 + * 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。 + * 给你整数 n ,返回 arr 最后剩下的数字。 + + 示例 1: + 输入:n = 9 + 输出:6 + 解释: + arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] + arr = [2, 4, 6, 8] + arr = [2, 6] + arr = [6] + + * 链接:https://leetcode.cn/problems/elimination-game/ + */ +//不会 +public class LastRemaining { + /* +我们关心的其实不是整个数组,而是: + +当前这一轮的 首项 a1 是多少; +当前剩下多少个数 cnt; +当前每两个数之间的间隔 step。 +每一轮删除操作之后: + +元素个数减半 cnt = cnt / 2。 +间隔翻倍 step = step * 2。 +首项 a1 根据「正向 / 反向」和剩余个数的奇偶性决定是否要往后挪。 + */ + public int lastRemaining(int n) { + int a1 = 1; // 当前序列的首项(第一轮就是1) + int k = 0; // 当前轮数,从0开始(偶数轮:从左往右;奇数轮:从右往左) + int cnt = n; // 当前序列里剩余的元素个数 + int step = 1; // 相邻两个数的间隔,第一轮是1,之后每轮翻倍 + + while (cnt > 1) { + if (k % 2 == 0) { // 偶数轮:从左往右删除 + // 左到右时,首项必然被删掉,所以新的首项要往后移一步 + a1 = a1 + step; + } else { // 奇数轮:从右往左删除 + // 如果剩余的个数是奇数,那么首项会被删掉,需要往后移一步 + // 如果是偶数,那么首项保留,不变 + a1 = (cnt % 2 == 0) ? a1 : a1 + step; + } + k++; // 进入下一轮 + cnt = cnt >> 1; // 等价于 cnt = cnt / 2,每一轮都会删掉一半(右移一位) + step = step << 1; // 等价于 step = step * 2,间隔翻倍 + } + return a1; + } + + //容易想,但时间复杂度高 + public int lastRemaining2(int n) { + // 初始化数组 [1, 2, ..., n] + List arr = new ArrayList<>(); + for (int i = 1; i <= n; i++) { + arr.add(i); + } + + boolean leftToRight = true; // true 表示从左到右,false 表示从右到左 + + // 模拟过程,直到只剩下一个数 + while (arr.size() > 1) { + //只收集未被删除的元素 + List next = new ArrayList<>(); + + if (leftToRight) { // 从左到右 + for (int i = 1; i < arr.size(); i += 2) { + next.add(arr.get(i)); + } + } else { // 从右到左 + for (int i = arr.size() - 2; i >= 0; i -= 2) { + next.add(arr.get(i)); + } + // 注意反向删除后,结果是反序的,要反转一下 + Collections.reverse(next); + } + + arr = next; // 更新为新的一轮 + leftToRight = !leftToRight; // 改变方向 + } + + return arr.get(0); // 最后剩下的唯一元素 + } + + public static void main(String[] args) { + LastRemaining solution = new LastRemaining(); + System.out.println(solution.lastRemaining(9)); // 输出 6 + } +} diff --git a/src/main/java/All/LongestSubstring.java b/src/main/java/All/LongestSubstring.java new file mode 100644 index 0000000..9391161 --- /dev/null +++ b/src/main/java/All/LongestSubstring.java @@ -0,0 +1,65 @@ +package All; + +import java.util.HashMap; + +/** + * 题目: 395. 至少有 K 个重复字符的最长子串 + * 描述:给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。 + * 如果不存在这样的子字符串,则返回 0。 + + 示例 1: + 输入:s = "ababbc", k = 2 + 输出:5 + 解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。 + + * 链接:https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/ + */ +//不会 +public class LongestSubstring { + /* +整体思路 + +我们要找的子串必须满足:每个字符的出现次数 ≥ k。 +如果整个字符串中有某些字符出现次数 < k,那么这些字符一定不可能出现在结果子串中。 +所以我们可以把这些字符当作分隔符,把字符串分割成若干子串,递归处理。 +递归过程 + +如果当前子串长度小于 k,直接返回 0(不可能满足条件)。 +统计每个字符出现次数。 +找到那些出现次数 < k 的字符,把它们作为“分隔符”,切开子串,递归求解每一部分。 +如果所有字符的出现次数都 ≥ k,那么整个子串就是合法解,直接返回长度。 +结束条件 + +子串长度 < k → 返回 0。 +子串完全合法 → 返回子串长度。 +否则递归分治,返回最大值。 + */ + public int longestSubstring(String s, int k) { + // 如果字符串长度小于 k,不可能有满足条件的子串 + if (s.length() < k) return 0; + + // 统计每个字符的出现次数 + HashMap counter = new HashMap<>(); + for (int i = 0; i < s.length(); i++) { + counter.put(s.charAt(i), counter.getOrDefault(s.charAt(i), 0) + 1); + } + + // 遍历所有字符,找到出现次数 < k 的字符 + for (char c : counter.keySet()) { + if (counter.get(c) < k) { + // 这个字符不能出现在任何合法子串中 + // 因此我们用它作为分隔符,切分字符串 + int res = 0; + for (String t : s.split(String.valueOf(c))) { + // 递归处理切分后的子串 + res = Math.max(res, longestSubstring(t, k)); + } + return res; // 返回最长的那个子串结果 + } + } + + // 如果所有字符出现次数都 >= k + // 那么整个字符串就是一个合法的解 + return s.length(); + } +} diff --git a/src/main/java/All/MaxCoins.java b/src/main/java/All/MaxCoins.java new file mode 100644 index 0000000..8fa0a72 --- /dev/null +++ b/src/main/java/All/MaxCoins.java @@ -0,0 +1,66 @@ +package All; +/** + * 题目: 312. 戳气球 + * 描述:有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。 + * 现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。 + * 求所能获得硬币的最大数量。 + + 示例 1: + 输入:nums = [3,1,5,8] + 输出:167 + 解释: + nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] + coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167 + + * 链接:https://leetcode.cn/problems/burst-balloons/ + */ +//不会 +public class MaxCoins { + /* +以 nums = [3,1,5,8],加虚拟气球 [1,3,1,5,8,1]。 + +我们看区间 (0, 5): + +如果最后戳 i=3 (值=5),此时左右邻居是 val[0]=1 和 val[5]=1。 +收益 = 1*5*1 + (0,3)区间最大值 + (3,5)区间最大值。 +如果最后戳 i=2 (值=1),邻居是 val[0]=1 和 val[5]=1, +收益 = 1*1*1 + (0,2)区间最大值 + (2,5)区间最大值。 +注意:我们根本不用管它前面戳没戳过谁,因为只要保证它是最后一个,它的邻居就固定成了边界。 + */ + int[][] memo; + int[] nums; + + public int maxCoins(int[] arr) { + int n = arr.length; + // 构造新数组,在两端加 1 + nums = new int[n + 2]; + nums[0] = nums[n + 1] = 1; + for (int i = 0; i < n; i++) nums[i + 1] = arr[i]; + + memo = new int[n + 2][n + 2]; + return dfs(0, n + 1); + } + + // 戳破 (l, r) 开区间内的所有气球 + //dfs(l, r) 表示 开区间 (l, r) 内的最大值 + int dfs(int l, int r) { + if (l + 1 >= r) return 0; // 区间内没有气球了 + if (memo[l][r] > 0) return memo[l][r]; + + int res = 0; + // 枚举最后一个戳破的气球 i 如果 i 是区间 (l, r) 里最后一个被戳的气球,那么它的邻居 一定是 l 和 r(因为 (l, r) 之间其它气球都已经戳光了)。 + for (int i = l + 1; i < r; i++) { + int coins = nums[l] * nums[i] * nums[r]; // 戳破 i 的得分 + coins += dfs(l, i) + dfs(i, r); // 左右子区间结果 + res = Math.max(res, coins); + } + + memo[l][r] = res; + return res; + } + + public static void main(String[] args) { + MaxCoins solver = new MaxCoins(); + System.out.println(solver.maxCoins(new int[]{3,1,5,8})); // 输出 167 + } +} diff --git a/src/main/java/tree/BuildTree.java b/src/main/java/tree/BuildTree.java index c077c51..a4180c3 100644 --- a/src/main/java/tree/BuildTree.java +++ b/src/main/java/tree/BuildTree.java @@ -33,9 +33,12 @@ public class BuildTree { return helper(0,cnt-1,0,cnt-1,preorder,inorder); } + + + private Map indexMap; - public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left) { + public TreeNode myBuildTree(int[] preorder, int preorder_left, int preorder_right, int inorder_left) { if (preorder_left > preorder_right) { return null; } @@ -49,20 +52,20 @@ public class BuildTree { int size_left_subtree = inorder_root - inorder_left; // 递归地构造左子树,并连接到根节点 // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素 - root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left); + root.left = myBuildTree(preorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left); // 递归地构造右子树,并连接到根节点 // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素 - root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1); + root.right = myBuildTree(preorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1); return root; } public TreeNode buildTree(int[] preorder, int[] inorder) { int n = preorder.length; // 构造哈希映射,帮助我们快速定位根节点 - indexMap = new HashMap(); + indexMap = new HashMap<>(); for (int i = 0; i < n; i++) { indexMap.put(inorder[i], i); } - return myBuildTree(preorder, inorder, 0, n - 1, 0); + return myBuildTree(preorder, 0, n - 1, 0); } }