11.1. 关键区别的引出
📜 [原文1]
定理 7.8 和 7.11 阐明了一个重要的区别。一方面,我们证明了在确定型单带和多带图灵机上测量的问题时间复杂度之间最多只有平方或多项式级别的差异。另一方面,我们展示了在确定型和非确定型图灵机上测量的问题时间复杂度之间最多存在指数级别的差异。
📖 [逐步解释]
这段话是在回顾和总结前面章节(定理 7.8 和 7.11,虽然这里没有给出具体内容,但我们可以根据上下文推断)得出的两个核心结论,并以此引出计算复杂性理论中的一个根本性划分。
- 确定性模型之间的关系:首先,它提到了确定型图灵机的不同变体,比如单带图灵机(只有一个存储和读写的工作带)和多带图灵机(有多个工作带)。结论是,这两个模型在计算能力上是等价的,并且在效率上的差异是“多项式级别”的。这意味着,如果一个问题在多带图灵机上可以用时间 $t(n)$ 解决,那么在单带图灵机上一定可以用 $t(n)^2$ 或者某个 $t(n)$ 的多项式,比如 $t(n)^k$ 的时间内解决。从宏观角度看,这种差异被认为是“不大”的。
- 确定性与非确定性模型的关系:其次,它对比了确定型图灵机和非确定型图灵机。结论是,这两个模型在效率上的差异可能是“指数级别”的。非确定型图灵机有一个“猜测”或“并行探索所有可能性”的能力。如果一个问题在非确定型图灵机上可以用时间 $t(n)$ 解决,在确定型图灵机上解决它可能需要 $2^{t(n)}$ 级别的时间。这种差异被认为是“巨大”的,是本质上的不同。
- 引出核心概念:通过这两个对比,作者强调了“多项式”和“指数”这两种增长率之间的鸿沟。这个鸿沟成为了划分问题“难易”程度的理论基础。
💡 [数值示例]
- 示例 1 (多项式差异):假设一个问题在一台多带图灵机上需要 $n^2$ 的时间来解决(其中 $n$ 是输入规模)。根据定理 7.8,如果我们将这个算法转换到一台单带图灵机上,所需时间可能是 $(n^2)^2 = n^4$。虽然 $n^4$ 比 $n^2$ 大,但它们都属于多项式函数。当 $n=100$ 时,$n^2 = 10,000$,$n^4 = 100,000,000$。数字虽然变大了,但仍在可计算的范畴内。
- 示例 2 (指数差异):假设一个问题在一台非确定型图灵机上需要 $n$ 的时间来解决。根据定理 7.11,如果用一台确定型图灵机来模拟这个过程,最坏情况下可能需要 $2^n$ 的时间。当 $n=100$ 时,$n=100$,而 $2^{100}$ 是一个巨大的数字(大约是 $1.26 \times 10^{30}$),远远超出了现代计算机的实际计算能力。这个差异是悬殊的。
⚠️ [易错点]
- 误解“多项式差异不大”:在理论层面,我们说多项式差异“不大”,是相对指数差异而言的。但在实际工程中,$n^2$ 和 $n^5$ 的算法性能差异是巨大的。理论计算机科学关注的是增长的“类别”和问题的“根本可解性”,因此选择忽略多项式内的差异,以建立一个更普适的理论。
- 确定性和非确定性模型的计算能力:需要明确的是,确定型图灵机和非确定型图灵机在“可计算性”上是等价的,即它们能解决的问题集合是完全相同的(都是图灵可识别语言)。它们的差异体现在“计算复杂性”或“效率”上。
📝 [总结]
本段的核心是建立一个重要的区分标准:计算模型之间的多项式时间差异被视为是可接受的、非本质的;而确定性与非确定性之间的指数时间差异,则被视为是根本性的、巨大的。这个区分为后续定义“易解问题”(P类问题)和“难解问题”(NP类问题)奠定了基础。
🎯 [存在目的]
本段的目的是为引入“P类”这一核心概念进行铺垫。通过强调多项式和指数的本质区别,作者说明了为什么我们将“多项式时间”作为衡量一个问题是否“易解”或“实际可计算”的标尺。
🧠 [直觉心智模型]
想象有两种交通工具。一种是自行车,另一种是汽车。它们都是“地面交通工具”。从一个城市到另一个很远的城市,骑自行车(单带图灵机)可能需要几天,而开车(多带图灵机)可能只需要几个小时。虽然时间差异很大,但都是“可预测的、有限的”增长(多项式关系)。
现在引入第三种交通工具:一个可以瞬间穿越到任何平行宇宙的传送门(非确定型图灵机)。你要找一条从北京到纽约的最佳路径。这个传送门可以瞬间帮你探索所有可能的路径并告诉你结果。而如果你只有汽车(确定型图灵机),你可能需要一条一条地去试,花费的时间可能是天文数字(指数关系)。自行车和汽车之间的差异,远小于它们与传送门之间的差异。
💭 [直观想象]
想象你在一个巨大的图书馆里找一本书。
- 多项式时间:图书馆管理员告诉你书在“三楼社科区”,你需要花时间走到三楼,然后在书架上寻找。如果图书馆更大($n$ 增加),你走路和找书的时间会相应增加(比如 $n^2$),但这是可以接受的。
- 指数时间:图书馆管理员不知道书在哪里,告诉你“它可能在图书馆的任何一个地方”。你唯一的办法就是从第一本书开始,一本一本地检查下去,直到找到为止。图书馆每增大一点,你需要检查的书可能就成倍增加($2^n$),这很快就会变得不切实际。
11.2. 多项式时间
📜 [原文2]
对我们而言,运行时间上的多项式差异被认为是小的,而指数差异被认为是大的。让我们看看为什么我们选择将这种区分放在多项式和指数之间,而不是放在其他一些函数类别之间。
📖 [逐步解释]
这段话提出了一个核心问题:为什么在计算复杂性理论中,我们选择以“多项式 vs. 指数”作为划分“易”与“难”的界线,而不是其他的函数划分,比如 $n^3$ vs $n^4$,或者 $n^{\log n}$ vs $2^n$?接下来的内容将为这个选择提供辩护和解释。这本质上是在说明将“多项式时间”定义为“高效”的合理性。
📝 [总结]
本段是一个承上启下的设问,引出对“多项式时间可解性”这一基本假设的论证。它点明了理论计算机科学中一个非常重要的哲学选择。
🎯 [存在目的]
其目的是激发读者思考,并为接下来从理论和实践两个层面论证“多项式时间是‘高效’的合理界定”这一观点做准备。
🧠 [直觉心智模型]
这就像在法律上定义“成年人”是18岁。为什么是18岁,不是17.5岁或者19岁?法律需要一条清晰的界线来区分不同的权利和义务。虽然17岁和18岁的人在心智上可能差别不大,但“18岁”这个标准在法律实践中是稳定和有效的。同样,理论计算机科学选择“多项式”作为一条清晰的界线,来区分“易处理”和“难处理”的问题。
💭 [直观想象]
想象一条河流,河的一边是平原(多项式),另一边是陡峭的悬崖(指数)。虽然平原上也有高低起伏的小山丘(比如 $n^2$ 和 $n^3$),但在平原上行走总是相对容易的。而一旦你试图跨过河流去爬悬崖,难度就会急剧增加。我们关心的就是“过河”这个行为,而不是在平原上爬一个高一点的山丘。
📜 [原文3]
首先,请注意通常出现的多项式(如 $n^{3}$)和通常出现的指数(如 $2^{n}$)增长率之间的巨大差异。例如,让 $n$ 为 1000,这是算法合理输入的规模。在这种情况下,$n^{3}$ 是 10 亿,这是一个庞大但可管理的数字,而 $2^{n}$ 是一个比宇宙中原子数量大得多的数字。多项式时间算法对于许多目的来说足够快,但指数时间算法却很少有用。
📖 [逐步解释]
这里给出了选择“多项式 vs. 指数”作为分界线的第一个,也是最直观的理由:增长率的巨大差异。
- 多项式增长:函数如 $n, n^2, n^3$ 等。当输入规模 $n$ 增大时,它们的函数值虽然也增大,但增长速度是相对“温和”的。作者举例 $n=1000$ 时,$n^3 = 1,000,000,000$(10亿)。对于现代计算机来说,执行10亿次操作是完全可能的(比如一个几 GHz 的CPU在一秒内就能执行几十亿次操作)。所以,这是一个“可管理”的数字。
- 指数增长:函数如 $2^n, 3^n, n!$ 等。当输入规模 $n$ 增大时,它们的函数值会以爆炸性的速度增长。作者举例 $n=1000$ 时,$2^{1000}$ 的值。$2^{10} \approx 10^3$,所以 $2^{1000} = (2^{10})^{100} \approx (10^3)^{100} = 10^{300}$。这个数字远远超过了可观测宇宙中原子的总数(大约是 $10^{80}$)。这样的计算量在任何实际的计算机上都是绝对无法完成的。
- 结论:因此,从实践角度看,多项式时间算法通常意味着问题是“可解的”、“可行的”;而指数时间算法通常意味着问题是“难解的”、“不可行的”,尤其当输入规模稍大时。
💡 [数值示例]
- 示例1:假设一个算法A的复杂度是 $n^3$,算法B的复杂度是 $2^n$。
- 当 $n = 10$ 时:
- 算法A需要 $10^3 = 1,000$ 次操作。
- 算法B需要 $2^{10} = 1,024$ 次操作。两者差不多。
- 当 $n = 20$ 时:
- 算法A需要 $20^3 = 8,000$ 次操作。
- 算法B需要 $2^{20} \approx 1,000,000$(一百万)次操作。差距开始显现。
- 当 $n = 50$ 时:
- 算法A需要 $50^3 = 125,000$ 次操作(非常快)。
- 算法B需要 $2^{50} \approx 1.12 \times 10^{15}$ 次操作(在现代计算机上需要数天甚至数周)。
- 当 $n = 1000$ 时:
- 算法A需要 $1000^3 = 10^9$ 次操作(几秒钟)。
- 算法B需要 $2^{1000} \approx 10^{301}$ 次操作(宇宙毁灭也算不完)。
⚠️ [易错点]
- 小n的情况:对于非常小的输入规模 $n$,一个指数算法可能比一个高次多项式算法更快。例如,当 $n=4$ 时,$n^{10} = 1,048,576$ 而 $2^n = 16$。但复杂性理论关注的是随着 $n$ 趋向于无穷大时的渐近行为,因为我们关心的是算法处理大规模问题的能力。
- 多项式的系数和次数:一个 $10^{100} \cdot n^2$ 的算法和一个 $1.0001^n$ 的算法,在理论上前者是多项式时间,后者是指数时间。但在实际中,对于大部分可行的 $n$,前者可能永远比后者慢。理论的划分是一种宏观抽象,它假设了“正常”的多项式和指数形式。
📝 [总结]
本段通过具体的数值对比,强有力地证明了多项式增长和指数增长在量级上的本质区别。这为将“多项式时间”等同于“实际可行”提供了最直接的实践依据。
🎯 [存在目的]
本段的目的是用一个非常直观且有冲击力的方式,让读者理解为什么指数时间算法通常是无用的,而多项式时间算法是解决问题的希望所在。
🧠 [直觉心智模型]
想象你在攒钱。
- 多项式增长:你每个月固定存1000元,或者每个月存的钱比上个月多100元。你的财富增长是线性的或二次的。虽然需要时间,但你可以明确地看到自己的存款在稳定增加。
- 指数增长:你投资了一个项目,每天收益翻倍。第一天1元,第二天2元,第三天4元... 一个月后,你将拥有超过10亿元。这种增长是爆炸性的、不可思议的。算法复杂度的差异也是如此。
💭 [直观想象]
想象折纸。
- 多项式时间:任务是“将一张纸裁成 $n$ 个小方块”。你需要进行 $n-1$ 次裁剪。工作量与 $n$ 成正比。
- 指数时间:任务是“将一张足够大的纸对折 $n$ 次”。每对折一次,厚度加倍。对折50次后,其厚度可以从地球达到太阳。这个厚度的增长就是指数级的,对应了指数算法在资源消耗上的爆炸性增长。
📜 [原文4]
指数时间算法通常在我们通过穷举搜索解决方案空间(称为暴力搜索)来解决问题时出现。例如,将一个数分解为其素因数的一种方法是搜索所有潜在的除数。搜索空间的大小是指数级的,因此这种搜索使用指数时间。有时,通过对问题更深入的理解,可以避免暴力搜索,这可能会揭示一种更实用的多项式时间算法。
📖 [逐步解释]
这段话解释了指数时间算法的一个常见来源,并指出了提升算法效率的途径。
- 来源:暴力搜索 (Brute-force search):许多问题的最直观解法是尝试所有可能性。这种方法被称为穷举搜索或暴力搜索。当可能性的数量随着输入规模 $n$ 呈指数增长时,暴力搜索算法就是指数时间的。
- 例子:因数分解:要分解一个数 $N$,最简单的方法是尝试从 2 到 $\sqrt{N}$ 之间的所有数,看哪个能整除 $N$。输入是数字 $N$,其“规模”通常用其二进制表示的位数 $n$ 来衡量,即 $n \approx \log_2 N$。所以 $N \approx 2^n$。搜索范围是到 $\sqrt{N} \approx \sqrt{2^n} = 2^{n/2}$。搜索空间的大小 $2^{n/2}$ 是输入规模 $n$ 的指数函数,因此这是一个指数时间的暴力算法。
- 改进途径:避免指数时间的关键在于找到比暴力搜索更聪明的方法。这通常需要对问题结构有更深刻的数学洞察。如果能找到一个巧妙的捷径,就有可能设计出多项式时间的算法。例如,虽然我们目前还没有找到大数分解的多项式时间经典算法,但对于其他许多问题(如下文的PATH问题),我们已经成功地从指数时间的暴力搜索改进到了多项式时间算法。
💡 [数值示例]
- 旅行商问题 (TSP):给定 $n$ 个城市和它们之间的距离,找到一条访问每个城市一次并返回起点的最短路径。
- 暴力解法:列出所有可能的城市访问顺序(排列),然后计算每条路径的总长度,最后找出最短的。对于 $n$ 个城市,有 $(n-1)!/2$ 种不同的路径。阶乘函数 $n!$ 的增长速度比指数函数 $2^n$ 还快。当 $n=20$ 时,$19!/2$ 是一个天文数字,暴力搜索完全不可行。这就是一个典型的指数时间算法。
- 改进:虽然TSP问题被认为是NP-Hard(普遍认为不存在多项式时间算法),但通过动态规划等技术(如Held-Karp算法),可以将其复杂度从 $O(n!)$ 降低到 $O(n^2 2^n)$。虽然仍然是指数时间,但已经比暴力搜索好得多,对于中等规模(如 $n=20-25$)的TSP问题变得可以计算。这个例子说明了“更深入的理解”可以如何优化算法,即使最终没能达到多项式时间。
⚠️ [易错点]
- 并非所有暴力搜索都是指数时间的:例如,在一个含 $n$ 个元素的数组中寻找最大值。暴力搜索就是遍历所有元素,比较并记录最大值。这个过程需要 $n-1$ 次比较,时间复杂度是 $O(n)$,是多项式时间的。一个搜索算法是否是指数时间,取决于其“搜索空间”的大小。
- 多项式时间算法的发现是重大突破:对于一个长期以来只有指数时间解法的问题,如果有人发现了第一个多项式时间算法,这在理论计算机科学领域是里程碑式的事件。它通常意味着我们对这个问题的内在结构有了全新的、更深刻的认识。
📝 [总结]
本段指出了指数时间复杂度的常见根源——对庞大的解空间进行暴力搜索。同时,它也给出了一个核心的研究方向:通过更深刻的洞察来设计“聪明”的算法,以避免暴力搜索,从而将指数时间复杂度降低到多项式时间复杂度。
🎯 [存在目的]
本段的目的是将抽象的“指数时间”与具体的算法设计方法“暴力搜索”联系起来,让读者明白这类“慢”算法是如何产生的。同时,也暗示了算法设计的核心魅力在于寻找避免暴力搜索的捷径。
🧠 [直觉心智模型]
想象你在一个巨大的迷宫里找出口。
- 暴力搜索:你没有地图,唯一的策略是“右手扶墙走”,即沿着一条墙壁一直走,确保能遍历所有路径。这个过程可能会让你在迷宫里绕非常非常久(指数时间)。
- 更深入的理解:有人给了你一张迷宫的地图(多项式时间算法)。你可以在地图上直接规划出一条通往出口的路径,然后按图索骥。或者,有人告诉你一个诀窍:“这个迷宫里所有的岔路口,只要选择朝北的路口走,就一定能到出口”。这就是避免了暴力搜索的“捷径”。
💭 [直观想象]
想象你忘记了你的4位数字密码。
- 暴力搜索:你从0000开始,一个一个地尝试,0001, 0002, ... 直到9999。你总共需要尝试 $10^4$ 次。如果密码是 $n$ 位,就需要 $10^n$ 次尝试,这就是指数级的搜索空间。
- 更深入的理解:你突然想起来,密码是你的生日(月和日)。你只需要尝试你的生日,一次就成功了。或者你记得密码的四个数字是1,2,3,4,但不记得顺序了。现在你只需要尝试这四个数字的排列组合($4! = 24$ 种),而不是全部 $10^4$ 种。这就是利用问题的额外信息来缩小搜索空间。
📜 [原文5]
所有合理的确定型计算模型都是多项式等价的。也就是说,任何一个模型都可以模拟另一个模型,且运行时间只增加多项式倍。当我们说所有合理的确定型模型都是多项式等价时,我们并不试图定义“合理”。然而,我们设想的这个概念足够广泛,足以包含那些能精确近似实际计算机上运行时间的模型。例如,定理 7.8 表明确定型单带和多带图灵机模型是多项式等价的。
📖 [逐步解释]
这段话提出了支持将“多项式时间”作为标准的一个强有力的理论依据:计算模型的稳健性。
- 多项式等价 (Polynomially equivalent):这个概念是指,如果我们在模型A上解决一个问题需要时间 $T(n)$,那么在模型B上模拟模型A来解决同一个问题,需要的时间是 $T(n)$ 的某个多项式,比如 $(T(n))^k$。反之亦然。
- 合理的确定型计算模型:这个短语没有一个严格的数学定义,但它通常指那些计算能力与图灵机相当,并且其一步操作能在图灵机上用多项式步数模拟的计算模型。这包括我们日常使用的计算机的底层模型(如随机存取机 RAM 模型)、多带图灵机、多维图灵机等。它排除了那些过于强大以至于不真实的模型(比如可以一步完成指数级运算的模型)。
- 核心论点:所有这些“合理”的模型都是多项式等价的。这意味着,一个问题如果在一个“合理”模型上是多项式时间可解的,那么它在所有其他“合理”模型上也是多项式时间可解的。反之,如果在一个模型上需要指数时间,在其他模型上也需要指数时间。
- 意义:这个性质使得“多项式时间可解性”这个概念摆脱了对具体计算模型的依赖。我们不必纠结于用的是单带图灵机还是现代电脑,只要一个问题被证明是“多项式时间可解的”,这个结论就具有普遍性。它描述的是问题本身的内在属性,而不是某个特定机器的属性。
💡 [数值示例]
- 示例:假设我们有一个算法,在RAM模型(可以看作是理想化的计算机,一步可以访问任意内存地址)上运行时间是 $T_{RAM}(n) = n^2$。我们可以证明,一台多带图灵机可以用 $O(n^2 \log n)$ 的时间模拟这个RAM模型上的计算,这仍然是 $n$ 的多项式。然后,根据定理 7.8,一台单带图灵机又可以用 $(O(n^2 \log n))^2 = O(n^4 \log^2 n)$ 的时间来模拟这台多带图灵机,这依然是 $n$ 的多项式。
- 结论:一个在RAM模型上是 $O(n^2)$ 的问题,在单带图灵机上是 $O(n^4 \log^2 n)$。尽管具体次数不同,但它们都属于“多项式时间”这个大家族。因此,“是否属于多项式时间”这个问题的答案,在这些模型中是不变的。
⚠️ [易错点]
- “合理”的边界:什么样的模型是不合理的?例如,一个假设可以一步计算 $2^n$ 的模型就是不合理的,因为它在物理上无法实现。量子计算机是一个有趣的边界情况,它是一种非确定型的模型,对于某些特定问题(如大数分解),它可能提供指数级的加速,挑战了传统确定型模型下的复杂性分类。但对于一般问题,它是否能解决所有NP问题还远未有定论。
- 等价性不保留具体的多项式次数:一个在模型A上是 $O(n)$ 的算法,在模型B上可能是 $O(n^5)$。多项式等价性只保证了“同属多项式”,不保证“同样高效”。
📝 [总结]
本段的核心论点是,“一个问题是否能在多项式时间内解决”这个判断是独立于具体计算模型的(在所有合理的确定型模型中)。这个模型无关性(或称稳健性)使得“多项式时间”成为一个深刻而可靠的数学概念,适合作为理论分析的基石。
🎯 [存在目的]
本段的目的是为“多项式时间”这个概念提供理论上的合法性。它告诉我们,我们不必担心我们的理论因为选择了图灵机而不是别的模型而变得狭隘,因为在多项式时间的框架下,所有合理的模型都是“一样”的。
🧠 [直觉心智模型]
想象一下“能够被翻译”这个概念。
- 一篇文章如果能被从中文翻译成英文,那么它大概率也能被翻译成法文、德文、日文等等(所有“合理的”人类语言)。翻译后的具体措辞、篇幅会有所不同(多项式的次数不同),但文章的“可翻译性”这个属性是不变的。
- 相比之下,一幅画或者一首音乐所传达的情感,可能就很难用语言“等价地”翻译出来(指数级的困难)。
- “多项式时间可解性”就像是“可翻译性”,它是一个在各种语言(计算模型)之间共通的属性。
💭 [直观想象]
想象一下不同品牌的计算器。
- 有的计算器(多带图灵机)有很强大的功能键,可以直接算 $x^2$。
- 有的计算器(单带图灵机)很基础,只能做加减乘除。
- 要在基础计算器上算 $n^2$,你需要做一次乘法 $n \times n$。
- 要在基础计算器上算 $n^3$,你需要做两次乘法 $(n \times n) \times n$。
- 虽然操作步骤不同,但对于任何多项式运算(如 $n^k$),你都可以在基础计算器上通过有限次(多项式次)的基本运算来完成。所以,一个问题“是否能用计算器在多项式步数内算出”,这个答案不取决于你用的是基础款还是高级款计算器。
📜 [原文6]
从现在开始,我们专注于时间复杂度理论中不受运行时间上的多项式差异影响的方面。忽略这些差异使我们能够发展一个不依赖于特定计算模型选择的理论。请记住,我们的目标是揭示计算的基本性质,而不是图灵机或任何其他特殊模型的性质。
📖 [逐步解释]
这段话是对前面论述的一个总结和宣告。
- 研究焦点的声明:作者明确指出,接下来的讨论将进入一个更高层次的抽象。我们将不再关心一个算法是 $O(n^2)$ 还是 $O(n^3)$,我们只关心它是不是 $O(n^k)$(即,是不是多项式时间的)。我们关注的是“多项式 vs. 非多项式”的二元划分。
- 理由重申:为什么要这么做?因为这样做可以构建一个普适的 (universal)、模型无关的 (model-independent) 理论。这个理论揭示的是计算问题本身 (problem) 的内在难度,而不是解决这个问题的特定算法 (algorithm) 或特定机器 (machine) 的效率。
- 最终目标:理论计算机科学的宏大目标是理解“计算”这一概念的本质和极限。通过忽略具体的、依赖于模型的细节(如多项式差异),我们可以更清晰地看到那些更根本、更永恒的性质。
📝 [总结]
本段是一个方法论上的宣言。它宣告了复杂性理论的一个核心原则:为了抓住问题的本质,我们选择忽略多项式级别的差异,从而建立一个更具普遍性和深刻性的理论框架。
🎯 [存在目的]
本段的目的是明确设定后续讨论的“游戏规则”,让读者调整自己的视角,从关注具体算法的性能优化,转向关注问题本身的可计算性分类。
🧠 [直觉心智模型]
这就像物理学家研究力学。牛顿力学在研究天体运动时,常常将行星视为一个质点,忽略其形状、大小、地表构成等细节。这样做当然会损失很多信息,但却能抓住引力作用这个核心规律,从而推导出普适的运动定律。复杂性理论忽略多项式差异,也是为了抓住问题的“计算引力”——其内在的、不可避免的复杂度。
💭 [直观想象]
想象一下绘制世界地图。
- 关注多项式差异:就像一个城市规划师,非常关心一条街道是20米宽还是30米宽,一座桥是双向四车道还是六车道。这些细节在城市规划中至关重要。
- 忽略多项式差异:就像一个地理学家,在绘制世界地图时,他只把纽约、伦敦、北京等城市标记为一个点。他关心的是这些城市之间的相对位置、连接关系以及它们所属的大陆板块。他忽略了城市内部的街道细节,因为他的目标是描绘全球的宏观结构。
📜 [原文7]
你可能会觉得忽略运行时间上的多项式差异是荒谬的。真正的程序员当然关心这些差异,并且努力工作只是为了让他们的程序运行速度快两倍。然而,我们早在引入渐近表示法时就忽略了常数因子。现在我们建议忽略更大的多项式差异,例如时间 $n$ 和时间 $n^{3}$ 之间的差异。
📖 [逐步解释]
这段话正面回应了读者(尤其是具有编程实践经验的读者)可能产生的质疑。
- 承认实践与理论的矛盾:作者承认,从一个“真正的程序员”或实践者的角度来看,忽略 $n$ 和 $n^3$ 之间的差异是“荒谬的”。在实际项目中,将一个 $O(n^3)$ 的算法优化到 $O(n \log n)$ 是巨大的成功,可能会让一个原本不可用的系统变得流畅。将程序速度提升两倍(即改变常数因子)也是工程师们日常追求的目标。
- 类比与递进:作者通过类比来为这种忽略进行辩护。他提醒读者,我们之前在学习算法基础时,就已经接受了渐近表示法(大O表示法)。当时,我们就学会了忽略常数因子(例如,认为 $2n$ 和 $100n$ 都是 $O(n)$)和低阶项(例如,认为 $n^2 + 100n$ 是 $O(n^2)$)。
- 提出更高层次的抽象:现在,作者建议我们把这种抽象再推进一步。之前我们忽略了 $n$ 和 $2n$ 的差异,现在我们建议忽略 $n$ 和 $n^3$ 的差异。这本质上是同一思想在不同层次上的应用:为了抓住更宏观的规律,我们需要忽略更细微的差别。
📝 [总结]
本段通过承认理论与实践的视角差异,并利用读者已有的“大O表示法”知识进行类比,试图说服读者接受“忽略多项式差异”这一更高层次的抽象,将其视为一种自然而然的理论递进。
🎯 [存在目的]
本段的目的是打消读者的疑虑,弥合理论抽象与工程实践之间的认知鸿沟,为引入P类的正式定义扫清思想障碍。
🧠 [直觉心智模型]
这就像学习生物学。
- 常数因子:研究两只猫,一只跑得快一点,一只慢一点。
- 多项式差异:研究猫和猎豹,它们都属于猫科动物,奔跑速度差异显著,但身体基本构造(四肢、脊椎等)是相似的。
- 指数差异:研究猫和蜗牛。它们的移动方式有着本质的不同。
- 复杂性理论关注的是“猫科动物 vs. 软体动物”这个层次的差异,而不是“这只猫 vs. 那只猫”或者“猫 vs. 猎豹”的差异。
💭 [直观想象]
想象不同级别的财富。
- 常数因子:你有100万,他有200万。你们都是“百万富翁”。
- 多项式差异:你有100万,他有10亿($1000 \times 100$万)。你们的财富差异巨大,但都还在可以用数字衡量的范围内。
- 指数差异:你有100万,而“他”拥有无穷的财富。这是一种本质上的、无法比较的差异。
- 我们首先学会了不区分100万和200万(都是百万富翁)。现在我们被要求,为了某种更宏大的分类,暂时不区分百万富翁和亿万富翁,把他们都归为“有限的富人”,以区别于那个拥有“无限财富”的存在。
📜 [原文8]
我们忽略多项式差异的决定并不意味着我们认为这些差异不重要。相反,我们确实认为时间 $n$ 和时间 $n^{3}$ 之间的差异是一个重要的差异。但是有些问题,例如因数分解问题的多项式性或非多项式性,不依赖于多项式差异,也同样重要。我们只是选择在这里专注于这类问题。为了看清森林而忽略树木,并不意味着一个比另一个更重要——它只是提供了一个不同的视角。
📖 [逐步解释]
这段是前一段的补充和升华,进一步阐明了理论视角的重要性。
- 再次肯定实践的重要性:作者强调,理论家们并非不食人间烟火。他们完全承认 $n$ 和 $n^3$ 的差异在实践中是“重要的”。
- 提出另一类同样重要的问题:但是,存在着另一类同样重要,甚至更为根本的问题。这些问题的答案不依赖于多项式的具体次数,而只依赖于“是不是多项式”。例如,“大数分解问题到底有没有一个多项式时间的解法?” 这个问题,如果答案是“有”,那么将对密码学等领域产生颠覆性的影响,无论那个多项式解法是 $n^2$ 还是 $n^{100}$。从“无”到“有”的突破,其意义远大于从 $n^3$ 到 $n^2$ 的优化。
- 森林与树木的比喻:这是一个非常经典的比喻。
- 树木 (Trees):指代算法的具体性能、多项式的次数、常数因子等细节。程序员和算法工程师通常专注于“树木”,力求让每一棵树都长得更高更壮。
- 森林 (Forest):指代问题的内在复杂性、大的复杂性类别(如P、NP)。理论计算机科学家专注于“森林”,试图描绘出整个计算世界的宏观版图。
- 结论:关注森林并不意味着树木不重要。两者都很重要,只是研究的视角 (perspective) 和层次 (level) 不同。
📝 [总结]
本段通过“森林与树木”的比喻,优雅地调和了理论与实践两种视角。它明确指出,复杂性理论选择关注“森林”,即问题的根本性质,这是一种不同的、但同样重要的科学追求。
🎯 [存在目的]
本段旨在赋予复杂性理论的研究动机一种崇高感和哲学深度,说明这门学科并非脱离实际,而是在探索更为根本的科学问题。
🧠 [直觉心智模型]
这就像医学研究。
- 树木:临床医生研究如何为某个病人更好地执行阑尾炎手术,比如采用微创技术减少疤痕和恢复时间。这是非常重要的实践问题。
- 森林:基础医学研究者在探索“癌症的成因是什么?”或者“人类为什么会衰老?”。这些问题的答案可能不会立刻转化为治疗手段,但它们是理解生命本质的关键,其长期影响可能更为深远。
💭 [直观想象]
想象一下探索一个新大陆。
- 树木:第一批登陆的拓荒者,他们关心的是如何搭建一个坚固的住所,如何找到水源,如何开垦一小块土地来种植。这些都是生存下去的细节。
- 森林:随后的探险家和地理学家,他们关心的是这个大陆的整体形状,山脉的走向,河流的分布,以及它在地球上的位置。他们绘制地图,为后续更大规模的探索和开发提供宏观指导。
11.3. P类的定义
📜 [原文9]
📖 [逐步解释]
这是一个引子,告诉读者,经过前面所有的铺垫和论证,我们终于要正式引入本节,乃至整个复杂性理论中最重要的核心定义之一了。
📜 [原文10]
定义 7.12
P 是指在确定型单带图灵机上可在多项式时间内判定的语言类。换句话说,
$$
\mathrm{P}=\bigcup_{k} \operatorname{TIME}\left(n^{k}\right) .
$$
📖 [逐步解释]
这是P类的正式定义。
- P的含义:P 代表 Polynomial time(多项式时间)。
- 核心内容:P类是一个语言的集合(a class of languages)。什么样的语言(问题)属于这个集合呢?标准是:存在一台确定型图灵机,它可以在多项式时间内判定这个语言。
- 判定 (decide):意味着图灵机对于任何输入,都能在有限时间内停机,并给出明确的“接受”或“拒绝”的答案。
- 多项式时间 (polynomial time):意味着图灵机的运行步数(时间)是输入长度 $n$ 的一个多项式函数,即 $O(n^k)$,其中 $k$ 是某个固定的常数。
- 确定型单带图灵机:定义中特意选用了最基础的计算模型——确定型单带图灵机。根据之前的论证,因为所有合理的确定型模型都是多项式等价的,所以选择哪个模型作为基准并不影响P类的最终构成。选择最简单的模型可以使定义更清晰、更基本。
- 数学化表达:
- $\operatorname{TIME}(t(n))$:这是一个复杂性类,表示所有能被确定型单带图灵机在 $O(t(n))$ 时间内判定的语言的集合。
- $\operatorname{TIME}(n^k)$:特指能在 $O(n^k)$ 时间内判定的语言集合。
- $\bigcup_{k} \operatorname{TIME}\left(n^{k}\right)$:这个表达式的意思是,把所有 $k \geq 0$ 的 $\operatorname{TIME}(n^k)$ 集合都取并集。也就是说,一个语言只要存在任何一个 $k$,使得它可以在 $O(n^k)$ 时间内被判定,那么它就属于P类。这包含了 $O(n)$, $O(n^2)$, $O(n^{100})$ 等所有多项式时间可判定的语言。
💡 [数值示例]
- 排序问题:给定一个包含 $n$ 个整数的列表,将其从小到大排序。著名的排序算法如“归并排序”或“快速排序”的时间复杂度是 $O(n \log n)$。因为 $n \log n$ 的增长速度比 $n^2$ 慢,所以它肯定也是 $O(n^2)$。因此,排序问题属于 $\operatorname{TIME}(n^2)$。由于它属于某个 $\operatorname{TIME}(n^k)$(这里 $k=2$ 就足够了),所以排序问题属于P类。
- 查找问题:在一个排好序的含 $n$ 个元素的数组中查找一个元素。使用二分查找,时间复杂度是 $O(\log n)$。因为 $\log n$ 的增长比 $n$ 慢,所以它也是 $O(n)$。因此,这个问题属于 $\operatorname{TIME}(n)$,从而也属于P类。
⚠️ [易错点]
- P是语言的集合:要牢记P类是一个集合,它的成员是“语言”(或者说“判定问题”),而不是“算法”或“图灵机”。说“一个算法属于P”是错误的,正确的说法是“这个问题(语言)属于P,因为存在一个多项式时间的算法来判定它”。
- 存在性:只要存在一个多项式时间的算法,该问题就属于P。可能我们还不知道这个算法是什么,或者我们目前只知道一些指数时间的算法,但这不代表问题不属于P。
- 判定 vs. 识别:定义中用的是“判定”,要求图灵机对所有输入都必须停机。这排除了那些对于不属于语言的输入会无限循环的图灵机(图灵识别机)。
📝 [总结]
本段给出了P类的正式数学定义:它是在确定型计算模型上,所有能在多项式时间内解决的判定问题的集合。这个定义是计算复杂性理论的基石。
🎯 [存在目的]
本段的目的是提供一个精确的、无歧义的、数学化的定义,为后续所有关于P类的讨论建立一个共同的、坚实的基础。
[直觉心-智模型]
P类就像一个“好学生俱乐部”。
- 入会标准:你必须能在“合理的时间内”(多项式时间)完成任何交给你的作业(判定任何输入)。
- 俱乐部成员:是所有“好学生”(问题/语言),而不是他们完成作业的方法(算法)。
- 多项式时间:有的学生做得快($O(n)$),有的慢一点($O(n^3)$),但只要不是慢得离谱(指数时间),都能被接纳。
- 确定型:学生必须思路清晰,一步一步地做,不能靠“蒙”或“直觉”(非确定性)。
💭 [直观想象]
想象一个自动化工厂。
- 输入:一批原材料(输入字符串 $w$)。
- 任务:判断这批原材料是否合格($w$ 是否属于语言 $L$)。
- P类工厂:这个工厂的生产线(图灵机)设计得非常高效。无论原材料有多少(输入规模 $n$),都可以在一个可预测、合理的时间内(如 $n^2$ 分钟)给出“合格”或“不合格”的标签,然后停机。
- 非P类工厂:可能需要指数级的时间,或者对于不合格的原料,生产线会卡住死循环。
📜 [原文11]
P 类在我们的理论中扮演着核心角色,并且之所以重要,是因为
- P 对于所有与确定型单带图灵机多项式等价的计算模型来说都是不变的,并且
- P 大致对应于在计算机上实际可解决的问题类。
📖 [逐步解释]
这段话总结了P类之所以如此重要的两个核心原因。
- 理论上的稳健性 (Robustness):正如之前所论证的,“是否属于P类”这个属性不依赖于我们选择哪种合理的确定型计算模型。无论你用的是图灵机、RAM模型,还是C++、Java语言(的底层模型),一个问题是P问题,它就永远是P问题。这使得P类成为一个非常坚实的、适合进行理论研究的对象。它具有模型无关性。
- 实践上的相关性 (Relevance):P类粗略地划定了在现实世界中,哪些问题是“实际可计算的”或“易解的”(tractable)。虽然一个 $O(n^{100})$ 的算法在实践中并不可行,但将P类作为“易解”的门槛,在历史上被证明是一个非常有用的指导原则。
📝 [总结]
P类的重要性体现在两个方面:它在理论上是稳健的、普适的,同时在实践上又与“可行性”紧密相关。它完美地连接了抽象的计算理论和具体的计算实践。
🎯 [存在目的]
本段的目的是向读者强调P类这个概念的价值,解释为什么我们要花这么多时间来定义和研究它。
[直-觉心智模型]
P类就像物理学中的“能量守恒定律”。
- 理论稳健性:无论是在力学、电磁学还是热力学中,能量守恒定律都成立。它的普适性使其成为物理学的一块基石。
- 实践相关性:工程师可以利用能量守恒定律来设计高效的发动机、发电站等。它为工程实践提供了根本的指导和约束。
💭 [直观想象]
P类就像食谱中的“家常菜”。
- 理论稳健性(模型无关性):一道家常菜(比如番茄炒蛋),无论你用的是电磁炉、燃气灶还是柴火灶(不同的计算模型),只要你按照菜谱(算法)来做,都能做出来。可能火候、时间略有不同(多项式次数不同),但这道菜“可烹饪”的属性是不变的。
- 实践相关性(实际可解决):家常菜通常都可以在一个合理的时间内(比如半小时)完成,适合日常烹饪。与之相对的是一些需要烹饪几十个小时甚至好几天的“国宴大菜”(指数时间问题),它们在日常生活中就是“不实际的”。
📜 [原文12]
当一个问题属于 P 时,我们有一种在 $n^{k}$ 时间内解决它的方法,其中 $k$ 是某个常数。这种运行时间是否实用取决于 $k$ 和应用程序。当然,$n^{100}$ 的运行时间不太可能具有任何实际用途。然而,将多项式时间称为实际可解决性的门槛已被证明是有用的。一旦为一个以前看似需要指数时间的问题找到了多项式时间算法,就意味着对它有了关键的洞察,并且通常会随后进一步降低其复杂度,常常达到实际有用的程度。
📖 [逐步解释]
这段话是对“P类 ≈ 实际可解”这个说法的进一步阐释和辩护。
- 承认P类算法的实用性差异:作者承认,并非所有P类算法都是实用的。一个算法的运行时间是 $O(n^k)$,它的实用性高度依赖于指数 $k$ 的大小以及常数因子。$O(n^2)$ 通常是实用的,而 $O(n^{100})$ 几乎肯定是不实用的。
- P类作为“门槛”的价值:尽管存在上述差异,但将“多项式时间”作为“实际可解性”的门槛或分水岭,依然是非常有用的。这个门槛的意义不在于精确描述每一个算法的可用性,而在于提供一个分类标准。
- 从“无”到“有”的突破:本段最重要的论点是,为一个问题找到第一个多项式时间算法,是一个重大的理论突破。
- 关键洞察:这通常标志着我们对该问题的内在结构有了质的飞跃的理解,找到了避免暴力搜索的“捷径”。
- 后续优化:历史经验表明,一旦第一个(可能很慢的,如 $O(n^{10})$)多项式时间算法被发现,研究者们往往能在此基础上进行改进和优化,最终得到一个在实践中真正有用的(如 $O(n^3)$ 或 $O(n^2)$)算法。从指数到多项式的跨越,是最困难也是最关键的一步。
💡 [数值示例]
- 线性规划问题:在1979年之前,解决线性规划问题最著名的算法是“单纯形法”,它在绝大多数情况下表现优异,但其最坏情况下的时间复杂度是指数级的。学术界一直在寻找一个严格的多项式时间算法。
- 突破:1979年,苏联数学家 Leonid Khachiyan 提出了“椭球算法”,证明了线性规划问题属于P类。然而,这个算法的复杂度非常高,实践中比单纯形法慢得多。
- 后续优化:椭球算法的发现激发了新的研究。1984年,Narendra Karmarkar 提出了“内点法”,这是另一个多项式时间算法,并且在实践中对于某些大规模问题比单纯形法更有效。
- 这个例子完美地诠释了本段的观点:从指数到多项式(椭球算法)的突破是关键的理论洞察,它打开了一扇大门,最终引向了真正实用的新算法(内点法)。
📝 [总结]
本段的核心思想是,P类作为“易解问题”的标志,其真正的威力在于它代表了从指数复杂度的“黑暗时代”到多项式复杂度的“光明时代”的质的飞跃。第一个多项式时间算法的发现,就像是点燃了希望的火种,后续的优化工作会使其最终燎原。
🎯 [存在目的]
本段旨在通过阐述“从0到1”的突破性意义,来回应“高次多项式不实用”的批评,从而捍卫将P类定义为“易解问题”的合理性和远见。
🧠 [直觉心智模型]
这就像人类学习飞行。
- 指数时间:早期人们只能通过模仿鸟类扇动翅膀的方式尝试飞行,这注定会失败,无论你造的翅膀有多精巧(暴力搜索)。
- 第一个多项式时间算法(高次):莱特兄弟第一次成功试飞。他们的飞机非常简陋,只能飞几十秒,离地几米高($O(n^{100})$),几乎没有任何实用价值。但这是一个划时代的突破,因为它证明了“人造机械可以飞行”这件事是可能的。它基于空气动力学原理,是一种全新的“关键洞察”。
- 后续优化(低次):在莱特兄弟的基础上,工程师们不断改进发动机、机翼设计、材料等,最终发展出了现代客机($O(n^2)$),可以安全、高效地进行洲际旅行。
💭 [直观想象]
想象从石器时代到铁器时代。
- 指数时间:用石头去磨一块铁矿石,想把它磨成一把剑。这几乎是不可能完成的任务。
- 第一个多项式时间算法(高次):有人偶然发现了“冶炼”技术,即在高温下可以从矿石中提取出铁。最初的冶炼技术可能效率极低,耗费大量燃料,只能得到一小块劣质的铁。这不实用,但它证明了“可以从石头中炼出铁”。
- 后续优化(低次):人们不断改进炉子、鼓风技术、除杂工艺,最终发展出成熟的炼钢技术,可以大规模生产优质的钢铁。
1.2. P 中问题的例子
12.1. 算法分析的约定
📜 [原文13]
当我们提出一个多项式时间算法时,我们给出它的高级描述,不涉及特定计算模型的特性。这样做可以避免磁带和磁头移动的繁琐细节。我们在描述算法时遵循某些约定,以便能够分析它的多项式性。
📖 [逐步解释]
这段话是在说明,为了证明一个问题属于P类,我们描述其算法时所采用的抽象层次。
- 高级描述 (High-level description):我们不会用图灵机的语言(状态转移、磁带操作)来描述算法。那种描述太底层、太繁琐了,会掩盖算法的核心思想。相反,我们会使用一种类似于伪代码的、人类易于理解的语言。
- 模型无关:这种高级描述是独立于具体计算模型的。它不关心底层是单带图灵机还是现代计算机,从而使得我们的论证更具通用性。
- 遵循约定:虽然是高级描述,但它并不是随意的。它必须遵循一定的规范,使得我们能够清晰地分析其时间复杂度,并确信它能在多项式时间内完成。接下来的段落会详细说明这些约定。
📝 [总结]
本段设定了后续算法描述和分析的风格:采用高级、模型无关但又足够严谨的伪代码形式,以便于进行多项式时间复杂度的分析。
🎯 [存在目的]
本段旨在告诉读者,接下来的算法证明将是易于理解的,我们会聚焦于算法的逻辑,而不是陷入图灵机的实现细节中。
🧠 [直觉心智模型]
这就像写菜谱。一个好的菜谱会告诉你“将牛肉切成薄片,用酱油腌制15分钟”,而不会告诉你“用你的左手拿起刀,以30度角切下,每片厚度控制在2毫米...”。前者是高级描述,抓住了核心步骤;后者是底层细节,过于繁琐。我们用写菜谱的方式来写算法。
💭 [直观想象]
这就像给别人指路。
- 高级描述:“沿着这条路直走,在第三个红绿灯左转,看到麦当劳就到了。”
- 底层描述:“左脚向前迈出60厘米,然后右脚向前迈出60厘米,重复此动作1000次...”。
- 显然,高级描述更清晰、更有用,并且足以让我们估算出大概需要的时间(比如“大概要走15分钟”)。
📜 [原文14]
我们继续用带编号的阶段来描述算法。现在我们必须对实现每个阶段所需的图灵机步骤数量以及算法使用的总阶段数敏感。
📖 [逐步解释]
这里具体说明了描述算法的约定之一:分阶段描述。
- 带编号的阶段 (Numbered stages):算法将被分解为一系列清晰的、按顺序执行的步骤或阶段。
- 对时间敏感:在进行复杂性分析时,我们必须关注两件事:
- 总阶段数:算法总共执行了多少个阶段?这个总数必须是输入规模 $n$ 的一个多项式。
- 每个阶段的成本:单个阶段的操作,如果用图灵机来实现,需要多少步骤?每个阶段自身也必须能在多项式时间内完成。
- 多项式的复合:如果一个算法包含多项式数量的阶段,并且每个阶段都能在多项式时间内完成,那么这个算法的总运行时间也是多项式的。因为多项式的多项式(比如 $(n^2) \cdot (n^3) = n^5$)仍然是一个多项式。
💡 [数值示例]
- 假设一个算法有以下结构:
- FOR i = 1 to n: (这个循环执行 $n$ 次,阶段数为 $O(n)$)
- ... 一些操作 ... (假设这些操作需要 $O(n^2)$ 时间)
- 分析:
- 总阶段数(循环次数):$O(n)$,是多项式。
- 每个阶段的成本:$O(n^2)$,是多项式。
- 总时间复杂度:$O(n) \times O(n^2) = O(n^3)$,仍然是多项式。
- 结论:这个算法是一个多项式时间算法。
📝 [总结]
本段给出了分析高级描述算法复杂度的核心方法:将总时间分解为“阶段数量”和“每阶段时间”,并分别证明这两者都是多项式的。
🎯 [存在目的]
本段的目的是提供一个清晰、可操作的框架,用于分析后续将要介绍的算法的复杂度,以证明它们属于P类。
[直觉心-智模型]
这就像估算一个建筑工程的总工期。
- 总阶段数:工程分为几个阶段?比如“打地基”、“盖主体结构”、“内部装修”。
- 每个阶段的成本:每个阶段需要多少天?比如打地基30天,盖主体100天,装修60天。
- 总工期:就是所有阶段时间的总和。我们要确保阶段数量和每个阶段的工期都在一个“合理”的范围内。
💭 [直观想象]
这就像分析做一顿饭的时间。
- 阶段:1. 洗菜切菜; 2. 炒菜; 3. 煮饭。
- 时间分析:
- 阶段1需要15分钟。
- 阶段2需要10分钟。
- 阶段3需要20分钟。
- 总共需要 15+10+20=45分钟。
- 如果菜的数量是 $n$,可能洗菜切菜时间是 $O(n)$,炒菜时间也是 $O(n)$,总时间就是 $O(n)$,是一个多项式。
📜 [原文15]
当我们分析一个算法以证明它在多项式时间内运行时,我们需要做两件事。首先,我们必须给出当算法在长度为 $n$ 的输入上运行时所使用的阶段数的多项式上界(通常用大 $O$ 符号表示)。然后,我们必须检查算法描述中的各个阶段,以确保每个阶段都可以在合理的确定型模型上以多项式时间实现。我们在描述算法时选择阶段,以便使这第二部分分析易于进行。当这两项任务都完成后,我们就可以得出结论,该算法在多项式时间内运行,因为我们已经证明它运行了多项式数量的阶段,每个阶段都可以在多项式时间内完成,并且多项式的复合仍然是多项式。
📖 [逐步解释]
这段是上文的进一步展开和强调,清晰地列出了证明一个算法是多项式时间算法的两步证明法。
- 第一步:分析阶段数量:证明算法执行的总步骤数或总阶段数有一个多项式上界。也就是说,总阶段数是 $O(n^k)$,其中 $n$ 是输入长度,$k$ 是常数。这通常通过分析算法中的循环结构来完成。
- 第二步:分析单个阶段:要确保我们描述的每个阶段都不是“魔法”。我们必须有信心,每一个高级描述的阶段(如“扫描所有边”、“标记节点”)都可以在一个标准的计算模型(如图灵机)上,用多项式数量的基本操作来实现。作者提到,在设计高级描述时,就会刻意地将阶段划分成这种“易于分析”的模块。
- 结论:一旦这两步都完成,就可以得出结论:算法整体是多项式时间的。理由是“多项式的复合仍然是多项式”(A polynomial of a polynomial is a polynomial)。
📝 [总结]
本段明确给出了一个模板化的证明策略,用于判断一个高级描述的算法是否属于P类。这个策略将复杂的分析任务分解为两个更简单、更清晰的子任务。
🎯 [存在目的]
本段的目的是为读者提供一个清晰的“工具”或“检查清单”,以便能够跟随并理解后续的证明过程。
[直觉心-智模型]
这就像审计一家公司的年度项目报告。
- 第一步:检查项目数量:公司今年做了多少个项目?如果项目数量本身多得不合理(指数级),那肯定有问题。项目数量必须是“可控的”(多项式)。
- 第二步:抽查单个项目:随机抽取几个项目,深入检查其账目和执行过程,确保每个项目本身都是合规、合理的,没有造假或无法实现的内容。
- 结论:如果项目数量合理,且单个项目也都合理,那么整个公司的年度项目报告就是可信的。
💭 [直观想象]
这就像规划一次长途火车旅行。
- 第一步:看换乘次数:从北京到拉萨,需要换乘几次?如果需要换乘成百上千次,这个路线规划就不合理。换乘次数必须是有限且合理的。
- 第二步:看每段旅程:每一段火车的行驶时间是多久?如果其中有一段需要开几十年,那也是不行的。每段旅程的时间都必须是合理的。
- 结论:如果换乘次数少,且每段旅程时间都合理,那么整个旅行计划就是可行的。
📜 [原文16]
需要注意的一点是问题所使用的编码方法。我们继续使用尖括号表示法 $\langle\cdot\rangle$ 来表示一个或多个对象到字符串的合理编码,而不指定任何特定的编码方法。现在,一种合理的方法是允许以多项式时间将对象编码和解码为自然的内部表示或其他合理的编码。熟悉的图、自动机等编码方法都是合理的。但请注意,用于编码数字的一元表示法(如数字 17 被一元字符串 111111111111111111 编码)不合理,因为它比真正的合理编码(如任何 $k \geq 2$ 的 $k$ 进制表示法)指数级地大。
📖 [逐步解释]
这段话指出了分析复杂度时一个非常重要但又容易被忽略的细节:输入的编码方式。
- 编码 (Encoding):图灵机处理的是字符串。任何复杂的数学对象(如图、数字、公式)都必须首先被“翻译”成一个字符串,才能作为图灵机的输入。这个翻译过程就是编码。我们用 $\langle O \rangle$ 表示对象 $O$ 的编码字符串。
- 合理编码 (Reasonable encoding):一个编码方式是“合理的”,有两个要求:
- 简洁性:编码后的字符串长度不应该比表示该对象所需信息的本质长度大太多。具体来说,它应该与我们通常使用的表示法(如二进制、十进制)在长度上是多项式关系的。
- 编解码效率:从对象到字符串的编码过程,以及从字符串到对象的解码过程,都应该能在多项式时间内完成。
- 不合理的编码例子:一元表示法 (Unary):
- 用 $n$ 个 "1" 来表示数字 $n$。例如,数字 17 表示为 11111111111111111。
- 问题所在:一个数字 $N$ 的值是其二进制表示长度 $n$ 的指数函数 ($N \approx 2^n$)。而一元表示法的长度就是 $N$ 本身。所以,一元表示的长度是二进制表示长度的指数级。
- 为什么不合理:如果使用一元编码,一个本来需要指数时间才能解决的问题(相对于二进制编码),可能会“伪装”成一个多项式时间问题。例如,测试一个数 $N$ 是否为素数的暴力算法需要 $O(\sqrt{N})$ 时间。如果输入是二进制编码,长度为 $n \approx \log N$,那么复杂度是 $O(\sqrt{2^n}) = O(2^{n/2})$,是指数级的。但如果输入是一元编码,长度为 $m=N$,那么复杂度是 $O(\sqrt{m})$,这变成了多项式级!这是通过“作弊”——即极大地拉长输入——来实现的,它没有改变问题的根本难度。因此,一元编码被认为是不合理的。
- 合理的编码例子:二进制、十进制、邻接矩阵/邻接表(用于图)等都是合理的编码。
💡 [数值示例]
- 编码数字 1,000,000
- 二进制表示 (合理): $10^6 \approx 2^{20}$。其二进制长度大约是 20。
- 一元表示 (不合理): 需要写 1,000,000 个 '1'。
- 比较:一元表示的长度 (一百万) 是二进制表示长度 (20) 的指数级倍数。
⚠️ [易错点]
- 复杂度是关于输入长度的函数:始终要记住,时间复杂度 $T(n)$ 中的 $n$ 是输入字符串的长度,而不是输入的数值大小或者图的节点数。选择不同的编码方式会改变 $n$,从而可能改变复杂度的类别。
- 图的表示:对于图算法,我们通常分析其复杂度是关于节点数 $V$ 和边数 $E$ 的函数。这是可以接受的,因为标准的图编码方式(邻接矩阵或邻接表)的长度 $n$ 总是 $V$ 和 $E$ 的多项式函数。例如,邻接矩阵大小为 $V^2$,邻接表大小为 $V+E$。所以,一个关于 $V, E$ 的多项式时间算法,也必然是关于输入长度 $n$ 的多项式时间算法。
📝 [总结]
本段强调了在讨论复杂性时,必须假设采用“合理”的编码方式。合理的编码应保证编码后的字符串长度与对象信息的内在规模成多项式关系,以避免通过拉长输入来人为地降低算法复杂度。
🎯 [存在目的]
本段的目的是堵上一个理论上的漏洞。通过明确排除像一元编码这样的“作弊”手段,确保我们对问题难度的分析是公平和有意义的。
🧠 [直觉心智模型]
这就像衡量一个学生的阅读速度。
- 合理编码:给他一本正常印刷的书。我们测量他读完这本书需要的时间,然后用“字/分钟”来评估他的速度。
- 不合理编码:给他一本特殊的“书”,每个字都印在一张单独的A4纸上。一万字的书就变成了一万页。然后我们测量他“翻完”这本书的时间。他可能翻得很快,但我们能说他的阅读速度很快吗?不能。我们只是通过一种极度冗余的“编码”方式,让他看起来在单位“页数”内花的时间很少。
[直-观想象]
这就像测量汽车的油耗。
- 合理编码:用“升/百公里”来衡量。这是一个公认的标准。
- 不合理编码:为了让油耗数据好看,你说你的车“每毫米只消耗0.0001升油”。这个数字看起来很小,但因为你把距离单位搞得非常小,所以这个数据毫无意义,无法与其他车辆进行公平比较。一元编码就是这种把“测量单位”搞得特别小的把戏。
📜 [原文17]
本章中遇到的许多计算问题都包含图的编码。图的一种合理编码是其节点和边的列表。另一种是邻接矩阵,其中 $(i, j)$ 项为 1 表示从节点 $i$ 到节点 $j$ 有一条边,为 0 则表示没有。当我们分析图上的算法时,运行时间可以根据节点数而不是图表示的大小来计算。在合理的图表示中,表示的大小是节点数的多项式。因此,如果我们分析一个算法并表明其运行时间是节点数的多项式(或指数),我们就知道它是输入大小的多项式(或指数)。
📖 [逐步解释]
这段话将上一段关于编码的讨论,具体应用到了图问题上。
- 图的合理编码:
- 邻接表 (Adjacency list):一个数组,每个数组元素对应一个节点,存放一个链表,链表里是所有与该节点相连的节点。
- 邻接矩阵 (Adjacency matrix):一个二维方阵,矩阵的第 $(i, j)$ 个元素表示节点 $i$ 和节点 $j$ 之间是否有边。
- 这两种都是合理的,因为它们的编码大小(空间复杂度)与图的节点数 $V$ 和边数 $E$ 都是多项式关系。邻接表的空间是 $O(V+E)$,邻接矩阵是 $O(V^2)$。
- 分析图算法的惯例:在分析图算法时,我们通常不直接用输入字符串长度 $n$ 作为变量,而是使用更有意义的图参数:节点数 $V$ (有时用 $m$ 或 $n$) 和 边数 $E$。
- 惯例的合理性:这样做是允许的,因为输入编码的长度 $n$ 与 $V$ 和 $E$ 是多项式相关的。例如,对于邻接矩阵编码, $n = O(V^2)$。如果一个算法的时间复杂度是 $V$ 和 $E$ 的多项式,比如 $O(V^3 + E \log V)$,那么它也必然是 $n$ 的多项式。
- $O(V^3) = O((\sqrt{n})^3) = O(n^{1.5})$,这是 $n$ 的多项式。
- $O(E \log V) = O(V^2 \log V) = O(n \log \sqrt{n}) = O(n \log n)$,这也是 $n$ 的多项式。
- 因此,整个算法复杂度是输入长度 $n$ 的多项式。
- 结论:所以,为了方便,我们可以直接分析算法关于 $V$ 和 $E$ 的复杂度。只要它是 $V$ 和 $E$ 的多项式,我们就可以断定该问题属于P类。
📝 [总结]
本段为分析图算法的复杂度提供了一个便利的“捷径”:我们可以直接使用节点数 $V$ 和边数 $E$ 作为参数来分析,而不用时刻想着如何转换回输入字符串的长度 $n$,因为它们之间的关系是多项式的。
🎯 [存在目的]
本段旨在简化后续图算法的分析过程,让读者和作者都可以使用更自然、更直观的图论参数($V$ 和 $E$)来进行讨论。
🧠 [直觉心智模型]
这就像分析一个公司的运营效率。
- 输入长度 $n$:公司的全部文档、邮件、会议记录的总大小。
- $V$ 和 $E$:公司的员工数量和部门之间的协作关系数量。
- 我们通常会说“这个决策流程需要通过5个部门($V$),涉及到10个协作环节($E$)”,而不会说“这个决策流程需要处理掉3GB的文档($n$)”。使用员工作为分析单位更直观,并且我们知道,文档总量和员工数量肯定是正相关的(多项式关系),所以这种分析是有效的。
💭 [直观想象]
这就像估算粉刷一个房子的时间。
- 输入长度 $n$:购买的所有油漆、刷子、梯子等物品的清单字符串长度。
- $V$ 和 $E$ (类比):房子的房间数和墙壁面积。
- 我们肯定会根据“有多少个房间、多大面积”来估算工时,而不会根据购物清单的长度来估算。这是因为墙壁面积和购物清单长度是相关的,但前者是更直接、更自然的度量。
12.2. PATH 问题
📜 [原文18]
第一个问题涉及有向图。有向图 $G$ 包含节点 $s$ 和 $t$,如下图所示。PATH 问题是确定是否存在从 $s$ 到 $t$ 的有向路径。令
PATH $=\{\langle G, s, t\rangle \mid G$ 是一个有从 $s$ 到 $t$ 的有向路径的有向图$\}$。

图 7.13
PATH 问题:存在从 $s$ 到 $t$ 的路径吗?
📖 [逐步解释]
这里正式引入了第一个要证明属于P类的问题:PATH 问题。
- 问题定义:
- 输入:一个三元组 $\langle G, s, t \rangle$。
- $G$:一个有向图 (directed graph)。
- $s$:图 $G$ 中的一个节点,称为起始节点 (start node)。
- $t$:图 $G$ 中的另一个节点,称为目标节点 (target node)。
- 问题:判断在图 $G$ 中,是否存在一条从节点 $s$ 出发,沿着边的方向,最终能到达节点 $t$ 的路径 (path)。
- 语言形式化定义:
- PATH 是一个语言(问题的集合)。
- 一个字符串属于 PATH 语言,当且仅当这个字符串是 $\langle G, s, t \rangle$ 的一个“合理编码”,并且在图 $G$ 中确实存在一条从 $s$到 $t$ 的有向路径。
- 图示:图中展示了一个例子。$s$ 是起始点,$t$ 是目标点。我们可以直观地看到,存在路径 $s \rightarrow a \rightarrow b \rightarrow t$。因此,这个图所对应的编码字符串 $\langle G, s, t \rangle$ 就属于 PATH 语言。
📝 [总结]
本段定义了PATH问题,也叫有向图可达性问题 (Directed Graph Reachability),并将其形式化为一个语言。这是图论中最基本的问题之一。
🎯 [存在目的]
本段旨在提出一个具体的、经典的计算问题,并以它为例来完整地演示如何证明一个问题属于P类。
🧠 [直觉心智模型]
PATH 问题就像是在一个单行道的城市交通网络中,问你“从你家($s$)开车能不能到公司($t$)?”。你需要看地图,沿着单行道的方向,看是否存在一条通路。
💭 [直观想象]
- 图 G:一张地铁线路图,箭头表示列车的运行方向。
- 节点 s:你所在的地铁站,比如“人民广场站”。
- 节点 t:你想去的地铁站,比如“世纪公园站”。
- PATH 问题:只坐地铁(不能出站换公交),能不能从“人民广场站”到“世纪公园站”?
📜 [原文19]
定理 7.14
PATH $\in \mathrm{P}$。
📖 [逐步解释]
这是一个定理的声明,它的意思是“PATH问题是P类问题”,即存在一个多项式时间的确定型算法来解决PATH问题。接下来的证明部分将构造出这样一个算法并分析其复杂度。
📜 [原文20]
证明思路 我们通过提供一个判定 PATH 的多项式时间算法来证明这个定理。在描述该算法之前,我们先观察一下,解决这个问题的暴力算法不够快。
PATH 的暴力算法通过检查 $G$ 中所有可能的路径来确定是否存在任何从 $s$ 到 $t$ 的有向路径。可能的路径是 $G$ 中长度最多为 $m$ 的节点序列,其中 $m$ 是 $G$ 中的节点数。(如果存在任何从 $s$ 到 $t$ 的有向路径,那么存在一条长度最多为 $m$ 的路径,因为重复节点从来不是必需的。)但是,这类可能的路径的数量大约是 $m^{m}$,这相对于 $G$ 中的节点数是指数级的。因此,这种暴力算法使用指数时间。
📖 [逐步解释]
这段话是正式证明前的“思路分析”,它首先否定了一个直观但低效的方法——暴力搜索。
- 暴力算法思路:
- 枚举出图中所有可能的路径。
- 检查这些路径中,有没有一条是从 $s$ 开始,到 $t$ 结束的。
- 路径长度的界定:如果存在一条 $s \to t$ 的路径,那么一定存在一条不含重复节点的简单路径。在含有 $m$ 个节点的图中,简单路径的长度最多是 $m-1$(即经过 $m$ 个不同的节点)。所以我们只需要检查长度不超过 $m$ 的路径。
- 暴力算法的复杂度分析:
- 一条长度为 $k$ 的路径是一个节点序列 $(v_0, v_1, ..., v_k)$。
- 从 $s$ 出发,第一步可以走到 $s$ 的邻居节点,假设有 $d_s$ 个选择。第二步又可以从这些邻居节点出发,又有若干选择...
- 粗略地估计,从 $s$ 出发的长度为 $k$ 的路径数量可能是 $d^{k-1}$(其中 $d$ 是节点的平均出度)。如果考虑所有长度到 $m$ 的路径,总数会非常巨大。
- 作者给出了一个更粗略但更具说明性的上界:$m^m$。想象一条路径是长度为 $m$ 的节点序列,每个位置都可以是 $m$ 个节点中的任意一个,总共就有 $m^m$ 种可能性。这是一个非常宽松的上界,但足以说明路径总数是指数级的。
- 结论:暴力搜索路径的方法是一个指数时间算法,它不够快,不能用来证明 PATH $\in \mathrm{P}$。
💡 [数值示例]
- 假设一个图有10个节点 ($m=10$)。
- 暴力搜索需要考虑所有可能的路径。路径数量的上界是 $10^{10}$,这是一个非常大的数字。如果图是全连接的(任意两点都有边),那么从s出发长度为9的路径就有 $9!$ 种,也是一个巨大的数字。
- 这个例子说明,即使对于一个小图,暴力搜索也是不可行的。
⚠️ [易错点]
- 简单路径:这里的关键洞察是,如果一条路径中有环(重复了节点),我们可以把环去掉,得到一条更短的路径。例如路径 $s \to a \to b \to c \to b \to t$,我们可以简化为 $s \to a \to b \to t$。这意味着我们只需要关心简单路径,其长度不会超过节点总数 $m$。这个观察虽然帮助限定了搜索范围,但可能路径的数量仍然是指数级的。
📝 [总结]
本段排除了一个错误的、低效的算法思路(暴力枚举所有路径),并解释了为什么它是指数时间的。这为引出真正高效的多项式时间算法做了铺垫。
🎯 [存在目的]
本段的目的是通过展示一个“坏”的例子,来凸显接下来要介绍的“好”的算法的巧妙之处。它告诉我们,不能用最无脑的方法,需要更聪明的策略。
[直觉心-智模型]
这就像在迷宫里找出口。
- 暴力算法:在每个岔路口,都复制一个你自己,让每个“你”去探索一条路。很快,就会有指数级数量的“你”在迷宫里乱撞。
- 高效算法:你拿着一罐油漆。每走过一个路口,就在地上做一个标记。这样你就不会重复走同样的路,也不会在同一个地方绕圈子。
💭 [直观想象]
这就像寻找你的祖先。
- 暴力算法:你试图列出你所有可能的第 $k$ 代祖先。你有2个父母,4个祖父母,8个曾祖父母... 第 $k$ 代有 $2^k$ 个祖先。这是一个指数增长。
- 而PATH问题更像是:已知某个人是你祖先,判断另一个人是不是他的后代。
📜 [原文21]
为了获得 PATH 的多项式时间算法,我们必须做一些避免暴力搜索的事情。一种方法是使用图搜索方法,例如广度优先搜索。在这里,我们依次标记 $G$ 中所有可以从 $s$ 通过长度为 1、然后 2、然后 3 直到 $m$ 的有向路径到达的节点。将这种策略的运行时间限制在多项式时间内是容易的。
📖 [逐步解释]
这里提出了解决PATH问题的高效算法的核心思想:图搜索。
- 核心策略:不再枚举路径,而是枚举节点。我们关心的是“哪些节点可以从 $s$ 到达”,而不是“从 $s$ 出发有哪些路径”。这是一个关键的视角转换。
- 标记法:算法的核心机制是“标记” (mark)。我们用一个标记来记录所有已经被确认“可以从 $s$ 到达”的节点。
- 逐步扩展:
- 初始时,只有 $s$ 本身是可达的,所以标记 $s$。
- 然后,所有与 $s$ 直接相连的节点(路径长度为1)都变成可达的,标记它们。
- 然后,所有与这些新标记的节点直接相连的节点(路径长度为2)也变成可达的,标记它们。
- ... 这个过程不断重复,就像在水面上投下一颗石子,波纹(被标记的节点)一圈一圈地扩散开来。
- 算法思想来源:这个思想就是经典的广度优先搜索 (Breadth-First Search, BFS) 或 深度优先搜索 (Depth-First Search, DFS)。BFS是按路径长度(距离 $s$ 的远近)逐层标记,DFS是沿着一条路径走到黑再返回。两者都可以解决可达性问题。文中描述的更接近BFS的思想。
- 多项式时间:作者断言,这种策略的运行时间很容易被限制在多项式时间内。因为图中总共只有 $m$ 个节点,每次标记至少会增加一个新标记的节点,所以标记过程最多进行 $m$ 轮。每一轮需要检查图中的边,边的数量也是有限的。所以总时间是节点数和边数的多项式,而不是指数。
📝 [总结]
本段介绍了解决PATH问题的正确思路:使用图搜索算法(如BFS),通过逐步标记从起点 $s$ 可达的节点,来代替暴力枚举路径。这个思路的转变是
从指数时间到多项式时间的关键。
🎯 [存在目的]
本段旨在给出算法的核心思想,让读者在看到具体算法步骤之前,就能理解其背后的巧妙之处。
[直觉心-智模型]
这就像病毒传播。
- $s$ 是第一个感染者。
- 第一轮,他感染了所有和他接触的人(邻居节点)。
- 第二轮,这些新感染者又感染了他们接触的人。
- PATH问题就相当于问:“病毒最终能否传播到 $t$ ?”
- 这个传播过程,最多把图里的所有人都感染一遍就结束了,所花的时间和社交网络的大小(节点和边的数量)是相关的,而不是和可能的传播路径数量相关。
💭 [直观想象]
这就像“传话游戏”。
- $s$ 是第一个知道秘密的人。
- 他把秘密告诉他的朋友们(标记邻居)。
- 他的朋友们再告诉他们的朋友们。
- 整个过程就像一个连锁反应,秘密在人群中扩散。我们要看最后 $t$ 能不能听到这个秘密。
📜 [原文22]
证明 PATH 的多项式时间算法 $M$ 如下操作。
$M=$ “输入 $\langle G, s, t\rangle$,其中 $G$ 是一个带节点 $s$ 和 $t$ 的有向图:
- 在节点 $s$ 上放置一个标记。
- 重复以下步骤,直到没有额外的节点被标记:
- 扫描 $G$ 的所有边。如果发现一条边 $(a, b)$ 从一个已标记的节点 $a$ 到一个未标记的节点 $b$,则标记节点 $b$。
- 如果 $t$ 被标记,则接受。否则,拒绝。”
📖 [逐步解释]
这是PATH问题多项式时间算法 $M$ 的具体步骤描述。
- 输入:一个有向图 $G$ 和两个节点 $s, t$。
- 阶段 1 (初始化):将起始节点 $s$ 标记为“可达”。这是我们的出发点。
- 阶段 2 (主循环):这是一个重复执行的过程,是算法的核心。循环的终止条件是“在一整轮扫描后,没有任何新的节点被标记”,这意味着可达节点的集合已经稳定,无法再扩展了。
- 阶段 3 (扩展):在每一轮循环中,算法会遍历图中的所有边。对于每一条边 $(a, b)$,它会检查:
- 边的起点 $a$ 是否已经被标记?
- 边的终点 $b$ 是否尚未被标记?
- 如果同时满足这两个条件,就意味着我们找到了一个新的可达节点 $b$(因为我们可以从 $s$ 到达 $a$,再从 $a$ 走一步到 $b$)。于是,就将 $b$ 标记起来。
- 阶段 4 (结果判断):当循环结束后(即所有能从 $s$ 到达的节点都已被标记),检查目标节点 $t$ 是否在被标记的节点集合中。
- 如果 $t$ 被标记了,说明存在一条从 $s$ 到 $t$ 的路径,算法接受输入。
- 如果 $t$ 没有被标记,说明从 $s$ 出发无法到达 $t$,算法拒绝输入。
💡 [数值示例]
- 图:$s \to a$, $s \to c$, $a \to b$, $b \to t$, $c \to d$。
- 算法执行过程:
- 阶段 1: 标记 $s$。当前标记集合:$\{s\}$。
- 进入循环 (第1轮):
- 扫描边 $s \to a$:$s$ 已标记,$a$ 未标记。标记 $a$。标记集合:$\{s, a\}$。
- 扫描边 $s \to c$:$s$ 已标记,$c$ 未标记。标记 $c$。标记集合:$\{s, a, c\}$。
- 扫描其他边... 本轮有新节点被标记,继续循环。
- 进入循环 (第2轮):
- 扫描边 $a \to b$:$a$ 已标记,$b$ 未标记。标记 $b$。标记集合:$\{s, a, c, b\}$。
- 扫描其他边... 本轮有新节点被标记,继续循环。
- 进入循环 (第3轮):
- 扫描边 $b \to t$:$b$ 已标记,$t$ 未标记。标记 $t$。标记集合:$\{s, a, c, b, t\}$。
- 扫描其他边... 本轮有新节点被标记,继续循环。
- 进入循环 (第4轮):
- 扫描所有边。例如,扫描 $s \to a$,$a$ 已被标记,无操作。扫描 $c \to d$,c 已标记,d 未标记,标记 d。标记集合:$\{s, a, c, b, t, d\}$。
- 进入循环 (第5轮):
- 扫描所有边, 发现没有新的节点可以被标记了。循环终止。
- 阶段 4: 检查 $t$。$t$ 在标记集合中。接受。
📝 [总结]
本段给出了一个清晰、完整且正确的算法,用于解决PATH问题。该算法通过迭代地标记所有可达节点来工作。
🎯 [存在目的]
本段旨在提供一个具体的算法实现,以便接下来对其进行复杂度分析。
🧠 [直觉心智模型]
该算法就像是在做“家谱”染色。
- 把你自己($s$)染成蓝色。
- 然后规定:任何一个蓝色的人的儿子,如果还是白色的,就把他染成蓝色。
- 不断重复第二步,直到没有新的儿子可以被染色为止。
- 最后,看某个人($t$)是不是蓝色的,就知道他是不是你的后代了。
💭 [直观想象]
这就像在一张纸上画迷宫,然后用一桶水从起点 $s$ 倒下去。
- 水首先浸湿了 $s$ 点。
- 然后水会沿着迷宫的通道(边)流动。
- 水会不断蔓延,浸湿所有能流到的地方(可达节点)。
- 当水不再蔓延时,你看终点 $t$ 有没有湿。湿了,就说明路是通的。
📜 [原文23]
现在我们分析这个算法,以证明它在多项式时间内运行。显然,阶段 1 和 4 只执行一次。阶段 3 最多运行 $m$ 次,因为每次(除了最后一次)它都会在 $G$ 中标记一个额外的节点。因此,使用的总阶段数最多为 $1+1+m$,这是一个关于 $G$ 大小的多项式。
$M$ 的阶段 1 和 4 在任何合理的确定型模型上都很容易在多项式时间内实现。阶段 3 涉及对输入的扫描和测试某些节点是否被标记,这也很容易在多项式时间内实现。因此 $M$ 是 PATH 的多项式时间算法。
📖 [逐步解释]
这是对算法 $M$ 的复杂度分析,遵循了之前提出的“两步证明法”。
- 第一步:分析阶段数量
- 阶段 1 和 4:只执行一次,是常数时间。
- 阶段 2 (主循环):循环多少次?循环的条件是“有新的节点被标记”。
- 图 G 中总共有 $m$ 个节点。
- 每次循环(除了最后一次导致循环终止的空转)都至少会标记一个新的节点。
- 所以,标记新节点的循环最多发生 $m-1$ 次(因为 $s$ 已经先被标记了)。
- 加上最后一次空转,主循环最多执行 $m$ 次。
- 总阶段数:算法执行的“轮数”或“宏观步骤”是 $O(m)$。因为 $m$ (节点数)是输入大小 $n$ 的多项式(通常 $m \le n$),所以总阶段数是多项式的。
- 第二步:分析单个阶段
- 阶段 1 和 4:标记一个节点或检查一个节点的标记,这都是非常快速的操作,显然是多项式时间(甚至是常数时间)。
- 阶段 3 (扫描所有边):这一步是循环内部的核心操作。
- 它需要遍历图中的所有边。设边数为 $E$。
- 对于每一条边 $(a, b)$,需要检查 $a$ 和 $b$ 的标记状态。
- 在图灵机上,这相当于扫描一遍输入的边列表,并维护一个记录已标记节点的列表或磁带区域。这些操作都可以在输入大小的多项式时间内完成。
- 如果用邻接矩阵表示,扫描所有边需要 $O(m^2)$ 时间。如果用邻接表,需要 $O(m+E)$ 时间。这些都是关于 $m$ 和 $E$ 的多项式。
- 结论:
- 算法的总时间复杂度 = (循环次数) $\times$ (每次循环内部操作的时间)
- 总时间 $\approx O(m) \times O(E)$ (如果用邻接表,每次扫描是 $O(E)$) $= O(mE)$。
- 在最坏情况下,一个有 $m$ 个节点的图,边数 $E$ 最多是 $O(m^2)$。
- 所以总时间复杂度是 $O(m \cdot m^2) = O(m^3)$。
- 这是一个关于节点数 $m$ 的多项式,因此它也是关于输入长度 $n$ 的多项式。
- 所以,算法 $M$ 是一个多项式时间算法,从而证明了 PATH $\in \mathrm{P}$。
⚠️ [易错点]
- 算法实现的效率:文中的算法描述是一种比较基础的实现,其复杂度是 $O(mE)$。实际中,标准的BFS或DFS算法使用队列或栈来管理待访问的节点,可以做到 $O(m+E)$ 的时间复杂度,效率更高。但无论是 $O(mE)$ 还是 $O(m+E)$,它们都是多项式时间,所以对于证明 PATH $\in \mathrm{P}$ 来说,这个较慢的算法已经足够了。
- $m$ 和 $n$ 的关系:要始终记得,我们的最终结论必须是关于输入长度 $n$ 的。这里的分析之所以可以用 $m$ 和 $E$,是因为我们确信 $m$ 和 $E$ 都小于等于 $n$,并且 $n$ 是 $m$ 和 $E$ 的多项式。
📝 [总结]
本段通过“两步证明法”严谨地分析了算法 $M$ 的时间复杂度,得出其运行时间是节点数和边数的多项式,从而完成了对“PATH $\in \mathrm{P}$”的证明。
🎯 [存在目的]
本段旨在完整地展示一个“问题属于P类”的证明过程,作为一个范例。它演示了如何将一个高级描述的算法,通过合理的分析,映射到多项式时间复杂度的结论上。
🧠 [直觉心智模型]
继续用“公司项目审计”的例子来分析复杂度。
- 总阶段数:公司有 $m$ 个员工。最坏情况下,CEO (节点 $s$) 的一个决策,需要一层一层地传递下去,每次至少传递给一个新员工。最多传递 $m-1$ 轮,所有员工就都知道了。所以信息传递的轮数是 $O(m)$。
- 单个阶段成本:在每一轮信息传递中,我们需要检查公司里的所有沟通渠道(边),看是否有“一个已知晓信息的员工 $a$”可以通知一个“未知晓信息的员工 $b$”。检查所有 $E$ 个渠道的时间是 $O(E)$。
- 总时间:$O(m) \times O(E) = O(mE)$。这是一个关于员工数和沟通渠道数的多项式,是“可控的”。
12.3. RELPRIME 问题
📜 [原文24]
我们来看另一个多项式时间算法的例子。如果 1 是能同时整除两个数的最大整数,则称这两个数互质。例如,10 和 21 互质,即使它们本身都不是素数,而 10 和 22 不互质,因为它们都可以被 2 整除。令 RELPRIME 是测试两个数是否互质的问题。因此
$$
\text { RELPRIME }=\{\langle x, y\rangle \mid x \text { 和 } y \text { 互质 }\} 。
$$
📖 [逐步解释]
这里引入了第二个要证明属于P类的问题:RELPRIME 问题。
- 互质 (Relatively Prime / Coprime) 的定义:如果两个整数 $x$ 和 $y$ 的最大公约数 (Greatest Common Divisor, GCD) 是 1,那么它们就互质。
- 例子1:10 和 21。10的约数是 {1, 2, 5, 10},21的约数是 {1, 3, 7, 21}。它们唯一的公约数是1,所以它们互质。注意,10和21都不是素数。
- 例子2:10 和 22。它们有公约数 2(除了1之外),所以它们不互质。
- 问题定义:
- 输入:两个自然数 $x$ 和 $y$ 的编码 $\langle x, y \rangle$。
- 问题:判断 $x$ 和 $y$ 是否互质。
- 语言形式化定义:
- RELPRIME 是一个语言。
- 一个字符串属于 RELPRIME,当且仅当它是两个互质的整数 $x, y$ 的合理编码。
📝 [总结]
本段定义了RELPRIME问题,即判断两个给定的整数是否互质,并将其形式化为一个语言。
🎯 [存在目的]
本段旨在提出一个新的、属于数论领域的计算问题,以展示P类问题的多样性,并引出一种与图搜索完全不同的经典算法——欧几里得算法。
[直觉心-智模型]
RELPRIME 问题就像问你:“两个齿轮(齿数分别为 $x$ 和 $y$),如果它们啮合转动,在它们都回到初始位置之前,第一个齿轮需要转多少圈,第二个齿轮需要转多少圈?” 如果它们互质,那么第一个齿轮需要转 $y$ 圈,第二个需要转 $x$ 圈,这是它们能同时回到初始位置的最小圈数。如果它们不互质,比如最大公约数是 $d$,那么第一个齿轮转 $y/d$ 圈,第二个转 $x/d$ 圈即可。互质意味着它们之间没有“公共的旋转周期因子”。
💭 [直观想象]
想象有两根不同长度的木棍,长度分别为 $x$ 和 $y$。你想用它们来测量一个很长的距离。如果它们互质,你通过组合这两根木棍,最终可以精确地测量出任何大于某个值的整数长度。如果它们不互质,比如长度是10和22,你能量出的所有长度都必然是2的倍数,你永远也量不出奇数长度。互质给了你更大的“表达能力”。
📜 [原文25]
定理 7.15
RELPRIME $\in \mathrm{P}$。
📖 [逐步解释]
这是定理声明,意为“RELPRIME问题是P类问题”。需要构造一个多项式时间的算法来证明它。
📜 [原文26]
证明思路 解决这个问题的一种算法是搜索两个数的所有可能的除数,如果没有大于 1 的公除数则接受。然而,以二进制或任何其他 $k \geq 2$ 的 $k$ 进制表示的数的量级,是其表示长度的指数级。因此,这种暴力算法搜索指数数量的潜在除数,并且具有指数运行时间。
📖 [逐步解释]
这段思路分析再次首先否定了暴力搜索方法。
- 暴力算法思路:
- 要判断 $\text{gcd}(x, y) = 1$,我们可以去计算 $\text{gcd}(x, y)$ 的值。
- 一个天真的方法是,找到 $x$ 的所有约数,找到 $y$ 的所有约数,然后找出其中最大的公共的那个。
- 或者更简单点,从 2 开始,一直到 $\min(x, y)$,逐个检查每个数 $i$ 是否能同时整除 $x$ 和 $y$。如果找到了一个,那么它们就不互质。如果直到最后都没找到,它们就互质。
- 复杂度分析:
- 关键点:输入是数字 $x$ 的编码,其长度为 $n \approx \log_2 x$。这意味着数字的值 $x$ 是其输入长度 $n$ 的指数函数 ($x \approx 2^n$)。
- 暴力搜索的范围是从 2 到 $\min(x, y)$。假设 $x < y$,搜索范围就是到 $x$ পর্যন্ত。
- 搜索的次数大约是 $x$,即 $2^n$。
- 这是一个关于输入长度 $n$ 的指数时间算法。因此,它不能用来证明 RELPRIME $\in \mathrm{P}$。
💡 [数值示例]
- 输入: $x = 10^9$ (十亿), $y = 10^9+7$ (一个素数)。
- 输入长度: $x$ 的二进制长度 $n \approx \log_2(10^9) \approx 30$。输入总长度大约是60。
- 暴力算法: 需要从2开始,一直检查到 $10^9$。这需要大约 $10^9$ 次除法运算。对于长度约为60的输入,这是一个指数级的计算量。
📝 [总结]
本段解释了为什么解决RELPRIME问题的暴力搜索方法(试除法)是指数时间的,因为它搜索的数字范围是输入长度的指数级。
🎯 [存在目的]
本段通过否定一个看似简单但实则低效的方法,来强调寻找更聪明的数论算法的必要性,从而引出古老而高效的欧几里得算法。
🧠 [直觉心智模型]
这就像大海捞针。
- 暴力搜索:给你一个针(比如数字2),让你去整个太平洋(从1到 $x$ 的所有数字)里找另一根一模一样的针。你需要把整个太平洋的水都过滤一遍。
- 高效算法:利用磁铁或其他属性,可以快速定位针的位置。
📜 [原文27]
相反,我们用一种古老的数值过程,称为欧几里得算法,来计算最大公约数。自然数 $x$ 和 $y$ 的最大公约数,记作 $\operatorname{gcd}(x, y)$,是能同时整除 $x$ 和 $y$ 的最大整数。例如,$\operatorname{gcd}(18,24)=6$。显然,$x$ 和 $y$ 互质当且仅当 $\operatorname{gcd}(x, y)=1$。我们在证明中将欧几里得算法描述为算法 $E$。它使用 $\bmod$ 函数,其中 $x \bmod y$ 是 $x$ 除以 $y$ 的整数除法的余数。
📖 [逐步解释]
这里提出了解决 RELPRIME 问题的正确方法:欧几里得算法 (Euclidean algorithm)。
- 欧几里得算法:这是一个非常古老(源于古希腊)且极其高效的算法,用于计算两个整数的最大公约数 (GCD)。
- GCD:最大公约数,如 $\text{gcd}(18, 24) = 6$。
- 与问题的关系:判断 $x, y$ 是否互质,等价于计算 $\text{gcd}(x, y)$ 并判断其结果是否为 1。所以,只要我们能高效地计算GCD,就能高效地解决RELPRIME问题。
- 核心操作: 算法依赖于模运算 (mod function)。$x \bmod y$ 得到的是 $x$ 除以 $y$ 的余数。例如,$24 \bmod 18 = 6$。
- 核心原理: 欧几里得算法基于一个深刻的数论原理:$\operatorname{gcd}(x, y) = \operatorname{gcd}(y, x \bmod y)$。通过这个递推关系,可以不断地将大数转换成小数,直到其中一个数为0,此时另一个数就是最大公约数。
📝 [总结]
本段介绍了将使用欧几里得算法来高效计算最大公约数,从而解决RELPRIME问题。这是从指数时间暴力搜索到多项式时间算法的关键。
🎯 [存在目的]
本段旨在引入一个经典的、非显然的、但极为高效的算法,作为第二个P类问题的解决方案。
[直觉心-智模型]
欧几里得算法就像用两把不同长度的尺子 $x, y$ (假设 $x>y$) 来测量它们自身的公共长度。
- 用短尺子 $y$ 去量长尺子 $x$,量了几次之后,剩下的一小段长度就是 $x \bmod y$。
- 深刻的洞察是:$x$ 和 $y$ 的最大公约数,也必然是 $y$ 和剩下那段 $x \bmod y$ 的最大公约数。
- 现在问题就从“求 $x, y$ 的公约数”变成了“求 $y, x \bmod y$ 的公约数”,数字变小了。
- 不断重复这个过程,直到能量尽(余数为0),那么最后那把非0长度的尺子就是最大公约数。
📜 [原文28]
证明 欧几里得算法 $E$ 如下。
$E=$ “输入 $\langle x, y\rangle$,其中 $x$ 和 $y$ 是二进制表示的自然数:
- 重复直到 $y=0$:
- 将 $x \leftarrow x \bmod y$ 赋值。
- 交换 $x$ 和 $y$。
- 输出 $x$。”
算法 $R$ 使用 $E$ 作为子程序来解决 RELPRIME。
$R=$ “输入 $\langle x, y\rangle$,其中 $x$ 和 $y$ 是二进制表示的自然数:
- 对 $\langle x, y\rangle$ 运行 $E$。
- 如果结果是 1,接受。否则,拒绝。”
📖 [逐步解释]
这里给出了两个算法的具体描述:计算GCD的算法 $E$ 和解决RELPRIME问题的算法 $R$。
- 算法 E (欧几里得算法)
- 输入:两个数 $x, y$。
- 阶段 1 (循环):当 $y$ 不为 0 时,就一直重复以下操作。
- 阶段 2 (取模):计算 $x$ 除以 $y$ 的余数,并将这个余数赋值给 $x$。
- 阶段 3 (交换):交换 $x$ 和 $y$ 的值。
- 阶段 4 (输出):当循环结束时(即 $y=0$ 时),此时的 $x$ 就是原始输入的最大公约数。
- 算法 R (RELPRIME 判定器)
- 输入:两个数 $x, y$。
- 阶段 1:调用算法 $E$ 作为子程序,计算出 $\text{gcd}(x, y)$。
- 阶段 2:检查 $E$ 的输出结果。如果是 1,就接受;如果不是 1,就拒绝。
💡 [数值示例]
- 计算 gcd(24, 18) 使用算法 E:
- 初始: $x=24, y=18$。
- 第1轮循环:
- $y \neq 0$。
- $x \leftarrow 24 \bmod 18 = 6$。现在 $x=6$。
- 交换 $x, y$。现在 $x=18, y=6$。
- 第2轮循环:
- $y \neq 0$。
- $x \leftarrow 18 \bmod 6 = 0$。现在 $x=0$。
- 交换 $x, y$。现在 $x=6, y=0$。
- 第3轮循环:
- $y=0$。循环终止。
- 输出: $x=6$。所以 $\text{gcd}(24, 18) = 6$。
- 用算法 R 判断 $\langle 10, 21 \rangle$ 是否互质:
- 运行 $E$ 在 $\langle 10, 21 \rangle$ 上 (内部会先交换成 $\langle 21, 10 \rangle$):
- $x=21, y=10 \implies x=1, y=10$
- $x=10, y=1 \implies x=0, y=1$
- $x=1, y=0 \implies$ 循环结束,输出 $x=1$。
- $R$ 得到结果 1。接受。
📝 [总结]
本段清晰地描述了用于计算GCD的欧几里得算法 E 和利用 E 解决 RELPRIME 问题的判定算法 R。算法 R 将问题转化为了计算 GCD。
🎯 [存在目的]
本段旨在提供解决问题的具体算法步骤,以便后续进行正确性和复杂度分析。
📜 [原文29]
显然,如果 $E$ 在多项式时间内正确运行,那么 $R$ 也会,因此我们只需要分析 $E$ 的时间和正确性。这个算法的正确性众所周知,所以我们不再讨论。
📖 [逐步解释]
这段话将证明的焦点缩小到了算法 $E$ 上。
- 归约:算法 $R$ 的工作量基本上就是调用一次算法 $E$ 再加上一次比较。所以 $R$ 的时间复杂度主要由 $E$ 的时间复杂度决定。如果 $E$ 是多项式时间的,那么 $R$ 也是。
- 分析任务:因此,我们只需要分析算法 $E$ 的两个方面:
- 正确性 (Correctness):算法 $E$ 是不是真的能计算出最大公约数?
- 时间复杂度 (Time Complexity):算法 $E$ 是不是在多项式时间内完成?
- 省略正确性证明:作者指出,欧几里得算法的正确性是一个非常经典的数论结论(基于 $\text{gcd}(x, y) = \text{gcd}(y, x \bmod y)$),在此不再赘述。
- 聚焦时间分析:所以,剩下的唯一任务就是分析算法 $E$ 的时间复杂度。
📝 [总结]
本段将复杂的证明任务简化为单一目标:分析欧几里得算法的时间复杂度。
🎯 [存在目的]
本段旨在简化证明流程,让读者聚焦于最核心的、与复杂性理论直接相关的部分——时间分析。
📜 [原文30]
为了分析 $E$ 的时间复杂度,我们首先证明阶段 2 的每次执行(除了第一次)都会使 $x$ 的值至少减半。执行阶段 2 后,$x<y$,因为 $\bmod$ 函数的性质。执行阶段 3 后,$x>y$,因为两者已交换。因此,当随后执行阶段 2 时,$x>y$。如果 $x / 2 \geq y$,那么 $x \bmod y<y \leq x / 2$,并且 $x$ 至少减半。如果 $x / 2<y$,那么 $x \bmod y=x-y<x / 2$,并且 $x$ 至少减半。
📖 [逐步解释]
这是对算法 $E$ 时间复杂度的核心分析。其策略是证明在循环中,数值 $x, y$ 的规模在快速缩小。
- 核心论点:每次循环(严格来说是每两轮循环),数值会显著减小。作者这里给出了一个更强的结论:一次循环后,其中一个数会至少减半。
- 分析单次循环后的数值变化 (假设初始 $x > y$):
- x_new = x % y (x_new 是新 x)
- y_new = y
- 然后交换,变成 (y, x % y)。
- 作者的分析思路 (稍微有点绕,我们简化一下):
- 考虑算法中的一对变量 $(x, y)$,假设在循环开始时 $x>y$。
- 经过 x_new = x % y 和交换后,新的变量对是 $(y, x \bmod y)$。
- 关键是证明其中一个值会快速变小。
- 拉梅定理 (Lamé's Theorem) 是分析欧几里得算法复杂度的标准方法,它表明算法的步数不会超过较小数的十进制位数的5倍,这是一个对数关系,因此是多项式时间。
- 作者给出的简化证明:
- 考虑在第 $i$ 轮循环开始时的值 $(x_i, y_i)$,假设 $x_i > y_i$。
- 第 $i$ 轮循环后,值变为 $(x_{i+1}, y_{i+1}) = (y_i, x_i \bmod y_i)$。
- 第 $i+1$ 轮循环后,值变为 $(x_{i+2}, y_{i+2}) = (x_i \bmod y_i, y_i \bmod (x_i \bmod y_i))$。
- 可以证明 $x_{i+2} < x_i / 2$。
- 情况1:如果 $y_i \le x_i / 2$。那么 $x_{i+2} = x_i \bmod y_i < y_i \le x_i / 2$。
- 情况2:如果 $y_i > x_i / 2$。那么 $x_i \bmod y_i = x_i - y_i < x_i - x_i / 2 = x_i / 2$。所以 $x_{i+2} = x_i \bmod y_i < x_i/2$。
- 结论:在两种情况下,经过两轮循环,原先较大的数 $x_i$ 对应的新数 $x_{i+2}$ 的值至少会减半。
📝 [总结]
本段通过一个精彩的分类讨论,证明了在欧几里得算法的执行过程中,数值的规模(以其中较大的数为代表)每经过两次迭代,就会至少减半。这是一个指数级的衰减速度。
🎯 [存在目的]
本段旨在提供算法快速收敛的数学证明,这是得出多项式时间复杂度的关键一步。
📜 [原文31]
$x$ 和 $y$ 的值在每次执行阶段 3 时都会交换,因此 $x$ 和 $y$ 的原始值在每两次循环中至少都会减半。因此,阶段 2 和 3 执行的最大次数是 $2 \log _{2} x$ 和 $2 \log _{2} y$ 中较小者。这些对数与表示的长度成正比,使得执行的阶段数为 $O(n)$。$E$ 的每个阶段都只使用多项式时间,因此总运行时间是多项式的。
📖 [逐步解释]
这是基于上一段的分析,对算法 $E$ 的时间复杂度做出的最终结论。
- 第一步:分析阶段数量(循环次数)
- 我们已经证明了,每两次循环,数值至少减半。
- 一个数 $x$ 被反复除以2,需要多少次才能降到1?答案是 $\log_2 x$ 次。
- 由于是每两次循环减半,所以总循环次数大约是 $2 \log_2 x$ 或 $2 \log_2 y$ 这个量级。
- 输入数字 $x$ 的二进制表示长度 $n_x \approx \log_2 x$。
- 因此,循环次数是 $O(\log x) = O(n_x)$。如果总输入长度是 $n = n_x + n_y$,那么循环次数就是 $O(n)$。
- $O(n)$ 是一个多项式(一次多项式),所以阶段数量满足多项式上界。
- 第二步:分析单个阶段
- 循环内部的操作是取模(阶段2)和交换(阶段3)。
- 在图灵机上,做两个二进制数的除法(以得到余数)是一个标准算术过程,可以用多项式时间(比如 $O(n^2)$)完成。
- 交换两个数也是多项式时间。
- 所以,每个阶段的成本是多项式的。
- 结论:
- 总时间复杂度 = (循环次数) $\times$ (每次循环的成本)
- 总时间 $\approx O(n) \times O(n^2) = O(n^3)$。
- $O(n^3)$ 是一个关于输入长度 $n$ 的多项式。
- 因此,算法 $E$ 是多项式时间的。由于 $R$ 只是调用 $E$,所以 $R$ 也是多项式时间的。
- 最终结论:RELPRIME $\in \mathrm{P}$。
📝 [总结]
本段完成了对欧几里得算法的复杂度分析。通过证明循环次数与输入长度成线性关系(对数于数值大小),且每次循环的操作是多项式时间的,最终得出整个算法是多项式时间的,从而证明了 RELPRIME $\in \mathrm{P}$。
🎯 [存在目的]
本段旨在完成对第二个例子的证明,展示了如何通过分析数值的衰减速度来确定算法的复杂度。
🧠 [直觉心智模型]
这就像用二分法查找。每次都把搜索范围缩小一半。一个大小为 $N$ 的范围,只需要 $\log N$ 次就能找到目标。欧几里得算法的数值下降速度也是对数级的,所以它的运行步数和输入数值的“位数”(即 $\log N$)成正比,而不是和数值大小 $N$ 本身成正比。
12.4. 上下文无关语言的判定
📜 [原文32]
多项式时间算法的最后一个例子表明,每个上下文无关语言都可以在多项式时间内判定。
📖 [逐步解释]
这是一个引言,预告了本节将要证明的第三个,也是一个非常普适和重要的结论:所有上下文无关语言 (Context-Free Language, CFL) 都是P类问题。这意味着,由上下文无关文法 (CFG) 所描述的任何语言(比如许多编程语言的语法),其成员资格问题(即判断一个字符串是否符合该语法)都是可以在多项式时间内解决的。
📜 [原文33]
定理 7.16
每个上下文无关语言都是 P 的成员。
📖 [逐步解释]
这是定理的正式声明。用语言来说就是,For every language $L$ that is a CFL, $L \in \mathrm{P}$。
📜 [原文34]
证明思路 在定理 4.9 中,我们证明了每个 CFL 都是可判定的。为此,我们为每个 CFL 提供了一个判定它的算法。如果该算法在多项式时间内运行,则当前定理作为推论成立。让我们回顾一下该算法,并找出它是否运行得足够快。
📖 [逐步解释]
证明思路的第一步是回顾之前在“可计算性”章节中学过的内容。
- 回顾定理 4.9:这个定理(虽然我们这里看不到原文)说明了所有CFL都是可判定的 (decidable)。这意味着存在一个算法,对于任何字符串 $w$,都可以判断 $w$ 是否属于该CFL,并且该算法一定会停机。
- 检查旧算法:证明定理4.9时,肯定提供了一个具体的判定算法。现在的问题是:那个旧的算法是不是多项式时间的?
- 如果是,那么定理7.16就直接得证了,无需新算法。
- 如果不是,我们就需要找到一个更快的、多项式时间的算法。
📝 [总结]
本段的策略是,首先看看我们已有的工具(旧的判定算法)是否足够强大,如果不够,再寻找新的工具。
🎯 [存在目的]
本段的目的是将新问题(多项式时间可解性)与已学知识(可判定性)联系起来,并引出对旧算法效率的分析。
📜 [原文35]
令 $L$ 是由乔姆斯基范式下的 CFG $G$ 生成的 CFL。根据问题 2.38,任何字符串 $w$ 的推导都有 $2n-1$ 步,其中 $n$ 是 $w$ 的长度,因为 $G$ 处于乔姆斯基范式。当输入是长度为 $n$ 的字符串时,$L$ 的判别器通过尝试所有可能的 $2n-1$ 步推导来工作。如果其中任何一个推导出了 $w$,则判别器接受;否则,它拒绝。
📖 [逐步解释]
这里描述了那个旧的、用于证明可判定性的算法。
- 前提:乔姆斯基范式 (Chomsky Normal Form, CNF):任何CFL都可以由一个CNF文法生成。CNF的规则只有两种形式:$A \to BC$ 或 $A \to a$(其中 $A, B, C$ 是变量,$a$ 是终结符)。这个范式简化了分析。
- 推导步数:对于一个长度为 $n$ 的字符串 $w$ ($n>0$),如果它能被CNF文法 $G$ 生成,那么生成它的推导树一定有 $2n-1$ 步。这是一个可以被证明的、CNF文法的重要性质。
- 旧算法的思路 (暴力搜索):
- 给定输入字符串 $w$ (长度为 $n$)。
- 算法就去枚举所有长度为 $2n-1$ 的推导序列。
- 对于每个枚举出的推导,看看它最终生成的字符串是不是 $w$。
- 如果找到了一个能生成 $w$ 的推导,就接受。
- 如果试遍了所有可能的 $2n-1$ 步推导,都无法得到 $w$,就拒绝。
📝 [总结]
本段描述了一个基于暴力枚举推导过程的、用于判定CFL的算法。
🎯 [存在目的]
本段的目的是具体化那个“旧算法”,以便接下来分析其效率。
📜 [原文36]
对该算法的快速分析表明它不是在多项式时间内运行的。具有 $k$ 步的推导的数量可能是 $k$ 的指数级,因此该算法可能需要指数时间。
📖 [逐步解释]
这里对旧算法的效率给出了判决:不是多项式时间。
- 原因:在推导的每一步,我们可能有多种规则可以选择。
- 比如,从开始符号 $S$ 出发,如果有规则 $S \to AB$ 和 $S \to CD$,就有两种选择。
- 下一步,从 $A$ 出发可能又有多种选择...
- 经过 $k$ 步之后,可能的推导序列的数量会随着 $k$ 指数级增长。
- 复杂度:由于要枚举的推导数量是指数级的,这个暴力算法的时间复杂度是指数时间的。
- 结论:这个旧算法不够快,不能用来证明 CFL $\in \mathrm{P}$。我们需要一个全新的、更聪明的算法。
💡 [数值示例]
- 假设一个文法有 $|V|$ 个变量,每个变量平均有 $r$ 条规则。
- 一个长度为 $k$ 的推导,粗略地说,其可能的数量可能是 $r^k$ 的量级。
- 对于输入 $w$ (长度 $n$),推导步数是 $k = 2n-1$。
- 那么推导的数量大约是 $r^{2n-1}$,这是关于 $n$ 的指数函数。
📝 [总结]
本段指出了暴力枚举推导的算法是指数时间的,因为它探索的“推导空间”是指数级大小的。
🎯 [存在目的]
本段的目的是正式否定旧算法的有效性(在多项式时间意义上),从而引出对新算法——动态规划——的需求。
📜 [原文37]
为了获得多项式时间算法,我们引入了一种强大的技术,称为动态规划。该技术利用对较小子问题信息的积累来解决较大的问题。我们记录任何子问题的解决方案,以便我们只需要解决它一次。我们通过制作所有子问题的表格并系统地输入其解决方案来做到这一点。
📖 [逐步解释]
这里引入了解决该问题的关键技术:动态规划 (Dynamic Programming)。
- 动态规划核心思想:
- 分而治之:将一个大问题分解成许多更小的、相互重叠的子问题。
- 记忆化/表格法:解决这些子问题,并将它们的解存储起来(通常在一个表格中),以避免重复计算。
- 自底向上构建:从最小的子问题开始,利用已解出的子问题的答案,逐步构建出更大子问题的答案,直到最终解决原始的大问题。
- 应用到CFL判定:我们将用动态规划来避免重复地去检查“某个变量能否生成某段子串”这样的问题。
📝 [总结]
本段介绍了即将使用的高级算法技术——动态规划,并简述了其核心思想:通过查表解决子问题,自底向上构建最终解。
🎯 [存在目的]
本段旨在为后续的CYK算法(虽然文中没给名字,但本质就是它)提供理论武器和思想指导。
🧠 [直觉心智模型]
动态规划就像是考试前做一本答案详细的习题集。
- 你从第一章(最小的子问题)开始做。
- 做完一题,就对一下答案,并把解题思路(子问题的解)记在笔记本(表格)上。
- 当你做到后面的章节(更大的问题)时,如果发现需要用到前面某道题的结论,你不用再算一遍,直接翻笔记本(查表)就行了。
- 这样,你系统地、不重复地做完了整本习题集,最终掌握了所有知识,可以应对期末考试(原始问题)。
📜 [原文38]
在这种情况下,我们考虑确定 $G$ 中的每个变量是否生成 $w$ 的每个子字符串的子问题。算法将此子问题的解决方案输入到一个 $n \times n$ 的表格中。对于 $i \leq j$,表格的 $(i, j)$ 项包含生成子字符串 $w_{i} w_{i+1} \cdots w_{j}$ 的变量集合。对于 $i>j$,表格项未使用。
📖 [逐步解释]
这里具体说明了如何将动态规划应用到CFL判定问题上。
- 子问题定义:我们的子问题是:“对于输入字符串 $w$ 的任意一个子串 $w[i..j]$ (从第 $i$ 个字符到第 $j$ 个字符),有哪些变量可以生成它?”
- 表格设计:
- 我们创建一个 $n \times n$ 的二维表格,名为 table。
- table[i][j] 这个单元格用来存储一个变量的集合。
- 这个集合里的变量,就是所有能够生成子串 $w[i..j]$ 的变量。
- 表格的下标 $i$ 代表子串的起始位置,$j$ 代表子串的结束位置。
- 因为子串的起始位置 $i$ 不可能大于结束位置 $j$,所以当 $i>j$ 时,表格项是无用的(可以只用上/下三角部分)。
💡 [数值示例]
- 输入: $w = \text{baba}$ (长度 $n=4$)。
- 表格:一个 $4 \times 4$ 的表格 table。
- 子问题举例:
- 我们要计算 table[1][1]:能生成子串 $w[1..1]$ (即 "b") 的所有变量。
- 我们要计算 table[2][4]:能生成子串 $w[2..4]$ (即 "aba") 的所有变量。
- 最终目标:我们最关心的是 table[1][4],即能生成整个字符串 $w$ ("baba") 的变量集合。如果这个集合里包含了文法的开始符号 $S$,那么就说明 $w$ 属于这个语言。
📝 [总结]
本段定义了动态规划算法的核心:子问题是“哪些变量能生成哪个子串”,而用于存储子问题解的“备忘录”是一个二维表格。
🎯 [存在目的]
本段旨在清晰地构建出动态规划算法所需的数据结构(表格)和它所要解决的子问题,为描述算法的具体流程做准备。
💭 [直观想象]
这个表格就像一张地图填空。
- 地图的横轴是子串的终点,纵轴是子串的起点。
- 每个格子 $(i, j)$ 都需要我们填上“能生成从 $i$ 到 $j$ 这段路(子串)的交通工具(变量)”。
- 我们的目标是填满这张表,最后看看左下角到右上角的那个大格子 $(1, n)$ 里,有没有“飞机”(开始符号S)。
📜 [原文39]
该算法填充 $w$ 的每个子字符串的表格项。首先,它填充长度为 1 的子字符串的项,然后是长度为 2 的子字符串的项,依此类推。
它利用较短长度的项来协助确定较长长度的项。
例如,假设算法已经确定了哪些变量生成了所有长度达 $k$ 的子字符串。为了确定变量 $A$ 是否生成特定长度为 $k+1$ 的子字符串,算法将该子字符串以 $k$ 种可能的方式分成两段非空部分。对于每次分割,算法检查每个规则 $A \rightarrow B C$ 以确定 $B$ 是否生成第一段,$C$ 是否生成第二段,使用之前计算的表格项。如果 $B$ 和 $C$ 都生成了各自的部分,$A$ 则生成该子字符串,因此被添加到相关的表格项中。算法通过检查规则 $A \rightarrow \mathrm{b}$ 的表格,从长度为 1 的字符串开始这个过程。
📖 [逐步解释]
这段话描述了动态规划的“自底向上”的构建过程。
- 构建顺序:算法不是随意填充表格的,而是按照子串的长度从小到大来填充。
- 第1步:处理所有长度为 1 的子串 ($w[1..1], w[2..2], ..., w[n..n]$),填充表格的对角线 table[i][i]。
- 第2步:处理所有长度为 2 的子串 ($w[1..2], w[2..3], ...$),填充 table[i][i+1]。
- ...
- 第n步:处理长度为 $n$ 的子串 ($w[1..n]$),填充最终的目标 table[1][n]。
- 递推关系(核心思想):如何利用短子串的解来构建长子串的解?
- 目标:我们要判断变量 $A$ 能否生成一个长子串 $w[i..j]$。
- 利用规则 $A \to BC$:根据CNF的规则,如果 $A$ 能生成 $w[i..j]$,那么它一定是先变成了两个变量 $B$ 和 $C$,然后 $B$ 生成了子串的前半部分,C生成了后半部分。
- 分割子串:所以,我们将子串 $w[i..j]$ 分割成两部分:$w[i..k]$ 和 $w[k+1..j]$,其中 $k$ 是分割点,可以从 $i$ 取到 $j-1$。
- 查表:对于每一种分割方式,我们去查已经计算好的表格项:
- 变量 $B$ 在不在 table[i][k] 里?(B 能否生成前半部分?)
- 变量 $C$ 在不在 table[k+1][j] 里?(C 能否生成后半部分?)
- 得出结论:如果在所有规则 $A \to BC$ 和所有可能的分割点 $k$ 中,只要有一次,我们发现 B 在 table[i][k] 中 并且 C 在 table[k+1][j] 中,我们就可以断定 $A$ 能够生成 $w[i..j]$,并把 $A$ 加入到 table[i][j] 中。
- 初始条件(Base Case):这个递推过程的起点是什么?是长度为 1 的子串。
- 对于子串 $w[i..i]$(就是一个字符 $b=w_i$),变量 $A$ 能生成它,当且仅当文法中存在规则 $A \to b$。
- 所以,填充表格对角线 table[i][i] 的过程就是检查所有 $A \to b$ 形式的规则。
💡 [数值示例]
- 文法 G: $S \to AB, A \to \text{a}, B \to \text{b}$。
- 输入 w: "ab" ($n=2$)。
- 算法过程:
- 处理长度为 1 的子串:
- 子串 $w[1..1]$ = "a": 检查规则,发现 $A \to \text{a}$。所以在 table[1][1] 中加入 $A$。table[1][1] = {A}。
- 子串 $w[2..2]$ = "b": 检查规则,发现 $B \to \text{b}$。所以在 table[2][2] 中加入 $B$。table[2][2] = {B}。
- 处理长度为 2 的子串:
- 子串 $w[1..2]$ = "ab": 我们要填充 table[1][2]。
- 唯一的分割点 $k=1$。我们要将 "ab" 分割成 "a" ($w[1..1]$) 和 "b" ($w[2..2]$)。
- 现在检查所有 $X \to YZ$ 形式的规则。我们有规则 $S \to AB$。
- 查表:变量 $A$ 在 table[1][1] 中吗?是。
- 查表:变量 $B$ 在 table[2][2] 中吗?是。
- 因为两个条件都满足,所以我们可以把 $S$ 加入到 table[1][2] 中。table[1][2] = {S}。
- 最终检查:
- 算法结束。检查 table[1][n] 即 table[1][2]。
- table[1][2] 中包含开始符号 $S$。
- 接受 "ab"。
📝 [总结]
本段详细解释了动态规划算法(即CYK算法)如何自底向上地、按子串长度顺序填充表格。它阐明了算法的起始条件(处理长度为1的子串)和递推关系(利用短子串的解和文法规则来填充长子串的解)。
🎯 [存在目的]
本段旨在将动态规划的抽象思想,转化为一个具体的、可执行的填充表格的流程,为给出最终的算法伪代码铺平道路。
📜 [原文40]
证明 以下算法 $D$ 实现了该证明思路。令 $G$ 为生成 CFL $L$ 的乔姆斯基范式下的 CFG。假设 $S$ 是开始变量。(回想一下,空字符串在乔姆斯基范式语法中被特殊处理。算法在阶段 1 处理 $w=\varepsilon$ 的特殊情况。)注释出现在双括号内。
$D=$ “输入 $w=w_{1} \cdots w_{n}$:
- 对于 $w=\varepsilon$,如果 $S \rightarrow \varepsilon$ 是一个规则,则接受;否则,拒绝。$\llbracket w=\varepsilon$ 的情况 $\rrbracket$
- 对于 $i=1$ 到 $n$:【 检查每个长度为 1 的子字符串 $\rrbracket$
- 对于每个变量 $A$:
- 测试 $A \rightarrow \mathrm{b}$ 是否是一个规则,其中 $\mathrm{b}=w_{i}$。
- 如果是,将 $A$ 放入 table $(i, i)$。
- 对于 $l=2$ 到 $n$: $\quad \llbracket l$ 是子字符串的长度 $\rrbracket$
- 对于 $i=1$ 到 $n-l+1$: $\quad \llbracket i$ 是子字符串的起始位置 $\rrbracket$
- 令 $j=i+l-1$。 $\quad \llbracket j$ 是子字符串的结束位置 $\rrbracket$
- 对于 $k=i$ 到 $j-1$: $\quad \llbracket k$ 是分割位置 $\rrbracket$
- 对于每个规则 $A \rightarrow B C$:
- 如果 table $(i, k)$ 包含 $B$ 且 table $(k+1, j)$ 包含 $C$,将 $A$ 放入 table $(i, j)$。
- 如果 table $(1, n)$ 包含 $S$,接受;否则,拒绝。”
📖 [逐步解释]
这是动态规划算法(CYK算法)的完整伪代码描述。
- 输入: 字符串 $w$。
- 阶段 1 (特殊情况): 处理空字符串 $\varepsilon$。CNF文法通常不允许 $A \to \varepsilon$ 规则,除非是 $S \to \varepsilon$ 且 $S$ 不出现在任何规则的右侧。这里做了简化处理:如果文法允许生成空串,并且输入就是空串,则接受。
- 阶段 2-5 (处理长度为1的子串):
- 这是一个双重循环。外层 for i=1 to n 遍历所有起始位置。
- 内层 for each variable A 遍历所有变量。
- 核心操作 (阶段 4-5): 检查是否存在规则 $A \to w_i$。如果存在,就把变量 $A$ 填入对角线格 table(i, i)。
- 阶段 6-11 (处理长度大于1的子串):
- 这是一个三重嵌套循环,是算法的主体。
- 外层循环 (阶段 6) for l=2 to n: 按子串长度 $l$ 从 2 到 $n$ 迭代。
- 中层循环 (阶段 7) for i=1 to n-l+1: 遍历所有可能的起始位置 $i$,对于给定的长度 $l$。结束位置 $j$ 由 $i$ 和 $l$ 确定 (阶段 8)。
- 内层循环 (阶段 9) for k=i to j-1: 遍历当前子串 $w[i..j]$ 的所有可能分割点 $k$。
- 最内层操作 (阶段 10-11):
- 遍历文法中所有的 $A \to BC$ 规则。
- 对于每个规则,检查 $B$ 是否能生成前半段(查 table(i, k)),以及 $C$ 是否能生成后半段(查 table(k+1, j))。
- 如果都可以,就把 $A$ 加入 table(i, j)。
- 阶段 12 (最终判断):
- 所有表格填充完毕后,检查 table(1, n) (代表整个字符串 $w$ 的格子)。
- 如果开始符号 $S$ 在这个格子里,说明 $S$ 可以生成 $w$,接受。
- 否则,拒绝。
📝 [总结]
本段给出了解决CFL判定问题的CYK算法的完整、严谨的伪代码。它系统地实现了前面讨论的动态规划思想。
🎯 [存在目的]
本段旨在提供一个可供进行复杂度分析的、精确的算法描述。
📜 [原文41]
现在我们分析 $D$。每个阶段都很容易实现在多项式时间内运行。阶段 4 和 5 最多运行 $nv$ 次,其中 $v$ 是 $G$ 中变量的数量,它是一个与 $n$ 无关的固定常数;因此这些阶段运行 $O(n)$ 次。阶段 6 最多运行 $n$ 次。每次阶段 6 运行时,阶段 7 最多运行 $n$ 次。每次阶段 7 运行时,阶段 8 和 9 最多运行 $n$ 次。每次阶段 9 运行时,阶段 10 运行 $r$ 次,其中 $r$ 是 $G$ 的规则数量,它是另一个固定常数。因此,算法的内层循环——阶段 11——运行 $O\left(n^{3}\right)$ 次。总计显示 $D$ 执行了 $O\left(n^{3}\right)$ 个阶段。
📖 [逐步解释]
这是对算法 $D$ (CYK算法) 的时间复杂度分析。
- 分析前提:
- $n$ = 输入字符串 $w$ 的长度。
- $v$ = 文法 $G$ 中变量的数量。
- $r$ = 文法 $G$ 中规则的数量。
- 关键:$v$ 和 $r$ 是常数,它们只跟文法有关,跟输入字符串的长度 $n$ 无关。
- 分析各部分复杂度:
- 阶段 1: $O(1)$,常数时间。
- 阶段 2-5 (处理长度1子串):
- 外层循环 i=1 to n: $n$ 次。
- 内层循环 each variable A: $v$ 次。
- 内部操作: $O(1)$。
- 总时间: $O(n \cdot v)$。因为 $v$ 是常数,所以这部分是 $O(n)$。
- 阶段 6-11 (处理长子串):
- 循环 l (长度): from 2 to n -> $O(n)$ 次。
- 循环 i (起点): from 1 to n-l+1 -> 最多 $O(n)$ 次。
- 循环 k (分割点): from i to j-1 -> 子串长度最多是 $n$,所以最多 $O(n)$ 次。
- 循环规则 (阶段 10): each rule A->BC -> $r$ 次。
- 核心操作 (阶段 11): 查表和插入。可以认为是 $O(1)$。
- 总时间: 三层关于 $n$ 的循环,所以是 $O(n \cdot n \cdot n) = O(n^3)$。更精确地说是 $O(n^3 \cdot r)$。因为 $r$ 是常数,所以是 $O(n^3)$。
- 阶段 12: $O(1)$。
- 结论:
- 算法中最耗时的部分是阶段 6-11 的三重循环,其时间复杂度是 $O(n^3)$。
- 因此,整个算法 $D$ 的时间复杂度是 $O(n^3)$。
- $O(n^3)$ 是一个多项式。
- 最终结论: 每个 CFL 都存在一个多项式时间的判定算法,因此,每个 CFL 都属于 P 类。证明完毕。
📝 [总结]
本段通过对CYK算法的伪代码进行逐层分析,得出了其时间复杂度为 $O(n^3)$ 的结论。由于 $O(n^3)$ 是多项式时间,这便证明了所有上下文无关语言都属于P类。
🎯 [存在目的]
本段旨在完成对第三个例子,也是最重要的一个普适性结论的证明。它展示了动态规划如何将一个看似需要指数时间搜索的问题,转化为一个高效的多项式时间算法。
🧠 [直觉心智模型]
分析CYK算法的复杂度,就像计算建造一个金字塔需要多少块石头。
- 金字塔的底座是 $n \times n$ 的。
- 我们要填满一个 $n \times n \times n$ 的立方体空间(类比 $O(n^3)$)。
- 循环 l 就像是决定我们要建造第几层。
- 循环 i 和 j 确定了我们在这一层的哪个位置放置石块。
- 循环 k 是为了确定当前这块石头要由下面哪两块石头支撑起来。
- 总的工作量,和金字塔的体积($n^3$)成正比。