LeetCode 1415. 长度为 n 的开心字符串中字典序第 k 小的字符串

LeetCode 1415. 长度为 n 的开心字符串中字典序第 k 小的字符串

题目描述

一个「开心字符串」定义为:

  • 仅由小写英文字母 'a''b''c' 组成。
  • 字符串中任意相邻两个字符不相同。

给你两个整数 nk,你需要将长度为 n 的所有开心字符串按字典序排序,并返回排在第 k 位的字符串。如果长度为 n 的开心字符串少于 k 个,则返回空字符串。

示例:

复制代码
输入:n = 3, k = 4
输出:"acb"
解释:长度为 3 的开心字符串共 12 个,按字典序排序为:
"aba", "abc", "aca", "acb", "bab", "bac", "bca", "bcb", "cab", "cac", "cba", "cbc"
第 4 个是 "acb"。

思路分析

长度为 n 的开心字符串总数是固定的:

  • 第一个字符有 3 种选择。
  • 之后每个字符有 2 种选择(不能与前一个相同)。
    因此总数为 3 × 2^(n-1)

我们可以利用这个规律,直接构造出第 k 个字符串 ,而不需要生成所有字符串。

因为每个位置的字符选择可以看作一个「分支」,k 的值隐含了从根到叶子的路径信息。

数学映射

将 k 转换为 0‑based 索引(k--)。

将长度为 n 的所有开心字符串按字典序排列后,它们可以被划分为 3 大块,每块对应首字符为 'a''b''c'。每块的大小恰好是 2^(n-1)

因此,首字符可以由 k / 2^(n-1) 确定。

之后,在确定的块内,剩余位置的选择可以继续用同样的思路分解:

每个后续位置都有 2 种可能(避开前一个字符),它们又构成大小为 2^(n-1-i) 的子块。

利用 k 的二进制位来表示这些分支:右移 (n-1-i) 位后取最低位,得到 0 或 1,对应当前位置的「基础偏移」。

避开相邻相同字符的映射技巧

当我们用偏移量构造当前字符时,直接 'a' + offset 可能会与前一个字符相等或冲突。

实际上,可选的两个字符是按字典序排列的(较小的对应 offset=0,较大的对应 offset=1),而前一个字符被排除。

我们可以先计算一个临时字符 c = 'a' + offset,然后检查它是否 大于等于 前一个字符:

  • 如果 c >= prev,说明它落在了前一个字符或之后的字符上,需要再加 1 跳过前一个字符。
  • 否则,直接使用 c

这个条件可以正确处理所有情况(可代入验证)。

代码实现(C++)

cpp 复制代码
class Solution {
public:
    string getHappyString(int n, int k) {
        // 1. 判断 k 是否超出总数
        if (k > 3 << (n - 1)) return "";
        
        // 2. 转为 0-based 索引
        k--;
        
        // 3. 初始化结果字符串为全 'a'
        string ans(n, 'a');
        
        // 4. 确定第一个字符
        ans[0] += k >> (n - 1);
        
        // 5. 确定后续每个字符
        for (int i = 1; i < n; i++) {
            // 提取当前位的偏移 (0 或 1)
            ans[i] += (k >> (n - 1 - i)) & 1;
            // 调整:如果当前字符 >= 前一个字符,需要加1
            if (ans[i] >= ans[i - 1])
                ans[i]++;
        }
        
        return ans;
    }
};

代码详解

  1. 总数判断
    3 << (n - 1) 等价于 3 * 2^(n-1),若 k 大于总数则返回空串。

  2. 0‑based 索引

    因为 k 从 1 开始计数,而后续位运算希望从 0 开始,所以 k--

  3. 初始化字符串

    创建一个长度为 n 的字符串,全部初始化为 'a',之后通过增加偏移量来得到 'b''c'

  4. 首字符确定
    k >> (n - 1) 得到 k 除以 2^(n-1) 的整数部分,结果为 0、1 或 2,分别对应 'a''b''c'

    加到 ans[0] 上即得到正确首字符。

  5. 后续字符确定

    对于第 i 个位置(i 从 1 开始):

    • k >> (n - 1 - i) 右移后,最低位表示当前块内的分支选择(0 或 1)。
    • & 1 取出这一位,加到 ans[i] 上,得到临时字符(可能是 'a''b',也可能是 'a'+1'b''a'+2'c' 吗?注意:临时字符只加 0 或 1,所以只能是 'a''b'。这符合预期:当前位置可选的两个字符正是较小和较大的两个字符,它们与 'a' 的差值恰好是 0 和 1(但需要跳过前一个字符,所以最终可能变成 'c')。
    • 调整:如果这个临时字符 大于等于 前一个字符 ans[i-1],则说明它落在了前一个字符或它的下一个字符上,由于前一个字符被禁止,所以必须再 +1,跳到下一个可用字符。
    • 这个调整逻辑将偏移量 0/1 正确映射到两个合法的可选字符上。

举例验证

n = 3, k = 4 为例:

  • 总数 = 3 << 2 = 12,k=4 ≤12,继续。
  • k-- → 3(0‑based)。
  • 首字符:3 >> 2 = 0ans[0] = 'a'
  • i=1:(3 >> 1) & 1 = 1ans[1] += 1 得到 'b'
    因为 'b' >= 'a' 成立,所以 ans[1]++'c'。此时字符串为 "ac?"
  • i=2:(3 >> 0) & 1 = 1ans[2] += 1 得到 'b'
    检查 'b' >= 'c'?不成立,保持 'b'。最终结果为 "acb",与预期一致。

复杂度分析

  • 时间复杂度:O(n),仅需一次遍历。
  • 空间复杂度:O(1),除了结果字符串外只使用常数空间。

总结

这种方法利用数学计数和位运算,直接从 k 解码出字符串的每一位,避免生成所有字符串。

关键在于理解 k 的二进制位如何映射到分支选择,以及巧妙的「≥ 前一个字符则加 1」调整规则,使得相邻字符不相同。

代码简洁高效,是这道题的最优解之一。

相关推荐
美好的事情能不能发生在我身上1 小时前
Leetcode热题100中的:技巧专题
算法·leetcode·职场和发展
荣光属于凯撒1 小时前
P15755 [JAG 2025 Summer Camp #1] JAG Box
c++·算法·贪心算法
AI科技星2 小时前
基于v≡c空间光速螺旋量子几何归一化统一场论第一性原理的时间势差本源理论
人工智能·线性代数·算法·机器学习·平面
X-⃢_⃢-X2 小时前
二、索引的数据结构
数据结构·mysql
云泽8082 小时前
蓝桥杯算法精讲:哈夫曼编码的贪心思想与落地实现
算法·职场和发展·蓝桥杯
x_xbx2 小时前
LeetCode:53. 最大子数组和
算法·leetcode·职场和发展
菜菜小狗的学习笔记2 小时前
剑指Offer算法题(一)数组与矩阵
线性代数·算法·矩阵
仰泳的熊猫2 小时前
题目2269:蓝桥杯2016年第七届真题-冰雹数
开发语言·数据结构·c++·算法·蓝桥杯
冷徹 .2 小时前
2023ICPC山东省赛
c++·算法