目录
[1. 字母大小写全排列](#1. 字母大小写全排列)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
[2. 优美的排列](#2. 优美的排列)
[2.1 题目解析](#2.1 题目解析)
[2.2 解法](#2.2 解法)
[2.3 代码实现](#2.3 代码实现)
1. 字母大小写全排列
https://leetcode.cn/problems/letter-case-permutation/submissions/667324337/
给定一个字符串 s
,通过将字符串 s
中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例 1:
输入:s = "a1b2"
输出:["a1b2", "a1B2", "A1b2", "A1B2"]
示例 2:
输入: s = "3z4"
输出: ["3z4","3Z4"]
提示:
1 <= s.length <= 12
s
由小写英文字母、大写英文字母和数字组成
1.1 题目解析
题目本质
题目要求对给定字符串 s,将其中的字母在大小写之间切换,生成所有可能的组合。数字保持不变。每个字母有两种选择(原样 or 切换大小写),因此结果集本质是一个"二叉决策树"的遍历。
常规解法
最直观的办法就是:从左到右遍历字符串,每遇到一个字母,就分两支递归;遇到数字,就单一路径继续。
问题分析
如果直接考虑所有可能字符串,再去筛选,会浪费大量时间,因为字符的相对顺序固定,只有大小写可变。复杂度必然是 2^L(L 为字母数量),这是最优的结果规模。
思路转折
要想写法清晰高效:
-
把问题建模为回溯,每个位置做决策。
-
用一个 StringBuffer path 存储当前路径,每走一步 append 字符,回退时 delete。
-
递归终止条件是走到字符串末尾 (pos == s.length()),此时把路径加入答案。
-
判断是否是字母,用 ASCII 范围判断即可;切换大小写用 ±32 或 ^32,简单高效。
1.2 解法
算法思想
-
参数 pos 表示当前下标。
-
如果 pos == s.length(),说明所有字符处理完,把路径加入结果。
-
对当前字符 ch:
-
路径一:原样加入,递归下一个位置;
-
路径二:如果是字母,再切换大小写,递归下一个位置。
-
-
回溯时保证 append 和 delete 成对出现。
**i)**初始化 path、ret。
**ii)**从 pos=0 调用 dfs。
**iii)**当 pos == s.length(),把 path.toString() 加入 ret 并返回。
**iv)**取 ch = s.charAt(pos):
-
追加原字符,递归 pos+1,回溯删除;
-
若是字母:切换大小写后再追加,递归 pos+1,回溯删除。
**v)**返回结果 ret。
易错点
-
判断字母要用 AND:'a' <= ch && ch <= 'z',不要写成 OR。
-
切换大小写只对 A-Z/a-z 有效。
-
终止条件必须是 pos == s.length()。
-
回溯操作必须对称:append → dfs → delete。
1.3 代码实现
java
class Solution {
StringBuffer path;
List<String> ret;
String s;
public List<String> letterCasePermutation(String _s) {
path = new StringBuffer();
ret = new ArrayList<>();
s = _s;
dfs(s, 0);
return ret;
}
public void dfs(String s, int pos) {
if (pos == s.length()) {
ret.add(path.toString());
return;
}
char ch = s.charAt(pos);
// 原样加入
path.append(ch);
dfs(s, pos + 1);
path.deleteCharAt(path.length() - 1);
// 如果是字母,再切换大小写
if (('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')) {
char tmp = change(ch);
path.append(tmp);
dfs(s, pos + 1);
path.deleteCharAt(path.length() - 1);
}
}
public char change(char ch) {
if ('a' <= ch && ch <= 'z') return (char)(ch - 32);
else return (char)(ch + 32);
}
}
复杂度分析
-
时间复杂度:O(n * 2^L),其中 n 是字符串长度,L 是字母个数。每条路径长度为 n,共 2^L 条路径。
-
空间复杂度:O(n),递归深度和临时路径存储,不计结果集
2. 优美的排列
https://leetcode.cn/problems/beautiful-arrangement/
假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm
(下标从 1 开始 ),只要满足下述条件 之一 ,该数组就是一个 优美的排列 :
perm[i]
能够被i
整除i
能够被perm[i]
整除
给你一个整数 n
,返回可以构造的 优美排列 的 数量 。
示例 1:
输入:n = 2
输出:2
解释:
第 1 个优美的排列是 [1,2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是 [2,1]:
- perm[1] = 2 能被 i = 1 整除
- i = 2 能被 perm[2] = 1 整除
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 15
2.1 题目解析
题目本质
把 1..n 的数字按位置 1..n 进行放置,要求每个位置 i 与放入的数字 x 满足"整除二选一":x % i == 0 || i % x == 0。本质是"受约束的全排列计数",典型解为回溯(DFS)+ 剪枝。
常规解法
暴力生成所有 n! 个排列,再逐一检查是否满足整除条件,统计个数。
问题分析
直接生成 n! 规模太大(n≤15 时,n! 远超可承受范围)。且"整除条件"是可在局部(位置级)提前检查的,没必要等到完整排列后再判定。预估合理策略是:在构造过程中就剪掉不可能的分支,让搜索树大幅缩小。
思路转折
要想高效 → 必须"边放置边校验"并尽早剪枝:
-
位置从 1 递增到 n;每次只尝试未使用的数字 x;
-
若 (x % pos == 0 || pos % x == 0) 才继续递归;否则剪掉该分支;
-
一旦放完 n 个位置,就计数并立即返回 ,避免继续无意义循环(这是易错点)。
如果需要进一步优化(如 n 接近上限 15),可考虑位掩码 DP(f[pos][mask])把状态重用,时间复杂度约 O(n * 2^n),但回溯剪枝通常已可通过。
2.2 解法
算法思想
-
用 pos 表示当前要放置的位置(1..n)
-
枚举未使用数字 x,若满足 (x % pos == 0 || pos % x == 0),则选择 x 并递归到 pos+1。
-
当 pos > n,说明前 1..n 都放好了,计数 ret++ 并返回。
-
用 vis[x] 标记数字是否被使用,实现"不重复放置"。
**i)**初始化全局结果计数 ret=0,访问数组 vis[1..n]=false。
**ii)**从 pos=1 调用 dfs(n, 1)。
iii) 若 pos > n:说明放满,ret++,并 return(关键)。
**iv)**枚举 x 从 1..n:若 !vis[x] 且 (x % pos == 0 || pos % x == 0):
**v)**返回 ret。
易错点
-
终止条件达成后要立即返回;否则会在 pos = n+1 层继续枚举,导致错误累加。
-
位置从 1 开始;vis 长度写 n+1 下标更直观。
-
剪枝条件写法 (x % pos == 0 || pos % x == 0) 不要颠倒逻辑。
2.3 代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
int ret;
boolean[] vis;
public int countArrangement(int n) {
ret = 0;
vis = new boolean[n + 1]; // 使用 1..n
dfs(n, 1);
return ret;
}
public void dfs(int n, int pos) {
// 放完 1..n 个位置,计数并立即返回
if (pos > n) {
ret++;
return; // 易错点:必须 return,避免继续枚举 pos = n+1 的分支
}
for (int x = 1; x <= n; x++) {
if (!vis[x] && (x % pos == 0 || pos % x == 0)) {
vis[x] = true;
dfs(n, pos + 1);
vis[x] = false;
}
}
}
}
复杂度分析
-
时间复杂度:回溯 + 剪枝,最坏上界接近 O(n!),但由于整除约束强,实际分支大幅减少,n≤15 可通过。
-
空间复杂度:O(n),递归深度与访问标记数组;不计最终计数结果。