图灵机、时间与空间复杂度:从 P/NP 到 PSPACE、EXPTIME

上一篇文章里,我们已经讲过 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\} \]

例如:001001 属于 \(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}\)。