LeetCode 1415. 长度为 n 的开心字符串中字典序第 k 小的字符串
题目描述
一个「开心字符串」定义为:
- 仅由小写英文字母
'a'、'b'、'c'组成。 - 字符串中任意相邻两个字符不相同。
给你两个整数 n 和 k,你需要将长度为 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;
}
};
代码详解
-
总数判断
3 << (n - 1)等价于3 * 2^(n-1),若k大于总数则返回空串。 -
0‑based 索引
因为 k 从 1 开始计数,而后续位运算希望从 0 开始,所以
k--。 -
初始化字符串
创建一个长度为 n 的字符串,全部初始化为
'a',之后通过增加偏移量来得到'b'或'c'。 -
首字符确定
k >> (n - 1)得到 k 除以2^(n-1)的整数部分,结果为 0、1 或 2,分别对应'a'、'b'、'c'。加到
ans[0]上即得到正确首字符。 -
后续字符确定
对于第 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 = 0→ans[0] = 'a'。 - i=1:
(3 >> 1) & 1 = 1→ans[1] += 1得到'b'。
因为'b' >= 'a'成立,所以ans[1]++→'c'。此时字符串为"ac?"。 - i=2:
(3 >> 0) & 1 = 1→ans[2] += 1得到'b'。
检查'b' >= 'c'?不成立,保持'b'。最终结果为"acb",与预期一致。
复杂度分析
- 时间复杂度:O(n),仅需一次遍历。
- 空间复杂度:O(1),除了结果字符串外只使用常数空间。
总结
这种方法利用数学计数和位运算,直接从 k 解码出字符串的每一位,避免生成所有字符串。
关键在于理解 k 的二进制位如何映射到分支选择,以及巧妙的「≥ 前一个字符则加 1」调整规则,使得相邻字符不相同。
代码简洁高效,是这道题的最优解之一。