上一篇文章里,我们已经讲过 P、NP 和 NP-Complete:P 是可以高效求解的问题,NP 是可以高效验证答案的问题,而 NP-Complete 是 NP 里最"硬核"的一批问题。
但如果继续追问下去,会出现一些更底层的问题:什么叫算法?怎样算高效?怎么精确定义时间和空间?
作为程序员,我们很容易想用 Python、C++ 来理解这些概念:用 Python 写出一个快速排序只需要十行,假定输入和比较是常数,一下就能推出快速排序的复杂度。但真实编程语言太复杂了:一行 Python 可能调用大量底层逻辑,不同 CPU、缓存、编译器、运行时都会影响实际执行时间。
复杂度理论不想被这些细节绑架。它需要一个极简、稳定、可以写成数学定义的计算模型。
这就是图灵机的作用。图灵机不是真实的计算机,也不是一种真实编程语言。它更像复杂度理论里的"极简虚拟机":足够简单,方便证明;又足够强大,可以表达普通计算机能做的计算。
图灵机
图灵机像一台只有极少指令的机器,它包括:
- 一条无限长的纸带,类似无限长数组;
- 一个搭在纸带上的读写头,类似当前指针;
- 一组状态,类似程序当前运行到的逻辑分支;
- 一张状态转移表。
每一步,图灵机只看两个东西:当前状态和当前格子里的符号。然后它根据状态转移表决定:写什么符号、切换到哪个状态、读写头往左还是往右移动。
如果写成转移规则,大概是:
\[\delta(q, a) = (q', b, D) \]
意思是:如果当前处于状态 \(q\),读到符号 \(a\),那么进入状态 \(q'\),把当前格子写成 \(b\),然后按照方向 \(D\) 移动。
这看起来非常原始,但正是这种原始性,让它适合作为复杂度理论的地基。
复杂度理论关心的不是某个 Python 函数在你的电脑上跑了几秒,而是:是否存在一种计算过程,能在某个资源上界内解决某个问题。图灵机就是用来严谨表达"计算过程"的。
字母表、字符串和语言
字母表 \(\Sigma\) 是一个有限符号集合,例如 \(\{0,1\}\) 或 \(\{a,b\}\)。所有由这些符号组成的有限字符串构成 \(\Sigma^*\)。
一个语言 \(L\) 就是 \(\Sigma^*\) 的某个子集。换句话说:
\[L \subseteq \Sigma^* \]
在复杂度理论里,判定问题通常会被转化成语言。这其实就是把"问题"写成了"字符串集合"。
例如,判定问题"这个二进制字符串里是否至少有一个 1?"可以对应到语言:
\[L = \{w \in \{0,1\}^* \mid w \text{ 中至少有一个 } 1\} \]
如果一个字符串属于这个集合,答案就是"是";如果不属于,答案就是"否"。

一个最小例子:判断字符串里有没有 1
考虑判定问题:给定一个 \(\{0,1\}^*\) 上的字符串,它是否至少包含一个 1?
对应的语言是:
\[L = \{w \in \{0,1\}^* \mid w \text{ 中至少有一个 } 1\} \]
例如:00100 和 1 属于 \(L\);00000 和空串 \(\varepsilon\) 不属于 \(L\)。
用高级语言写,核心逻辑是这样的:
python
def has_one(s):
for ch in s:
if ch == "1":
return True
return False
现在用图灵机实现同样的逻辑。输入字母表取:
\[\Sigma = \{0,1\} \]
纸带字母表取:
\[\Gamma = \{0,1,\_\} \]
其中 _ 表示空白符。状态集取:
- \(q_0\):起始状态,正在向右扫描寻找
1; - \(q_{\text{acc}}\):接受状态,停机;
- \(q_{\text{rej}}\):拒绝状态,停机。
转移规则 \(\delta\) 如下:
| 当前状态 | 读入符号 | 写入符号 | 新状态 | 移动方向 |
|---|---|---|---|---|
| \(q_0\) | 0 | 0 | \(q_0\) | R |
| \(q_0\) | 1 | 1 | \(q_{\text{acc}}\) | - |
| \(q_0\) | _ |
_ |
\(q_{\text{rej}}\) | - |
这里的 - 表示接受或拒绝后立即停机,移动方向已经不重要。
工作过程是:
- 从纸带最左端开始,读写头对准输入的第一个字符;
- 若读到
0,不做修改,向右一格,保持状态 \(q_0\); - 若读到
1,进入接受状态 \(q_{\text{acc}}\) 并停机; - 若读到空白符
_,说明已经扫描完整个输入且没有发现1,进入拒绝状态 \(q_{\text{rej}}\) 并停机。
如果输入长度是 \(n\),这台机器最多扫描 \(n\) 个非空白符号,然后可能再看一个空白符。因此总步数最多是 \(n+1\),时间复杂度为 \(O(n)\)。
这个例子展示了:一个简单的判定问题,可以严格用图灵机的状态和转移表刻画,从而为"存在一台 \(O(n)\) 时间的机器"这样的复杂度论断提供形式化基础。
Configuration
Configuration 指的是机器在某一瞬间的完整状态。对普通图灵机来说,一个 configuration 至少包含:
- 当前状态;
- 纸带上的内容;
- 读写头所在位置。
可以把它想象成程序运行时的一张快照。比如某一刻纸带是:
text
B a a b b B
^
并且机器当前处于状态 \(p_0\),那么这就是一个 configuration。
整个计算过程可以写成一串 configuration 的变化:
\[K_0 \to K_1 \to K_2 \to \cdots \]
这件事后面非常重要。因为我们会用 configuration 来定义时间、空间,以及把非确定性计算转化成图上的可达性问题。

多带图灵机
最基础的图灵机只有一条纸带。输入、中间结果、辅助标记都挤在这一条纸带上。这就像写程序时只给你一个数组和一个指针。理论上已经可以写出任何程序,但很多算法描述起来会特别别扭。
例如,判断一个字符串是否是"连续多个字符 a + 连续同样多个字符 b",我们要识别:
\[L = \{a^n b^n \mid n \ge 1\} \]
单带图灵机的朴素做法是"配对标记":
- 找一个还没有匹配的
a,标记掉; - 向右找一个还没有匹配的
b,标记掉; - 再回到左边找下一个
a; - 重复这个过程。
读写头会在纸带上来回跑。不是问题本身很难,而是单带模型太笨。
多带图灵机(k-tape Turing machine)更像真实程序。一个多带图灵机有 \(k\) 条纸带,每条纸带都有自己的读写头。可以把它想成:
text
Tape 1: 初始输入 & 工作区 1
Tape 2: 工作区 2
Tape 3: 工作区 3
...
这就更接*我们*时写程序的感觉:输入数组、临时数组、计数器、缓冲区,各自分开。用两带图灵机识别 \(a^n b^n\) 就自然很多:
- 第一条带从左到右扫描输入;
- 每看到一个
a,就在第二条带写一个x; - 开始看到
b后,每看到一个b,就在第二条带消掉一个x; - 如果输入结束时刚好消完,接受;
- 如果
a多了、b多了,或者中间格式乱了,拒绝。
用朴素单带配对标记算法,时间是 \(O(n^2)\);用多带模型,可以很自然地做到 \(O(n)\)。
既然单带和多带会影响时间,那为什么复杂度理论仍然敢用图灵机定义 P、NP、PSPACE 这些类?原因是:这些大类很粗,它们通常只关心"多项式"还是"指数级"这样的尺度,而不是 \(n\)、\(n \log n\)、\(n^2\) 之间的精细差别。
多带图灵机可以被单带图灵机模拟。模拟可能带来多项式时间损失,但不会把多项式时间变成指数时间。因此,对于 P、NP、PSPACE 这种大粒度复杂度类,单带、多带、合理的 RAM 模型之间通常不会改变本质结论。
所以我们可以这样理解:
- 图灵机适合做数学定义和证明;
- 多带图灵机适合描述算法;
- 真实编程语言适合工程实现;
他们都是合理模型,都适用 P、NP、PSPACE 这些大类。
时间复杂度类
有了图灵机和 configuration,我们可以正式定义"时间"。
对一台判定语言的图灵机来说,时间复杂度不是只看某个输入上最短的接受路径,而是要看它在所有长度为 \(n\) 的输入上是否都能被某个函数上界控制。
更具体地说,一台确定性机器在输入 \(x\) 上运行了多少步才停机,这就是它在 \(x\) 上的运行时间。若对所有长度为 \(n\) 的输入,它都能在 \(O(f(n))\) 步内停机并给出正确接受/拒绝,那么我们说它在 \(O(f(n))\) 时间内判定这个语言。
确定性和非确定性
确定性机器的每一步最多只有一个选择。给定当前状态和当前读到的符号,下一步是唯一的。之前的例子都是确定性的。
非确定性机器则允许在某些状态下有多个可能的下一步:
text
path 1
/
state -- path 2
\
path 3
注意,这里的"多个选择"不是无限多个选择。标准非确定性图灵机的转移规则仍然是有限描述的,所以每一步的分支数是有限的。只是经过很多步之后,整棵计算树可能有指数多条路径。
非确定性机器的接受规则是:只要存在至少一条计算路径最终接受,那么输入就被接受;如果没有任何接受路径,那么输入被拒绝。
这正是 NP 这个概念的底层理论表达:非确定性机器可以"猜"一条好路径,然后在多项式时间内验证它。

DTIME 和 NTIME
\(\text{DTIME}(f)\) 表示这样一类语言:存在一台确定性图灵机 \(M\),使得对所有输入 \(x\),机器都在 \(O(f(|x|))\) 时间内停机,并且:
\[x \in L \iff M(x) \text{ accepts} \]
也就是说,语言 \(L\) 可以被确定性算法在 \(O(f(n))\) 时间内判定。
\(\text{NTIME}(f)\) 表示这样一类语言:存在一台非确定性图灵机 \(M\),使得对所有长度为 \(n\) 的输入,所有计算分支都在 \(O(f(n))\) 时间内停机,并且:
\[x \in L \iff \text{存在至少一条计算路径接受 } x \]
也就是说,语言 \(L\) 可以被非确定性算法在 \(O(f(n))\) 时间内判定。
重新定义 P 和 NP
现在我们可以从更底层的角度重新定义 P 和 NP。
P 是确定性多项式时间类:
\[\text{P} = \bigcup_{k \ge 1} \text{DTIME}(n^k) \]
也就是说,只要一个语言能在某个固定多项式时间内被确定性图灵机判定,它就属于 P。
NP 是非确定性多项式时间类:
\[\text{NP} = \bigcup_{k \ge 1} \text{NTIME}(n^k) \]
也就是说,只要一个语言能在某个固定多项式时间内被非确定性图灵机判定,它就属于 NP。
以 SAT 为例:输入一个布尔公式,问是否存在一种变量赋值让公式为真。非确定性机器可以先"猜"一个赋值,然后确定性地检查这个赋值是否满足公式。由于猜测和验证阶段都在多项式时间内完成,所以 SAT 属于 NP。
依旧,P 指能多项式时间内自己算出答案;NP 指如果有人给我一个候选答案,我能多项式时间内检查它。非确定性图灵机就是这个"候选答案"的理论化描述。
NP 的另一种等价定义:证书与验证器
我们常说 NP 是"答案容易验证"的问题。这个说法可以被精确定义。
一个语言 \(L\) 属于 NP,当且仅当存在一个多项式时间算法 \(V(x,y)\),以及一个多项式 \(p(n)\),使得:
\[x \in L \iff \exists y,\ |y| \le p(|x|),\ V(x,y)=1 \]
这里的 \(y\) 通常叫作 certificate、witness,或者"证书"。算法 \(V\) 叫作 verifier,也就是验证器。
这个定义的意思是:
- 如果 \(x\) 是一个 yes-instance,那么存在一个长度不超过多项式的证书 \(y\),让验证器接受;
- 如果 \(x\) 是一个 no-instance,那么无论别人给出什么多项式长度的证书,验证器都不会接受。
以 SAT 为例,证书就是一组变量赋值。验证器只需要把这组赋值代入公式,检查公式是否为真。这个检查过程显然是多项式时间。
非确定性图灵机定义和证书验证器定义是等价的。非确定性机器的"猜测路径"可以看成证书;验证器读取证书并检查它是否真的能导向接受。
off-line 图灵机
讨论时间时,普通图灵机已经够用了。但为了讨论空间,还需要引入 off-line Turing machine。
如果输入和工作区都在同一条纸带上,就会出现一个麻烦:输入本身就占了 \(n\) 个格子。如果把输入也算进空间,那么任何算法至少都需要 \(n\) 空间。这样我们就很难讨论 \(\log n\) 空间算法。
于是 off-line 图灵机把输入带和工作带分开:
text
Input tape: 只读的输入
Work tape: 用来计算
输入带是只读的,工作带是可读写的。而空间复杂度不计入输入带,只统计工作带用了多少格子。
显然普通图灵机能做的,off-line 图灵机也能做,反之亦然。这个定义的意义是排除输入本身所占的空间,让空间复杂度能够被干净地定义。比如一个 \(O(\log n)\) 空间算法并不是说它连输入都只能读 \(O(\log n)\) 个字符,而是说它除了只读输入之外,只需要 \(O(\log n)\) 个工作格子。
空间复杂度类
对于 off-line 图灵机,空间使用量指的是机器在工作带上访问过的格子数量。
和时间复杂度一样,空间复杂度类也不是只看某一条"最省空间"的接受路径,而是要求机器在所有输入上都有统一的空间上界。
DSPACE 和 NSPACE
\(\text{DSPACE}(f)\) 表示这样一类语言:存在一台确定性 off-line 图灵机 \(M\),可以用 \(O(f(n))\) 工作空间判定该语言。
也就是说,对所有输入 \(x\),机器都只使用 \(O(f(|x|))\) 个工作格子,并且正确接受或拒绝。
\(\text{NSPACE}(f)\) 表示这样一类语言:存在一台非确定性 off-line 图灵机 \(M\),它在所有计算分支上都只使用 \(O(f(n))\) 工作空间,并且:
\[x \in L \iff \text{存在至少一条计算路径接受 } x \]
为了避免机器无限循环,讨论空间类时通常也要求机器是判定器,或者可以等价地转化为在有限 configuration graph 上的可达性问题。
PSPACE 和 NPSPACE
\(\text{PSPACE}\) 是确定性多项式空间:
\[\text{PSPACE} = \bigcup_{k \ge 1} \text{DSPACE}(n^k) \]
\(\text{NPSPACE}\) 是非确定性多项式空间:
\[\text{NPSPACE} = \bigcup_{k \ge 1} \text{NSPACE}(n^k) \]
LOGSPACE 和 NLOGSPACE
\(\text{LOGSPACE}\),也常记作 \(\text{L}\),是确定性对数空间:
\[\text{L} = \text{DSPACE}(\log n) \]
\(\text{NLOGSPACE}\),也常记作 \(\text{NL}\),是非确定性对数空间:
\[\text{NL} = \text{NSPACE}(\log n) \]
\(\log n\) 空间看起来很小,但它足够保存一个输入位置、一个节点编号或者一个计数器。比如输入长度是 \(n\),要表示"我现在指向第几个字符",只需要 \(O(\log n)\) 位。
有些算法不需要存整个输入的副本,只需要反复读输入,并保存少量位置和状态信息。
基本包含关系
现在我们已经有了一系列定义。它们之间有几条非常自然的包含关系。
第一,确定性是非确定性的特例:
\[\text{DTIME}(f) \subseteq \text{NTIME}(f) \]
\[\text{DSPACE}(f) \subseteq \text{NSPACE}(f) \]
因为非确定性机器可以选择不分叉。这符合直觉:如果一个问题能被确定性算法解决,那么当然也能被非确定性算法解决。
第二,时间上界会推出空间上界:
\[\text{DTIME}(f) \subseteq \text{DSPACE}(f) \]
\[\text{NTIME}(f) \subseteq \text{NSPACE}(f) \]
原因很简单:一台机器运行 \(f(n)\) 步,最多也只能访问 \(O(f(n))\) 个工作格子。
反过来通常不成立。一个算法可以用很少空间运行很久,因为空间可以反复复用,而时间一旦花掉就不能复用。
因此我们有:
\[\text{P} \subseteq \text{NP} \subseteq \text{PSPACE} \]
其中 \(\text{NP} \subseteq \text{PSPACE}\) 的理由是:多项式时间算法最多只能用多项式空间。
Constructible Function
在复杂度理论里,我们通常希望时间和空间上界是可以"构造"出来的。否则可能会出现一些非常病态、无法实际计数或划分资源的函数。
Time constructible :函数 \(T(n)\) 是 time constructible,意思是存在一台图灵机,对于任意长度为 \(n\) 的输入,都恰好运行 \(T(n)\) 步后停机。直觉上,这台机器能自己"数出"这个时间预算,必须能算出 \(T(n)\) 本身。。
Space constructible :函数 \(S(n)\) 是 space constructible,意思是存在一台图灵机,可以恰好划出 \(S(n)\) 个工作格子。直觉上,这台机器能自己"划出"这个空间预算。
这些定义主要是为了排除一些特别病态的函数。我们日常见到的复杂度函数,比如 \(n\)、\(n^2\)、\(n^3\)、\(2^n\)、\(\log n\),基本都可以认为是 constructible 的。
Configuration graph 与空间机器
如果一台 off-line 图灵机只使用 \(S(n)\) 工作空间,那么它可能出现的 configuration 数量是有限的。
一个 configuration 大致包含:
- 当前状态;
- 工作带内容;
- 工作带读写头位置;
- 输入带读头位置。
工作带内容有指数多种可能,输入带读头位置有 \(n\) 种可能,状态和读写头信息只带来较小的乘法因子。
因此,当 \(S(n) \ge \log n\) 时,configuration 总数可以写成:
\[2^{O(S(n))} \]
直观上可以记成"指数于空间大小"。
于是,非确定性机器的计算过程可以看成一张图:
- 节点是所有可能的 configuration;
- 边表示一步合法转移;
- 初始 configuration 是起点;
- 接受 configuration 是目标点。
这样一来,非确定性空间计算就变成了图可达性问题:初始 configuration 是否能到达某个接受 configuration?
这个视角非常强大。它可以推出:
\[\text{NSPACE}(S(n)) \subseteq \text{DTIME}(2^{O(S(n))}) \]
意思是:如果一个问题能用非确定性 \(S(n)\) 空间解决,那么也可以用确定性"指数于 \(S(n)\) 的时间"解决。
当 \(S(n)=\log n\) 时,有:
\[\text{L} \subseteq \text{NL} \subseteq \text{P} \]
这条关系很重要:非确定性对数空间虽然看起来有"猜"的能力,但因为空间太小,总 configuration 数量只有多项式级别,所以确定性机器可以在多项式时间内搜索完。
Savitch 定理
Savitch 定理是:
\[\text{NSPACE}(S(n)) \subseteq \text{DSPACE}(S(n)^2) \]
条件是 \(S(n) \ge \log n\),并且 \(S\) 是 space constructible。
这条定理的含义非常强:非确定性空间可以被确定性空间模拟,而且只损失*方级空间。
这和时间复杂度形成鲜明对比。非确定性时间通常只能用确定性时间做指数级暴力模拟;但非确定性空间只需要确定性空间的*方级模拟。
还是看 configuration graph。我们要判断初始 configuration 是否能到达接受 configuration。如果直接存整张图,会需要指数空间。Savitch 的技巧是不存图,而是递归判断可达性。
定义一个过程:
text
CanReach(u, v, t)
表示:是否存在一条从 configuration u 到 configuration v,长度不超过 t 的路径。
如果存在这样一条路径,那么一定存在某个中点 m,使得:
u能在t/2步内到达m;m能在t/2步内到达v。
于是我们枚举所有可能的中点 m,递归检查这两个子问题。
这个算法的时间可能非常长,但空间很省。递归深度大约是 \(O(S(n))\),每层保存一个 configuration 需要 \(O(S(n))\) 空间,所以总空间是:
\[O(S(n)^2) \]
取 \(S(n)=n^k\),一方面:
\[\text{PSPACE} \subseteq \text{NPSPACE} \]
因为确定性是非确定性的特例。
另一方面,如果某个语言属于 \(\text{NPSPACE}\),它就属于某个 \(\text{NSPACE}(n^k)\)。由 Savitch 定理,它又属于:
\[\text{DSPACE}(n^{2k}) \]
这仍然是多项式空间,所以属于 PSPACE。
因此可以推出:
\[\text{NPSPACE} = \text{PSPACE} \]
这和 P vs NP 很不一样。我们不知道 P 是否等于 NP,但我们知道 PSPACE 和 NPSPACE 相等。这说明在多项式空间复杂度里,非确定性没有带来额外的类级别能力。
图可达性问题
输入一个有向图 \(G\) 和两个点 \(s,t\),问题是:是否存在一条从 \(s\) 到 \(t\) 的路径?
这个问题通常叫作有向图可达性,或者 STCON / GAP。
它属于 NL,可以这样证明:
- 从 \(s\) 开始;
- 非确定性地猜一个下一步邻居;
- 移动到这个邻居;
- 重复;
- 如果到达 \(t\),接受;
- 如果走了超过 \(n-1\) 步还没到达,就拒绝这条路径。
整个过程中,我们不需要保存完整路径,只需要保存:
- 当前节点;
- 已经走了多少步。
对于有 \(n\) 个节点的图,一个节点编号只需要 \(O(\log n)\) 空间,步数计数器也只需要 \(O(\log n)\) 空间。
所以:
\[\text{GAP} \in \text{NL} \]
再由 Savitch 定理:
\[\text{NL} \subseteq \text{DSPACE}(\log^2 n) \]
所以 GAP 可以用确定性 \(O(\log^2 n)\) 空间解决。
这个例子很适合帮助记忆 Savitch 定理:非确定性地猜路径很容易,但确定性地判断可达性也可以在*方对数空间内完成。
指数时间类
P 和 NP 只涉及多项式时间。但还有很多问题,我们只知道指数时间算法,或者可以证明它们不在 P 中。
这里需要小心:对很多自然的 NP-complete 问题,比如 SAT、旅行商问题的决策版本,我们并没有证明它们必须需要指数时间。我们只是还不知道它们是否有多项式时间算法。说它们"需要指数时间"通常要依赖更强的假设,例如 P vs NP、ETH 或 SETH。
指数时间类用来刻画比多项式时间更大的时间范围。
常见的指数时间可以分为两个层次:
- 单指数时间 :运行时间为 \(2^{O(n)}\),例如 \(2^n\)、\(2^{3n}\);
- 多项式指数时间 :运行时间为 \(2^{n^k}\) 或 \(2^{\text{poly}(n)}\),例如 \(2^n\)、\(2^{n^2}\)、\(2^{n^{100}}\)。
常见符号是:
\[\text{E} = \text{DTIME}(2^{O(n)}) \]
\[\text{EXP} = \text{EXPTIME} = \bigcup_{k \ge 1} \text{DTIME}(2^{n^k}) \]
类似地,非确定性版本是:
\[\text{NE} = \text{NTIME}(2^{O(n)}) \]
\[\text{NEXP} = \text{NEXPTIME} = \bigcup_{k \ge 1} \text{NTIME}(2^{n^k}) \]
显然有:
\[\text{E} \subseteq \text{EXP} \]
以及:
\[\text{NE} \subseteq \text{NEXP} \]
指数时间算法通常来自显式枚举或搜索,例如枚举子集、枚举排列、暴力搜索解空间、某些动态规划等。但要注意:有指数时间算法,不等于已经证明没有多项式时间算法。
典型 complete problems
为了把抽象复杂度类和具体问题联系起来,可以记住一些典型 complete problems。
| 复杂度类 | 典型 complete problem | 直觉 |
|---|---|---|
| NP | SAT、3-SAT、CLIQUE、TSP-decision | 猜一个解,然后多项式时间验证 |
| NL | 有向图可达性 STCON / GAP | 猜一条从 \(s\) 到 \(t\) 的路径 |
| P | Circuit Value Problem | 按电路拓扑顺序计算输出 |
| PSPACE | TQBF / QBF | 带交替量词的布尔公式 |
| EXP | generalized chess、generalized checkers、某些长博弈问题 | 状态空间指数级增长 |
这张表可以建立直觉:每个复杂度类通常都有一些"代表性难题",它们刻画了这个类的本质难度。
coNP 和 coNL
如果一个语言 \(L\) 属于某个复杂度类,我们也可以问它的补语言:
\[\overline{L} = \Sigma^* \setminus L \]
是否也属于类似的复杂度类。
coNP 是 NP 的补类。直觉上,NP 是"yes 答案容易验证",coNP 是"no 答案容易验证"。
例如,TAUT 问题问一个布尔公式是否对所有赋值都为真。它的补问题是"是否存在一个赋值让公式为假",这显然属于 NP。因此 TAUT 属于 coNP。
我们不知道:
\[\text{NP} \stackrel{?}{=} \text{coNP} \]
如果能证明 \(\text{NP} \ne \text{coNP}\),那么立刻可以推出 \(P \ne NP\)。但这同样是开放问题。
空间类里有一个非常漂亮的结论:
\[\text{NL} = \text{coNL} \]
这就是 Immerman--Szelepcsényi 定理。它说明非确定性对数空间在取补时是封闭的。这一点和我们对 NP / coNP 的未知情况形成了鲜明对比。
层次定理:更多资源真的更强
空间层次定理
空间层次定理说,如果 \(S'(n)\) 比 \(S(n)\) 小得足够多,那么给更多空间确实能解决更多语言。
一个常见表述是:若 \(S(n)\) 是 space constructible,且:
\[S'(n) = o(S(n)) \]
那么:
\[\text{DSPACE}(S'(n)) \subsetneq \text{DSPACE}(S(n)) \]
直觉上:给机器更多空间,确实能解决更多语言。例如 \(\log n = o(n)\),所以线性空间确实比对数空间更强。
时间层次定理
时间层次定理说,如果 \(T(n)\) 比 \(t(n)\) 大得足够多,那么给更多时间也确实能解决更多语言。
一个常见表述是:如果 \(T(n)\) 是 time constructible,且:
\[t(n) \log t(n) = o(T(n)) \]
那么:
\[\text{DTIME}(t(n)) \subsetneq \text{DTIME}(T(n)) \]
这里出现额外的 \(\log t(n)\),和通用模拟的编码、维护与模拟开销有关。
一个重要推论是:
\[\text{P} \subsetneq \text{EXP} \]
也就是说,确定性指数时间确实严格强于确定性多项式时间。这是复杂度理论中少数几个已被证明的真包含关系之一。
为什么能证明 \(\text{P} \ne \text{EXPTIME}\),却证明不了 \(\text{P} \ne \text{NP}\)?
因为时间层次定理主要依赖对角化。它可以区分"多项式时间"和"指数时间"这样的资源差距,但无法直接处理非确定性带来的"猜测"能力。P vs NP 的困难,不只是时间够不够的问题,还涉及"搜索"和"验证"之间是否真的有本质差异。
非确定性时间也有层次定理。它可以推出:
\[\text{NP} \subsetneq \text{NEXP} \]
层次定理的意义是:复杂度类之间的分层不只是"看起来不同",而是确实存在一些问题,必须给足够多的时间或空间才能解决。
总结
到这里,我们可以整理出一张大致的地图:

其中一些关系是已知的:
- \(\text{L} \subseteq \text{NL} \subseteq \text{P}\);
- \(\text{P} \subseteq \text{NP}\);
- \(\text{NP} \subseteq \text{PSPACE}\);
- \(\text{PSPACE} = \text{NPSPACE}\);
- \(\text{PSPACE} \subseteq \text{EXP}\);
- \(\text{P} \subsetneq \text{EXP}\);
- \(\text{NP} \subsetneq \text{NEXP}\);
- \(\text{NL} = \text{coNL}\)。
也有一些开放问题仍然未知:
- \(\text{P}\) 是否等于 \(\text{NP}\);
- \(\text{NP}\) 是否等于 \(\text{coNP}\);
- \(\text{L}\) 是否等于 \(\text{NL}\);
- \(\text{NP}\) 是否等于 \(\text{PSPACE}\);
- \(\text{PSPACE}\) 是否等于 \(\text{EXP}\)。