LeetCode算法日记 - Day 59: 字母大小写全排列、优美的排列

目录

[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),递归深度与访问标记数组;不计最终计数结果。

相关推荐
岑梓铭2 小时前
《考研408数据结构》第三章(3.1 栈)复习笔记
数据结构·笔记·考研·408
Archie_IT2 小时前
嵌入式八股文篇——P1 关键字篇
c语言·开发语言·单片机·mcu·物联网·面试·职场和发展
workflower3 小时前
将图片中的图形转换为可编辑的 PPT 图形
java·开发语言·tomcat·powerpoint·个人开发·结对编程
未知陨落3 小时前
LeetCode:81.爬楼梯
算法·leetcode
SHtop113 小时前
排序算法(golang实现)
算法·golang·排序算法
卡戎-caryon3 小时前
【Java SE】06. 数组
java·开发语言
想躺平的咸鱼干3 小时前
Spring AI Alibaba
java·人工智能·spring
Rain_is_bad4 小时前
初识c语言————数学库函数
c语言·开发语言·算法
老华带你飞4 小时前
学生信息管理系统|基于Springboot的学生信息管理系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·学生信息管理系统