Specify a TM that treats the input binary string as a little-endian binary integer n and decrements n by 1 if n > 0 but leaves it unchanged if n = 0.
先把算法想清楚,再翻译成状态转移表。
① 二进制减一的本质
小端序(little-endian)意味着磁带最左边那一格是最低位,读写头初始就停在这一格——这对我们极其有利,因为减一恰好是从最低位开始处理的。
十进制里 ...000 − 1 要借位;二进制的规则更简单:从最低位往高位扫,把遇到的每一个 0 改成 1,直到遇见第一个 1,把它改成 0,然后就结束了,更高位一律不动。
验证一下:n = 12,大端 1100,小端写成 0011。从左(最低位)扫:0→1、0→1,遇到第一个 1→改成 0,停。得到小端 1101,还原成大端就是 1011 = 11 = 12−1。✓
那 n = 0 怎么办?全是 0,一路把它们都翻成了 1,却始终没遇到 1——扫到磁带末尾的空白 − 时才发现。所以需要一个撤销阶段:往回走,把刚才翻出来的 1 全部改回 0。
② 还有一个容易漏掉的约束
课程的 TM 定义里,输出是运行结束时读写头所在格子往后的那串二进制。所以不能"算完就地停下"——必须把读写头送回最低位那一格再停机,否则输出会少几位。这就是下面 B、C 两个"回程"状态存在的理由。
③ 状态转移表
| 当前状态 | 读到 | 写入 | 移动 | 转到 | 含义 |
|---|---|---|---|---|---|
| A | 0 | 1 | 右 → | A | 还没遇到 1,把这个 0 翻成 1,继续往高位找 |
| A | 1 | 0 | 左 ← | B | 找到了:把它改成 0,减法完成,开始回程 |
| A | − | − | 左 ← | C | 扫到头都没有 1 ⇒ n = 0,进入撤销阶段 |
| B | 0 或 1 | 不变 | 左 ← | B | 回程:一路左移,不改动任何内容 |
| B | − | − | 右 → | H | 越过左边界了,退回第一格,停机 |
| C | 1 | 0 | 左 ← | C | 撤销:把刚才翻出来的 1 改回 0 |
| C | − | − | 右 → | H | 撤销完毕,退回第一格,停机 |
一共 3 个工作状态 + 1 个终止状态,转移表 7 条。注意状态 B 里不需要区分读到 0 还是 1——因为经过状态 A 之后,左边的格子要么是被翻成 1 的,要么是那个刚被改成 0 的格子,两种情况都只需要路过。
④ 三种情况各走一遍
| 输入 | 过程 | 输出 |
|---|---|---|
| 0011 n = 12 |
0011 →A 1011 →A 1111 →A(找到) 1101 →B 1101 →B(越界)→H 1101 | 1101 = 11 ✓ |
| 1 n = 1 |
1 →A(找到,左移越界) →B →H 0 | 0 = 0 ✓ |
| 000 n = 0 |
000 →A 100 →A 110 →A 111− →A(空白) 111 →C 110 →C 100 →C(越界)→H 000 | 000 原封不动 ✓ |
如果输入是大端序(最高位在左),同样一道减一题会麻烦得多:读写头得先跑到最右端找到最低位,再往左处理借位,还要处理"最高位变 0 后要不要截断"的问题。小端序让"借位方向"和"读写头自然移动方向"一致,程序短了一半——这正是 TM 这类模型的典型特征:数据的物理排布方式直接决定算法复杂度,因为 TM 完全局部,换个方向就要多跑一趟。