图谱连接

草稿

多项式时间归约

把一个判定问题翻译成另一个判定问题,并保持 Yes/No 答案不变。

concept beginner complexityalgorithmsreductionsnp-hardness

问题从哪里来

假设你已经有一个很快的可达性求解器,但它要求图写成邻接表(adjacency map):

{ A: [B], B: [C], C: [] }, 问:A 能到达 C 吗?

现在收到的输入却是边列表(edge list):

[(A,B), (B,C)], 问:A 能到达 C 吗?

你一定要重新写一个求解器吗?不一定。可以先把边列表实例翻译成邻接表实例,再调用已经可信的求解器,并返回同一个 Yes/No 答案。

使用已有的求解器我已经有邻接表求解器。遇到边列表输入时,是否一定要重新写一个求解器?
源问题 A:HasPathPairList[(A,B), (B,C)]问题: A -> C?源问题 Yes
f按出邻居分组多项式时间翻译器
目标问题 B:HasPathAdjacencyMap{ A: [B], B: [C], C: [] }同一个问题: A -> C?目标问题 Yes
B 求解器Yes同一个问题,不同编码

本页只讲这个想法:一个快速、保持答案的实例翻译器。这个例子是格式适配器,不是困难性证明。

第一个朴素混淆

记号会写成 A <=p B,读作“A 在多项式时间内归约到 B”。学习者最常见的早期错误,是把求解器方向弄反。

如果 A <=p B,源实例从 A 开始,翻译器产生一个 B 的实例,然后你需要 B 的求解器。

给定 A <=p B,需要哪个求解器?

这里先只讲算法转移。困难性箭头稍后再讲。

A <=p B
源问题 A需要回答的实例
f把 A 翻译成 B
目标问题 B翻译后的实例

选择翻译后的实例要送去的那个求解器。

把 A 的实例翻译成 B,使用 B 的求解器,再把这个 Yes/No 答案作为 A 的答案返回。

这里先只讲算法转移(algorithm transfer)。困难性转移的说法要等证明流水线和复杂度模型都看清之后再出现。

它为什么有用

如果没有翻译器,边列表版本的可达性问题看起来像一个新问题:要写新解析、新遍历设置,还要重新证明。

有了翻译器,工作就小很多:把同一张图整理成已有求解器需要的表示,再复用那个求解器。

如果 B 能解翻译后的实例,就不必从零求解 A归约让源实例经过目标问题求解器。
源问题 A:HasPathPairList[(A,B), (B,C)]问题: A -> C?源问题 Yes
f按出邻居分组多项式时间翻译器
目标问题 B:HasPathAdjacencyMap{ A: [B], B: [C], C: [] }同一个问题: A -> C?目标问题 Yes
B 求解器Yes同一个问题,不同编码

同一个模式之后也会用于困难性证据。我们不必对每个目标问题都从零证明,而是让一个被怀疑的源问题通过保持 Yes/No 的翻译器连接到多个目标问题。

核心发明

归约(reduction)不是源问题的求解器。它是一个函数 f,负责改变实例的格式或问题设置,同时保持判定答案。

请把路径例子作为主图像。下面的小算术例子只是为了让表格和代码草图更紧凑:

  • 源问题 TwoNumberSum4:输入 a, b;问 a + b = 4 吗?
  • 目标问题 TargetSum:输入 a, b, target;问 a + b = target 吗?
  • 翻译器:复制 ab,再设置 target = 4
仅用于机制演示的玩具真值表这是格式适配器,不是困难性或硬度证据。
源实例源答案目标实例 f(x)目标答案用途
(1, 3)Yes(1, 3, 4)Yes普通的 Yes 保持行。
(2, 2)Yes(2, 2, 4)Yes多个源问题 Yes 实例都可以正确映射。
(1, 1)No(1, 1, 4)NoNo 答案也必须被保持。
(0, 4)Yes(0, 4, 4)Yes带有 0 的边界感行。
(5, 0)No(5, 0, 4)No另一个用于练习的 No 行。

Yes 行和 No 行都重要。只把 Yes 实例送到 Yes 实例,并不足以构成有效归约。

形式化版本

对判定问题(decision problem)AB,如果存在一个函数 f 满足下面两点,就写作 A <=p B

  1. f 可以在多项式时间(polynomial time)内计算,时间按源实例的编码长度度量。
  2. 对每个源实例 xx in A 当且仅当 f(x) in B

这里 in A 的意思是“这是问题 A 的 Yes 实例”。iff 表示“当且仅当”,所以它同时包含 Yes 保持和 No 保持。

归约就是翻译器 f对每个源实例 x,f(x) 都是一个 Yes/No 答案相同的目标实例。
源问题 A:HasPathPairList[(A,B), (B,C)]问题: A -> C?源问题 Yes
f按出邻居分组多项式时间翻译器
目标问题 B:HasPathAdjacencyMap{ A: [B], B: [C], C: [] }同一个问题: A -> C?目标问题 Yes
B 求解器Yes同一个问题,不同编码
iff 合约量化的是源实例反向方向只适用于被产生出来的目标实例 f(x),不是任意 B 实例。
源问题 A(1, 3): Yes(1, 1): No(5, 0): No
for every xx in A iff f(x) in Bx notin A iff f(x) notin B
B 中 f 的像(1, 3, 4): Yes(1, 1, 4): No(5, 0, 4): No
其他 B 实例不作承诺

这里不要求从任意 B 实例反向翻译回来。

iff 中看起来像反向的那一半很容易被读过头。它说的是配对实例 f(x)。它不表示 B 的任意实例都来自 A,也不要求存在从 B 任意实例翻译回 A 的反向翻译器。

证明方向

一旦已经证明 A <=p B,如果 B 有多项式时间求解器,那么 A 也有多项式时间求解器:

function solveA(x: SourceInstance): boolean {
  const y = reduceAtoB(x);
  return solveB(y);
}

这个源问题求解器只做四件事:接收 x,计算 f(x),调用目标求解器,然后返回同一个 Yes/No 答案。

证明流水线:B 求解器变成 A 求解器

逐步查看开头使用的同一个路径编码例子。

1 / 4
接收 A 中的 x接收边列表形式的可达性实例。源问题 Yes
计算 f(x)按出发节点分组出邻居,构造邻接表。
调用 B 求解器运行已有的邻接表可达性求解器。目标问题 Yes
返回同一答案把同一个答案返回给原来的边列表实例。源问题 Yes目标问题 Yes

接收边列表形式的可达性实例。

接收 A 中的 x接收边列表形式的可达性实例。
计算 f(x)按出发节点分组出邻居,构造邻接表。
调用 B 求解器运行已有的邻接表可达性求解器。
返回同一答案把同一个答案返回给原来的边列表实例。

实现草图

对仅用于机制演示的算术例子,翻译器故意很小:

function reduceTwoNumberSum4ToTargetSum(source: { a: number; b: number }) {
  return { a: source.a, b: source.b, target: 4 };
}

这段代码没有证明任何问题困难。它只是让归约合约可以被测试:每一行都满足 sourceAnswer(x) === targetAnswer(f(x))

证明步骤代码职责
receive-source接收一个源实例 x
compute-target在多项式时间内计算 y = f(x)
solve-targety 上调用问题 B 的求解器
return-answer把同一个 Yes/No 答案返回给 x

正确性直觉

正确性说明了为什么最后一行可以把目标问题答案当作源问题答案返回。

如果 xA 的 Yes 实例,那么 f(x)B 的 Yes 实例。如果 xA 的 No 实例,那么 f(x)B 的 No 实例。因此,目标求解器在 f(x) 上的答案,正好就是 x 所需要的答案。

iff 合约量化的是源实例反向方向只适用于被产生出来的目标实例 f(x),不是任意 B 实例。
源问题 A(1, 3): Yes(1, 1): No(5, 0): No
for every xx in A iff f(x) in Bx notin A iff f(x) notin B
B 中 f 的像(1, 3, 4): Yes(1, 1, 4): No(5, 0, 4): No
其他 B 实例不作承诺

这里不要求从任意 B 实例反向翻译回来。

注意没有承诺的部分:目标求解器可能知道很多从未被这个翻译器产生过的 B 实例。这些无关实例不在本归约的正确性声明里。

复杂度

翻译器必须快,而且它的输出不能把指数级工作藏进一个巨大目标实例里。

下面的模型中,源编码长度是 n,翻译工作量是 n^2,映射后的目标编码长度至多是 n^2,目标求解器花费目标编码长度的三次方。总时间是 n^2 + (n^2)^3 = n^2 + n^6,仍然是多项式。

多项式翻译器加多项式求解器仍是多项式这个简单模型保持目标大小 <= n^2,翻译工作量为 n^2,目标求解时间为 targetSize^3。
n翻译工作量目标编码长度目标求解时间组合时间
5252515,62515,650
101001001,000,0001,000,100
30900900729,000,000729,000,900
映射后大小有多项式上界n^2 + (n^2)^3 = n^2 + n^6

输入大小指编码描述长度,不是输入里某个数值本身的大小。这也是算术例子只作为机制演示的原因。

困难性预告

上面的证明说:A <=p B 加上 B 的快速求解器,会得到 A 的快速求解器。

困难性证据的预告版使用同一句话做反事实思考:如果 B 有快速求解器,那么这个归约会给 A 一个快速求解器;因此,任何让我们怀疑 A 很难快速求解的理由,也会落到 B 上。

困难性预告要放在算法转移之后这里只用入门表述。正式的 NP-hard 定义属于下一个节点。
有效预告:A <=p B如果 B 有快速求解器,A 也会得到一个;因此对 A 快速求解器的怀疑也会落到 B 上。若 B 快,则 A 也快。
错误箭头:B <=p A这条箭头只说明 B 可以使用 A 的求解器,并不能把对 A 的怀疑转移到 B。不能证明 B 困难。

本页不证明 P != NP,不正式定义 NP-hardness,也不声称这些玩具例子本身很困难。

后续链条预览

后续节点会用真正的构造来实例化同一个保持合约:

后续课程链条会反复使用同一个保持合约这里只是预览,不在本页展开构造细节。
没有归约每个目标都要从零证明。
有归约一个被怀疑的源问题可以通过保持答案的翻译器连接多个目标。
Circuit-SAT <=p SAT电路可满足 iff 公式可满足
SAT <=p 3SAT公式可满足 iff 3CNF 可满足
3SAT <=p Clique3SAT 公式可满足 iff 图中有 k-clique

上面的箭头只是路线图。具体构造细节属于那些后续节点。

常见混淆

需要排除的无效捷径

归约同时需要答案保持合约和多项式时间翻译器合约。

单向蕴含不够
missing-reverse-direction

两个 Yes 行保持为 Yes,但一个源问题 No 行变成目标问题 Yes。这不是 iff。

Yes -> Yes: Yes 仍为 YesYes -> Yes: Yes 仍为 YesNo -> Yes: 错误:No 变成了 Yes
指数时间翻译器显示无效状态
在翻译器里先求解显示无效状态
困难性箭头方向错误显示无效状态
解对象 vs Yes/No 答案显示无效状态

请记住这些边界:

  • A <=p B 表示 B 的求解器可以帮助求解 A
  • 归约保持判定答案,不一定保持同样形状的见证对象。
  • 如果翻译器内部做指数搜索,它就不是多项式时间归约。
  • 玩具格式适配器可以讲清机制,但不能证明困难性。

图谱连接

P 与 NP 提供了判定问题和多项式时间的词汇。本节点加入比较工具:在判定问题之间做快速、保持答案的翻译。

局部图谱位置这是本页局部学习地图;淡化的未来节点还不是图数据中的边。
p-vs-np判定问题和多项式时间
->已实现图边
polynomial-time-reductions保持答案的翻译器
->未来后续
np-hardness尚未实现

下一个计划节点 np-hardness 会命名正式的困难性转移规则。它现在还没有链接成路线,因为站点里还没有这个节点。

预测练习

练习归约合约

每张卡都使用本页的一个确定性示例。

边列表 [(A,B), (B,C)] 询问 A 是否能到达 C。翻译后,邻接表仍询问同一个可达性问题。答案被保持了吗?

选择一个答案后显示检查结果。

对仅用于机制演示的玩具行 (1,1),源问题问 1 + 1 = 4,目标问题问 1 + 1 = target 4。必须发生什么?

选择一个答案后显示检查结果。

给定 A <=p B,翻译后可以用哪个求解器来求解 A?

选择一个答案后显示检查结果。

如果 n = 10,翻译工作量是 n^2,目标编码长度也是 n^2。组合后的求解器仍是多项式时间吗?

选择一个答案后显示检查结果。

  1. A <=p B 中,为什么要求解器调用发生在 B
  2. 为什么必须保持 No 实例,而不只是保持 Yes 实例?
  3. 即使目标求解器很快,为什么指数时间翻译器仍然无效?
  4. 对不在 f 的像里的目标实例,本归约到底承诺了什么?

图谱连接 : 多项式时间归约