目录

题目描述

LCP 17. 速算机器人

题意分析

机器人有两个寄存器 xy,初始值分别是 1 和 0。指令串 s 只由 AB 两种字符组成:读到 A 时把 x 更新成 2x + y,读到 B 时把 y 更新成 2y + x。要求把整串指令执行完,返回 x + y

约束里最显眼的信号是 s 的长度不超过 10,而且指令之间没有任何跳转、循环或撤销:第 i 条指令只依赖执行完前 i-1 条之后的寄存器值。这种"状态完全由前缀决定、且规模极小"的形态,说明不需要任何搜索或状态压缩,顺序推进两个整数就够了。

边界上要留意三点:x 的初值是 1 而不是 0,写错会让整条链路全为 0;每条指令只改一个寄存器,另一个保持不变;字符集只有 A/B 两种,所以 else 分支等价于 B,不必再写第三个分支。数值范围方面,长度 10 时 x + y 只有 $2^{10} = 1024$,int 完全够用。

解法:按题意模拟

核心思路

最朴素的想法是照抄题面:准备两个变量,从左到右扫一遍指令串,遇到什么就改什么。这个想法在这题上没有瓶颈——指令数最多 10 条,扫一遍就是 10 次加法,已经是下界了,因此不需要做任何优化。

真正值得在面试里说出口的是这里的不变量。设执行完前 k 条指令后寄存器为 $x_k, y_k$,那么无论第 k+1 条是 A 还是 B:如果是 A,新的和为 $(2x_k + y_k) + y_k = 2(x_k + y_k)$;如果是 B,新的和为 $x_k + (2y_k + x_k) = 2(x_k + y_k)$。两条路径给出完全相同的和。于是有不变量:

\[x_k + y_k = 2^k \cdot (x_0 + y_0) = 2^k\]
也就是说答案只和指令串的长度有关,和每条指令具体是 A 还是 B 毫无关系,最终结果恒为 $2^{ s }$。模拟写法是这个不变量的直接展开,二者答案必然一致;面试里先给模拟保证正确,再把这个不变量说出来,就是加分点。

解题步骤

  • 初始化 x = 1y = 0。这是题目给定的初态。之所以不能图省事写成 x = y = 0,是因为不变量 $x + y = 2^k$ 的基底就是 $x_0 + y_0 = 1$,初值一旦归零,后面所有的翻倍都作用在 0 上,答案恒为 0。
  • 从左到右逐字符扫描 s。方向必须是从左到右,因为第 i 条指令的输入是执行完前 i-1 条之后的寄存器值,反向扫描会破坏这个依赖顺序,得到的是另一个完全不同的过程。
  • 读到 A 就执行 x = 2 * x + y,否则执行 y = 2 * y + x。关键在于每一步只更新一个寄存器,另一个原封不动地作为"旧值"参与运算。如果两个都在同一步里更新,就相当于把两条指令压在一起执行,语义完全变了。
  • 扫描结束后返回 x + y。返回的是两个寄存器的和,不是其中某一个。这里也可以顺手用不变量自查:返回值应当恰好等于 $2^{ s }$。

s = "AB" 走一遍:初始 x = 1, y = 0,此时 $x + y = 1 = 2^0$。读到第一个字符 A,执行 x = 2*1 + 0 = 2y 保持 0,此时 x = 2, y = 0,和为 $2 = 2^1$,符合不变量。读到第二个字符 B,执行 y = 2*0 + 2 = 2x 保持 2,此时 x = 2, y = 2,和为 $4 = 2^2$。扫描结束,返回 2 + 2 = 4

换一条指令串 s = "AA" 再走一遍验证不变量:初始 x = 1, y = 0;第一个 Ax = 2, y = 0;第二个 Ax = 2*2 + 0 = 4, y = 0;返回 4 + 0 = 4。和 "AB" 的答案一致,正好印证了"答案与指令内容无关,只与长度有关"。

代码实现

class Solution {
    public int calculate(String s) {
        int x = 1;
        int y = 0;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == 'A') {
                x = 2 * x + y;
            } else {
                y = 2 * y + x;
            }
        }
        return x + y;
    }
}
func calculate(s string) int {
    x, y := 1, 0
    for i := 0; i < len(s); i++ {
        if s[i] == 'A' {
            x = 2*x + y
        } else {
            y = 2*y + x
        }
    }
    return x + y
}

复杂度分析

  • 时间复杂度:$O( s )$。只对指令串做一次线性扫描,每个字符对应常数次加法与乘法,没有回溯也没有嵌套循环。
  • 空间复杂度:$O(1)$。全程只有 xy 两个 int 变量和一个循环下标,与输入长度无关。

关键点总结

  • 先找"两条分支的公共效果",再决定要不要真的分情况AB 看似两种不同变换,但对 $x + y$ 这个聚合量的作用完全相同。遇到多分支状态机时,先问一句"有没有某个线性组合在所有分支下都同样变化",往往能一眼看穿题目。
  • 不变量要写成等式而不是一句话。把结论落到 $x_k + y_k = 2^k$ 这种可验证的形式,才能拿它做自查和归纳证明;停留在"和会翻倍"的口头描述,很容易在推广到别的题时失效。
  • 面试视角:先给能过的模拟,再给 $O(1)$ 的数学解。面试官出这题不是要看你会不会写 for 循环,而是看你能否从模拟里读出结构。稳妥的答法是先用 30 秒说清模拟并写出来,然后主动补一句"其实答案恒为 $2^{ s }$,因为每条指令都让 $x+y$ 翻倍",把主动权拿回来。
  • 规模极小的约束是"不必优化"的许可|s| ≤ 10 明确告诉你朴素模拟稳过,此时纠结常数优化是浪费时间,把精力放在把不变量讲清楚上收益更高。
  • 状态更新的原子性要显式确认。每步只写一个寄存器、另一个读旧值,这是"顺序执行"语义的直接体现。凡是同时更新多个互相引用的变量时,都要停下来确认用的是不是同一时刻的快照。

易错点总结

  • 错误写法:int x = 0, y = 0; → 用例 s = "A"x = 2*0 + 0 = 0,返回 0,而正确答案是 2。初值写错会让整条翻倍链塌成常数 0,且所有用例都错,反而不容易定位。
  • 错误写法:int x = 0, y = 1;(把两个初值弄反) → 用例 s = "A"x = 2*0 + 1 = 1,返回 1 + 1 = 2,恰好蒙对;但用例 s = "AA":第一步 x = 1,第二步 x = 2*1 + 1 = 3,返回 4,虽然也对——因为不变量对任何 $x_0+y_0=1$ 的初值都成立。真正会错的是 s = "" 之外的题面变体,一旦题目改成返回 x 而非 x+y,这个错误立刻暴露,属于"用例碰巧掩盖"的高危写法。
  • 错误写法:x = 2 * x + y; y = 2 * y + x; 写在同一次迭代里,不加 if 分支 → 用例 s = "A"x 变成 2 后 y 又被更新成 2*0 + 2 = 2,返回 4,而正确答案是 2。这等于把一条指令当两条执行,长度为 n 的串会得到 $4^n$ 量级的结果。
  • 错误写法:y = 2 * y + x 里的 x 误用成刚更新过的值(先算 x 再在同一条 B 分支里引用) → 用例 s = "BB":正确过程是 y = 2*0+1 = 1,再 y = 2*1+1 = 3,返回 4;若中途污染了 x,会得到 x + y ≠ 4,直接违反 $x+y=2^{ s }$,用不变量一算就能发现。
  • 错误写法:return x;return y; → 用例 s = "AB":正确答案是 4,但 return x 得到 2、return y 也得到 2。题目要的是两个寄存器之和,只返回单个寄存器会在几乎所有非平凡用例上偏小。
  • 错误写法:for (int i = 1; i < s.length(); i++)(下标从 1 开始) → 用例 s = "AB":漏掉首字符 A,只执行 B,得到 x = 1, y = 2*0+1 = 1,返回 2 而非 4。字符串下标从 0 起,循环条件写成 i < s.length() 才恰好覆盖全部字符。
  • 错误写法:if (c == 'A') {...} else if (c == 'B') {...} 但漏写 else 分支后又在外层做了别的处理 → 用例 s = "AB" 本身不会错,但如果误把判断写成 if (c == 'B') 在前、else 当作 A,逻辑就整体反了:"AB" 会先更新 y 再更新 x,得到 y = 1, x = 2*1+1 = 3,返回 4——又一次被不变量"救"回来,但过程完全错误。判断字符时务必和题面逐字对齐。
  • 错误写法:用 s.charAt(i) == "A"(Java 里拿 char 和 String 比) → 直接编译失败;换成 equals 又要先 String.valueOf,白白引入装箱开销。Java 中单字符比较应写 == 'A'
  • 错误写法:Go 里写 for _, c := range s 后用 c == 'A' 但把 c 当成 byte 处理下标 → 本题字符集是 ASCII 不会出错,但 range string 产生的是 rune 和字节偏移,一旦题面换成含多字节字符的串,i 就不再是"第几个字符"。按下标 s[i] 遍历语义更直白。
  • 错误写法:用 Math.pow(2, s.length()) 直接返回浮点结果 → 用例 s 长度为 10 时得到 1024.0,强转 int 虽然对,但浮点在更长的串上会丢精度,且面试官会追问"为什么敢用浮点算整数幂"。要么老实模拟,要么用位移 1 << s.length()

相似题目

题目 难度 考察点
874. 模拟行走机器人 中等 状态里多了朝向和障碍集合,需要哈希去重而非纯算术递推
1041. 困于环中的机器人 中等 靠"一轮指令后朝向是否复位"做周期性判定,而不是直接算终值
面试题 16.22. 兰顿蚂蚁 中等 网格无限大且格子颜色会反复翻转,必须动态维护包围盒
面试题 08.02. 迷路的机器人 中等 从确定性模拟升级成带回溯与剪枝的路径搜索