还是构造大佬

最后更新于 2025-08-03 11:30:38
作者
分类 题解

闲话

随机跳题刷到的,一看是黑题,AC率竟有 $0%$,但是这个图灵机的形式让我觉得很有意思。

于是开始想,想到了正解大概思路,然后写出来,随便调了几下就过了。

其实这道题不算很难,但是好像 GCJ 赛时没有人通过?(雾

题意

为了方便,在此简化亿下题意,先解释什么是图灵机:

有一个无限长的纸带,初始上面都写着 $0$。

有一个机器,初始所在位置为 $0$,分为控制器和表头两个部分,表头有一个状态。

控制器接受一些规则,形如“若表头状态为 $a$ 且所在格子写着 $b$,则将格子中的数改为 $c$,状态改为 $d$,然后左/右(题目中为 WE)移动一个单位长度”。

每一次根据所在位置和状态对应的规则执行一步操作,直到当表头状态为 Halt(题目中为 R)时停止运行图灵机。

题目要求的就是:构造不超过 $30$ 条规则,使得图灵机在 $X$ 步内恰好在 $N$ 这个位置停机。

小数据集:$0 \leq N \leq 500$,$X = 250,000$,时限 $30$ 秒。

大数据集:$0 \leq N \leq 5000$,$X = 150,000$,时限 $60$ 秒。

思路

分析

首先拿到这种构造题,先写一个可视化工具或 SPJ (代码在后面)方便观察调试。

考虑一个暴力:将状态设为已经走过的步数,每走一次加 $1$,当状态为 $n$ 时停机。

虽然它花费步数最少,从不回头,但这显然浪费了纸带的存储功能,需要用 $O(n)$ 条规则。

为了利用纸带,不妨将剩下的步数写在纸带上。每次遍历一遍存在纸带上的数,将纸带上数的减一。

当纸带上的数归零时,就完成了计数,直接停机即可。

但是 $X=150000=5000\times30$,也就是说平均每个规则只能被调用 $O(n)$ 次,卡得比较紧。

所以我们不能来回太多次,应该带着纸带一起走,边移动边把纸带往前面也移动一格。

考虑如何在纸带上存数字,不妨选定一个进制 $k$,因为这是 OI 题目,这里选定二进制(后面会解释原因)。

特别地,由于纸带上所有数默认为零,我们将二进制中每个数位上的数 $+1$ 以便和 $0$ 区分开:

例如:$5=(101)_2$,在这里记为 $(2,1,2)$。

综上,图灵机的运行大概分为四个阶段:

Step I. 初始化,将题目中的 $N$ 按位从高到低存在纸带上。(从左到右遍历)

Step II. 将纸带上的数减一,若数字已经为 $0$ 则跳转 Step III,否则跳转 Step IV。(从右到左遍历)

Step III. 复制纸带上的数到下一位,跳转 Step II。(从左到右遍历)

Step IV. 停机。

可以发现,这样的安排总是左右横跳,没有出现无意义的遍历,可以在 $O(n\log n)$ 步内停机。

然后按步构造规则就可以了。

Tips:

  • 下文所说的“左边”“右边”,均指二进制位最左/右的下一个,即第一个 $0$ 的位置。
  • 重要的事情说两遍:记录的二进制数位上的数会 $+1$。

Step I 初始阶段

首先,Step I 的实现是 naive 的,状态为 $S_i$(初始 $i=0,S_0=0$)时就填充从高到低第 $i$ 位上的数,然后向右移动,状态切换为 $S_{i+1}$。于是表头从左边移动到了右边。

初始转移:

S0 0 -> E S1 t0
S1 0 -> E S2 t1
...  -> ...
Sn-1 0 -> E Sn tn-1

Step II 减法阶段

现在我们在数字的右侧,此时从高到低存储的好处就体现出来了。

此时定义状态 $M_1$,表示未完成减法,需要借位(更高位继续 $-1$)。

此时定义状态 $M_2$,表示减法已经完成,不需要借位(更高位不变)。

当状态为 $S_n$ 时,往回走并改为 $M_1$ 状态。

过渡转移:

Sn 0 -> W M1 0

减法转移:

//重要的事情说三遍:记录的二进制数位上的数会 +1
M1 1 -> W M1 2 //这一位是 0,减了 1 之后为 1,后面仍需继续借位
M1 2 -> W M2 1 //这一位是 1,减了 1 之后为 0,后面不需要借位了    
M2 1 -> W M2 1 //不需要借位,保持原样
M2 2 -> W M2 2 //不需要借位,保持原样

完成后表头从右边移动到了左边。

Step III 移动阶段

当表头在左侧时若状态为 $M_2$,则说明减法成功,需要将数字移动到下一位。

现在我们在数字的左侧,设 $C_i$ 表示上一位为 $i$,初始 $i=0$。

若这一位是 $j$,则将这一位改为 $i$,状态改为 $C_j$,向右移动进入下一位。

特别地,此时需要考虑 $0$,因为左边的位置需要归零保证复杂度,而且占用新的位置也需要考虑 $0$。

特别地,当状态为 $C_0$ 且纸带上的数为 $0$ 时,即到达最右边,需要结束移动阶段,进入新一轮减法阶段。

移动转移:

//Ci j -> E Cj i
C0 0 -> W M1 0 //完成循环
C0 1 -> E C1 0
C0 2 -> E C2 0
C1 0 -> E C0 1
C1 1 -> E C1 1
C1 2 -> E C2 1
C2 0 -> E C0 2
C2 1 -> E C1 2
C2 2 -> E C2 2

Step IV 结束阶段

当表头在左侧时若状态为 $M_1$,则说明减法失败,即当前数字已经为 $0$。

显然,数字的二进制位最左边就是位置 $n$ ,但是我们在最左边的下一个,也就是位置 $n-1$。

很好,我们的图灵机输入 $n$ 能到达位置 $n-1$,不妨将输入的 $n$ 先 $+1$。

那么我们的图灵机输入 $n+1$ 能到达位置 $n$,直接停机,完成任务。

复盘

简单复盘一下,若用 $k$ 进制,不考虑状态数和纸带上的数的大小限制($10^6$ 想必用不完)。

需要 $\log_kN+1+2k+1+(k+1)^2+1=k^2+4k+5+\log_kN$ 个转移。

丢进画图工具里你会发现只有 $k=2$ 这个整数始终在 $N\le5000$ 时满足条件,所以只能选二进制。

只要理解了思路,代码超级好写。实现时对于 $S,M,C$ 函数随便取不同的值就可以了(特别地,满足$S_0=0$)。

代码

哈哈哈,居然是首A。

不是一分钟的时间限制我就跑了 $4$ 毫秒?

建议降低时限以免被别有用心之人利用。

code.cpp
spj.cpp