《量子计算实践—Java样例》第九章:德沃伦斯-约萨(Deutsch-Jozsa)算法

295 阅读33分钟

本章内容涵盖:

  • 从经典函数中获取信息
  • 函数求值与函数属性
  • 对应于经典黑盒函数的量子门
  • 理解德沃伦斯算法和德沃伦斯-约萨算法

在本章中我们讨论的德沃伦斯-约萨算法展示了典型量子算法的一些特点。直接实际应用可能有限,但该算法是解释量子算法通常遵循的逻辑的强大工具。

当解决方案不是问题

你知道168,153这个数是否能被3整除吗?有很多方法可以找到答案。例如,你可以简单地拿一个计算器并获得结果:

168153 / 3 = 56051

这个除法的结果是56,051。但这并不是问题的关键。实际上,我们并不关心这个结果。但多亏了这个计算,我们知道了真正的答案:因为小数点后没有任何数字,所以我们可以得出结论,这个数确实可以被3整除。

还有另一种简单的方法来回答这个问题,你可能了解这个简单的技巧:将组成这个数字的各个数字相加,看这个和是否能被3整除。如果是的话,原始数字也能被3整除。让我们来做一下这个计算:

1 + 6 + 8 + 1 + 5 + 3 = 24

由于24可以被3整除,我们可以得出结论,168,153也可以被3整除。

第一种方法(使用计算器)给我们了除法的结果,并提供了真正的答案。第二种方法(各个数字之和)只提供了真正的答案,而不是除法的结果。

这个问题的重要性在于,在许多情况下,我们关注的是某个东西(例如数字或函数)的特定属性。我们对函数的求值不感兴趣,但我们想以某种方式获取有关函数的信息。评估函数通常是最简单的方法,但通过间接查看函数的属性并从中得出结论可能更有效。

这在量子计算中特别有趣。一个具有n个量子比特的量子计算机在检查一个特定函数时一次只能进行一次函数评估。将量子电路应用于给定的输入量子比特集将导致这些量子比特的状态被修改。对它们进行测量会得到特定的结果,如果你想要新的结果,你需要再次运行电路。尽管我们可以将Hadamard门应用于输入量子比特,将它们带入叠加态-这样我们就可以同时评估处于0和1状态的量子比特-但我们不能魔法般地创建新的量子比特来保存不同情况的信息。对于具有两个量子比特的系统,这在图9.1中有所示。

image.png

内部计算可以包含等效于许多评估的内容,但我们不能同时获取它们。我们受限于n个量子比特的结果。但这通常足以解决问题。对于我们的数字是3的倍数还是不是3的倍数的情况,一个单独的量子比特足以存储答案。我们不需要对除法函数进行评估。

在本章中,我们使用量子计算演示这种方法。我们研究作用于n位的函数f的一个属性,而不关心单个函数评估。我们展示了用传统方法检索属性需要2n-1 + 1次函数评估。通过量子算法,可以用一次评估获得该属性。

我们使用的函数非常简单,并且对于这个问题没有直接的应用案例。但它展示了量子计算的一个重要方面,并且解释了为什么量子计算通常与“指数”复杂性联系在一起。你可以很容易地看出,输入位数越多,用经典方法解决问题就越困难。数字n确实在指数中,并且正如我们在第一章中展示的,指数函数很快会得到巨大的值。如果一个量子算法可以在一次评估中解决相同的问题(或者一般情况下在少于指数数量的方程中解决问题),这对于量子计算机来说是一个巨大的优势。

我们将逐步介绍实现这一目标的算法。我们将遵循图9.2所示的方法。

image.png

首先,我们讨论函数的属性以及如何以经典方式获取它们。接下来,我们将这些函数转换为称为"oracle"的量子块。我们展示了实现这一点的一些要求。一旦我们可以创建一个代表经典函数的量子oracle,这个oracle可以在量子电路中使用。对这个量子电路进行一次评估将得到我们寻找的函数的属性。

函数的属性

在大多数涉及函数的典型情况下,你关心的是找到函数的结果。例如,考虑函数y(x)=x2y(x) = x^2

如果我们想知道这个函数在x = 4和x = 7时的值,我们需要对这个函数进行求值:

y(4)=42=16y(4) = 4^2 = 16
y(7)=72=49y(7) = 7^2 = 49

然而,在某些情况下,函数的求值并不重要,而是关注函数的特性。在这方面,量子算法可能会有所帮助。我们将在第11章中讨论一个例子,即函数的周期性。我们对函数的单个求值不感兴趣,而是对其周期性感兴趣。周期函数是指在固定的周期内,相同的值模式会重复出现。以下是一个例子:

截屏2023-07-21 14.05.20.png

从这个表格中,你可以看出这个函数的周期性为3:对于每个x的值,函数的结果与应用于x + 3的函数相同。这是一个例子,其中函数的属性比函数的单个求值本身更有趣。

常数函数和平衡函数

通常,我们考虑的函数表示为f(x),其中f是作用于一个称为x的输入变量的函数。特定输入的函数求值也称为结果,有时记为y,其中y = f(x)。

在本章中,我们从一个非常简单的函数家族开始,它具有简单的属性。我们从一个仅有单个输入位的函数开始,并且稍后扩展为具有n个输入位的函数。在所有情况下,函数的结果只能是0或1。

我们在这里讨论的函数具有一个特殊的属性:它们要么是平衡函数,要么是常数函数。当结果不依赖于输入时,函数被称为常数函数。在我们的情况下,这意味着对于所有输入情况,结果要么为0,要么为1。当结果在50%的情况下为0,而在其他情况下为1时,函数被称为平衡函数。

德沃伦斯算法(稍后将讨论)处理一个名为f的函数,它以单个位(布尔值)作为输入,并且也产生单个位作为输出。该函数只对0和1进行操作,并且其结果只能是0或1。两个输入选项和两个输出选项的组合为该函数带来了四种可能的情况,我们称之为f1,f2,f3和f4:

f1:f(0)=0andf(1)=0f1: f(0) = 0 and f(1) = 0
f2:f(0)=0andf(1)=1f2: f(0) = 0 and f(1) = 1
f3:f(0)=1andf(1)=0f3: f(0) = 1 and f(1) = 0
f4:f(1)=1andf(1)=1f4: f(1) = 1 and f(1) = 1

根据这些定义,可以看出f1和f4是常数函数,而f2和f3是平衡函数。

在许多经典算法中,了解特定值的函数输出是很重要的。而在许多量子算法中,了解所考虑函数的属性是很有用的。

这是考虑量子算法时所需的"不同思维"的一部分。由于叠加的存在,量子计算机可以同时评估许多可能性;但由于获得结果需要测量,叠加消失,我们回到了单个值。因此,增加的价值在于函数的评估,而不在于函数评估的结果。

在德沃伦斯算法中,我们提供了一个函数,但我们不知道这是什么函数。我们只知道它是f1、f2、f3或f4,但这就是我们所知道的。现在我们需要找出这个函数是常数还是平衡的。我们的任务不是确定所提供的函数是f1、f2、f3还是f4。我们被要求考虑函数的一个属性,而不是函数本身。示意图如图9.3所示。

image.png

在我们能以100%的确定性回答这个问题之前,我们需要多少次函数评估呢?如果我们只做一次评估(只计算f(0)或f(1)中的一个),我们没有足够的信息。

假设我们测量了f(0)并且结果是1。根据之前的表格,在这种情况下,我们的函数可能是f3(平衡函数)或f4(常数函数)。所以,我们没有足够的信息。如果测量f(0)的结果是0,表格显示该函数可能是f1(常数函数)或f4(平衡函数)。同样,这证明了测量f(0)是不足以得出函数是常数还是平衡的结论的。它可能是两者之一。

事实证明,我们需要进行两次经典函数评估,然后才能确定所提供的函数是常数还是平衡的。让我们编写一个Java应用程序来演示这一点。你可以在示例代码的ch09/function目录中找到这个应用程序。

static final List<Function<Integer, Integer>> functions = new ArrayList<>();
 
    static {
        Function<Integer, Integer> f1 = (Integer t) -> 0;                ❶
        Function<Integer, Integer> f2 = (Integer t) -> (t == 0) ? 0 : 1;
        Function<Integer, Integer> f3 = (Integer t) -> (t == 0) ? 1 : 0;
        Function<Integer, Integer> f4 = (Integer t) -> 1;
        functions.addAll(Arrays.asList(f1, f2, f3, f4));
    }
 
    public static void main(String[] args) {
        Random random = new Random();
        for (int i = 0; i < 10; i++) {                                   ❷
            int rnd = random.nextInt(4);
            Function<Integer, Integer> f = functions.get(rnd);           ❸
            int y0 = f.apply(0);                                         ❹
            int y1 = f.apply(1);
            System.err.println("f" + (rnd + 1 + " is a "                 ❺
                    + ((y0 == y1) ? "constant" : "balanced")
                    + " function"));
        }
    }

❶ 准备四个可能的函数。这一步只需要做一次。

❷ 我们将进行10次实验。

❸ 随机选择一个函数,对其实现一无所知

❹ 进行两次函数评估:一次用输入0,一次用输入1

❺ 如果这两次评估的结果相似,函数是常数;否则,函数是平衡的。

在这段代码中,我们在静态块中创建了四个可能的函数。我们这样做是因为我们希望强调函数的创建和确定它们是常数还是平衡的应该被视为两个独立的过程。

在函数创建之后,应用程序真正开始执行。在for循环内,随机选择一个函数。根据两次函数评估,我们可以确定函数是常数还是平衡的。

该应用程序的可能输出如下:

f4 is a constant function
f4 is a constant function
f3 is a balanced function
f1 is a constant function
f2 is a balanced function
f2 is a balanced function
f1 is a constant function
f4 is a constant function
f3 is a balanced function
f2 is a balanced function

正如预期的那样,该应用程序对每个循环都得出了正确的答案。但在每个循环中,我们执行了两次函数评估。正如之前展示的,单次评估是不足以得出函数是否平衡的结论的。

可逆量子门

到目前为止,我们已经讨论了经典函数及其属性。在我们讨论这些函数的量子等效部分之前,我们需要更详细地了解量子门的要求。

在前面的章节中,您学习了并使用了几种量子门。这些门与您在经典计算中遇到的门有许多相似之处。然而,它们也存在一些基本的差异。

量子门是通过利用量子力学的特性实现的,因此它们必须遵守与量子力学相关的要求和限制。量子门的一个关键要求是它必须是可逆的。这意味着当应用一个量子门到给定的初始状态时,应该存在另一个量子门将结果带回到初始状态。在量子系统中,信息不能简单地消失。在特定量子门应用之前在系统中的信息应该是可恢复的。我们在第7章中简要介绍了可逆门的概念。由于这是一个非常重要的概念,我们现在将更深入地探讨这个主题。

到目前为止,我们讨论的所有门都是可逆的。让我们用一个简单的例子来展示这一点:Pauli-X门。将系统带回到原始状态的门在应用Pauli-X门之后是另一个Pauli-X门。我们可以通过两种方式解释这一点:

  • 实验证据
  • 数学证明

实验证据

让我们创建一个简单的量子应用程序,在一个单独的量子比特上应用一个Pauli-X门,然后再应用另一个Pauli-X门。与仅考虑量子比特处于|0>或|1>状态的特殊情况不同,我们将人为地初始化量子比特,使其有75%的机会被测量为1。该电路如图9.4所示。

image.png

这个例子的代码可以在示例代码库的ch09/reversibleX目录下找到。相关代码如下所示:

QuantumExecutionEnvironment simulator =
                new SimpleQuantumExecutionEnvironment();
        Program program = new Program(1);                ❶
        Step step0 = new Step();                         ❷
        step0.addGate(new X(0));
 
        Step step1 = new Step();                         ❸
        step1.addGate(new X(0));
        program.addStep(step0);                          ❹
        program.addStep(step1);
        program.initializeQubit(0,.5);                   ❺
 
        Result result = simulator.runProgram(program);   ❻
        Renderer.showProbabilities(program,1000);        ❼
        Renderer.renderProgram(program);

❶ 创建一个具有单个量子比特的量子应用程序

❷ 第一步(step0)对量子比特应用一个Pauli-X门。

❸ 第二步(step1)对量子比特应用另一个Pauli-X门。

❹ 将这些步骤添加到量子程序中

❺ 使用alpha值为0.5初始化单个量子比特,这导致测量0的概率为25%

❻ 执行量子程序

❼ 渲染运行此程序1,000次的统计结果

运行这个电路1,000次的结果如图9.5所示。正如预期的那样,在应用两个Pauli-X门后,测量到0的概率约为25%,测量到1的概率约为75%。这与我们人为设置的量子比特的初始值相符。

image.png

数学证明

在第4章中,我们解释了将门应用于量子比特的数学等效方式:我们将门矩阵与量子比特的概率向量相乘。假设初始量子比特描述如下:

image.png

或者使用向量表示:

image.png

将两个Pauli-X门应用于这个量子比特,使其进入以下状态。

image.png

其中X是定义Pauli-X门的矩阵。从第四章我们知道矩阵的结构,因此我们可以写成:

image.png

使用矩阵乘法(在附录B中解释),我们可以将其写成如下形式:

image.png

从方程式9.2可以看出,量子比特的最终状态,表示为ψ,可以写成如下形式:

image.png

这与量子比特的初始状态完全相同,如方程式9.1所示。

这表明在应用两个Pauli-X门之后,量子系统的状态与一开始的状态完全相同——在应用这两个Pauli-X门之前的状态。因此,我们已经证明了Pauli-X门确实是一个可逆门,并且通过应用另一个Pauli-X门可以撤销Pauli-X门对量子系统造成的改变。现在,您已经学会了Pauli-X门是一个可逆的量子门,您可以使用相同的技巧来证明您所学过的其他门也是可逆的。

注意:到目前为止,我们介绍的门具有特殊的特性,即它们是它们自己的逆门。这并不总是成立,量子门不一定要是它们自己的逆门。

定义一个“Oracle”(预言机)

在许多量子算法中,我们经常使用术语"oracle"。在创建Deutsch算法时,我们将使用一个oracle,因此现在需要进行一些解释。

"Oracle"用于描述一个量子黑盒,类似于早期介绍的函数(f1、f2、f3和f4),它们可以被看作是经典黑盒一样。在内部,这些函数有一些计算流程;但在程序的主循环中,我们假装不了解内部细节,只是针对不同的输入值对函数进行评估。

同样的概念可以应用于oracle。在内部,一个oracle由一个或多个量子门组成,但通常我们不知道具体是哪些门。通过查询oracle(例如,发送输入并测量输出),我们可以更多地了解oracle的性质。由于oracle由量子门组成,oracle本身也需要是可逆的。

因此,oracle可以被看作是本章前面讨论的黑盒函数的量子等价物。oracle和函数都执行一些计算,但我们不知道这些计算的内部细节。如图9.6所示。

image.png

让我们来看一个简单量子应用中使用的oracle的例子。在使用Strange模拟器时,你通过提供代表oracle的矩阵来定义它。听起来好像我们在欺骗:我们是否真的在定义一些我们后来声称对其一无所知的东西?答案是肯定的。我们确实创建了这个oracle,因为总得有人来创建它。在真实的量子应用中,oracle是一个外部组件,提供给我们使用。

注意:函数的创建和oracle的创建应被视为完全独立的过程。在接下来的算法中,我们假设有人为我们创建了一个oracle。算法本身并不知道oracle是如何被创建的,它的复杂程度如何,等等。这通常令人困惑,因为为了演示算法,我们显然需要一个oracle。然而,创建oracle的复杂性不应被视为算法复杂性的一部分。只需要假设某人(包括你自己、另一位开发者、一个真实的硬件设备,或者自然本身)创建了这个oracle并提供给了你。

之前我们学过,所有的量子门都可以用一个矩阵来表示。一个oracle包含一个或多个门,因此oracle本身也可以用一个矩阵来表示。oracle可以被看作是提供给你的量子电路的一部分。你看不到组成oracle的门,但你可以在你的量子程序中使用它们。

在Strange中,通过提供代表oracle的矩阵来定义它。在真实的硬件情况下,oracle就是直接可用的。在软件模拟中,我们需要以某种方式定义oracle的行为,因此需要提供一个矩阵。图9.7展示了一个oracle如何成为量子程序的一部分。

image.png

我们将创建一些代码,其中我们在一个量子程序中应用一个oracle。我们通过提供一个可能看起来很熟悉的矩阵来创建这个oracle。建议你不要查看矩阵的内容,因为这样会削弱oracle的神秘感。相反,你可以查看整个程序的结果,尝试找出oracle在做什么。

让我们考虑以下代码片段,它取自ch09/oracle的示例中。

QuantumExecutionEnvironment simulator =
           new SimpleQuantumExecutionEnvironment();
    Program program = new Program(2);                ❶
    Step step1 = new Step();
    step1.addGate(new Hadamard(1));                  ❷
 
 
    Complex[][] matrix =  new Complex[][]{           ❸
            {Complex.ONE,Complex.ZERO,
                  Complex.ZERO,Complex.ZERO},
            {Complex.ZERO,Complex.ONE,
                  Complex.ZERO,Complex.ZERO},
            {Complex.ZERO,Complex.ZERO,
                   Complex.ZERO,Complex.ONE},
            {Complex.ZERO,Complex.ZERO,
                   Complex.ONE,Complex.ZERO}
    };
 
    Oracle oracle = new Oracle(matrix);              ❹
 
    Step step2 = new Step();                         ❺
    step2.addGate(oracle);
 
    program.addStep(step1);                          ❻
    program.addStep(step2);
 
    Result result = simulator.runProgram(program);   ❼
    Renderer.showProbabilities(program,1000);
    Renderer.renderProgram(program);

❶ 创建一个需要两个量子比特的量子程序。

❷ 第一步对第二个量子比特应用Hadamard门。

❸ 创建一个包含复数的矩阵。暂时我们不解释这些数字的含义。

❹ 基于这个矩阵创建一个oracle。

❺ 创建第二步,其中应用这个oracle。

❻ 将这两个步骤添加到量子程序中。

❼ 执行该程序并显示其电路以及进行1,000次运行后的结果。

在这里创建的电路就是图9.7中所示的电路。运行这个电路1,000次的结果显示在图9.8中。

image.png

如果你观察这些统计结果,似乎只有两种可能的结果:|00> 或 |11>。从第5章我们记得,一个具有两个纠缠量子比特的电路具有相同的概率:测量到|00>的概率是50%,测量到|11>的概率也是50%。这表明我们创建的oracle,结合初始的Hadamard门,导致了两个纠缠的量子比特。在第5章,你通过在Hadamard门后应用CNot门来创建两个纠缠的量子比特。因此,我们可以得出结论,我们创建的oracle的行为类似于CNot门。如果我们作弊并查看oracle的内容,我们会发现代表oracle的矩阵与CNot门的矩阵相匹配。这个练习表明,我们可以将oracle应用于量子电路中。我们可以在不知道它的内部细节的情况下应用oracle。

从函数到oracle

在Deutsch算法中,我们展示了通过单次评估就可以找出一个给定函数是常数还是平衡的。在此之前,我们需要将经典函数转换为量子操作。

我们不能简单地将一个函数应用于一个量子比特。记住,我们解释过所有的量子门都需要是可逆的。如果一个函数被应用后,无法恢复原始输入,那么它无法在量子电路中使用。因此,该函数首先需要被转换为可逆的oracle。

在本节中,我们将演示如何创建oracles,并在接下来的部分中解释Deutsch算法。类似于将经典函数提供给经典算法一样,我们将一个oracle提供给量子算法。

我们之前在本章中描述的每个经典函数都可以用一个特定的oracle来表示。由于有四种可能的函数,我们也有四种可能的oracles。

构建基于函数的oracle的一般方法如图9.9所示。在这种方法中,我们有一个输入量子比特称为| x>,还有一个额外的量子比特称为| a>。

image.png

该oracle保持| x>量子比特处于其原始状态,而将| a>量子比特替换为a和f(x)之间的异或操作。让我们更详细地研究一下这个oracle:我们将研究它对我们之前定义的四个函数的表现如何。

常数函数

第一个函数f1是一个常数函数,无论输入是什么,它始终返回0。因此,由于对于任何x的值,f(x) = 0,第二个量子比特的输出状态可以简化如下:

af(x)= a  0= aa ⊕ f(x) = a ⊕ 0 = a

得到的oracle可以如图9.10所示。在这种情况下,oracle实际上是一个单位矩阵。在输入和输出之间,|x>和|a>都保持不变。因此,oracle内部的隐藏逻辑可以用图9.11中的图示来表示。

image.png

image.png

因此,代表oracle的矩阵可以写成如下形式:

image.png

该矩阵是一个单位矩阵:在测量量子比特时不会改变概率。将该矩阵与任何概率向量相乘都会得到原始的概率向量。

注意:图9.11中显示的电路并不是唯一能得到单位矩阵的电路。许多其他电路在两个量子比特上操作并将两个量子比特返回相同的状态。例如,对每个量子比特应用两个Pauli-X门将得到完全相同的状态。这是oracle的黑盒特性的一部分:我们不知道其内部细节,通常也不关心这些细节。我们想要研究oracle的特定属性,而不是它的内部实现。

第四个函数f4是一个常数函数,无论输入是什么,它始终返回值1。因此,在应用oracle后,第二个量子比特的输出可以写成如下形式:

af(x)=a1=aˉa ⊕ f(x) = a ⊕ 1 = ā

上方带有横线的变量表示该变量的反相,这在本例中对应着对|a>应用Pauli-X门。因此,这个oracle可以如图9.12所示的图示来表达。

image.png

平衡函数

让我们来看看第二个经典函数f2。这个函数的定义如下:

f(0)=0f(0) = 0
f(1)=1f(1) = 1

这也可以简单地写成 f(x) = x。

将其应用于图9.9中oracle的一般描述,该图示化简为图9.13。

image.png

这正是如果oracle有一个CNot门时得到的状态。因此,对应于f2函数的oracle的一个可能电路如图9.14所示。

image.png

因此,这个oracle的矩阵表示也是CNot门的矩阵表示:

image.png

Deutsch算法

Deutsch算法只需要对oracle进行一次评估,就能知道所考虑的函数是常数还是平衡的。让我们从一个简单的方法开始,假设我们只需要应用oracle并测量结果。

这部分代码位于ch9/applyoracle中。在解释算法之前,我们首先指向代码中创建不同oracle的部分。如前所述,创建oracle不是试图找出一个函数是否平衡的算法的一部分。虽然出于实际原因,我们在同一个Java类文件中创建了oracle和算法,但应强调,创建oracle的人(可能知道问题的答案)和创建算法的人不是同一个人。

从前面的部分,应该清楚有四种不同类型的oracle。无数个oracle可以用来表示本章开始时讨论的简单函数,但它们都对应着我们在前面部分展示的四个门矩阵之一。算法会要求选择一个随机的oracle,构建这个随机oracle的代码如下。

static Oracle createOracle(int f) {                   ❶
    Complex[][] matrix = new Complex[4][4];           ❷
 
    switch (f) {
        case 0:                                       ❸
            matrix[0][0] = Complex.ONE;
            matrix[1][1] = Complex.ONE;
            matrix[2][2] = Complex.ONE;
            matrix[3][3] = Complex.ONE;
            return new Oracle(matrix);
        case 1:                                       ❹
            matrix[0][0] = Complex.ONE;
            matrix[1][3] = Complex.ONE;
            matrix[2][2] = Complex.ONE;
            matrix[3][1] = Complex.ONE;
            return new Oracle(matrix);
        case 2:                                       ❺
            matrix[0][2] = Complex.ONE;
            matrix[1][1] = Complex.ONE;
            matrix[2][0] = Complex.ONE;
            matrix[3][3] = Complex.ONE;
            return new Oracle(matrix);
        case 3:                                       ❻
            matrix[0][2] = Complex.ONE;
            matrix[1][3] = Complex.ONE;
            matrix[2][0] = Complex.ONE;
            matrix[3][1] = Complex.ONE;
            return new Oracle(matrix);
        default:                                      ❼
            throw new IllegalArgumentException("Wrong
                                index in oracle");
    }
}

❶ 当调用这个函数时,需要提供一个整数来指示返回哪种类型的oracle。

❷ 在所有情况下,结果是一个4×4的复数矩阵。

❸ 如果调用者提供0,将返回一个具有与f1对应的矩阵的oracle。

❹ 如果调用者提供1,将返回一个具有与f2对应的矩阵的oracle。

❺ 如果调用者提供2,将返回一个具有与f3对应的矩阵的oracle。

❻ 如果调用者提供3,将返回一个具有与f4对应的矩阵的oracle。

❼ 如果我们执行到这里,说明调用者提供了错误的值,我们会抛出一个异常。

现在我们有了一个根据我们提供的值返回对应oracle的代码,我们可以专注于算法,该算法应该检测oracle是否与一个常数函数或一个平衡函数相关联。

我们从一种简单的方法开始,只是将oracle应用于两个初始为0的量子比特。我们希望结果会以100%的置信度告诉我们底层函数是否是平衡的。

static void try00() {                                    ❶
    QuantumExecutionEnvironment simulator =
            new SimpleQuantumExecutionEnvironment();
    Program program = null;
    for (int choice = 0; choice < 4; choice++) {         ❷
        program = new Program(2);                        ❸
 
        Step oracleStep = new Step();                    ❹
        Oracle oracle = createOracle(choice);
        oracleStep.addGate(oracle);
        program.addStep(oracleStep);
 
        Result result = simulator.runProgram(program);   ❺
        Qubit[] qubits = result.getQubits();
 
        boolean constant =
                  (choice == 0) || (choice == 3);        ❻
 
        System.err.println((constant ? "C" : "B") +
             ", measured = |" + qubits[1].measure() +
             " , " + qubits[0].measure()+">");
    }
}

❶ 这个函数被称为try00,因为它将oracle应用于两个初始状态为|0>的量子比特。

❷ 遍历四种可能的oracle类型。

❸ 创建一个包含两个量子比特的量子程序。

❹ 创建与循环索引“choice”相对应的oracle,并将其添加到程序中。

❺ 执行程序并获取结果。

❻ 基于循环索引“choice”,我们知道oracle是对应于一个平衡函数还是一个常数函数。我们打印这个信息以及两个量子比特的测量结果。

这个应用的结果如下:

C, measured = |00>
B, measured = |00>
B, measured = |10>
C, measured = |10>

请注意,由于我们没有使用叠加态,这些结果总是相同的。

让我们来研究这些结果。我们可以测量到两种可能的结果:结果要么是|00>或|10>。不幸的是,单个结果并不能告诉我们函数是常数(用C表示)还是平衡的(用B表示)。例如,如果我们测量到|00>,函数可能是f1或f2;但由于第一个是常数函数,第二个是平衡函数,我们无法得到我们问题的答案。

我们可以聪明一点,尝试再次运行这个应用程序,但这次我们首先使用Pauli-X门将一个或两个量子比特翻转到|1>状态。这在相同的文件中的代码中可以看到;在查看示例之前,自己创建这个代码是一个很好的练习。

这样做,我们运行了四个版本的简单程序,每个版本对应着两个量子比特不同的初始状态。结果如下所示:

Use |00> as input
C, measured = |00>
B, measured = |00>
B, measured = |10>
C, measured = |10>
 
Use |01> as input
C, measured = |01>
B, measured = |11>
B, measured = |01>
C, measured = |11>
 
Use |10> as input
C, measured = |10>
B, measured = |10>
B, measured = |00>
C, measured = |00>
 
Use |11> as input
C, measured = |11>
B, measured = |01>
B, measured = |11>
C, measured = |01>

如果你分析这个结果,你会得出结论:通过单次评估,这些版本都不足以检测oracle是否对应于一个常数函数还是一个平衡函数。

但是到目前为止,我们还没有使用强大的叠加态。现在我们要使用它。在编写算法的代码之前,图9.15显示了导致这个结果的量子电路。

image.png

我们从两个量子比特开始。第一个量子比特,q[0],将被评估。但是,我们不是在状态|0>下进行两次评估,然后在状态|1>下进行第二次评估,而是对其应用Hadamard变换将其带入叠加态。你可以将此视为在一个量子步骤中同时评估两种可能的值。

第二个量子比特,q[1],最初也是|0>,我们首先通过应用Pauli-X门将其翻转为|1>。然后,在该量子比特上应用Hadamard门。然后,这两个量子比特作为输入传递给我们在前面部分讨论的oracle。在应用了oracle之后,我们舍弃第二个量子比特。在第一个量子比特上,我们应用了一个Hadamard门,然后对该量子比特进行测量。

现在,我们来看一下这个算法的优点。以下陈述可以在数学上得到证明:如果测量结果为0,则我们可以确保oracle所表示的函数是平衡的。如果测量结果为1,则我们知道所考虑的函数是常数函数。

如果你对其中的数学原理感兴趣,数学上可以证明在应用了电路后,第一个量子比特测量为0的概率由以下公式给出:

image.png

如果f是一个常数函数,这将始终得到1。如果f是一个平衡函数,这将始终得到0。

与其证明这一点,我们将创建代码来运行这个电路,针对四个不同的函数进行评估,并测量输出,以验证我们的陈述是否正确。这个算法的有趣之处在于,我们设法使第一个量子比特依赖于所有值的评估。我们没有对所有这些评估进行测量,但这并不是最初的问题。最初的目标是确定一个给定的函数是常数还是平衡的。

下面的代码实现了该算法,取自ch09/deutsch示例:

QuantumExecutionEnvironment simulator =
            new SimpleQuantumExecutionEnvironment();
    Random random = new Random();
    Program program = null;
    for (int i = 0; i < 10; i++) {                       ❶
        program = new Program(2);                        ❷
        Step step0 = new Step();
        step0.addGate(new X(1));                         ❸
 
        Step step1 = new Step();                         ❹
        step1.addGate(new Hadamard(0));
        step1.addGate(new Hadamard(1));
 
        Step step2 = new Step();
        int choice = random.nextInt(4);
        Oracle oracle = createOracle(choice);            ❺
        step2.addGate(oracle);                           ❻

        Step step3 = new Step();
        step3.addGate(new Hadamard(0));                  ❼
 
        program.addStep(step0);                          ❽
        program.addStep(step1);
        program.addStep(step2);
        program.addStep(step3);
        Result result = simulator.runProgram(program);   ❾
        Qubit[] qubits = result.getQubits();             ❿
        System.err.println("f = " + (choice+1) +
              ", val = " + qubits[0].measure());
    }

❶ 循环将被执行10次,每次都使用一个随机的oracle。

❷ 创建一个包含两个量子比特的量子程序。

❸ 第一步对第二个量子比特应用一个Pauli-X门。

❹ 第二步对两个量子比特应用Hadamard门。

❺ 选择一个随机的oracle(从预定义的列表中)。

❻ 将oracle添加到量子电路中。

❼ 对第一个量子比特应用另一个Hadamard门。

❽ 将这些步骤添加到量子程序中。

❾ 执行量子程序。

❿ 对第一个量子比特进行测量,根据其值,我们可以知道oracle是对应于一个常数函数还是平衡函数。

如果你运行这个应用程序,你会看到电路,控制台会显示类似以下的输出:

f = 3, val = 1
f = 3, val = 1
f = 3, val = 1
f = 1, val = 0
f = 4, val = 0
f = 4, val = 0
f = 2, val = 1
f = 1, val = 0
f = 2, val = 1
f = 4, val = 0

在这个输出的每一行中,首先打印函数的类型(f1、f2、f3或f4),然后是第一个量子比特的测量值。正如你所看到的,对于f2和f3(平衡函数),这个值始终为1;而对于f1和f4(常数函数),它为0。

Deutsch-Jozsa算法

Deutsch算法表明一个在经典方法中需要两次评估的特定问题可以通过使用量子算法进行单次评估来解决。虽然这可能听起来有点令人失望,但这个原理是非常有前景的。Deutsch算法可以很容易地扩展为Deutsch-Jozsa算法,其中输入函数不是作用在单个布尔值上,而是作用在n个布尔值上。在这种情况下,函数可以表示为:

f(x0,x1,...,xn1)f(x0, x1, ..., xn–1)

该公式表示函数的输入为n位,每位可以是0或1。我们已经获得了这样的一个函数,并且被告知这个函数要么是常数(即它始终返回0或始终返回1),要么是平衡的(在一半的情况下返回0,在另一半情况下返回1)。

Deutsch算法是这种情况的一个特殊情况,其中n = 1。在这种情况下,只有两种可能的输入情况。如果n = 2,则有四种可能的输入情况。一般而言,在n个输入位时有2n2^n种情况。

在我们能够100%确定函数是常数还是平衡之前,我们需要进行多少次经典评估?假设我们评估了可能情况的一半(2n/22^n/2,即2n12^n–1)。如果至少有一个结果是0,并且至少有一个结果是1,那么我们就知道该函数不是常数,所以它必定是平衡的。但是如果所有的评估结果都是1,我们能得出什么结论?在这种情况下,看起来函数是常数。但我们仍然需要进行一次额外的评估,因为其他所有评估的结果可能都是0。为了100%确定,一个有n位输入的函数需要2n1+12^n–1 +1次评估,然后我们才能得出结论,该函数是平衡的还是常数的。

然而,使用类似Deutsch算法的量子电路,只需要进行一次评估。这一点的重要性在于它表明,对于经典方法需要指数复杂度的问题,量子算法非常有效。

Deutsch-Jozsa算法与Deutsch算法非常相似。它在ch09/deutschjozsa中显示,并且相关代码片段如下所示:

static final int N = 3;                                     ❶
 
...
 
    QuantumExecutionEnvironment simulator =
             new SimpleQuantumExecutionEnvironment();
    Random random = new Random();
    Program program = null;
    for (int i = 0; i < 10; i++) {
        program = new Program(N+1);                         ❷
        Step step0 = new Step();
        step0.addGate(new X(N));                            ❸
 
        Step step1 = new Step();
        for (int j = 0; j < N+1; j++) {                     ❹
            step1.addGate(new Hadamard(j));
        }
 
        Step step2 = new Step();
        int choice = random.nextInt(2);
        Oracle oracle = createOracle(choice);               ❺
        step2.addGate(oracle);
 
        Step step3 = new Step();
        for (int j = 0; j < N; j++) {
            step3.addGate(new Hadamard(j));                 ❻
        }
 
        program.addStep(step0);
        program.addStep(step1);
        program.addStep(step2);
        program.addStep(step3);
        Result result = simulator.runProgram(program);      ❼
        Qubit[] qubits = result.getQubits();
        System.err.println("f = " + choice + ", val = "
            + qubits[0].measure());
    }

❶ 定义我们使用多少输入位(在这种情况下,是三个)。

❷ 创建一个包含N + 1个量子比特的程序。我们需要N个量子比特用于输入位,还需要一个额外的辅助量子比特。

❸ 对辅助量子比特应用一个Pauli-X门。

❹ 对所有的量子比特应用一个Hadamard门,使它们进入叠加态。

❺ 将一个随机的oracle添加到电路中。

❻ 对所有输入量子比特(不包括辅助量子比特)应用一个Hadamard门。

❼ 执行程序并测量第一个量子比特的结果。

当我们有三个输入量子比特时,该算法的电路如图9.16所示。如果我们应用这个电路,可以再次证明测量第一个量子比特得到0的概率由以下公式给出:

image.png

image.png

与Deutsch算法类似,当f(x)是一个常数函数时,该方程式表明测量第一个量子比特得到0的概率是100%。当f(x)是一个平衡函数时,第一个量子比特将总是被测量为1(因为测量得到0的概率为零)。

示例中的代码随机选择了两个预定义的oracle之一。第一个oracle对应于恒等门(Identity gate),这对应于一个总是返回0的常数函数。第二个oracle对应于CNot门,其中在最后一个输入量子比特为1时,辅助量子比特将被交换。

再次强调,我们的目标不是创建这些oracle。你可以假设这些oracle以某种方式提供给你,而你需要找出它们是否对应于常数或平衡函数。

结论

在本章中,你创建了Deutsch-Jozsa算法。虽然这个算法没有直接的实际应用,但你取得了一个重要的里程碑。在本书中首次创建了一个量子算法,可以比相应的经典算法更快地执行任务。真正的加速效果只能在使用真实的量子计算机时才能看到;但是你所创建的算法明确表明,解决一个特定的问题只需要一次评估,而在经典计算中需要指数级的评估次数。

量子计算中两个非常重要但具有挑战性的部分是:

  1. 创建像这样经过验证比相应的经典算法更快的量子算法。
  2. 寻找这些算法的实际应用场景。

在接下来的章节中,我们将讨论两个满足这些要求的算法。

总结

  • 有些问题可以在不计算最终结果的情况下解决。
  • 函数的计算和函数的属性之间存在差异。
  • 经典算法可以区分平衡函数和常数函数,但它们需要进行多次函数计算才能实现。
  • 量子oracle是与经典黑盒函数相关的黑盒。
  • 使用Deutsch-Jozsa算法,你可以检测提供的oracle是否对应于平衡函数或常数函数。