本章涵盖内容:
- 理解Grover搜索算法
- Grover搜索算法与现有数据存储系统的关系
- 如何在经典Java代码中使用Grover搜索
在本章中,我们回答开发者提出的两个重要问题:
- 何时使用Grover搜索算法是一个好主意?
- 这个算法是如何工作的?
Grover搜索算法是最受欢迎和最知名的量子算法之一。尽管它的名字是“搜索”算法,但这个算法并不是真正意义上替代目前在经典软件项目中使用的搜索算法。在本章中,我们将解释哪些问题可以从Grover搜索算法中受益。阅读完本章后,您将能够判断所涉及的特定应用是否可以使用Grover搜索算法。如果可以,您可以立即使用Strange中的经典API来使用Grover的算法。
我们是否需要另一种搜索架构呢?
许多优秀的库、协议和技术可用于搜索结构化和非结构化数据系统。Grover搜索算法并不与这些技术竞争。
传统的搜索架构
在计算机中,搜索数据库是最受欢迎的任务之一。许多IT应用程序采用三层架构,如图10.1所示。
在这种方法中涉及到的三个层次如下:
- 用户界面(或表示层)允许交互,请求输入并呈现输出。通常是一个独立的桌面应用程序,移动应用程序或网站,但也可以是一个API。
- 中间层处理业务逻辑、规则和流程。这一层处理来自表示层的请求,并可能需要访问数据来处理传入的请求。
- 数据层确保所有数据在数据存储中得到存储和检索。很多时候,数据可以通过开发者友好的API获得,从而可以根据不同的标准以高性能的方式进行搜索或修改数据。
在许多情况下,中间层需要查询数据层以根据特定要求找到特定的数据。许多应用程序的质量和性能很大程度上取决于如何灵活、可靠和高效地处理这些查询。因此,数据存储和检索领域在今天的IT行业中非常重要。
有许多不同的方法来存储和检索数据,这个领域在不断发展。有关系型和非关系型数据库以及SQL与NoSQL方法。量子计算,尤其是Grover的搜索算法,并不提供一种新的存储和检索数据的架构。
警告:尽管Grover的搜索算法的名字暗示它涉及搜索技术,但它并不涵盖通常在讨论搜索架构时讨论的方面。
因此,Grover的搜索算法不是现有搜索软件的替代品。然而,它可以在实现搜索功能的现有或新的软件库和项目中有用。因此,该算法可以在不同的数据库技术中使用。
什么是Grover搜索算法?
现在我们已经讨论了Grover的搜索算法的内容,接下来将解释它是如何运作的。我们将在这里简要介绍,通过后续章节的内容,搜索应用程序和Grover的算法之间的联系将变得清晰明了。
假设我们有一个黑盒子,它需要一个整数作为输入,并返回一个输出。输出通常为0,除了一个特定的输入值(通常标记为w),在这种情况下,输出为1。Grover的搜索算法允许我们以高效的方式检索这个特定的输入值。
黑盒子的概念如图10.2所示。黑盒子会检查所提供的输入是否等于w。如果是,则输出为1。否则,输出为0。重要的是要意识到我们不知道黑盒子内部是如何工作的。它可能包含一个非常简单或非常复杂的算法。我们必须假设有人创建了这个黑盒子并交给了我们,我们并不知道它是如何被创建的。实际上,如果我们是黑盒子的创造者,那么编写算法来检索w的值就没有意义了,因为我们已经使用该值来创建了黑盒子。
不知何故,黑盒子包含有关值w的信息,并且通过以巧妙的方式查询它,Grover的搜索算法可以检索该值。Grover的搜索算法的输入不是一个数字、搜索查询或SQL字符串,而是我们刚刚讨论过的黑盒子。这在图10.3中有所解释。
接下来,我们将解释Grover的算法如何帮助解决传统的搜索问题。我们从一个经典的搜索问题开始,并逐步优化,直到我们可以引入Grover的搜索算法。
经典的搜索问题
在大多数企业 IT 案例中,会使用多个数据库,其中包含表和行。为了演示目的,我们将创建一个简单的数据存储,其中包含人员的年龄和国家信息;参见表 10.1。
搜索应用程序可以使用这些数据回答以下问题:
- 谁是36岁并住在澳大利亚的人?
- 有多少人住在俄罗斯?
- 是否有一个名叫“Joe”的人住在希腊?
- 给我所有年龄大于34岁的人的名字。
我们将创建一个应用程序来回答一个具体的问题:“找到年龄为29岁且住在墨西哥的人。”从表10.1中,您可以看到答案是“Albert”。
注意:与此问题匹配的 SQL 查询可能是 SELECT * FROM PERSON WHERE PERSON.AGE=29 AND PERSON.COUNTRY=MEXICO。我们将使用 Java 代码而不是 SQL 查询,因为这样的方法允许我们逐步介绍 Grover's 搜索算法背后的思想。
我们首先使用传统方法来回答这个问题。然后,我们重新表述问题,以便使用更接近 Grover's 算法的更功能性方法来处理它。最后,我们使用 Grover's 算法来实现这个搜索的功能。这个过程对应于图10.4中所示的心智模型。
一般准备工作
即将展示的例子中会共享一些通用的代码,我们不会在每个例子中都重复展示它们。在本节中,我们将讨论这些通用代码,这样在本章的其余部分,我们可以将重点放在搜索算法本身上。
PERSON类 在开始编写搜索功能之前,我们先定义我们要处理的数据。表中的每一行表示一个人,因此我们创建一个名为Person的Java类。以下代码可以在ch10/classicsearch示例中找到。
public class Person {
private final String name; ❶
private final int age; ❷
private final String country; ❸
public Person(String name, int age, String cntry) { ❹
this.name = name;
this.age = age;
this.country = cntry;
}
public String getName() { ❺
return this.name;
}
public int getAge() { ❻
return this.age;
}
public String getCountry() { ❼
return this.country;
}
}
❶ 一个人有一个名字。
❷ 一个人有一个年龄。
❸ 一个人住在一个国家。
❹ 创建Person对象时,需要定义这三个属性。
❺ 返回人的姓名的方法。
❻ 返回人的年龄的方法。
❼ 返回人所在国家的方法。
我们将在接下来的所有示例中使用这个Person类。
创建数据库 虽然在Java中有许多高质量的数据库库可用,但在本示例中,我们将坚持使用一个非常简单的数据库表示。所有Person类的实例都存储在一个简单的Java List对象中,因为这是Java平台的标准类,我们希望避免引入不必要的依赖来理解量子计算。正如我们之前提到的,Grover搜索算法的目标不是创建另一个经典数据库库。相反,我们将在不依赖于特定类型数据库的情况下解释该算法。
以下代码填充我们的数据库:我们只需将若干Person实例添加到List中,这样List就成为我们的数据存储。您可以在ch10/classicsearch中的Main.java文件中找到这段代码。
List<Person> prepareDatabase() {
List<Person> persons = new LinkedList<>();
persons.add(new Person("Alice", 42, "Nigeria"));
persons.add(new Person("Bob", 36, "Australia"));
persons.add(new Person("Eve", 85, "USA"));
persons.add(new Person("Niels", 18, "Greece"));
persons.add(new Person("Albert", 29, "Mexico"));
persons.add(new Person("Roger", 29, "Belgium"));
persons.add(new Person("Marie", 15, "Russia"));
persons.add(new Person("Janice", 52, "China"));
return persons;
}
我们在所有的示例中都使用这个方法。当您调用这个方法时,您将获得一个匹配预定义表格10.1的Person项目的列表。
搜索列表
现在,我们编写搜索数据存储以找到原始问题“找到年龄为29岁且居住在墨西哥的人”的典型方法的代码。在给定一个可能是问题答案的人员列表的情况下,以下方法会遍历所有的人员,直到找到满足条件的人员:这个方法也可以在ch10/classicsearch中的Main.java文件中找到。
Person findPersonByAgeAndCountry(List<Person> persons,
int age, String country) {
boolean found = false; ❶
int idx = 0; ❷
while (!found && (idx<persons.size())) { ❸
Person target = persons.get(idx++); ❹
if ((target.getAge() == age) && ❺
(target.getCountry().equals(country))) {
found = true; ❻
}
}
System.out.println("Got result in "+idx+" tries"); ❼
return persons.get(idx-1); ❽
}
❶ 布尔变量,指示我们是否已经得到了结果
❷ 索引,告诉我们正在检查的元素位置
❸ 只要我们没有结果且索引仍然小于总元素数,我们执行以下循环。
❹ 从列表中获取正在考虑的元素
❺ 检查该元素的属性(年龄和国家)
❻ 如果属性匹配,我们将布尔变量翻转为true,以便不需要无谓地执行循环。
❼ 打印评估的次数
❽ 将结果返回给调用者
注意:如果您熟悉Java Stream API,您可能会注意到可以使用它来实现相同的功能。请继续阅读;我们将在下一节回到这个问题。在这种情况下,过程化的方法更容易解释,并且它让我们能够计算需要多少次评估。
如果人员列表包含答案,我们可以保证这个函数返回正确的结果。如果我们幸运的话,正确的人是列表中的第一个,我们可以在while循环内单次执行就得到答案。如果我们运气不好,正确的人是列表中的最后一个,那么这个函数需要n次评估,其中n是列表中元素的数量。平均而言,该算法将在返回正确结果之前需要n/2次评估。
classicsearch应用程序的主方法运行search函数10次,并且每次打印所需的评估次数。调用findPersonByAgeAndCountry方法的函数如下所示:
void complexSearch() {
for (int i = 0; i < 10; i++) {
List<Person> persons = prepareDatabase();
Collections.shuffle(persons);
Person target = findPersonByAgeAndCountry(persons, 29, "Mexico");
System.out.println("Result of complex search= " + target.getName());
}
}
注意,在进行查找操作之前,我们对人员列表进行了洗牌,以确保结果是真正随机的。因此,运行该应用程序的输出类似于以下内容:
Got result after 8 tries
Result of complex search = Albert
Got result after 1 tries
Result of complex search = Albert
Got result after 2 tries
Result of complex search = Albert
Got result after 3 tries
Result of complex search = Albert
Got result after 5 tries
Result of complex search = Albert
Got result after 7 tries
Result of complex search = Albert
Got result after 2 tries
Result of complex search = Albert
Got result after 1 tries
Result of complex search = Albert
Got result after 2 tries
Result of complex search = Albert
Got result after 5 tries
Result of complex search = Albert
使用函数进行搜索
前面一节的示例代码非常灵活:我们可以通过为 findPersonByAgeAndCountry 方法提供不同的年龄或不同的国家来轻松修改搜索条件。然而,这并不是 Grover 的搜索算法的工作方式。使用 Grover 的搜索算法,我们不提供搜索参数,而是必须提供一个单一函数,在所有输入情况下都返回 0,并且只有在一个特定输入情况下返回 1。
在本章中,我们经常使用变量 w 来表示导致函数评估结果为 1 的输入情况——换句话说,w 就是我们要找的值。我们可以这样定义它:
在本节中,我们修改之前的经典示例,以使用函数式方法,然后将其在概念上映射到下一节的量子算法。
我们执行相同的搜索查询,但不是逐个检查条目的属性,而是对每个条目应用一个函数。当函数的评估结果为 1 时,我们知道我们找到了正确的条目。
我们使用 Java 的 Function API 来实现这一点。由于函数将始终评估为整数(0 或 1),我们使用 ToIntFunction 接口。首先,我们需要创建这个函数。
ToIntFunction<Person> f29Mexico ❶
= (Person p) -> ❷
((p.getAge() == 29) &&
(p.getCountry().equals("Mexico"))) ? 1 : 0; ❸
❶ 创建一个 ToIntFunction,该函数以 Person 对象作为输入,并返回一个整数作为输出。
❷ 当应用该函数时,参数 p 包含提供的 Person 对象。
❸ 如果提供的 Person 对象的年龄为 29,且国家为墨西哥,该函数返回 1。在其他所有情况下,函数返回 0。
注意:这是一个针对特定问题的固定函数。如果我们想要找到年龄为36岁的人,我们需要创建一个新的函数。
现在我们有了这个函数,我们可以编写一些Java代码,对人员列表进行迭代,并在每个条目上应用该函数,直到函数返回1——这意味着找到了正确的答案。以下是代码示例:
Person findPersonByFunction(List<Person> persons,
ToIntFunction<Person> function) {
boolean found = false;
int idx = 0;
while (!found && (idx<persons.size())) {
Person target = persons.get(idx++);
if (function.applyAsInt(target) == 1) {
found = true; ❶
}
}
System.out.println("Got result in "+idx+" tries");
return persons.get(idx-1);
}
❶ 与之前的示例不同,我们将函数应用于目标。当函数返回1时,我们知道目标是正确的结果。
虽然方法不同,但所需的时间量与前一个算法相似。我们仍然遍历列表中的每个人,检查考虑的人的年龄和国家是否满足我们的条件。
第二种方法更接近我们在下一节中讨论的量子方法。我们不需要提供多个参数,而是提供一个函数。搜索算法不必创建该函数,但必须对其进行评估。这两种方法之间的差异在图10.5中突显出来。
请注意,我们在图中称之为搜索函数的实现仍然使用的是经典代码。但是,它使我们更接近Grover的搜索算法在量子计算机上的工作方式。
提示:正如在前一节中暗示的,我们可以使用Java Stream API重写搜索方法,使其更加函数化。最终结果将与之前显示的结果相同。
与其显式地遍历所有候选者(或至少直到找到匹配项),我们可以使用Java Stream API,如下面的代码片段所示:
Person findPersonByAgeAndCountry(List<Person> persons,
Function<Person> function) {
return persons.stream() ❶
.filter(p -> function.applyAsInt(p) ==1 ) ❷
.findFirst().get(); ❸
}
❶ 使用Java Stream API创建一个包含所有人员的流,我们将在接下来的代码中对其进行评估。
❷ 逐个过滤流中的所有人员实例,并仅保留在应用提供的函数时返回1的人员。
❸ 如果有多个候选者满足条件,我们选择第一个并返回该人员。
在这个代码片段中,我们使用Java 8中引入的Stream API来过滤所有可能的选项,只返回具有所需年龄和国家的人员。关于Java Stream API的讨论在网上有很多资源,但这本书不涵盖该内容。
量子搜索:使用Grover的搜索算法
Grover的算法与前一节讨论的基于函数的搜索有一些相似之处。当提供一个与前面描述的函数类似的黑盒子(或量子Oracle)时,Grover的算法可以在大约√n步内返回唯一的输入w,该输入将导致函数的评估结果为1,每一步都需要进行一次Oracle评估。
注意:换句话说,虽然经典搜索算法需要平均n/2次函数评估,但Grover的算法在√n次函数评估中达到了相同的目标。
对于小型列表,这并不令人印象深刻。一个包含八个元素的列表需要平均四次函数评估进行经典搜索,而量子搜索则需要大约三次评估。然而,对于大型列表,优势变得明显。一个包含100万个元素的列表需要平均50万次评估,而在最坏的情况下,可能需要100万次经典评估。而使用Grover算法只需要约1000次量子评估。
注意:Grover的搜索算法需要预先定义的函数评估次数,我们后面会证明这个次数是√n。与经典方法相反,没有一个最好情况的一次评估或最坏情况的n次评估。在量子应用中,我们只能进行一次测量。如果在进行了最优步数之前尝试查看结果,当我们测量到的数值是错误的时候,我们将无法继续进行量子计算。因此,所需的评估次数是固定的。
经典搜索与使用Grover算法的量子搜索所需评估次数之间的差异在图10.6中进行了可视化。我们只展示了包含最多100个元素的列表的经典搜索和Grover搜索算法之间的差异。正如您所看到的,列表的大小越大,差异变得更加显著。因此,Grover的算法在包含大量元素的列表中特别有用。
在本章的结尾,我们将解释Grover的算法是如何工作的。对于开发者来说,了解算法何时适用通常比了解它的工作原理更重要。因此,我们首先解释如何使用Strange中内置的Grover功能,它隐藏了底层实现细节。
Strange量子库包含一个经典方法,其底层使用Grover的搜索算法来实现搜索操作。该经典方法的签名类似于我们之前在本章中讨论的示例的签名:
public static<T> T search(List<T> list, Function<T, Integer> function);
该方法接受两个参数作为输入:
- 类型为T的元素列表,其中T可以是Person或任何其他Java类。
- 一个函数,它以类型T的元素作为输入,并返回1(如果提供的输入是我们要查找的元素)或0(在其他所有情况下)。
以下代码片段来自ch10/quantumsearch示例,展示了我们如何使用这个方法:
void quantumSearch() {
Function<Person, Integer> f29Mexico ❶
= (Person p) -> ((p.getAge() == 29) &&
(p.getCountry().equals("Mexico"))) ? 1 : 0;
List<Person> persons = prepareDatabase(); ❷
Collections.shuffle(persons); ❸
Person target = Classic.search(persons, f29Mexico); ❹
System.out.println("Result of function Search = " ❺
+ target.getName());
}
❶ 创建一个类似于前面示例中的函数。
❷ 创建初始数据库。
❸ 随机打乱数据库中的元素顺序。
❹ 调用search方法,该方法在内部调用了Grover的搜索算法。
❺ 打印结果。
强调一点,所提供的函数是在算法外部创建的。在这种情况下,函数被称为f29Mexico,而Classic.search方法不需要了解该函数的内部。该函数被评估,但对于算法来说,这个评估是一个黑盒。
概率与振幅
在我们解释Grover搜索算法为何能够检测预期结果之前,我们需要讨论概率与量子系统实际状态之间的关系。我们将解释状态向量和概率向量之间的区别,状态向量描述了量子系统在每一时刻的状态,而概率向量包含测量特定结果的概率。
概率(Probabilities)
在本书中,我们强调了概率的重要性。在应用量子电路后,我们将得到一些可能处于不同状态的量子比特。概率向量描述了我们测量特定值的可能性。许多量子算法的目标是以一种方式操纵概率向量,使得测量结果与原始问题相关性很高。
在Grover的搜索算法中,如果我们想要在2n个元素的列表中进行搜索,我们需要n个量子比特。例如,如果我们的列表有128个元素,则需要7个量子比特。如果我们要处理130个元素,则需要8个量子比特,以此类推。应用Grover的算法后,我们希望得到一组量子比特,在测量时返回我们在列表中正在搜索的元素的索引。这在图10.7中对最多64个元素的列表进行了说明。
假设我们正在搜索的元素的索引为25。初始时,所有量子比特的值都是0。应用Grover的算法后,我们对量子比特进行测量,希望测量到的值是0、1、1、0、0和1,这是25的二进制表示。
使用6个量子比特,有 = 64种可能的结果,因此我们有一个包含64个元素的概率向量。Grover的算法的目标是最大化第25个元素的值,并尽量减小其他所有元素的值。我们将展示,在大多数情况下,找到正确值的概率非常高,但不是100%。
Grover的搜索算法需要多个步骤。我们将展示每个步骤都会增加找到正确答案的概率。
概率可以用数字或对应的水平条形图来表示。概率为1表示该特定结果必定被测量到,对应于填充的水平条形图。概率为0意味着没有机会测量到该特定结果,对应于空的水平条形图。0和1之间的数字对应于部分填充的水平条形图。
如果我们有一个有三个量子比特的系统,我们有八种可能的结果。图10.8展示了该系统的两组不同的概率,分别使用数字向量和条形图表示。请注意,所有概率的总和应等于1。
振幅
概率向量中的概率是实数、非负数。目前,我们已经成功地隐藏了量子比特与其概率向量之间的内部关系。为了理解Grover搜索算法利用了什么,有必要对概率背后的内容进行一些解释。再次强调,我们不会深入探讨数学或物理,但我们将解释为什么理解仅概率并不是全部内容的重要性。
状态向量中的值实际上是复数。一个复数 c 包含实部和虚部,表示为 c = a + ib,其中 i 是定义为 i = √–1 的虚数单位。
在这个等式中,a 和 b 都是实数。(有关复数的更多信息,请参阅 en.wikipedia.org/wiki/Comple… 。)
概率向量只包含实数和非负数。我们如何将状态向量中的值转换为概率向量?这是通过取状态向量中的值的模的平方来完成的。
复数的模定义如下:
这相当于勾股定理(参见 en.wikipedia.org/wiki/Pythag…),在直角三角形中,斜边的长度被计算为其他两边长度的平方和的平方根,如图10.9所示。这个定理可以用著名的方程式: 来描述,并且还可以通过说明 c 是正交向量 a 和 b 的矢量和来可视化。
因此,如果我们用p表示概率,我们可以对上述方程进行平方,得到。
在这个方程中,a和b都是实数。因此,概率p也将始终是一个正数。当我们知道a和b时,计算p很容易。然而,给定概率p时,许多a和b的组合会导致相同的p值。即使在状态只包含实部的简单情况下(例如,b = 0),两个不同的a值也会导致相同的p值;a和- a都给出相同的概率:
在图10.10中,展示了状态向量和概率向量之间的差异,这是一个简单的情况,其中状态向量只包含实数值。状态向量中有四个条目的值为0,三个条目的值为0.5,一个条目的值为-0.5。通过对这些值进行平方,我们得到了同一图中的概率向量。仅仅通过观察概率向量,我们无法区分值为0.5的状态和值为-0.5的状态。
NOTE:概率向量包含关于特定结果被测量的可能性的信息。它基于状态向量,但后者包含有关量子系统内部状态的更多信息。这种内部状态对于许多量子算法都很重要。
我们现在讨论Grover搜索算法的不同步骤。
Grover's search算法背后的原理
Strange中可用的Classic.search方法允许我们仅使用经典计算来使用Grover's搜索算法。它的主要好处之一是它使开发人员能够理解哪些问题可以从Grover's搜索中受益。
对于大多数开发人员来说,了解量子算法的内部工作可能不太重要,但以下是一些基本了解对我们有益的原因:
- Grover's搜索算法不需要经典函数作为输入,而是使用与经典函数对应的量子Oracle。我们将在接下来的章节中讨论这个Oracle。
- 了解Grover's搜索算法如何利用量子计算特性有助于我们创建或理解其他量子算法。
在接下来的章节中,我们将解释Grover's搜索算法的各个部分。但是数学证明将被省略。
运行样例代码
ch10/grover目录中的代码允许您逐步运行Grover's搜索算法。我们使用这些代码来解释算法。算法的实现在一个名为doGrover的方法中,其签名如下:
private static void doGrover(int dim, int solution)
这个方法的第一个参数dim指定了应该涉及多少个量子位。第二个参数solution指定了我们正在搜索的元素的索引。再次强调,在真实场景中,提供我们要寻找的答案(解决方案)是没有意义的。应该有人提供给我们一个黑盒函数。不过,在这个例子中,代码使用解决方案来构建黑盒。
该示例的主要方法非常简短:
public static void main (String[] args) {
doGrover(6,10);
}
我们只需调用doGrover方法,并指定我们有一个6个量子位的系统(这样我们可以容纳一个包含个元素的列表),目标元素位于索引10处。当我们运行该示例时,将显示量子电路,展示算法的步骤,以及每个步骤后的概率向量。图像类似于图10.11。
简要地说,我们将解释电路及其可视化,但首先我们快速查看结果。图的右侧显示了在应用算法后的量子位。量子位1和3(表示为q[1]和q[3])被测量为1的概率非常高(99.8%),而所有其他量子位被测量为1的概率非常低(0.2%)。因此,在测量量子位时,很有可能得到以下序列:
0 0 1 0 1 0
请注意,此序列在左侧显示最高阶的量子位(q[5]),在右侧显示最低阶的量子位(q[0])。将此二进制序列转换为十进制数是通过在相应的量子位为1时加上二的幂次来实现的:
001010
这是数字10的二进制表示。因此,Grover's search算法返回了我们正在搜索的元素的索引。
从图10.11中,我们还可以清楚地看出该算法包含一个被重复多次的步骤。其流程如图10.12所示。
每次调用该步骤都会应用一个量子oracle,用O表示,和一个扩散算子,用D表示。量子oracle与我们所给出的函数相关联,这将在本章后面解释,扩散算子也会在后面解释。
在每次调用该步骤后,都会生成概率向量。在第一步之后,该步骤会对每个量子比特应用Hadamard门,所有选项的概率都相同。在第二步之后,所有选项的概率都相对较低。索引10处的元素概率较高,但仍然较低。因此,如果在第一步之后测量系统,测量到正确答案10的可能性还是相当大,但测量到其他答案的可能性更大。
然而,随着每一步的进行,测量到10的概率会增加。在最后一步,测量到10的概率为99.8%。
在stepbystepgrover示例中,与前面讨论的示例非常相似,但它处理的是仅有四个元素的列表,因此可以通过两个量子比特来处理。这使得解释更加简单,我们在以下子节中使用该示例。请注意,我们还添加了更多的概率可视化。在每一步之后,都会生成概率向量,结果如图10.13所示。
超position (叠加态)
Grover的算法中的第一步是将所有量子比特带入超position状态。这种方法在量子算法中经常使用,因为它允许同时处理不同的情况。
在我们的两个量子比特示例中,两个量子比特最初的状态为|0>。因此,初始状态向量可以表示为:
概率向量是通过将状态向量中的每个元素取平方得到的,它看起来完全相同。1的平方是1,0的平方是0。该向量中的第一个元素对应于在|00>状态(也就是初始状态)下测量两个量子比特的概率。
在对两个量子比特同时应用Hadamard门之后,状态向量变为:
该向量中的每个元素都有幅度1/2或0.5。在对应的概率向量中,每个元素等于1/4或0.25,这是1/2的平方。因此,在应用Hadamard门后,概率向量可以表示为:
提示:状态向量显示振幅,而概率向量显示概率。对于实数,概率是振幅的平方。这在图10.14中的概率信息门中也得到了展示。
量子Oracle
经典版的Grover搜索算法的主要要求是我们提供一个函数,对于一个特定的值返回1,对于其他所有值返回0。你学到了Grover搜索算法将这个函数视为黑盒子,对这个函数的内部没有任何了解。然而,这是一个经典函数。如果我们真的想使用量子算法,我们需要一个与这个经典函数相关联的量子Oracle。
图10.15形象地展示了Grover搜索算法经典版本和量子版本之间的区别:在经典版本中,黑盒子由一个经典函数实现,而在量子版本中,黑盒子由一个量子Oracle实现。显然,代表黑盒子的经典函数与代表相同黑盒子的量子Oracle之间存在关联。
与经典函数f(x)相关的量子Oracle执行以下操作:对于任何不是特定值w的|x>,f(x)返回0,因此原始值|x>保持不变。如果值w经过了Oracle,结果为-|x>。
让我们给出一个具体的例子。假设我们有一个包含四个元素的列表。在这种情况下,我们需要两个量子位(因为2的2次方等于4)。我们希望找到的元素的索引是2。因此,我们将传递给经典算法的函数如下所示:
这对应于以下矩阵定义的Oracle:
在示例中创建这个量子Oracle的代码如下:
static Oracle createOracle(int dim, int solution) { ❶
int N = 1<<dim; ❷
System.err.println("dim = "+dim+" hence N = "+N);
Complex[][] matrix = new Complex[N][N];
for (int i = 0; i < N;i++) { ❸
for (int j = 0 ; j < N; j++) {
if (i != j) {
matrix[i][j] = Complex.ZERO; ❹
} else {
if (i == solution) {
matrix[i][j] = Complex.ONE.mul(-1); ❺
} else {
matrix[i][j] = Complex.ONE; ❻
}
}
}
}
Oracle answer = new Oracle(matrix); ❼
return answer; ❽
}
❶ 这个方法创建了一个神谕(一个特殊的门),并且它会被调用时带上一个维度参数(量子比特的数量)和正确的解(当应用于经典函数时会返回结果1的值)。
❷ 神谕由一个尺寸为N的矩阵表示:例如,对于三个量子比特,我们需要一个8×8的矩阵。
❸ 循环遍历矩阵的所有行(通过索引i)和所有列(通过索引j)。
❹ 如果考虑的元素不是对角元素,它的值为0。
❺ 如果考虑的元素是对角元素,并且它所在的行(因此也包括列)与正确的解匹配,矩阵元素应该为-1。
❻ 如果考虑的元素是对角元素,但它所在的行(因此也包括列)与正确的解不匹配,矩阵元素应该为1。
❼ 通过将矩阵传递给其构造函数来创建神谕。
❽ 返回神谕。
当将这个函数应用于我们有两个量子比特(dim = 2)且正确解为2(solution = 2)的示例时,你可以验证这个函数创建的4×4矩阵与之前显示的矩阵相匹配。
在将这个门应用于之前获得的概率向量之前,我们将其分别应用于一个包含错误值的状态向量和一个包含正确值的状态向量。由于正确值是2,表示1的状态向量是错误的状态向量。正如之前所述,应用经典函数f(1)的结果是0;由于神谕的定义,我们期望将神谕应用于|01>时不会改变输入值。让我们通过以下矩阵乘法再次确认:
正如你所看到的,状态没有改变。接下来,我们将这个神谕应用于表示2的状态向量,因为经典函数f(2)返回1。状态向量2对应于量子比特序列|10>,因此让我们将量子神谕应用于这个向量:
进行矩阵乘法显示状态向量被倒置了。
提示:从这个简单的例子中,你可以看到虽然状态向量发生了改变,但概率向量仍然与原始输入相同。1的平方等于-1的平方,所以仅仅通过查看概率向量,我们不会注意到任何差异。
理论上,我们可以将这个神谕应用于表示可能索引的每一个状态向量。在除一种情况外,其它所有情况下,结果与输入相同。当我们将神谕应用于正确的值时,结果将被倒置。然而,这意味着我们需要平均N/2次评估才能找到正确的值。使用之前创建的叠加态,我们可以将神谕应用于所有可能输入状态的组合。
将这个矩阵与在应用Hadamard门后得到的状态向量相乘,结果如下:
请注意,这个向量中的第三个元素对应于状态|10>(即值为2),现在变为负数了。然而,如果我们查看概率向量,所有元素仍然都等于1/4,正如图10.16所示。
量子神谕不改变概率。如果我们现在对系统进行测量,我们将有相同的机会测量任何值。然而,量子电路本身使用的是振幅,而这些振幅被修改了。在下一步中,我们利用了这一点。
这种情况展示了状态向量(包含振幅)与概率向量(包含概率)之间的一个非常重要的区别。通常我们谈论概率,但在这种情况下,让我们深入研究一下振幅。
图10.17显示了应用量子神谕后的状态向量,其中四个不同的振幅表示为水平线。向右的线表示正振幅,向左的线表示负振幅。
概率是振幅的模的平方。所以,如果振幅是0.5,概率就是0.25。如果振幅是-0.5,概率也是0.25,正如图10.16所示。
注意:通过将量子神谕应用于所有可能状态的叠加态,我们将量子系统置于一种状态,其中所需的结果与所有其他可能结果不同。然而,我们目前还不能测量这个结果。
格劳弗扩散算子:增加概率
格劳弗搜索算法的下一步是将扩散算子应用于系统的状态。这个算子在某种程度上使得量子系统的隐含信息更加显式。从之前的步骤中记得,系统已经包含了我们需要的信息(所需结果的入口具有负振幅),但我们还不能测量它(所有概率相等)。这个扩散算子可以通过应用量子门或创建其矩阵来构造。(你可以在Strange或ch10/grover示例的代码中看到它的实现。)
这段代码可以在ch10/grover示例中的Main类的createDiffMatrix(int dim)方法中找到。得到的扩散算子是一个数学构造,最终需要增加接收正确结果的概率,同时减小所有其他概率。这个算子背后的数学概念对于开发者来说不太重要,所以我们不详细解释。相反,我们在高层次上解释这个算子的作用。你可以在网上找到证明;例如,在卡内基梅隆大学的这篇关于格劳弗算法的文章中:www.cs.cmu.edu/~odonnell/q…。 扩散算子进行关于均值的反转,步骤如下:
- 对状态向量中的所有值进行求和。
- 计算平均值。
- 将所有值替换为关于均值对称的值。
让我们计算一下这个算子对我们当前状态向量的作用。状态向量中的四个元素是1/2、1/2、-1/2、1/2。这些元素的和为1:
因此,平均值为。
现在我们需要将这些元素(它们要么是1/2,要么是-1/2)围绕着1/4这个值进行"镜像"。正如图10.18所示,将1/2进行镜像得到0。有趣的是,将-1/2进行镜像得到1!
注意:格劳弗搜索算法的扩散算子允许我们获得负振幅的更高概率。
格劳弗搜索算法的真正威力来自量子神谕和扩散算子的结合。量子神谕可以翻转目标值的振幅符号,而扩散算子可以使所有振幅围绕均值进行反转,从而将负振幅放大成为最大的元素。在这种特定情况下,只需要一步就足以找到原始问题的正确答案。我们获得了一个神谕,并且只需对该神谕进行一次评估,就足以确定在索引2处得到了原始函数的正确答案。
如果量子比特超过两个,测量到正确答案的概率将大于测量到任何其他选项的概率,但并非100%。在这种情况下,需要多次应用量子神谕和扩散算子。
可以通过数学证明,提供最优结果的步数是最接近√N * Π/4的值。在ch10/grover的Main类的doGrover方法中,通过以下结构实现:
private static void doGrover(int dim, int solution) {
int N = 1 << dim;
double cnt = Math.PI*Math.sqrt(N)/4;
...
for (int i = 1; i < cnt; i++) {
// apply a step
}
...
}
回顾图10.11,在N = 64的情况下,我们在进行了六步后得到了不错的结果。事实上,根据刚刚展示的算法,最优步数是6.28。
注意:如果我们应用的步数超过最优值,结果的质量将会下降。因此,强烈建议按照我们展示的算法进行操作。
结论
格劳弗搜索算法是最受欢迎的量子算法之一。在本章中,你了解到虽然该算法本身与搜索数据库无关,但它可以在需要搜索非结构化列表的应用中使用。
通常情况下,量子算法会增加测量正确答案的概率,减少测量错误答案的概率。
在没有任何先验知识的情况下,所有可能的答案具有相同的概率。在应用算法的一步之后,正确答案的概率已经比其他可能结果更高。在应用最优步数(最接近√N * Π/4的数值)之后,正确答案将具有最高的概率。
总结
- 经典算法在无结构列表中搜索特定元素可以用Java编写一个Java函数实现。
- 使用量子等效的经典Java函数,你可以使用量子算法来进行相同的搜索。
- 经典算法找到所需元素所需的时间与列表中元素的数量成线性比例关系。而在量子方法中,使用格劳弗搜索算法,找到这个元素所需的时间与元素数量的平方根成比例关系。
- 可以使用Strange来实现格劳弗搜索算法。