先记住一下概念:
如果在多项式时间内能解决一个问题,这个问题就属于 P 类问题。
如果在多项式时间内能验证/猜出一个问题的一个解,这个问题就属于 NP 类问题。
NP 问题与非 P 类问题是两个概念。
简单地说,如果可以用问题 B 的解法解决问题 A,问题 A 便可以约化为问题 B。
如果所有 NP 问题都可以在多项式时间内约化到某个 NP 问题,那么这个问题就属于 NPC 问题。
(P=NP)问题:是否所有的 NP 问题都是 P 类问题。
是否所有问题都能在多项式时间内被解决呢?很遗憾,答案是否定的。比如,输出从(n) 个互不相同的整数的全排列。不管你用什么方法,打印结果总得用阶乘级的时间。不可解问题 (Undecidable Decision Problem) 甚至根本不可能找到一个正确的算法,如停机问题 (The Halting Problem)。
有人非说这种问题不是“正规”的问题,“正规”的问题是让程序对一个问题回答 Yes 或 No(即判定性问题),或者求一个什么的最优值(即最优化问题)。那么,根据这个定义,求一个图的哈密顿回路到现在还没有找到多项式级的算法。事实上,这个问题就是我们后面要说的 NPC 问题,这里先按下不表。
接下来引入 NP 问题的概念。这个就有点难理解了,或者说容易理解错误。在这里强调,NP 问题不等于非 P 类问题。如果在多项式时间内能验证/猜出一个问题的一个解,这个问题就属于 NP 类问题。。
比方说某个求最短路径的问题,问从起点到终点是否有一条小于 100 个单位长度的路线。你根据数据画好了图,但怎么也算不出来,于是来问我:你看怎么选条路走得最少?我说,我 RP 很好,肯定能随便给你指条很短的路出来。然后我就胡乱画了几条线,说就这条吧。你按我指的这条把权值加起来一看,嘿,神了,路径长度 98,比 100 小。于是答案出来了,存在。别人会问他这题怎么做出来的,我就可以说,因为我找到了一个比 100 小的解。
在这个题中,找一个解很困难,但验证一个解很容易(只需要 (O(n)) 的时间复杂度)。那么,只要我 RP 好,猜到的方案总是最优的,我一定能在多项式的时间里找到一个解,解决这个问题。这就是 NP 问题。
有没有不是 NP 问题的问题,即不能在多项式时间里去验证一个解?例如,我们知道哈密顿回路是 NP 问题,因为验证一条路是否恰好经过了每一个顶点非常容易;但我要把问题换成这样:试问一个图中是否不存在哈密顿回路?这样问题就没法在多项式的时间里进行验证了,因为除非你试过所有的路,否则你不敢断定它“没有哈密顿回路”。
之所以要定义 NP 问题,是因为通常只有 NP 问题才可能找到多项式的算法。我们不会指望一个连多项式地验证一个解都不行的问题存在一个解决它的多项式级的算法。相信读者很快可以明白,信息学中的号称最困难的问题——
(P=NP) 问题实际上是在探讨 NP 问题与 P 类问题的关系。
很显然,所有的 P 类问题都是 NP 问题。也就是说,能多项式地解决一个问题,必然能多项式地验证一个问题的解——既然正解都出来了,验证任意给定的解也只需要比较一下就可以了。关键是,人们想知道,是否所有的 NP 问题都是 P 类问题。我们可以再通过集合的角度来说明。如果把所有 P 类问题归为一个集合 (P) 中,把所有 NP 问题划进另一个集合 (NP) 中,那么,显然有 (P ∈ NP)。现在,所有对 NP 问题的研究都集中在“(P=NP)问题”上,其实就一句话:证明或推翻 (P=NP)。
NP问题一直都是信息学的巅峰。很引人注目但难以解决。在信息学研究中,这是一个耗费了无数科学家大量时间和精力也没有解决的终极问题,好比物理学中的大统一和数学中的歌德巴赫猜想。
目前为止大家还“啃不动”这个问题,但人们普遍认为 (P=NP) 不成立,也就是说,多数人相信,存在至少一个不可能有多项式级复杂度的算法的 (NP) 问题。人们如此坚信 (P≠NP),是因为在研究 NP 问题的过程中找出了一类非常特殊的 NP 问题叫做 NP-完全 (NP-Complete) 问题,也即所谓的 NPC 问题。正是 NPC 问题的存在,使人们相信 (P≠NP)。下文将花大量篇幅介绍 NPC 问题,你从中可以体会到 NPC 问题使 (P≠NP) 很有可能成立。
为了说明 NPC 问题,我们先引入一个概念——约化 (Reducibility,又译“归约”)。简单地说,一个问题 A 可以约化为问题 B 的含义即是,可以用问题 B 的解法解决问题 A,或者说,问题 A 可以“变成”问题 B。
《算法导论》上举了这么一个例子。比如说,现在有两个问题:求解一个一元一次方程和求解一个一元二次方程。那么我们说,前者可以约化为后者,意即知道如何解一个一元二次方程那么一定能解出一元一次方程。我们可以写出两个程序分别对应两个问题,那么我们能找到一个“规则”,按照这个规则把解一元一次方程程序的输入数据变一下,用在解一元二次方程的程序上,两个程序总能得到一样的结果。这个规则即是:两个方程的对应项系数不变,一元二次方程的二次项系数为0。按照这个规则把前一个问题转换成后一个问题,两个问题就等价了。
同样地,我们可以说,哈密顿回路可以约化为 TSP(Travelling Salesman Problem,旅行商问题):在哈密顿回路问题中,两点相连即这两点距离为 0,两点不直接相连则令其距离为 1,于是问题转化为在 TSP 中,是否存在一条长为 0 的路径。哈密顿回路存在当且仅当 TSP 中存在长为 0 的回路。
“问题 A 可约化为问题 B”有一个重要的直观意义:B 的时间复杂度高于或等于 A 的时间复杂度。也就是说,问题 A 不比问题 B 难。这很容易理解。既然问题 A 能用问题 B 来解决,倘若 B 的时间复杂度比 A 的时间复杂度还低了,那 A 的算法就可以改进为 B 的算法,两者的时间复杂度还是相同。正如解一元二次方程比解一元一次方程难,因为解决前者的方法可以用来解决后者。
很显然,约化具有一项重要的性质:约化具有传递性。如果问题 A 可约化为问题 B,问题 B 可约化为问题 C,则问题 A 一定可约化为问题 C。这个道理非常简单,就不必阐述了。
现在再来说一下约化的标准概念就不难理解了:如果能找到这样一个变化法则,对任意一个程序 A 的输入,都能按这个法则变换成程序 B 的输入,使两程序的输出相同,那么我们说,问题 A 可约化为问题 B。
当然,我们所说的“可约化”是指的可“多项式地”约化(Polynomial-time Reducible),即变换输入的方法是能在多项式的时间里完成的。约化的过程只有用多项式的时间完成才有意义。
从约化的定义中我们看到,一个问题约化为另一个问题,时间复杂度增加了,问题的应用范围也增大了。通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法,来代替复杂度虽然低,但只能用于很小的一类问题的算法。再回想前面讲的 P 和 NP 问题,联想起约化的传递性,自然地,我们会想问,如果不断地约化上去,不断找到能“通吃”若干小 NP 问题的一个稍复杂的大 NP 问题,那么最后
是否有可能找到一个时间复杂度最高,并且能“通吃”所有的 NP 问题的一个超级 NP 问题?
答案居然是肯定的。也就是说,存在这样一个 NP 问题,所有的 NP 问题都可以约化成它。换句话说,只要解决了这个问题,那么所有的 NP 问题都解决了。这种问题的存在难以置信,并且更加不可思议的是,这种问题不只一个,它有很多个,它是一类问题。这一类问题就是传说中的 NPC 问题,也就是 NP-完全问题。NPC 问题的出现使整个 NP 问题的研究得到了飞跃式的发展。再次回到全文开头,我们可以看到,人们想表达一个问题不存在能多项式时间内解决的高效算法时,应该说它“属于 NPC 问题”。
NPC 问题的定义非常简单。同时满足下面两个条件的问题就是 NPC 问题。
首先,它得是一个 NP 问题;
然后,所有的 NP 问题都可以约化到它。
证明一个问题是 NPC 问题也很简单。先证明它至少是一个 NP 问题,再证明其中一个已知的 NPC 问题能约化到它(由约化的传递性,则 NPC 问题定义的第二条也得以满足;至于第一个 NPC 问题是怎么来的,下文将介绍),这样就可以说它是 NPC 问题了。
既然所有的 NP 问题都能约化成 NPC 问题,那么只要任意一个 NPC 问题找到了一个多项式的算法,那么所有的 NP 问题都能用这个算法解决了, P 也就等于 NP 了。因此,给 NPC 找一个多项式算法不大可能。因此前文才说,“正是 NPC 问题的存在,使人们更加相信 (P≠NP)”。我们可以就此直观地理解,NPC 问题目前没有多项式的有效算法,只能用指数级甚至阶乘级复杂度的搜索。
顺便讲一下 NP-Hard 问题。NP-Hard 问题是这样一种问题,它满足 NPC 问题定义的第二条但不一定要满足第一条(就是说,NP-Hard问题要比 NPC 问题的范围广)。NP-Hard 问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法。事实上,由于NP-Hard放宽了限定条件,它将有可能比所有的NPC问题的时间复杂度更高从而更难以解决。
不要以为 NPC 问题是一纸空谈,NPC 问题是存在的,确实有这么一个非常具体的问题属于 NPC 问题。它就是逻辑电路问题。这是第一个 NPC 问题,其它的 NPC 问题都是由这个问题约化而来的。因此,逻辑电路问题是 NPC 类问题的“鼻祖”。
逻辑电路问题是指的这样一个问题:给定一个逻辑电路,问是否存在一种输入使输出为True。
什么叫做逻辑电路呢?一个逻辑电路由若干个输入,一个输出,若干“逻辑门”和密密麻麻的线组成。看下面一例,不需要解释你马上就明白了。
┌────┐
│输入1├─→┐ ┌──┐
└────┘ └─→┤ │
│ or ├→─┐
┌────┐ ┌─→┤ │ │ ┌──┐
│输入2├─→┤ └──┘ └─→┤ │
└────┘ │ ┌─→┤AND ├──→输出
└────────┘┌→┤ │
┌────┐ ┌───┐ │ └──┘
│输入3├─→┤NOT├─→────┘
└────┘ └───┘
这是个较简单的逻辑电路,当输入1、输入2、输入3分别为True、True、False或False、True、False时,输出为True。
有输出无论如何都不可能为True的逻辑电路吗?有。下面就是一个简单的例子。
┌───┐
│输入1├→─┐ ┌──┐
└───┘ └─→┤ │
│AND ├─→┐
┌─→┤ │ │
│ └──┘ │ ┌──┐
│ └→┤ │
┌───┐ │ │AND ├─→输出
│输入2├→─┤ ┌──┐ ┌→┤ │
└───┘ └→┤NOT ├→──┘ └──┘
└──┘
上面这个逻辑电路中,无论输入是什么,输出都是False。我们就说,这个逻辑电路不存在使输出为True的一组输入。
回到上文,给定一个逻辑电路,问是否存在一种输入使输出为True,这即逻辑电路问题。
逻辑电路问题属于NPC问题。这是有严格证明的。它显然属于NP问题,并且可以直接证明所有的NP问题都可以约化到它(不要以为NP问题有无穷多个将给证明造成不可逾越的困难)。证明过程相当复杂,其大概意思是说任意一个NP问题的输入和输出都可以转换成逻辑电路的输入和输出(想想计算机内部也不过是一些 0和1的运算),因此对于一个NP问题来说,问题转化为了求出满足结果为True的一个输入(即一个可行解)。
有了第一个NPC问题后,一大堆NPC问题就出现了,因为再证明一个新的NPC问题只需要将一个已知的NPC问题约化到它就行了。后来,Hamilton 回路成了NPC问题,TSP问题也成了NPC问题。现在被证明是NPC问题的有很多,任何一个找到了多项式算法的话所有的NP问题都可以完美解决了。因此说,正是因为NPC问题的存在,P=NP变得难以置信。P=NP问题还有许多有趣的东西,有待大家自己进一步的挖掘。攀登这个信息学的巅峰是我们这一代的终极目标。现在我们需要做的,至少是不要把概念弄混淆了。