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

标签: 字符串 回溯

难度: Medium

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

  • 仅包含小写字母 ['a', 'b', 'c'].
  • 对所有在 1 到 s.length - 1 之间的 i ,满足 s[i] != s[i + 1] (字符串的下标从 1 开始)。

比方说,字符串 "abc""ac","b" 和 "abcbabcbcb" 都是开心字符串,但是 "aa""baa" 和 "ababbc" 都不是开心字符串。

给你两个整数 n 和 k ,你需要将长度为 n 的所有开心字符串按字典序排序。

请你返回排序后的第 k 个开心字符串,如果长度为 n 的开心字符串少于 k 个,那么请你返回 空字符串 。

示例 1:

输入:n = 1, k = 3
输出:"c"
解释:列表 ["a", "b", "c"] 包含了所有长度为 1 的开心字符串。按照字典序排序后第三个字符串为 "c" 。

示例 2:

输入:n = 1, k = 4
输出:""
解释:长度为 1 的开心字符串只有 3 个。

示例 3:

输入:n = 3, k = 9
输出:"cab"
解释:长度为 3 的开心字符串总共有 12 个 ["aba", "abc", "aca", "acb", "bab", "bac", "bca", "bcb", "cab", "cac", "cba", "cbc"] 。第 9 个字符串为 "cab"

示例 4:

输入:n = 2, k = 7
输出:""

示例 5:

输入:n = 10, k = 100
输出:"abacbabacb"

提示:

  • 1 <= n <= 10
  • 1 <= k <= 100

Submission

运行时间: 18 ms

内存: 16.2 MB

class Solution:
    def getHappyString(self, n: int, k: int) -> str:
        tot=3*pow(2,n-1)
        if tot<k:
            return ''
        k-=1
        ret=[]
        tot//=3
        ret.append('abc'[k//tot])
        k%=tot
        tot>>=1
        while tot:
            prev=ret[-1]
            if k&tot==0:
                if prev=='a':
                    ret.append('b')
                else:
                    ret.append('a')
            else:
                if prev=='c':
                    ret.append('b')
                else:
                    ret.append('c')
            # k%=tot
            tot>>=1
        return ''.join(ret)

Explain

解题思路首先通过计算可能的开心字符串总数来确定是否存在第 k 个开心字符串。计算方式为 `3*pow(2,n-1)`,这考虑了字符串的首位有三种可能,且每增加一个字符,选择的可能性就翻倍。如果总数小于 k,直接返回空字符串。若存在,使用二进制位操作的方式来构建第 k 个字符串。首先确定首字符,然后根据二进制中的位来确定每个位置的字符,保证相邻字符不同。

时间复杂度: O(n)

空间复杂度: O(n)

class Solution:
    def getHappyString(self, n: int, k: int) -> str:
        tot=3*pow(2,n-1)  # 计算长度为 n 的开心字符串总数
        if tot<k:  # 如果 k 大于开心字符串总数
            return ''  # 返回空字符串
        k-=1  # 将 k 转换为基于 0 的索引
        ret=[]  # 初始化结果字符串列表
        tot//=3  # 计算第一位之后每个字符的分支数
        ret.append('abc'[k//tot])  # 确定首字符
        k%=tot  # 更新 k 为剩余部分
        tot>>=1  # 减小分支数
        while tot:  # 循环直到字符串完成
            prev=ret[-1]  # 获取上一个字符
            if k&tot==0:  # 根据 k 的当前位决定下一个字符
                if prev=='a':
                    ret.append('b')
                else:
                    ret.append('a')
            else:
                if prev=='c':
                    ret.append('b')
                else:
                    ret.append('c')
            # k%=tot  # 更新 k (注释掉,因为不需要再次赋值)
            tot>>=1  # 减小分支数
        return ''.join(ret)  # 将列表转换为字符串并返回

Explore

开心字符串的定义是一个只由'a'、'b'、'c'组成的字符串,且相邻的字符不相同。首先,字符串的第一个字符可以是'a'、'b'或'c'中的任意一个,因此有3种选择。对于后续的每个字符,由于它不能与前一个字符相同,每次选择的可能性就从3种减少到2种。例如,如果第一个字符是'a',那么第二个字符可以是'b'或'c',即2种可能。因此,对于长度为n的字符串,第一个字符有3种选择,而每个后续字符(共n-1个)都有2种选择,所以总的组合数是`3 * 2^(n-1)`,这也可以表示为`3 * pow(2, n-1)`。

在题解中,二进制位操作用于决定每个位置的字符,以保证相邻字符不同。具体的方法是:首先,通过整除和取模操作确定首字符,然后通过二进制位的检查来决定每个后续字符。例如,如果当前字符是'a',根据二进制位的值(0或1),我们决定下一个字符是'b'还是'c',这样可以确保不重复。具体的逻辑是,如果二进制的当前位是0,我们选择字典序较小的另外两个字符中的一个;如果是1,则选择字典序较大的。这种通过位值决定字符的方法可以在每一步都自动保证字符的不同性,避免了重复和冲突。

在题解中,`tot`的初始值是根据首字符后每个字符的分支数(即两种可能)计算的。在while循环中,每次迭代都会通过右移操作(`tot >>= 1`)减半`tot`的值,这样做的目的是将`tot`更新为下一个字符的选择范围。每次迭代都对应于字符串中的一个新位置,而更新的`tot`值代表该位置以及之后所有位置的可能组合总数。通过比较`k`和`tot`的相对大小,我们可以决定当前位置使用哪个字符,从而一步步构造出所需的字符串。这种动态更新`tot`的方法使得算法能够有效地根据已确定的部分来决定剩余部分,保证了构造的效率和正确性。