大千世界,问题千万. 世间的一切都可以是我们的问题:
- 天上有没有神仙
- 地下有没有地府
- 明天会不会下雨
- 考试会不会及格
- 那个妹子会不会接受我
- 这个数组的最大值是不是42
- 那个数组的从小到大的排序结果是不是1,2,3,4,5
诸如此类的问题我们都可以称之为问题. 可以看出我们的问题都是有一个特定的问法
就是指某个事件/事物是否符合/满足一个特定的预期?
像这样的问题我们称之为决定性问题/是非问题(decisive problem),答案只有两种可能性,要么是真/是/true的,要么是假/非/false的
指时间复杂度用函数表示形如以下公式所表示:
在这里定义
-
$a,b,c,...,z,C, x$ 都是常数,$x$ 是指数 -
$n$ 是自变量,是数据的规模
对于以上定义,如果一个算法的时间复杂度明确到具体的参数
对于任意一个算法,只要某个算法A的时间复杂度小于等于这个值,就可以称这个算法A的时间复杂度在多项式时间之内
例如快排$O(nlogn)$, 顶堆取极值$O(1)$, 遍历一个数组$O(n)$,统统都属于多项式时间之内这个定义.
最后我们认为,如果一个操作可以在多项式时间之内完成,我们将认定这个操作是简单的操作.在这里,这个操作可以是任何定义.
复杂度分类是特指算法复杂度的分类,大体上分为两大类,一个是$x$,另一个$Nx$. 这里的x代表的是不同的复杂度,N代表的是非确定性, 例如P 和 NP, exp和Nexp, 甚至 fac和Nfac等等
在算法教学中,我们一般都是用P和NP作为举例,其他复杂度其实也是类似,只不过教学过程中不太用得到,因为复杂度实在是太高,人类比较难以想象,很难举出例子.甚至NP的例子在NPC的情况下就已经非常难以理解了,具体NPC是什么我们下面也会讲解.
-
$x$ 是指在$x$的时间复杂度内存在一个算法能够解决这个问题,具体我们会在下面用P来举例 -
$Nx$ 是指在$x$的时间复杂度内存在一个算法能够验证这个问题,具体我们也会在下面用NP来举例
下面我们以P作为例子来进行具体的讲解.
polynomial time, 指存在一个算法能在多项式时间之内解决
所谓的解决是指,对于一个问题的任何设置,总能在规定的时间范围内找到答案,在P问题的定义下当然就变成了在多项式时间内找到问题的解.
对于一个数组[3,1,2,5], 请问这个数组的最大值是否是$5$?
注意我们的题目全都是是非/决定性问题.
对于这个问题,我们只需要把数组进行遍历,每次记录当前已知的最大值$max$,如果遍历过程中发现当前元素比目前已知的最大值要大则更新当前最大值$max$.
非常容易看出,只需要$O(4)$(4为问题中数组的元素个数)的时间我们就能找到问题的解.对于这个问题,我们发现数组的最大值是$5$,正好符合题目中的预期,因此问题的结果为真/true
我们也可以延伸扩展这个问题到更加通用的一个问题,即: 给定一个长度为n数组A, 请问其中的最大值是否为$x$?
我们可以用同样的算法,对这个通用的问题进行解答,从而获得问题的真假性. 我们发现对于任意一个符合条件的这样的问题,我们都可以在$O(n)$时间内找到问题的解.
因此我们说,这样的问题可以在多项式时间内得到解决,因此这样的问题是属于P类问题.
事实上我们可以直接把这个问题归结为"在数组中找最大值"的问题这样更好理解,但由于要符合问题的通用性,我在这里并没有直接这么说.
对于一个连通图$G$,请问他的最小生成树(MST)的总权值是否小于等于$x$?
与上一个不同,这个例题我直接把问题的抽象形式写了出来,这样一是鼓励大家进行抽象思考,第二是图论问题要用文字简洁的写出来还是有点麻烦的,所以就偷了点懒.
对于这个问题来说,我们只需要找到这个$G$的MST, 无论是使用prim还是kruskal算法,然后将MST的权值加起来,和$x$进行比较即可.得到的答案就是我们的最终是非结果.
例如按照prim算法的性能最差为:
O(VlogV + ElogV) = O(ElogV)
可以近似的看成 多项式时间之内.故而我们称,求解G内MST权值和的问题也是属于P类的.
有一点要注意,对于P类问题来说,我们的最终解可以是真也可以是假,一切都要根据具体的题目和问题来进行回答.即使在规定时间内只能回答出假,由于这个是题目的条件约束导致的,并不会影响我们对于这个题目时间复杂度的判定标准.
全称为nondeterministic polynomial time. 具体是指,对于某一个问题,我不知道这个问题能不能在多少时间内解决, 但我知道,如果给我一个答案(certificate/proof),我可以在多项式时间内去验证这个答案的真伪性(true/false).可以注意这个答案的真假性和决定性问题(decisive problem)的真假性并不是完全一致.
决定性问题的真假性是指决定性问题本身的假设和设问的结果是否为真- 验证NP
答案的真假性是指将给定的答案(certificate/proof)代入源问题中,如果用源问题的假设我们发现给定的答案可以满足源问题的设问,则认为该答案是真的,否则是假的. 具体我们会在后面给出举例.
给定一个图G,求图中是否存在一个path,能够形成一个TSP.
为了能找到一个NP问题,我们直接用经典的TSP作为例子.简单说一下什么是TSP.
TSP是指在一个图中(我们不知道它的连通性,回环性等等),从某一个点出发,是否存在一条通路p,能够不重复,不遗漏地走过每一个节点,并且最后能够回到一开始的起点.
从这个问题的面书上我们就可以感觉问题的复杂度,这个问题乍一看就让人感觉不是很简单,似乎找不到一个简单的算法能够快速地计算出问题的解.我的意思是指,如果我们遍历所有可能的路径,应该总能找到问题一个结果:
- 如果遍历所有路径之后依然没有找到符合条件的路径,我们则认为这个图G不存在这样的TSP
- 如果遍历的过程中我们找到了一条路径,只要有一条路径,我们就判定这个图G存在一个TSP的解
实际上遍历一个无向连通图的时间复杂度为$O(V + E) \leq O(2V)$, ,而这仅仅是最最简单的图的时间复杂度,那么对于有向图,非连通图来说,遍历的时间复杂度则更加诡异. 更不用提这个时间复杂度仅仅是简简单单的的遍历而已,要说组成一个不重复,不遗漏的路径则需要更加高的时间复杂度. 目前科学界对这个问题的时间复杂度定义还没有一个准确的定论,因此解决这个问题似乎成了一个非常复杂的事情
因此,我们很难下定论说可以在多少时间内解决这个问题,更不用提多项式时间了,那么我们可以说这个问题有可能不是P问题.
但是,如果有一个算法之神从天而降,告诉我们某个具体的图G的TSP是这样这样那样那样的,给我们一个答案路径$p$,写在草稿纸上,然后拂袖离去.我们当然可以惊异于这个神奇的天神下凡,也可以把他给我们的答案去验证一下(难道只有理工男才有这样神奇的思维吗?).
我们如获至宝的拿着这个草稿纸,尝试去验证一下天神给我们的答案. 事实上验证这个答案的正确性似乎不是很难, 我们只需要:
- 确定这个答案$p$的所有点V和边E和我们原题的设定是完全符合的,没有作弊和更改题目的存在
- 验证这个答案是否是不重复,不遗漏地走过每一个点
- 最后这个路径会会到起点,形成一个闭合的路径
验证了以上步骤以后如果所有步骤都是真实无疑的,我们可以感谢一下算法之神天神下凡之举,感叹一下大佬果然是大佬.
那么有没有可能大佬给的答案是错误的呢?或者这个大佬根本就是一个装模作样的大佬,他可能是一个平时神神叨叨的脑瘫,看到我们每天琢磨算法心里非常鄙视,因此随便给我们一个答案来恶心我们一下.这个情况当然也是可能的,因此无论是天神下凡还是脑瘫秀智商,我们都要以严谨的态度以符合题目逻辑的方式去验证问题.
如果问题可以在多项式时间完成验证,并证明这个答案是真的,则我们认为这个问题是NP的
另一方面,如果问题尽管在多项式时间可以跑完验证的流程,如果验证出来结果是错的,则我们无法判定该问题是否是NP的,我们需要继续用其他的答案进行同样的验证,直到我们找到一个能验证为真的答案为止.
那么是否存在一些问题,这种问题无论你用什么答案去验证,都无法得到真的回答呢? 当然有可能,我们称这种问题为Yes instance,更数学的定义如下:
定义一个问题
-
$(\exists y \rightarrow \phi(x, y) = 1)\Rightarrow$ x是问题的$\phi$的一个yes instance -
$(\forall y \rightarrow \phi(x, y) = 0)\Rightarrow$ x是问题的$\phi$的一个no instance
给定一个布尔表达式,
可以非常直观的看出, 以上的具体题目非常简单,只需要让z取true的时候,x和y任意取值都能使整个表达式为真. 但事实上如果将表达式泛化后则会得到一个极其复杂的问题:
定义语句, 为若干布尔自变量$X_i$通过$\cup$,$\neg$和
()这几个操作符进行计算. 定义表达式,将若干语句通过$\cap$进行合并 给定一个布尔表达式,以及一系列自变量输入参数$X_i$, 如何设置这些布尔自变量才能使得该表达式为真?
事实上我们并没有一个简单的算法来计算出这些输入值,值得该表达式为真.唯一的办法就是遍历所有可能性来尝试所有布尔表达式的组合.因此该计算的算法复杂度为$O(2^n)$
但是,如果有人能给我们一个答案,写明所有的布尔值排布.我们就可以把这些输入值代入到表达式中,能够非常简单的遍历整个表达式并计算出最终结果.整个过程大概就是$O(n)$的复杂度
总结来说,NP类问题就是,在P时间内可以根据给定的答案来验证源问题是否为真的 那一类问题. 需要强调的是NP问题和P问题并不是互斥的,一个问题需要P时间来验证也可以在P时间内得到解决.如果满足后者条件则我们直接称该问题为P问题而不会在说它是NP问题,因此对P问题的优先级会比NP问题更高一点. 同时我们也可以感觉到,如果一个问题是P问题,那么似乎这个问题比那些NP问题是要难一点的,更复杂一点,好像一般都要难解一点.然而我们也并没有证据证明NP问题一定要比P问题难或者一样难,这就引出了一个世纪大难题 $$ P = NP ? $$ 关于这个问题的具体讨论本文不会多说,有兴趣可以查看其它网上资源
说完了P和NP那么我们要研究的问题主体就已经讨论完毕了,接下来讨论一个衍生的问题. 在说明具体的定义之前让我们来一起看看一个具体的例子:
- 在数组
[1,3,5,2,4]中最小是为1 - 在数组
[1,3,5,2,4]中,升序排序的结果为[1,2,3,4,5]
第一个问题是在一个数组中找到最小值,第二个问题是对数组排序.显而易见的是,第二个问题要比第一个问题更复杂一点点,因为第一个问题复杂度为$O(n)$而第二个问题是$O(nlogn)$. 而事实上,我们可以通过解决第二个问题来间接解决第一个问题,我们只需要对数组进行排序,自然就能找到数组中的最小值.尽管通过这种方式我们花费了更多时间去解决了一个原本可以更轻松解决的问题,看似是非常愚蠢的行为,但值得一提的是,我们不需要再为第一个问题写一个专用的算法.只需要利用第二个问题的算法,将该算法包装为一个黑盒,通过将对应的输入喂给这个黑盒,并从黑盒中取出对应的结果,并抽取数组的第一个元素就能得到我们第一个问题的答案了.
这就是所谓的泛用化,或者也有翻译称为归约.主要方法为:
- 有一个目标问题A,这个问题是你想要解决的问题,这个问题的算法/解法未知
- 已知一个问题B以及这个问题的算法.需要强调的是这个问题比A更加复杂
- 存在一个
简单的方法可以把问题A的输入和输出转化为问题B的输入和输出 - 通过将问题A的输入转化为问题B的输入,利用问题B的算法得出B的解,最后将这个解转化为问题A的解
通过以上这些方法我们可以合理利用一个已知的,更复杂的问题的算法/解法,通过输入输出的对应映射,和对算法解法的调用,成功解出目标问题的答案. 可以想象目标问题应该是更简单的,而已知的问题往往是更复杂的. 利用这种特性我们就可以通过利用复杂问题B的解法来解答所有的简单问题A.这些简单问题A们需要符合一个条件
这些A们和B的输入输出之间存在
简单的映射方式,
再次强调一遍,这个映射方式必须是简单的,换言之,必须要在多项式时间之内完成这一转换
值得一提的是,在讨论这一过程的时候我们往往不会在乎复杂问题更消耗时间这一事情,因为我们往往会认为通过优化算法,使得算法更贴近问题的现实才是学习算法分析的意义所在.而在讨论归约这个题目时,我们要暂时将这一执念抛在脑后.我们要解决的是一个更加宏伟的问题,即心安理得的偷懒 将问题的解法泛用化,找到一切问题的根本所在. 听起来有点哲♂学,有点抽象,但还是很有意思的.
说了这么多关于归约/泛化的例子和概念,让我们一起复习一下它的基本需求
- 存在一个更复杂的问题B的解法
- 想要解出一个更简单的问题A
- 简单问题A和复杂问题B之间存在某种输入和输出的的映射关系,这个映射操作可以在
多项式时间内完成
只要符合以上三个条件我们就说, A可以归约化为B,B的复杂度至少和A一样高,也有可能比A复杂得多得多
那么我们就会开始想象,有没有这样一种问题B,所有的问题(特指P和NP类的问题),都能归约到这个牛逼的究极问题B. 只要我们解出了这个究极问题,我们就能解决世界上的所有decisive problem.
由此我们引出了一个新的复杂度类别NPC.一个问题要被称为NPC问题需要符合以下几个条件
- 必须是一个NP问题
- 必须必所有NP问题都要复杂,难
- 所以其他问题都能归约成这个问题