文章

算法刷题系列(一):模拟法——从题意到状态更新

算法刷题系列(一):模拟法——从题意到状态更新

算法刷题系列(一):模拟法——从题意到状态更新

模拟法通常是最直接的算法:题目给出一组规则,我们按规则一步一步执行,并记录当前状态。它不一定需要复杂的数据结构,但很考验建模是否完整——变量代表什么、操作顺序是什么、边界如何处理,都要先说清楚。

这一篇从一个自创的机器人指令例子开始,演示如何把自然语言转换成状态更新,并分别用 Java 和 Python 实现。

一、什么时候考虑模拟?

题目如果描述了一个过程,并要求给出执行后的结果,通常可以先问:

  1. 当前状态是什么? 例如位置、方向、分数、库存或棋盘。
  2. 有哪些操作? 每个操作如何改变状态?
  3. 操作按什么顺序发生? 顺序是否会影响结果?
  4. 何时结束,输出什么? 需要输出最终状态,还是过程中第一次满足条件的时刻?

如果能用有限个变量完整表示当前状态,而且每个操作都能直接计算,就可以先尝试模拟。

二、例子:执行机器人指令

一个机器人从平面坐标 (0, 0) 出发,初始朝北。它会收到一串指令:

  • F:向当前朝向前进一步;
  • L:向左转 90 度;
  • R:向右转 90 度。

指令串为 FFRFLF,求最后的位置和朝向。

1. 定义状态

用三个变量描述机器人:

  • x:横坐标;
  • y:纵坐标;
  • direction:朝向编号。

把方向编号约定为:

编号方向坐标变化
0北(0, 1)
1东(1, 0)
2南(0, -1)
3西(-1, 0)

方向变化可以用模 4 运算:

  • 向右:(direction + 1) % 4
  • 向左:(direction + 3) % 4

向左不是简单地减 1 后取模,因为某些语言对负数取模的结果不同;加 3 再取模能统一得到非负方向编号。

2. 手动走一遍

指令位置变化后朝向
初始(0, 0)北
F(0, 1)北
F(0, 2)北
R(0, 2)东
F(1, 2)东
L(1, 2)北
F(1, 3)北

最终位置是 (1, 3),朝北。

三、算法步骤

  1. 初始化位置为 (0, 0),方向为北。
  2. 从左到右读取每条指令。
  3. 如果是 F,按当前方向对应的坐标增量更新位置。
  4. 如果是 L 或 R,只更新方向编号。
  5. 所有指令执行完后返回最终状态。

每条指令只处理一次,因此时间复杂度是 O(n),其中 n 是指令数。除输入外只使用常数个变量,额外空间复杂度是 O(1)。

四、Java 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RobotSimulation {
    // direction: 0 = north, 1 = east, 2 = south, 3 = west
    public static int[] execute(String commands) {
        int[] dx = {0, 1, 0, -1};
        int[] dy = {1, 0, -1, 0};

        int x = 0;
        int y = 0;
        int direction = 0;

        for (char command : commands.toCharArray()) {
            if (command == 'F') {
                x += dx[direction];
                y += dy[direction];
            } else if (command == 'R') {
                direction = (direction + 1) % 4;
            } else if (command == 'L') {
                direction = (direction + 3) % 4;
            } else {
                throw new IllegalArgumentException(
                    "Unsupported command: " + command
                );
            }
        }

        return new int[] {x, y, direction};
    }
}

execute("FFRFLF") 返回 {1, 3, 0},其中方向编号 0 表示北。

这里显式处理了未知指令,而不是静默忽略。若题目保证输入合法,可以按题目约束简化;若输入来自真实用户或外部系统,则应保留校验。

五、Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def execute(commands: str) -> tuple[int, int, int]:
    # direction: 0 = north, 1 = east, 2 = south, 3 = west
    dx = (0, 1, 0, -1)
    dy = (1, 0, -1, 0)

    x = 0
    y = 0
    direction = 0

    for command in commands:
        if command == "F":
            x += dx[direction]
            y += dy[direction]
        elif command == "R":
            direction = (direction + 1) % 4
        elif command == "L":
            direction = (direction + 3) % 4
        else:
            raise ValueError(f"Unsupported command: {command}")

    return x, y, direction

execute("FFRFLF") 返回 (1, 3, 0)。

六、边界情况与测试

空指令

输入为空字符串时,机器人不移动也不转向,结果应为 (0, 0, 0)。

只转向

输入 LLL 时,位置保持 (0, 0),方向变为西,也就是编号 3。

连续转满一圈

输入 RRRR 后方向应回到北。这个用例可以检查模运算和方向编号约定是否一致。

未知指令

输入中出现未定义字符时,示例实现会报错。若题意规定所有字符都合法,则该分支不会触发;若输入约束不明确,应先确认题目要求。

最简单的验证方式是用纸笔按相同规则走一遍短指令,再比较程序输出。模拟题往往错误在状态更新顺序,而不在循环语法。

七、模拟题常见错误

  1. 状态定义不完整:例如记录了位置,却忘记记录朝向。
  2. 方向顺序不统一:方向数组顺序和转向公式必须采用同一约定。
  3. 边界更新混乱:左转、右转的模运算写错,或负数取模行为与预期不同。
  4. 只看最终样例:中间状态不容易观察时,先构造只有一两步的小测试。
  5. 把模拟当成低效算法:如果题目要求逐项执行 n 条指令,O(n) 往往已经是最优量级;需要优化的是不必要的重复计算,而不是省略必需步骤。

八、如何从模拟继续优化

模拟是建立正确基线的好方法。写完后可以继续追问:

  • 每一步是否都必须逐个执行?能否把一段重复操作合并?
  • 是否可以用计数或哈希表减少重复查找?
  • 是否有连续区间可以用滑动窗口或前缀信息处理?
  • 状态空间很大时,能否用搜索、动态规划或数学规律跳过部分状态?

这些问题会自然连接到本系列后面的哈希表、双指针、前缀和、搜索和动态规划。不要为了“用高级算法”而优化;只有在数据规模或重复工作确实构成瓶颈时,才替换简单模拟。

小结

模拟法的核心是把题意转成状态和操作:先定义状态变量,再逐条应用规则,最后检查边界和结果。机器人例子中,坐标和朝向足以表示完整状态;每条指令只需常数时间处理,因此总复杂度为 O(n)。

接下来适合学习哈希表:当模拟过程中频繁查找“某个值是否出现过、出现了几次”时,哈希结构往往能把反复扫描转成近似常数时间查询。

本文由作者按照 CC BY 4.0 进行授权