diff --git a/算法周更班/class_2023_03_2_week/Code01_StoneGameII.java b/算法周更班/class_2023_03_2_week/Code01_StoneGameII.java new file mode 100644 index 0000000..2e1c92c --- /dev/null +++ b/算法周更班/class_2023_03_2_week/Code01_StoneGameII.java @@ -0,0 +1,182 @@ +package class_2023_03_2_week; + +// 爱丽丝和鲍勃继续他们的石子游戏 +// 许多堆石子 排成一行,每堆都有正整数颗石子 piles[i] +// 游戏以谁手中的石子最多来决出胜负。 +// 爱丽丝和鲍勃轮流进行,爱丽丝先开始。最初,M = 1。 +// 在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M +// 然后,令 M = max(M, X)。 +// 游戏一直持续到所有石子都被拿走。 +// 假设爱丽丝和鲍勃都发挥出最佳水平 +// 返回爱丽丝可以得到的最大数量的石头。 +// 测试链接 : https://leetcode.cn/problems/stone-game-ii/ +public class Code01_StoneGameII { + + // 暴力方法 + // 提交会超时,但是怎么尝试已经详细阐述明白 + // 提交时把stoneGameII1改名为stoneGameII + public static int stoneGameII1(int[] piles) { + return first1(piles, 0, 1); + } + + // 当前先手的情况下 + // 可以在piles[i....]范围上拿数字 + // 返回最大的收益 + public static int first1(int[] piles, int index, int m) { + // 没有数字了,收益肯定是0 + if (index == piles.length) { + return 0; + } + int best = 0; + int pre = 0; + // 先手根据m,做所有的尝试,选最好的 + // 1 2 3 .. 2m + // index....i 几个,j个 + // 1) index...index 1个 pre + // 2) index...index+1 2个 pre + // 3) index...index+2 3个 pre + // m = 100 1 2 3 ~ 200 1~2m + for (int i = index, j = 1; i < piles.length && j <= 2 * m; i++, j++) { + pre += piles[i]; + // j个 + // index...i (i+1.... + best = Math.max(best, pre + second1(piles, i + 1, Math.max(j, m))); + } + return best; + } + + // 当前后手的情况下 + // 可以在piles[i....]范围上拿数字 + // 返回最大的收益 + public static int second1(int[] piles, int index, int m) { + if (index == piles.length) { + return 0; + } + int worse = Integer.MAX_VALUE; + // 先手根据m,做所有的尝试,并且把最差的结果留给后手 + for (int i = index, j = 1; i < piles.length && j <= 2 * m; i++, j++) { + worse = Math.min(worse, first1(piles, i + 1, Math.max(j, m))); + } + return worse; + } + + // 暴力方法改成记忆化搜索 + // 可以直接通过 + // 提交时把stoneGameII2改名为stoneGameII + public static int stoneGameII2(int[] piles) { + int[][] f = new int[piles.length][piles.length + 1]; + int[][] s = new int[piles.length][piles.length + 1]; + for (int i = 0; i < piles.length; i++) { + for (int j = 0; j <= piles.length; j++) { + f[i][j] = -1; + s[i][j] = -1; + } + } + return first2(piles, 0, 1, f, s); + } + + // 当前先手的情况下 + // 可以在piles[i....]范围上拿数字 + // 返回最大的收益 + public static int first2(int[] piles, int index, int m, int[][] f, int[][] s) { + // 没有数字了,收益肯定是0 + if (index == piles.length) { + return 0; + } + if (f[index][m] != -1) { + return f[index][m]; + } + int best = 0; + int pre = 0; + for (int i = index, j = 1; i < piles.length && j <= 2 * m; i++, j++) { + pre += piles[i]; + best = Math.max(best, pre + second2(piles, i + 1, Math.min(piles.length, Math.max(j, m)), f, s)); + } + f[index][m] = best; + return best; + } + + // 当前后手的情况下 + // 可以在piles[i....]范围上拿数字 + // 返回最大的收益 + public static int second2(int[] piles, int index, int m, int[][] f, int[][] s) { + if (index == piles.length) { + return 0; + } + if (s[index][m] != -1) { + return s[index][m]; + } + int worse = Integer.MAX_VALUE; + for (int i = index, j = 1; i < piles.length && j <= 2 * m; i++, j++) { + worse = Math.min(worse, first2(piles, i + 1, Math.min(piles.length, Math.max(j, m)), f, s)); + } + s[index][m] = worse; + return worse; + } + + // 记忆化搜索改严格位置依赖的动态规划 + // 提交时把stoneGameII3改名为stoneGameII + public static int stoneGameII3(int[] piles) { + int n = piles.length; + int[][] f = new int[n + 1][n + 1]; + int[][] s = new int[n + 1][n + 1]; + for (int index = n - 1; index >= 0; index--) { + for (int m = 1; m <= n; m++) { + int pre = 0; + for (int i = index, j = 1; i < piles.length && j <= 2 * m; i++, j++) { + pre += piles[i]; + f[index][m] = Math.max(f[index][m], pre + s[i + 1][Math.min(n, Math.max(j, m))]); + } + s[index][m] = Integer.MAX_VALUE; + for (int i = index, j = 1; i < piles.length && j <= 2 * m; i++, j++) { + s[index][m] = Math.min(s[index][m], f[i + 1][Math.min(n, Math.max(j, m))]); + } + } + } + return f[0][1]; + } + + // 严格位置依赖的动态规划,一张表的版本 + // 提交时把stoneGameII4改名为stoneGameII + public int stoneGameII4(int[] piles) { + int n = piles.length, sum = 0; + // dp[i][m] : 表示piles[i....]范围上,在拿取范围是1~2*m的情况下,先手能获得的最大收益 + // 举个例子 : + // 比如当前先手,可以在piles[7...]范围上,在拿取范围是1 ~ 2 * 4(m)的情况下 + // 先手想获得最大收益 + // 当先手拿走7...9的块(这是长度为3的块), + // 后续轮到对手在piles[10...]范围上,在拿取范围是1~2*m的情况下,对手也会获得的最大收益 + // 假设piles[7...]整体的累加和是sum + // 整体的累加和 = 先手收益 + 后手收益,因为先手和后手一定刮分整个累加和 + // 所以,先手获得的最大值 = sum - 对手在先手时后续所有可能性中的最小值 + int[][] dp = new int[n][n + 1]; + for (int i = n - 1; i >= 0; i--) { + // i == n - 1 sum = arr[n-1] + // i == n - 2 sum += arr[n-2] + // i == n - 3 sum += arr[n-3] + sum += piles[i]; + for (int m = 1; m <= n; m++) { + // dp[4][1] ... + // dp[4][2] ... + // dp[i][m] + if (i + 2 * m >= n) { + // 如果先手当前的拿取范围,可以全包下数组所有的数 + // 那必然是全拿,不给对手任何机会 + dp[i][m] = sum; + } else { + // 如果先手当前的拿取范围,不能全包下数组所有的数 + // 那必然去是看看,对手在先手时后续所有可能性中的最小值 + // nextMin就是这个含义 : 对手在先手时后续所有可能性中的最小值 + int nextMin = Integer.MAX_VALUE; + for (int x = 1; x <= 2 * m; x++) { + nextMin = Math.min(nextMin, dp[i + x][Math.max(m, x)]); + } + // 先手获得的最大值 = sum - 对手在先手时后续所有可能性中的最小值 + dp[i][m] = sum - nextMin; + } + } + } + return dp[0][1]; + } + +} diff --git a/算法周更班/class_2023_03_2_week/Code02_ShortestCommonSupersequence.java b/算法周更班/class_2023_03_2_week/Code02_ShortestCommonSupersequence.java new file mode 100644 index 0000000..22f853f --- /dev/null +++ b/算法周更班/class_2023_03_2_week/Code02_ShortestCommonSupersequence.java @@ -0,0 +1,54 @@ +package class_2023_03_2_week; + +// 给出两个字符串 str1 和 str2 +// 返回同时以 str1 和 str2 作为子序列的最短字符串 +// 如果答案不止一个,则可以返回满足条件的任意一个答案。 +// 测试链接 : https://leetcode.cn/problems/shortest-common-supersequence/ +// 体系学习班,最长公共子序列问题 +// 大厂刷题班,章节11,根据动态规划表,生成路径 +public class Code02_ShortestCommonSupersequence { + + public static String shortestCommonSupersequence(String str1, String str2) { + int n = str1.length(); + int m = str2.length(); + int[][] dp = new int[n + 1][m + 1]; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + if (str1.charAt(i - 1) == str2.charAt(j - 1)) { + dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1); + } + } + } + // str1 n + // str2 m + // dp[n][m] n + m - dp[n][m] + char[] ans = new char[n + m - dp[n][m]]; + int ansi = ans.length - 1; + int i = n; + int j = m; + while (i > 0 && j > 0) { + // i str1的前缀长度 + // j str2的前缀长度 + if (dp[i][j] == dp[i - 1][j - 1] + 1 && str1.charAt(i - 1) == str2.charAt(j - 1)) { + ans[ansi--] = str1.charAt(i - 1); + i--; + j--; + } else if (dp[i][j] == dp[i - 1][j]) { + ans[ansi--] = str1.charAt(i - 1); + i--; + } else { + ans[ansi--] = str2.charAt(j - 1); + j--; + } + } + for (; i > 0; i--) { + ans[ansi--] = str1.charAt(i - 1); + } + for (; j > 0; j--) { + ans[ansi--] = str2.charAt(j - 1); + } + return String.valueOf(ans); + } + +} diff --git a/算法周更班/class_2023_03_2_week/Code03_FillCellsUseAllColorsWays.java b/算法周更班/class_2023_03_2_week/Code03_FillCellsUseAllColorsWays.java new file mode 100644 index 0000000..c93f0fe --- /dev/null +++ b/算法周更班/class_2023_03_2_week/Code03_FillCellsUseAllColorsWays.java @@ -0,0 +1,95 @@ +package class_2023_03_2_week; + +import java.util.Arrays; + +// 来自学员问题 +// 给定N、M两个参数 +// 一共有N个格子,每个格子可以涂上一种颜色,颜色在M种里选 +// 当涂满N个格子,并且M种颜色都使用了,叫一种有效方法 +// 求一共有多少种有效方法 +// 1 <= N, M <= 5000 +// 返回结果比较大,请把结果 % 1000000007 之后返回 +public class Code03_FillCellsUseAllColorsWays { + + // 暴力方法 + // 为了验证 + public static int ways1(int n, int m) { + return process(new int[n], new boolean[m + 1], 0, n, m); + } + + public static int process(int[] path, boolean[] set, int i, int n, int m) { + if (i == n) { + Arrays.fill(set, false); + int colors = 0; + for (int c : path) { + if (!set[c]) { + set[c] = true; + colors++; + } + } + return colors == m ? 1 : 0; + } else { + int ans = 0; + for (int j = 1; j <= m; j++) { + path[i] = j; + ans += process(path, set, i + 1, n, m); + } + return ans; + } + } + + // 正式方法 + // 时间复杂度O(N*M) + public static int MAXN = 5001; + + public static int[][] dp = new int[MAXN][MAXN]; + + public static int mod = 1000000007; + + public static int ways2(int n, int m) { + for (int i = 1; i <= n; i++) { + dp[i][1] = m; + } + for (int i = 2; i <= n; i++) { + for (int j = 2; j <= m; j++) { + dp[i][j] = (int) (((long) dp[i - 1][j] * j) % mod); + dp[i][j] = (int) ((((long) dp[i - 1][j - 1] * (m - j + 1)) + dp[i][j]) % mod); + } + } + return dp[n][m]; + } + + public static void main(String[] args) { + int N = 9; + int M = 9; + System.out.println("功能测试开始"); + for (int n = 1; n <= N; n++) { + for (int m = 1; m <= M; m++) { + int ans1 = ways1(n, m); + int ans2 = ways2(n, m); + if (ans1 != ans2) { + System.out.println("出错了!"); + System.out.println("n : " + n); + System.out.println("m : " + m); + System.out.println("ans1 : " + ans1); + System.out.println("ans2 : " + ans2); + break; + } + } + } + System.out.println("功能测试结束"); + + System.out.println("性能测试开始"); + int n = 5000; + int m = 4877; + System.out.println("n : " + n); + System.out.println("m : " + m); + long start = System.currentTimeMillis(); + int ans = ways2(n, m); + long end = System.currentTimeMillis(); + System.out.println("取余之后的结果 : " + ans); + System.out.println("运行时间 : " + (end - start) + " 毫秒"); + System.out.println("性能测试结束"); + } + +} diff --git a/算法周更班/class_2023_03_2_week/Code04_NumbersWithRepeatedDigits.java b/算法周更班/class_2023_03_2_week/Code04_NumbersWithRepeatedDigits.java new file mode 100644 index 0000000..85c64c9 --- /dev/null +++ b/算法周更班/class_2023_03_2_week/Code04_NumbersWithRepeatedDigits.java @@ -0,0 +1,145 @@ +package class_2023_03_2_week; + +// 给定正整数 n +// 返回在 [1, n] 范围内具有 至少 1 位 重复数字的正整数的个数。 +// 测试链接 : https://leetcode.cn/problems/numbers-with-repeated-digits/ +public class Code04_NumbersWithRepeatedDigits { + + public static int numDupDigitsAtMostN(int n) { + if (n <= 10) { + return 0; + } + // n = 23645 + // len = 5长度 + // n = 723645 + // offset = 100000 + int len = 1; + int offset = 1; + int tmp = n / 10; + while (tmp > 0) { + len++; + offset *= 10; + tmp /= 10; + } + // n = 23645 5长度 + // 1长度的有几个、2长度的有几个、3长度的有几个、4长度的有几个 + // 直接公式决定 + int noRepeat = 0; + for (int i = 1; i < len; i++) { + noRepeat += numAllLength(i); + } + // num = 723645 + // 单独求6长度的,数字不重复的有几个 + if (len <= 10) { + // int a = 0b 11 + int status = 0b1111111111; + // n = 732645 + // offset 100000 + // n / offset = 7 + // 1 2 3 ... 6 7-1 + // ==== + // n = 732645 + // offset 100000 + // 10000 + noRepeat += ((n / offset) - 1) * numberRest(offset / 10, status ^ 1); + // n = 732645 + // offset 100000 + // 7..... + noRepeat += process(offset / 10, status ^ (1 << (n / offset)), n); + } + return n + 1 - noRepeat; + } + + // 10进制长度必须为len的所有数中 + // 每一位数字都不重复的数有多少 + // 10 + // 9 * 9 + // 9 * 9 * 8 + // 9 * 9 * 8 * 7 + public static int numAllLength(int len) { + if (len > 10) { + return 0; + } + if (len == 1) { + return 10; + } + int ans = 9; + int cur = 9; + while (--len > 0) { + ans *= cur; + cur--; + } + return ans; + } + + // n = 732645 + // 10000 + // status : 还剩哪些数字可以选 + // 返回值 : 前面的数字都和n一样,剩下的位里有多少数字,剩下的数字都不同? + public static int process(int offset, int status, int n) { + if (offset == 0) { + // n = 732645 + // 0 + return 1; + } + int ans = 0; + // n = 732645 + // off 100 + // 732.... + // 7320... + // 7321... + // 7322... X + // 7323... X + // 7324... + // 7325... + // + // n = 732645 + // off 100 + // 6 + // 7326 % 10 = 6 + int first = (n / offset) % 10; + for (int cur = 0; cur < first; cur++) { + if ((status & (1 << cur)) != 0) { + ans += numberRest(offset / 10, status ^ (1 << cur)); + } + } + // n = 732645 + // off 10 + // 7326 .. + if ((status & (1 << first)) != 0) { + ans += process(offset / 10, status ^ (1 << first), n); + } + return ans; + } + + // offset : 1000, 还剩4长度 + // offset : 100000, 还剩7长度 + // status : 还有哪些数字可以选 + // 状态! + // 9 8 7 6 5 4 3 2 1 0 + // 0 1 1 1 0 0 0 1 1 1 + public static int numberRest(int offset, int status) { + // c 还有几种数字可以选! + // offset = 1000; 100 10 1 0 + // c = 7 6 5 4 3 + // ans = 1 * 7 * 6 * 5 * 4 + int c = hammingWeight(status); + int ans = 1; + while (offset > 0) { + ans *= c; + c--; + offset /= 10; + } + return ans; + } + + public static int hammingWeight(int n) { + n = (n & 0x55555555) + ((n >>> 1) & 0x55555555); + n = (n & 0x33333333) + ((n >>> 2) & 0x33333333); + n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f); + n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff); + n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); + return n; + } + +} diff --git a/算法周更班/class_2023_03_2_week/Code05_BraceExpansionII.java b/算法周更班/class_2023_03_2_week/Code05_BraceExpansionII.java new file mode 100644 index 0000000..c11d222 --- /dev/null +++ b/算法周更班/class_2023_03_2_week/Code05_BraceExpansionII.java @@ -0,0 +1,101 @@ +package class_2023_03_2_week; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +// 如果你熟悉 Shell 编程,那么一定了解过花括号展开,它可以用来生成任意字符串。 +// 花括号展开的表达式可以看作一个由 花括号、逗号 和 小写英文字母 组成的字符串 +// 定义下面几条语法规则: +// 如果只给出单一的元素 x,那么表达式表示的字符串就只有 "x"。R(x) = {x} +// 例如,表达式 "a" 表示字符串 "a"。 +// 而表达式 "w" 就表示字符串 "w"。 +// 当两个或多个表达式并列,以逗号分隔,我们取这些表达式中元素的并集 +// R({e_1,e_2,...}) = R(e_1) ∪ R(e_2) ∪ ... +// 例如,表达式 "{a,b,c}" 表示字符串 "a","b","c"。 +// 而表达式 "{{a,b},{b,c}}" 也可以表示字符串 "a","b","c"。 +// 要是两个或多个表达式相接,中间没有隔开时, +// 我们从这些表达式中各取一个元素依次连接形成字符串 +// R(e_1 + e_2) = {a + b for (a, b) in R(e_1) × R(e_2)} +// 例如,表达式 "{a,b}{c,d}" 表示字符串 "ac","ad","bc","bd"。 +// 表达式之间允许嵌套,单一元素与表达式的连接也是允许的。 +// 例如,表达式 "a{b,c,d}" 表示字符串 "ab","ac","ad"​​​​​​。 +// 例如,表达式 "a{b,c}{d,e}f{g,h}" +// 可以表示字符串 : +// "abdfg", "abdfh", "abefg", "abefh", +// "acdfg", "acdfh", "acefg", "acefh"。 +// 给出表示基于给定语法规则的表达式 expression +// 返回它所表示的所有字符串组成的有序列表。 +// 测试链接 : https://leetcode.cn/problems/brace-expansion-ii/ +public class Code05_BraceExpansionII { + + public static List braceExpansionII(String expression) { + return new ArrayList<>(process(expression.toCharArray(), 0).ans); + } + + public static class Info { + public TreeSet ans; + public int end; + + public Info(TreeSet a, int e) { + ans = a; + end = e; + } + } + + // exp[start..........] 遇到我的} 或者exp终止位置,停! + public static Info process(char[] exp, int start) { + // 最终的结果,返回 + TreeSet ans = new TreeSet<>(); + // 集合A X 集合B X 集合C , 遇到, parts清空 + // ................ 遇到, parts清空 + // 每一回的结果,加入到ans + List> parts = new ArrayList<>(); + // 收集字符 + StringBuilder builder = new StringBuilder(); + while (start != exp.length && exp[start] != '}') { + if (exp[start] == '{') { + addStringToParts(builder, parts); + Info next = process(exp, start + 1); + parts.add(next.ans); + start = next.end + 1; + } else if (exp[start] == ',') { + addStringToParts(builder, parts); + addPartsToSet(ans, parts); + start++; + parts.clear(); + } else { // 遇到英文字母 + builder.append(exp[start]); + start++; + } + } + addStringToParts(builder, parts); + addPartsToSet(ans, parts); + return new Info(ans, start); + } + + public static void addStringToParts(StringBuilder builder, List> parts) { + if (builder.length() != 0) { + parts.add(new TreeSet<>()); + parts.get(parts.size() - 1).add(builder.toString()); + builder.delete(0, builder.length()); + } + } + + public static void addPartsToSet(TreeSet ans, List> parts) { + process(parts, 0, "", ans); + } + + public static void process(List> list, int i, String path, TreeSet ans) { + if (i == list.size()) { + if (!path.equals("")) { + ans.add(path); + } + } else { + for (String cur : list.get(i)) { + process(list, i + 1, path + cur, ans); + } + } + } + +} diff --git a/算法课堂笔记/课堂内容汇总/每周有营养的大厂算法面试题(正在直播) b/算法课堂笔记/课堂内容汇总/每周有营养的大厂算法面试题(正在直播) index d803708..3b996b4 100644 --- a/算法课堂笔记/课堂内容汇总/每周有营养的大厂算法面试题(正在直播) +++ b/算法课堂笔记/课堂内容汇总/每周有营养的大厂算法面试题(正在直播) @@ -3150,15 +3150,61 @@ HH相信不同的贝壳会带来好运,所以每次散步完后,他都会随 +第061节 2023年3月第2周流行算法题目解析 + +爱丽丝和鲍勃继续他们的石子游戏 +许多堆石子 排成一行,每堆都有正整数颗石子 piles[i] +游戏以谁手中的石子最多来决出胜负。 +爱丽丝和鲍勃轮流进行,爱丽丝先开始。最初,M = 1。 +在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M +然后,令 M = max(M, X)。 +游戏一直持续到所有石子都被拿走。 +假设爱丽丝和鲍勃都发挥出最佳水平 +返回爱丽丝可以得到的最大数量的石头。 +测试链接 : https://leetcode.cn/problems/stone-game-ii/ + +给出两个字符串 str1 和 str2 +返回同时以 str1 和 str2 作为子序列的最短字符串 +如果答案不止一个,则可以返回满足条件的任意一个答案。 +测试链接 : https://leetcode.cn/problems/shortest-common-supersequence/ +体系学习班,最长公共子序列问题 +大厂刷题班,章节11,根据动态规划表,生成路径 - - - - - - - - +来自学员问题 +给定N、M两个参数 +一共有N个格子,每个格子可以涂上一种颜色,颜色在M种里选 +当涂满N个格子,并且M种颜色都使用了,叫一种有效方法 +求一共有多少种有效方法 +1 <= N, M <= 5000 +返回结果比较大,请把结果 % 1000000007 之后返回 + +给定正整数 n +返回在 [1, n] 范围内具有 至少 1 位 重复数字的正整数的个数。 +测试链接 : https://leetcode.cn/problems/numbers-with-repeated-digits/ + +如果你熟悉 Shell 编程,那么一定了解过花括号展开,它可以用来生成任意字符串。 +花括号展开的表达式可以看作一个由 花括号、逗号 和 小写英文字母 组成的字符串 +定义下面几条语法规则: +如果只给出单一的元素 x,那么表达式表示的字符串就只有 "x"。R(x) = {x} +例如,表达式 "a" 表示字符串 "a"。 +而表达式 "w" 就表示字符串 "w"。 +当两个或多个表达式并列,以逗号分隔,我们取这些表达式中元素的并集 +R({e_1,e_2,...}) = R(e_1) ∪ R(e_2) ∪ ... +例如,表达式 "{a,b,c}" 表示字符串 "a","b","c"。 +而表达式 "{{a,b},{b,c}}" 也可以表示字符串 "a","b","c"。 +要是两个或多个表达式相接,中间没有隔开时, +我们从这些表达式中各取一个元素依次连接形成字符串 +R(e_1 + e_2) = {a + b for (a, b) in R(e_1) × R(e_2)} +例如,表达式 "{a,b}{c,d}" 表示字符串 "ac","ad","bc","bd"。 +表达式之间允许嵌套,单一元素与表达式的连接也是允许的。 +例如,表达式 "a{b,c,d}" 表示字符串 "ab","ac","ad"​​​​​​。 +例如,表达式 "a{b,c}{d,e}f{g,h}" +可以表示字符串 : +"abdfg", "abdfh", "abefg", "abefh", +"acdfg", "acdfh", "acefg", "acefh"。 +给出表示基于给定语法规则的表达式 expression +返回它所表示的所有字符串组成的有序列表。 +测试链接 : https://leetcode.cn/problems/brace-expansion-ii/