题目描述
3304. 找出第 K 个字符 I
提示
Alice 和 Bob 正在玩一个游戏。最初,Alice 有一个字符串 word = "a"。
给定一个正整数 k。
现在 Bob 会要求 Alice 执行以下操作 无限次 :
将 word 中的每个字符 更改 为英文字母表中的 下一个 字符来生成一个新字符串,并将其 追加 到原始的 word。
例如,对 "c" 进行操作生成 "cd",对 "zb" 进行操作生成 "zbac"。
在执行足够多的操作后, word 中 至少 存在 k 个字符,此时返回 word 中第 k 个字符的值。
示例 1:
输入:k = 5
输出:”b”
解释:
最初,word = “a”。需要进行三次操作:
生成的字符串是 “b”,word 变为 “ab”。
生成的字符串是 “bc”,word 变为 “abbc”。
生成的字符串是 “bccd”,word 变为 “abbcbccd”。
示例 2:
输入:k = 10
输出:”c”
提示:
1 <= k <= 500
3307. 找出第 K 个字符 II
Alice 和 Bob 正在玩一个游戏。最初,Alice 有一个字符串 word = “a”。
给定一个正整数 k 和一个整数数组 operations,其中 operations[i] 表示第 i 次操作的类型。
现在 Bob 将要求 Alice 按顺序执行 所有 操作:
如果 operations[i] == 0,将 word 的一份 副本追加 到它自身。
如果 operations[i] == 1,将 word 中的每个字符 更改 为英文字母表中的 下一个 字符来生成一个新字符串,并将其 追加 到原始的 word。例如,对 “c” 进行操作生成 “cd”,对 “zb” 进行操作生成 “zbac”。
在执行所有操作后,返回 word 中第 k 个字符的值。
注意,在第二种类型的操作中,字符 ‘z’ 可以变成 ‘a’。
示例 1:
输入:k = 5, operations = [0,0,0]
输出:”a”
解释:
最初,word == “a”。Alice 按以下方式执行三次操作:
将 “a” 附加到 “a”,word 变为 “aa”。
将 “aa” 附加到 “aa”,word 变为 “aaaa”。
将 “aaaa” 附加到 “aaaa”,word 变为 “aaaaaaaa”。
示例 2:
输入:k = 10, operations = [0,1,0,1]
输出:”b”
解释:
最初,word == “a”。Alice 按以下方式执行四次操作:
将 “a” 附加到 “a”,word 变为 “aa”。
将 “bb” 附加到 “aa”,word 变为 “aabb”。
将 “aabb” 附加到 “aabb”,word 变为 “aabbaabb”。
将 “bbccbbcc” 附加到 “aabbaabb”,word 变为 “aabbaabbbbccbbcc”。
提示:
- 1 <= k <= 1014
- 1 <= operations.length <= 100
- operations[i] 可以是 0 或 1。
- 输入保证在执行所有操作后,word 至少有 k 个字符。
解题思路
这两道差不多,不知道为什么会把 3304 设置为简单题,可能是可以用暴力法直接模拟出来吧。如果从递归角度考虑,其实可以优先解决 3307。
由题目可以知道,每次操作后,字符串长度都会变为原来的两倍。因此,我们可以通过记录每次操作后字符串的长度来快速定位第 k 个字符。题目中已经保证在执行所有操作后,字符串至少有 k 个字符,所以我们可以直接计算出所有操作后的字符串长度,如果 operations 长度为 n,那么最终字符串的长度为
递归解法
那么现在考虑如何在 k 个字符,可以使用递归来解决这个问题。由于字符串的后一半是前一半的变换,因此首先判断第 k 个字符是在前一半还是后一半:
- 如果在前一半,那么相当于最后一次操作没有发生变化,直接递归到前一半;
- 如果在后一半,那么相当于最后一次操作发生了变化,需要先找到前一半的字符,然后根据最后一次操作的类型进行变换。
据此就可以构建出一个递归方程:
所以可以写出如下代码:
1 | func kthCharacter(k int64, operations []int) byte { |
应当说用递归的方式来解决这个问题是非常直观的,时间复杂度为 O(n),其中 n 是 operations 的长度。空间复杂度为 O(n),主要是递归栈的空间。
上面用了 min(len(operations)-1, 62) 是因为 k 的数据类型是 int64(也就是 -(1 << 63) ~ (1 << 63 - 1)),所以如果 len(operations) > 62,那么 2^{len(operations)-1} 的值会超过 int64 的范围,因此 k 一定落在字符串的前半段,可以直接递归到前半段。
迭代解法
参照上面的代码继续思考,就可以得到迭代法的思路:这里其实本质就是在统计第 k 个字符是由字符 a 在 n 操作中变化了多少次。所以可以从后往前遍历 len(operations),每次判断 k 是否在前半段,如果在前半段就继续往前遍历;如果在后半段,就根据操作类型确定字符是否发生变化。
1 | func kthCharacter(k int64, operations []int) byte { |
时间复杂度为 O(n),其中 n 是 operations 的长度。空间复杂度为 O(1),因为只使用了常数级别的额外空间。
到这里就完成了对 3307 的解答。而对于 3304,由于它的操作类型只有一种(即只进行字符变换),因此可以直接使用上面的迭代法。
1 | func findKthCharacter(k int) byte { |
这里由于没有 operations 数组,所以直接从 62 开始遍历,直到 0。每次判断 k 是否在前半段,如果在后半段就增加 ans 的值。当然考虑到题干中 k 的范围是 1 <= k <= 500,所以实际上只需要遍历到 8 就可以了,因为 8 已经足够覆盖所有可能的 k 值。
这里进一步分析就可以发现,不应该每次都从 62 开始遍历,而是可以根据 k 的值动态调整遍历的起始位置,也就是找到一个 i,使得
如何找到这个 i 呢?直接说结论,就是 bits.Len64(uint64(k)) - 1,也就是求出 k 的二进制位数长度,然后减去 1。这样就可以得到一个更简洁的代码实现。
代码实现
3304. 找出第 K 个字符 I
1 | func kthCharacter(k int) byte { |
基于此就可以完成对 3304 的解答。
3307. 找出第 K 个字符 II
1 | func kthCharacter(k int64, operations []int) byte { |

