Skip to content2. 包含翻倍
3614. 含特殊操作的字符串处理 II
概述
给定一个操作序列字符串 s,其中包含小写字母与三个特殊符号 #、%、*,需要回答:按顺序执行所有操作后得到的最终字符串中,第 k 个字符(从 0 开始计数)是什么?若 k 越界则返回 '.'。
直接构造字符串不可行——# 会使长度翻倍,最终长度可能指数增长,无法放入内存。可以采取“正向记录长度,再逆推位置”的方法,在不实际构造字符串的前提下反推出目标字符的来源。
基本概念
初始字符串为空。遍历 s 中的每个字符,按以下规则修改当前字符串:
- 小写字母(
a–z):追加该字母到末尾。 #:将当前字符串与自身拼接(长度翻倍)。%:反转整个字符串。*:删除最后一个字符;若字符串为空则无效果。
最终字符串的索引 k 满足 0 ≤ k < 最终长度。题目保证 k 是一个 32 位整数,负数视为越界。
工作原理
分两遍处理。
第一遍正向遍历 s,使用 BigInt 记录当前长度 len,每步执行后将 len 存入数组 lens(执行后长度)。这样后续可以随时获知任意操作前后的长度。
第二遍逆推:若 k 大于等于最终长度,直接返回 '.';否则将 k 转为 BigInt 类型的 pos,从序列末尾向前遍历,根据操作类型反向修正 pos,直到找到来源字符。
设当前操作为 ch,执行前长度为 prevLen,执行后长度为 currLen。逆推规则如下:
- 小写字母:如果
pos === prevLen,则pos恰好指向该字母新增的位置,直接返回该字符;否则pos不受影响,继续向前处理。 #:翻倍使得原字符串中位置i的字符同时出现在i与prevLen + i两处。逆推时通过pos %= prevLen将后半段映射回前半段。若prevLen === 0n,翻倍操作前后字符串均为空,无需进行位置映射,pos保持不变。%:反转将原位置i映射到新位置prevLen - 1 - i。逆推时使用公式pos = prevLen - 1n - pos。若prevLen === 0n,则反转空串不改变任何内容,pos同样保持不变。*:退格仅删除最后一个字符。被删除的字符不会出现在最终字符串中,因此逆推时pos代表的字符在删除操作发生时依然存在,pos无需调整。
逆推进行到某步直接返回字母,或遍历完成仍未返回,则理论上不会发生,可返回 '.' 作为安全兜底。
基本用法
函数签名(TypeScript):
ts
function processStr(s: string, k: number): strings:操作序列字符串,由题目定义。k:32 位整数,目标索引。- 返回值:最终字符串第
k个字符,越界时为'.'。
内部使用 BigInt 以避免 JavaScript Number 的精度限制(上限 2^53-1)。k 本身是 32 位整数但在逆推时转为 BigInt 参与运算。
Java 中对应实现可使用 BigInteger 或 long(当长度不会超出 2^63-1 时),Python 的 int 直接支持任意大整数。
示例
1. 纯字母序列
ts
processStr("abc", 2); // 'c'
processStr("abc", 3); // '.'- 正向长度记录:
[1, 2, 3]。 k = 2:pos = 2,逆推首次见到'c',此时prevLen = 2,pos === prevLen,返回'c'。k = 3:pos超出最终长度3,直接返回'.'。
2. 包含翻倍 #
ts
processStr("a#", 0); // 'a'
processStr("a#", 1); // 'a'
processStr("a#", 2); // '.'- 正向过程:
'a'→ 长度 1;'#'→ 长度 2,最终字符串为"aa"。 k = 0:pos = 0,操作'#'时prevLen = 1,pos %= 1n得0;继续向前,见到'a',prevLen = 0,pos === 0匹配,返回'a'。k = 1:pos = 1,'#'时pos %= 1n变为0,同样返回'a'。
3. 翻倍与退格组合
ts
processStr("ab*#", 1); // 'a'
processStr("ab*#", 2); // '.'- 正向模拟:
'a'→ 长度 1;'b'→ 长度 2;'*'→ 长度 1(字符串"a");'#'→ 长度 2(字符串"aa")。 k = 1:pos = 1,'#'时prevLen = 1,pos %= 1n得0;然后是'*',prevLen = 2,currLen = 1,pos保持0;再往前是'b',prevLen = 1,0 !== 1不匹配;最后'a',prevLen = 0,pos === 0,返回'a'。
4. 反转与退格
ts
processStr("a%*", 0); // '.'- 正向:
'a'→ 长度 1;'%'→ 反转后仍为"a",长度 1;'*'→ 退格后为空串,长度 0。最终长度 0,任何k均越界,返回'.'。
注意点
- 长度必须使用大整数:
#可使长度指数增长,Number上限2^53-1会溢出导致计算错误。Java 中若允许使用long,需评估上限是否可能超过2^63-1,否则应使用BigInteger。Python 的int天然支持。 prevLen为0n时#与%的处理:逆推过程中若遇到prevLen === 0n,说明操作前字符串为空。- 对于
#:空串翻倍后仍为空,两个“副本”均为空,不存在位置映射关系,pos保持原值不变(即不执行pos %= prevLen)。 - 对于
%:空串反转后仍为空,索引映射公式prevLen - 1 - pos在prevLen === 0时无定义,也不应套用,pos保持原值不变。
这两种情况下pos均无需调整,直接传递给更早的操作。这既避免了除零或负数索引,也符合操作的语义——空串上的任何变换都不产生新的字符位置关系。
- 对于
- 字母新增位置的判断:条件应为
pos === prevLen,而非pos === currLen - 1n。因为新增的字母占据操作前长度所指示的索引。 - 反转映射的基准长度:公式
pos = prevLen - 1n - pos使用操作前长度prevLen,而非操作后长度currLen。 - 输入
k可能为负:越界判断中需同时检查k < 0。 - 逆推遍历顺序:必须从后向前遍历操作。正向遍历仅记录长度,不应在此阶段尝试确定来源。
限制
- 空间复杂度 O(N),N 为
s的长度,需要存储每一步的长度。典型题目中 N ≤ 10⁵,内存可接受。 - 时间复杂度 O(N),正反向各遍历一次。
k为 32 位整数,保证了逆推中pos不会因输入而产生不可表示的大数值,也避免了极端索引导致大量取模运算带来的性能问题。- 若操作序列中出现连续的
#,长度将快速增长,BigInt所需的内存量会有所增加,但存储的仍只是数字,不会影响算法的可行性。
