Python-和-Q--量子计算学习指南-全-
Python 和 Q# 量子计算学习指南(全)
原文:Learn Quantum Computing with Python and Q#
译者:飞龙
前置内容
前言
在其大部分历史中,量子计算是一个物理学家领域——也许有少数人倾向于计算机科学,但并不一定。由 Michael A. Nielsen 和 Isaac L. Chuang 编写的流行教科书《量子计算与量子信息》仍然被认为是首选教科书,由两位量子物理学家编写。当然,计算机科学家一直存在,但一些理论家将他们写的代码行数少视为一种荣誉。这就是 Kaiser、我和 Granade 成长的世界。我很容易对着新一代学生挥舞拳头,大喊,“当我你们这个年龄的时候,我们不会写代码——我们被粉笔灰呛得咳嗽!”
当我们都是研究生时,我遇到了 Chris Granade。那时,我们为物理期刊撰写学术论文,其中包含被拒绝的代码行,因为“不是物理学。”但我们并没有气馁。现在,多年以后,这本书对我来说是最终的证明!这是一本教你所有你想知道和需要的关于量子计算的书,无需物理学——尽管,如果你真的想了解与物理学的联系,Kaiser 和 Granade 也提供了这方面的内容!还有表情符号!
!
从那时起,我已经走了很长的路,我非常感激 Granade,正如量子计算领域一样,他向我们中的许多人展示了“量子”和“计算”之间不仅仅是定理和证明。Kaiser 还教会了我关于软件开发者在开发量子技术中需要的触感,比我之前想象的要多。Kaiser 和 Granade 将他们的专业知识转化为文字和代码行,让所有人都能从中受益,就像我一样。
虽然目标是创建“不是教科书”,但这本书确实可以作为一个大学讲座中量子计算从物理系转向计算机科学系的入门书籍使用。对量子计算的兴趣正在迅速增长,而其中大部分兴趣并非来自物理学——软件开发者、运营经理和金融高管都想知道量子计算是什么,以及如何接触它。量子计算作为纯粹学术追求的日子已经一去不复返。这本书满足了日益增长的量子社区的需求。
虽然我提到了在量子计算领域物理学家比例的下降,但我并不想贬低他们。就像我曾经是一个软件开发卢德分子一样,这本书实际上是为任何人而写的——尤其是那些已经在该领域工作,并希望在熟悉的环境中了解量子计算软件方面的人。
打开你最喜欢的代码编辑器,准备打印(“你好,量子世界!”)。
Chris Ferrie,博士
量子软件与信息中心副教授
澳大利亚新南威尔士州悉尼
前言
我们对量子计算的热情已经持续了 20 多年,我们热衷于利用这些经验帮助更多的人参与到量子技术中来。我们一起完成了博士学位,在这个过程中,我们共同克服了研究问题、双关语比赛和桌游,帮助推动量子比特可能性的边界。在很大程度上,这意味着开发新的软件和工具来帮助我们和我们的团队进行更好的研究,这是“量子”和“计算”主题之间一个很好的桥梁。然而,在开发各种软件项目时,我们需要向我们的开发同事解释我们在做什么。我们一直在想,“为什么没有一本好的量子计算书籍,既技术性强又不像是教科书?”你们现在看到的这本书就是结果。![闪耀的心]
我们编写这本书是为了让开发者能够轻松阅读,而不是像其他量子计算书籍那样采用教科书风格。当我们自己学习量子计算时,这既令人兴奋,也有些令人害怕和令人生畏。其实不必这样,因为量子计算主题让人困惑的很多是它们被呈现的方式,而不是内容。
不幸的是,量子计算经常被描述为“奇怪”、“神秘”或超出我们的理解,而事实上,在 35 年的历史中,量子计算已经变得相当容易理解。通过结合软件开发和数学,你可以建立起理解量子计算和探索这个令人惊叹的新领域所需的基本概念。
我们编写这本书的目标是帮助你们学习关于这项技术的基础知识,并装备你们使用这些工具来构建明天的量子解决方案。我们专注于通过编写量子计算代码的动手经验。在第一部分,你们将使用 Python 构建自己的量子设备模拟器;在第二部分,你们将学习如何将新技能应用于使用 Q#和量子开发工具包编写量子应用程序;在第三部分,你们将学习实现一个算法,该算法分解整数的速度比已知最佳传统算法快得多——在整个过程中,都是你们在操作,这是你们的量子之旅。
我们已经尽可能包括了尽可能多的实际应用,但事实是,这正是你们发挥作用的地方!量子计算正处于一个十字路口,要继续前进,我们需要在量子计算机能做什么和不能做什么的巨大已知量与人们需要解决的问题之间架起一座桥梁。搭建这座桥梁将我们带离了仅适用于优秀研究的量子算法,转向了能够影响整个社会的量子算法。你们可以帮助搭建这座桥梁。欢迎加入你们的量子之旅;我们在这里是为了帮助让这个过程变得有趣!
致谢
在开始写这本书时,我们并不知道我们会遇到什么;我们只知道这样一个资源是必须存在的。写这本书给了我们一个巨大的机会,来完善和发展我们解释和教授我们所熟悉的内容的技能。我们在 Manning 与的所有人都很棒——迪尔德丽·希姆,我们的制作编辑;蒂芙尼·泰勒,我们的校对编辑;凯蒂·滕南特,我们的校对员;以及伊万·马丁诺维奇,我们的审稿编辑——他们帮助我们确保这本书是我们读者能得到的最好的书。
我们感谢奥利维亚·迪·马泰奥和克里斯·费里为我们提供的所有宝贵反馈和笔记,这有助于保持我们的解释既准确又清晰。
我们还要感谢在各个发展阶段审阅手稿的所有审稿人,他们的深思熟虑的反馈使这本书变得更好:阿兰·库尼奥,克莱夫·哈伯,大卫·雷蒙德,德布玛拉·贾什,迪米特里·丹尼松诺克,多明戈·萨拉扎尔,埃马努埃尔·梅迪纳·洛佩斯,杰夫·克拉克,哈维埃尔,卡蒂凯亚拉贾恩·拉詹德拉南,克日什托夫·卡米切克,库马尔·乌尼克里什南,帕斯夸尔·齐尔波利,帕特里克·雷根,保罗·奥托,拉法埃拉·文塔吉奥,罗纳德·蒂斯利尔,桑德·泽格尔德,史蒂夫·苏斯曼,汤姆·海曼,图安·A·特兰,沃尔特·亚历山大·马塔·洛佩斯,以及威廉·E·惠勒。
我们感谢所有帮助发现错误、错别字和改进说明位置的 Manning Early Access Program (MEAP) 订阅者。许多人还通过在我们的示例代码仓库中提交问题提供了反馈:我们也要感谢他们!
我们想感谢西雅图地区许多杰出的机构(特别是 Caffe Ladro、Miir、Milstead & Co.和 Downpour Coffee Bar),它们容忍我们一杯接一杯地喝咖啡,并热烈地讨论量子比特。同时,我们也要感谢 Fremont Brewing 的员工们,在我们需要一杯啤酒时,他们总是及时出现。当一位路过的陌生人询问我们正在做什么时,这总是一次受欢迎的打扰!
我们还要感谢微软量子系统团队的杰出成员,他们致力于为开发者提供可能的最佳工具,以便他们能够顺利进入量子计算领域。特别感谢贝蒂娜·海姆,她致力于使 Q#成为一种出色的语言,以及她作为一位好朋友的存在。
最后,我们要感谢我们的德国牧羊犬,奇维,它为我们提供了必要的分心和休息的理由。
莎拉·凯撒
我的家人一直在我身边,我感谢他们在我在这个项目中努力工作期间给予的所有耐心和鼓励。我想感谢我的治疗师,没有他这本书永远不会问世。最重要的是,我要感谢我的合著者和伴侣,克里斯。他们无论顺境还是逆境都一直陪伴着我,并始终鼓励和激励我去做他们知道我能做到的事情。
克里斯·格兰德
没有我的伴侣和合著者,Dr. Sarah Kaiser,那令人惊叹的爱与支持,这本书是不可能完成的。我们一起经历了更多,也取得了比我曾梦想的还要多的成就。我们共同的故事始终是关于创造一个更好、更安全、更具包容性的量子社区,而这本书则是我们在这条旅程上迈出另一步的奇妙机会。感谢你让这一切成为可能,Sarah。
没有我家人和朋友的帮助,这也不可能实现。感谢你们一直以来的支持,无论是分享可爱的宠物照片,还是对最新的头条新闻表示同情,或者在《动物之森》中一起熬夜观星。最后,我还感谢那些多年来一直依赖的、帮助我从许多新视角理解世界的出色在线社区。
关于这本书
欢迎来到《用 Python 和 Q#学习量子计算》!这本书将通过使用 Python 作为舒适的起点,逐步过渡到用微软开发的特定领域编程语言 Q#编写的解决方案,来介绍量子计算的世界。我们采用以示例和游戏驱动的教学方法,教授量子计算和开发概念,让你能够立即动手编写代码。
深入挖掘:可以浮潜!
量子计算是一个跨学科的研究领域,汇集了编程、物理、数学、工程和计算机科学的思想。在本书的某些时候,我们会花点时间指出量子计算如何借鉴这些其他领域的思想,将我们正在学习的概念置于更丰富的背景中。
虽然这些旁白旨在激发好奇心和进一步探索,但它们本质上都是旁枝末节。无论你是否深入挖掘,这本书都能让你从中学到享受量子编程在 Python 和 Q#中所需的一切。深入挖掘可能很有趣且富有启发性,但如果你不喜欢深入挖掘,那也没关系;浮潜也是完全可以的。
适合阅读这本书的人
这本书是为对量子计算感兴趣但几乎没有量子力学经验,但有一定编程背景的人而写的。随着你学习用 Python 编写量子模拟器和用 Q#编写量子程序(微软为量子计算开发的专用语言),我们将使用传统的编程思想和技巧来帮助你。对编程概念如循环、函数和变量赋值的一般理解将有所帮助。
同样,我们使用一些来自线性代数的数学概念,如向量和矩阵,来帮助我们描述量子概念;如果你熟悉计算机图形学或机器学习,许多概念都是相似的。我们在旅途中使用 Python 来回顾最重要的数学概念,但熟悉线性代数将有所帮助。
这本书的组织结构:路线图
本文本的目的是让您开始探索和使用量子计算的实际工具。本书分为三个部分,这些部分相互关联:
-
第一部分温和地介绍了描述量子比特(量子计算机的基本单元)所需的概念。本部分描述了如何在 Python 中模拟量子比特,这使得编写简单的量子程序变得容易。
-
第二部分描述了如何使用量子开发工具包和 Q#编程语言来组合量子比特并运行与任何已知经典算法不同的量子算法。
-
在第三部分,我们将前两部分中的工具和方法应用于学习量子计算机如何应用于现实世界问题,例如模拟化学性质。
此外,还有四个附录。附录 A 包含了本书中使用的工具的安装说明。附录 B 是一个快速参考部分,包括量子术语表、符号提醒以及可能有助于您阅读本书的代码片段。附录 C 是线性代数复习,附录 D 则深入探讨了您将要实现的一种算法。
关于代码
本书中使用的所有代码都可以在github.com/crazy4pi314/learn-qc-with-python-and-qsharp找到。完整的安装说明可在本书的存储库和附录 A 中找到。
本书中的示例也可以通过 mybinder.org 服务在线运行,无需安装任何东西。要开始,请访问bit.ly/qsharp-book-binder。
liveBook 讨论论坛
购买《用 Python 和 Q#学习量子计算》,包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在论坛中就本书发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问livebook.manning.com/#!/book/learn-quantum-computing-with-python-and-q-sharp/discussion。您还可以在livebook.manning.com/#!/discussion了解更多关于 Manning 论坛和行为准则的信息。
Manning 对我们读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向他们提出一些挑战性的问题,以免他们的兴趣转移!只要本书有售,论坛和以前讨论的存档将可通过出版社的网站访问。
其他在线资源
当你通过阅读这本书并运行提供的示例代码开始你的量子计算之旅时,你可能发现以下在线资源很有帮助:
-
Quantum Development Kit documentation (
docs.microsoft.com/azure/quantum/)—关于 Q#的概要文档和完整参考,包括自本书印刷以来的更改和新增内容 -
Quantum Development Kit samples (
github.com/microsoft/quantum)—使用 Q#的完整示例,包括独立使用和与 Python 和.NET 宿主程序一起使用,涵盖了广泛的不同应用 -
QuTiP.org (
qutip.org)—我们用于帮助这本书中数学的 QuTiP 包的完整用户指南
也有一些非常适合量子计算专家和新手的优秀社区。加入以下量子开发社区可以帮助你解决在过程中遇到的问题,同时也会让你有机会帮助他人:
-
qsharp.community (
qsharp.community)—一个 Q#用户和开发者的社区,包括聊天室、博客和项目仓库 -
Quantum Computing Stack Exchange (
quantumcomputing.stackexchange.com/)—一个询问量子计算问题的好地方,包括你可能有的任何 Q#问题 -
Women in Quantum Computing and Applications (
wiqca.dev)—一个包容性社区,为所有性别的人庆祝量子计算及其创造者 -
Quantum Open Source Foundation (
qosf.org/)—一个支持量子计算开源工具开发和标准化的社区 -
Unitary Fund (
unitary.fund/)—一个非营利组织,致力于创建一个对大多数人都有益的量子技术生态系统
进一步探索
量子计算是一个迷人的新领域,它提供了关于计算的新思考方式和新工具来解决难题。这本书可以帮助你开始量子计算,以便你能够继续探索和学习。然而,这本书不是教科书,也不打算让你仅凭这本书就为量子计算研究做好准备。就像经典算法一样,开发新的量子算法是一种数学艺术,就像其他任何事情一样;虽然我们在本书中涉及数学并使用它来解释算法,但有许多教科书可以帮助你在我们讨论的概念上建立。
一旦你阅读了这本书并开始了量子计算的学习,如果你想继续你的物理学或数学之旅,我们建议以下资源:
-
复杂性动物园 (
complexityzoo.net/Complexity_Zoo) -
量子算法动物园(
quantumalgorithmzoo.org) -
《复杂性理论:现代方法》由桑杰夫·阿罗拉和博阿兹·巴拉克(剑桥大学出版社,2009 年)著
-
《量子计算:温和的介绍》由伊莱诺·G·里费尔和沃尔夫冈·H·波拉克(麻省理工学院出版社,2011 年)著
-
《从德谟克利特到量子计算》由斯科特·阿伦森(剑桥大学出版社,2013 年)著
-
《量子计算与量子信息》由迈克尔·A·尼尔森和伊萨克·L·丘恩(剑桥大学出版社,2000 年)著
-
《量子过程、系统和信息》由本杰明·舒马赫和迈克尔·韦斯特莫兰德(剑桥大学出版社,2010 年)著
关于作者
莎拉·凯撒在滑铁卢大学量子计算研究所完成了物理学(量子信息)的博士学业。她的大部分职业生涯都在实验室里开发新的量子硬件,从建造卫星到破解量子密码学硬件。传播量子令人兴奋的地方是她的热情所在,她喜欢构建新的演示和工具,以帮助量子社区的成长。当她不在机械键盘上时,她喜欢皮划艇和为所有年龄段的读者写科学书籍。
克里斯·格兰德在滑铁卢大学量子计算研究所完成了物理学(量子信息)的博士学业,现在在微软的量子系统小组工作。他们致力于开发 Q#的标准库,并且是量子设备从经典数据中进行统计特征化的专家。克里斯还帮助斯科特·阿伦森准备了他的讲座,并出版了书籍《从德谟克利特到量子计算》(剑桥大学出版社,2013 年)。
关于封面插图
《用 Python 和 Q#学习量子计算》的封面上的插图被标注为“Hongroise”,或匈牙利女性。这幅插图取自雅克·格拉塞·德·圣索沃尔(1757–1810)的作品集,名为《不同国家的服饰》,于 1797 年在法国出版。每一幅插图都是手工精心绘制和着色的。格拉塞·德·圣索沃尔收藏中的丰富多样性生动地提醒我们,200 年前世界的城镇和地区在文化上有多么不同。
人们彼此隔离,说着不同的方言和语言。在街道或乡村,仅凭他们的服饰就能轻易地识别出他们居住的地方以及他们的职业或社会地位。从那时起,我们的着装方式发生了变化,当时地区间的多样性如此丰富,现在已经逐渐消失。现在很难区分不同大陆、不同城镇、地区或国家的人们。也许我们用文化多样性换取了更加多样化的个人生活——当然,是为了更加多样化且节奏更快的技术生活。
在难以区分一本计算机书与另一本计算机书的年代,曼宁通过基于两百年前丰富多样的地区生活所设计的书封面,庆祝了计算机行业的创新精神和主动性,这些画面由格拉塞特·德·圣索沃尔重新赋予生命。
第一部分:量子入门
这一部分书籍帮助我们为接下来的量子之旅做好准备。在第一章中,我们获得了更多关于量子计算、本书中学习量子计算的方法以及我们可以预期在哪里应用所学技能的背景信息。在第二章中,我们开始通过在 Python 中开发量子模拟器来编写代码。然后,我们使用这个模拟器来编程一个量子随机数生成器。接下来,在第三章中,我们将模拟器扩展到编程量子技术的密码学应用,例如 BB84 量子密钥交换协议。在第四章中,我们使用非局域游戏来了解纠缠,并将模拟器再次扩展以支持多个量子比特。在第五章中,我们学习如何使用一个新的 Python 包来帮助我们实现第四章中提到的非局域游戏的量子策略。最后,在第六章中,我们将模拟器最后一次扩展,添加新的量子操作,以便我们可以模拟像量子隐形传态这样的技术,并在我们的量子设备中练习移动数据。
1 介绍量子计算
本章涵盖
-
为什么人们对量子计算感到兴奋
-
量子计算机是什么
-
量子计算机能做什么和不能做什么
-
量子计算机如何与经典编程相关
量子计算在过去几年中越来越受欢迎,成为研究和炒作的热点。通过使用量子物理以新的奇妙方式执行计算,量子计算机可以影响社会,这是一个参与并学习如何编程量子计算机以及将量子资源应用于解决重要问题的激动人心的时期。
在所有关于量子计算提供的优势的喧嚣中,人们很容易忽视这些好处真正的范围。我们有关于当技术承诺超过现实时可能发生的事情的一些有趣的历史先例。在 20 世纪 70 年代,机器学习和人工智能由于 AI 的炒作和兴奋超过了其结果,资金大幅减少;这后来被称为“人工智能冬天”。同样,互联网公司在试图克服互联网泡沫破裂时也面临着同样的危险。
一个前进的方法是批判性地理解量子计算提供的承诺,量子计算机是如何工作的,以及量子计算的范围是什么和不是什么。在本章中,我们帮助你发展这种理解,以便你可以在本书的其余部分亲手编写自己的量子程序。
然而,抛开这一切,了解一个全新的计算模型真的很酷!随着你阅读这本书,你将学习如何通过编程模拟来了解量子计算机的工作原理,这些模拟可以在你今天的笔记本电脑上运行。这些模拟将展示我们预期真实商用量子编程的许多基本要素,同时实用的商用硬件正在上线。这本书是为那些有一些基本编程和线性代数经验,但没有量子物理或计算先验知识的人准备的。如果你对量子有一些了解,你可以跳到第二部分和第三部分,在那里我们将涉及量子编程和算法。
1.1 量子计算为什么重要?
计算技术正在以真正惊人的速度发展。三十年前,80486 处理器允许用户每秒执行 50 百万条指令(MIPS)。今天,像 Raspberry Pi 这样的小型计算机可以达到 5000 MIPS,而台式处理器可以轻松达到 50000 到 300000 MIPS。如果我们有一个特别困难的计算问题想要解决,一个非常合理的策略就是简单地等待下一代处理器使我们的生活更轻松,视频流更快,游戏更丰富多彩。
然而,对于许多我们关心的问题,我们并不那么幸运。我们可能希望获得两倍快的 CPU 将使我们能够解决两倍大的问题,但就像生活中的许多事情一样,“更多就是不同”。假设我们排序一个包含 1000 万个数字的列表,发现大约需要 1 秒钟。后来,如果我们想在 1 秒钟内排序一个包含 10 亿个数字的列表,我们需要一个快 130 倍的 CPU,而不仅仅是快 100 倍。在解决某些类型的问题时,这种情况会更糟:对于某些图形问题,从 1000 万个点增加到 10 亿个点将需要 13,000 倍更长的时间。
从在城市中疏导交通到预测化学反应等广泛的问题变得更加困难得多。如果量子计算是关于制造运行速度快 1000 倍的计算机,我们几乎无法解决我们想要解决的艰巨挑战。幸运的是,量子计算机更有趣。我们预计量子计算机将比经典计算机慢得多,但解决许多问题所需的资源将以不同的方式扩展,这样如果我们关注正确类型的问题,我们就可以突破“更多就是不同”的障碍。同时,量子计算机并不是万能的子弹——一些问题仍然很难。例如,虽然量子计算机可能有助于我们预测化学反应,但它们可能对其他困难问题帮助不大。
研究确切哪些问题我们可以获得这样的优势,并开发量子算法来实现这一点,一直是量子计算研究的一个重点。到目前为止,以这种方式评估量子方法非常困难,因为这需要广泛的数学技能来编写量子算法并理解量子力学的所有细微差别。
随着行业开始开发平台以帮助连接开发者和量子计算,然而,这种状况已经开始改变。通过使用微软的整个量子开发工具包,我们可以抽象掉量子计算的大部分数学复杂性,并开始真正理解和使用量子计算机。本书中教授的工具和技术使开发者能够探索和理解为这种新的硬件平台编写程序将是什么样的。
换句话说,量子计算不会消失,因此理解我们可以用它解决哪些问题确实非常重要!无论是否发生量子“革命”,量子计算在决定未来几十年如何开发计算资源方面已经起到了——并将继续起到——重要作用。这类决策受到量子计算的影响很大:
-
在信息安全中,哪些假设是合理的?
-
在学位课程中,哪些技能是有用的?
-
我们如何评估计算解决方案的市场?
对于我们这些在科技或相关领域工作的人来说,我们越来越需要做出这样的决定或为他们提供意见。我们有责任了解量子计算是什么,也许更重要的是,它不是什么。这样,我们将为参与这些新的努力和决策做好最佳准备。
除了这些,量子计算之所以如此吸引人,另一个原因是它既类似于又与经典计算截然不同。理解经典和量子计算之间的相似之处和不同之处,有助于我们理解计算的一般性基础。经典和量子计算都源于对物理定律的不同描述,因此理解计算可以帮助我们以新的方式理解宇宙。
然而,绝对关键的是,对量子计算感兴趣并没有一个正确或甚至最好的理由。无论是什么原因让你对量子计算研究或应用感兴趣,你都会在过程中学到一些有趣的东西。
1.2 什么是量子计算机?
让我们简要地谈谈量子计算机实际上由什么组成。为了便于这次讨论,让我们先简要地谈谈计算机这个术语的含义。
定义 A 计算机是一种设备,它以数据作为输入,并对此数据进行某种操作。
我们有很多被称为计算机的例子;请参见图 1.1 中的示例。

图 1.1 几种不同类型计算机的示例,包括由海军上将霍珀操作的 UNIVAC 主机、一个“人工计算机”房间,致力于解决飞行计算、一个机械计算器以及基于乐高积木的图灵机。每台计算机都可以用与手机、笔记本电脑和服务器相同的数学模型来描述。来源:NASA 提供的“人工计算机”照片。乐高图灵机照片由 Projet Rubens 提供,根据 CC BY 3.0 许可使用(creativecommons.org/licenses/by/3.0/).
所有这些共同之处在于,我们可以用经典物理学来模拟它们——也就是说,用牛顿运动定律、牛顿引力定律和电磁学来描述。这将帮助我们区分我们习惯使用的计算机(例如,笔记本电脑、手机、面包机、房屋、汽车和起搏器)以及我们在本书中学习的计算机。为了区分这两者,我们将能够用经典物理学描述的计算机称为经典计算机。令人高兴的是,如果我们用量子物理学来替换术语经典物理学,我们就有一个关于量子计算机的伟大定义!
定义 A 量子计算机是一种设备,它以数据作为输入,并使用只能用量子物理学描述的过程对数据进行某种操作。
换句话说,经典计算机和量子计算机的区别,正是经典物理和量子物理的区别。我们将在本书的后面更深入地探讨这个问题。但主要的区别在于规模:我们的日常经验主要是与足够大和足够热的物体打交道,尽管量子效应仍然存在,但平均来说它们并没有做什么。虽然量子力学在咖啡杯、面粉袋和棒球棒等日常物体尺度上也能工作,但结果表明,我们可以只用经典物理很好地描述这些物体的相互作用。
深入探讨:相对论怎么了?
量子物理适用于非常小或非常冷或很好地隔离的物体。同样,另一门称为相对论的物理学分支描述了足够大以至于引力能发挥重要作用或移动非常快——接近光速的物体。许多计算机依赖于相对论效应;确实,全球定位卫星对相对论有至关重要的依赖。到目前为止,我们主要比较的是经典物理和量子物理,那么相对论又如何呢?
事实上,所有使用相对论效应实现的计算也可以用纯经典计算模型,如图灵机来描述。相比之下,量子计算不能描述为比经典计算更快,而需要一个不同的数学模型。到目前为止,还没有提出一种使用相对论以相同方式工作的“引力计算机”的方案,所以在这本书中我们可以暂时不考虑相对论。
如果我们关注一个更小的尺度,在这个尺度上量子力学是必需的来描述我们的系统,那么量子计算就是使用小型、良好隔离的设备以有用的方式转换数据的艺术,这些数据无法仅用经典物理来描述。构建量子设备的一种方法是用小型经典计算机,如数字信号处理器(DSPs)来控制奇异材料。
物理学与量子计算
用于构建量子计算机的奇异材料有听起来可能令人畏惧的名字,如超导体和拓扑绝缘体。然而,我们可以从我们学习如何理解和使用经典计算机中找到安慰。
我们可以在不知道半导体是什么的情况下编程经典计算机。同样,我们构建量子计算机背后的物理学是一个迷人的主题,但学习如何编程和使用量子设备并不需要我们了解它。
量子设备在控制细节上可能有所不同,但最终所有量子设备都是由经典计算机和某种类型的控制电子设备控制的,并从中读取。毕竟,我们对经典数据感兴趣,所以最终必须与经典世界有一个接口。
注意:大多数量子设备必须保持非常冷和良好的隔离,因为它们可能对噪声极其敏感。
通过使用嵌入式经典硬件应用量子操作,我们可以操纵和转换量子数据。量子计算的力量来自于精心选择要应用的操作,以实现有用的转换并解决感兴趣的特定问题。
1.3 我们将如何使用量子计算机?

图 1.2 我们希望使用量子计算机的方式。漫画经 xkcd.com 许可使用。
理解量子计算机的潜力和局限性非常重要,尤其是在量子计算受到的热潮背景下。许多这种热潮背后的误解源于将类比外推到它们不再有任何意义的地方——所有类比都有其局限性,量子计算也不例外。模拟量子程序在实际中的行为可以是一种很好的方式,有助于测试和细化由类比提供理解。
小贴士 如果你曾经看到过关于量子计算新结果的描述,它们读起来像是“我们可以利用无限多个平行宇宙共同作用的力量,将同时存在于两个地方的猫进行量子传输,以治愈癌症”,那么你就看到了从类比有用的地方外推过度的危险。
关于量子计算,一个特别常见的混淆点是用户将如何使用量子计算机。作为一个社会,我们已经理解了什么是计算机:你可以用它来运行网络应用程序、编写文档和运行模拟。事实上,经典计算机在我们的生活中做了许多不同的事情,以至于我们甚至没有注意到什么是计算机,什么不是计算机。Cory Doctorow 通过指出“你的汽车就是一辆你坐进去的计算机”(DrupalCon 阿姆斯特丹 2014 大会主题演讲,www.youtube.com/watch?v=iaf3Sl2r3jE)来做出这一观察。
量子计算机,然而,可能将更加特殊化——我们预计量子计算机对于某些任务将是没有意义的。一个很好的模型是量子计算将如何融入我们现有的经典计算堆栈,那就是 GPU。GPU 是专门设计的硬件设备,用于加速特定类型的计算,如图形绘制、机器学习任务以及任何易于并行化的任务。你想要 GPU 来处理那些特定任务,但很可能不想用它来做所有事情,因为我们有更灵活的 CPU 来处理像检查电子邮件这样的通用任务。量子计算机将完全一样:它们将擅长加速特定类型的任务,但不适合广泛使用。
注意 编程量子计算机有一些限制,因此在没有特定量子优势的情况下,经典计算机将是首选。
经典计算仍然存在,并将是我们相互沟通和互动以及与我们的量子硬件的主要方式。即使要获取经典计算资源以与量子设备接口,在大多数情况下,我们还需要一个数字到模拟信号处理器,如图 1.3 所示。

图 1.3 量子设备通过使用数字信号处理器(DSP)与经典计算机交互的示例。DSP 向量子设备发送低功率信号,并放大返回到设备的非常低功率信号。
此外,量子物理学描述的是在非常小的尺度上(大小和能量)与周围环境隔离很好的事物。这给我们可以运行量子计算机的环境带来了一些严格的限制。一个可能的解决方案是将量子设备保存在低温冰箱中,通常接近绝对零度(-459.67°F,或-273.15°C)。虽然这在大数据中心不是问题,但维护一个稀释冰箱在台式机上就不是很合理,更不用说在笔记本电脑或手机上了。因此,量子计算机很可能会通过云服务使用,至少在它们首次商业化后的一段时间内是这样。
将量子计算机作为云服务使用类似于其他专用计算硬件的进步。通过在数据中心集中这些异构计算资源,可以探索除了最大用户之外难以在本地部署的计算模型:
-
专用游戏硬件(PlayStation Now,Xbox One)
-
极低延迟、高性能计算(例如,Infiniband)集群用于科学问题
-
大型 GPU 集群
-
可重编程硬件(例如,Catapult/Brainwave)
-
张量处理单元(TPU)集群
-
高持久性、高延迟的存档存储(例如,亚马逊冰川)
展望未来,像 Azure Quantum (azure.com/quantum) 这样的云服务将以类似的方式使量子计算的力量可用。
正如高速、高可用性的互联网连接使云计算对大量用户变得可访问一样,我们将能够在我们的最爱 WiFi 覆盖的海滩或咖啡店,甚至在我们观看远处壮丽的山脉时,从火车上使用量子计算机。
1.3.1 量子计算机能做什么?
作为量子程序员,如果我们有一个特定的问题,我们如何知道用量子计算机解决它是合理的?
我们仍在学习量子计算机能够做到的确切范围,因此我们还没有任何具体的规则来回答这个问题。到目前为止,我们已经发现了一些量子计算机在解决这些问题上相对于已知最佳经典方法具有显著优势的例子。在每种情况下,解决这些问题的量子算法都利用量子效应来实现优势,有时被称为量子优势。以下是有用的两个量子算法:
-
Grover 算法(在第十一章中讨论)可以在√N 步内搜索包含 N 个项目的列表。
-
Shor 算法(第十二章)可以快速分解大整数,例如那些用于加密保护私有数据的整数。
在这本书中,我们将看到更多关于量子优势的例子,但 Grover 算法和 Shor 算法是量子算法如何工作的良好例子:每个都使用量子效应将计算问题的正确答案与无效解区分开来。实现量子优势的一种方法是通过找到使用量子效应将经典问题的正确和错误解区分开来的方法。
量子优势是什么?
Grover 算法和 Shor 算法展示了两种不同的量子优势。将整数分解可能比我们想象的在经典计算中更容易。许多人非常努力地尝试快速分解整数,但并未成功,但这并不意味着我们可以证明分解是困难的。另一方面,我们可以证明 Grover 算法比任何经典算法都要快;难点在于它使用了一种不同类型的输入。
在量子计算中,寻找一个可证明的对于实际问题的优势是一个活跃的研究领域。尽管如此,量子计算机在解决问题方面可以成为强大的工具,即使我们无法证明永远不会有一个更好的经典算法。毕竟,Shor 算法挑战了信息安全领域大量假设的基础——数学证明是必要的,仅仅因为我们还没有建造出一个足够大的量子计算机来运行 Shor 算法。
量子计算机在模拟量子系统的性质方面也提供了显著的好处,为量子化学和材料科学开辟了应用。例如,量子计算机可以大大简化了解化学系统的基态能量。这些基态能量随后可以提供对反应速率、电子构型、热力学性质以及其他在化学中极具兴趣的性质的见解。
在开发这些应用的过程中,我们也看到了诸如量子密钥分发和量子计量学等衍生技术的显著优势,其中一些我们将在接下来的几章中看到。在为了计算目的学习控制和理解量子设备的过程中,我们还学会了成像、参数估计、安全等方面的宝贵技术。虽然这些在严格意义上不是量子计算的应用,但它们在很大程度上展示了从量子计算的角度进行思考的价值。
当然,当我们对量子算法的工作原理以及如何从基本原理构建新算法有具体理解时,发现量子计算机的新应用会容易得多。从这个角度来看,量子编程是一个很好的资源,可以学习如何发现全新的应用。
1.3.2 量子计算机不能做什么?
就像其他形式的专用计算硬件一样,量子计算机不会在所有事情上都做得很好。对于某些问题,经典计算机可能更适合这项任务。在开发量子设备的应用时,注意哪些任务或问题不适合量子计算是有帮助的。
简而言之,我们没有任何硬性规则可以快速决定哪些任务最好在经典计算机上运行,哪些任务可以利用量子计算机。例如,大数据风格应用对存储和带宽的要求很难映射到量子设备上,而我们可能只有相对较小的量子系统。当前的量子计算机只能记录不超过几十比特的输入,随着量子设备用于更苛刻的任务,这一限制将变得更加相关。尽管我们预计最终会建造比现在大得多的量子系统,但对于需要大量输入/输出才能解决的问题,经典计算机可能始终是更好的选择。
同样,那些高度依赖对大量经典输入进行随机访问的机器学习应用,在概念上很难用量子计算来解决。尽管如此,可能存在其他机器学习应用与量子计算映射得更为自然。寻找将量子资源应用于解决机器学习任务的最佳方法的努力仍在进行中。一般来说,那些输入和输出数据量小但需要大量计算才能从输入到输出的问题是量子计算机的良好候选者。
鉴于这些挑战,可能会诱使我们得出结论,量子计算机总是擅长那些输入和输出小但计算强度很高的任务。在媒体中,像量子并行性这样的概念很受欢迎,量子计算机有时甚至被描述为使用平行宇宙进行计算。
注意:“平行宇宙”的概念是一个很好的类比,可以帮助我们理解量子概念,但如果过度推论,可能会导致荒谬。有时,我们可以将量子计算的各个部分想象成不同的宇宙,它们之间无法相互影响,但这种描述使得我们难以思考这本书中将要学习的一些效应,例如干涉。如果过度推论,平行宇宙的类比也容易让人将量子计算想象成科幻电视剧如星际迷航中特别粘稠和有趣的一集,而不是现实。
然而,这未能传达的是,即使量子设备的态似乎包含所需的输出,我们也不总是明显知道如何使用量子效应从量子设备中提取有用的答案。例如,使用经典计算机分解整数N的一种方法是将每个潜在因子列出来,并检查它是否实际上是因子:
-
让i = 2。
-
检查N / i的余数是否为零。
-
如果是,则返回i是N的因子。
-
如果不是,则增加i并循环。
-
我们可以通过使用大量不同的经典计算机来加速这个经典算法,每个计算机对应我们想要尝试的每个潜在因子。也就是说,这个问题可以很容易地并行化。量子计算机可以在同一设备中尝试每个潜在因子,但结果证明,这还不足以比经典方法更快地分解整数。如果我们使用这种方法在量子计算机上操作,输出将是随机选择的潜在因子之一。实际正确的因子将以大约1/√N的概率出现,这并不比经典算法更好。
然而,正如我们在第十二章中将会看到的,我们可以使用其他量子效应,用量子计算机比已知的最佳经典分解算法更快地分解整数。Shor 算法所做的许多繁重工作是为了确保测量到正确因子的概率远大于测量到错误因子的概率。以这种方式取消错误答案是量子编程艺术中的一部分;对于我们可能想要解决的许多问题来说,这并不容易,甚至不可能做到。
要更具体地了解量子计算机能做什么,不能做什么,以及如何在这些挑战中用量子计算机做些酷的事情,采取更具体的方法是有帮助的。因此,让我们考虑一下量子程序究竟是什么,这样我们就可以开始编写自己的程序了。
1.4 什么是程序?
在这本书中,我们经常会发现,首先重新审视类似的传统概念,然后解释量子概念是有用的。特别是,让我们退一步,检查一下经典程序是什么。
定义 一个 程序 是一系列可以被经典计算机解释以执行所需任务的指令。税表、驾驶方向、食谱和 Python 脚本都是程序的例子。
我们可以编写经典程序来分解各种不同的任务,以便各种不同的计算机进行解释。见图 1.4 中的示例程序。

图 1.4 经典程序示例。税表、地图方向和食谱都是例子,其中一系列指令被经典计算机(如人)解释。这些可能看起来非常不同,但每个都使用一系列步骤来传达一个过程。
让我们看看一个简单的“hello, world”程序在 Python 中的样子:
>>> def hello():
... print("Hello, world!")
...
>>> hello()
Hello, world!
在最基本的形式上,这个程序可以被看作是一系列指令,这些指令被提供给 Python 解释器,然后解释器依次执行每条指令以实现某种效果——在这个例子中,就是在屏幕上打印一条消息。也就是说,程序是对一个任务的 描述,然后由 Python 解释,进而由我们的 CPU 解释,以实现我们的目标。这种描述与解释之间的相互作用促使我们将 Python、C 以及其他类似的编程工具称为 语言,强调编程是我们与计算机交流的方式。
在使用 Python 打印“Hello, world!”的例子中,我们实际上是在与 Python 语言的创始人 Guido van Rossum 进行交流。Guido 然后代表我们与我们所使用的操作系统的设计者进行交流。这些设计者反过来代表我们与设计我们所使用 CPU 的 Intel、AMD、ARM 或其他公司进行交流,等等。
1.4.1 什么是量子程序?
与经典程序一样,量子程序由一系列指令组成,这些指令被经典计算机解释以执行特定任务。然而,区别在于,在量子程序中,我们希望完成的任务涉及控制量子系统以执行计算。
因此,经典和量子程序中使用的指令也有所不同。一个经典程序可能用指令的形式描述一个任务,比如从互联网上加载一些猫的图片,最终可能用汇编指令如 mov(移动)来实现。相比之下,像 Q# 这样的量子语言允许程序员用类似 M(测量)的指令来表达量子任务。当在量子硬件上运行时,这些程序可能指示数字信号处理器向量子设备发送微波、无线电波或激光,并放大从设备出来的信号。
在本书的其余部分,我们将看到许多量子程序面临解决的任务类型的示例,或者至少是它们所面对的,以及我们可以使用哪些经典工具来使量子编程更容易。例如,图 1.5 展示了在 Visual Studio Code 中编写量子程序的示例,这是一个经典的集成开发环境(IDE)。

图 1.5 使用量子开发工具包和 Visual Studio Code 编写量子程序。我们将在第七章中详细介绍这个程序的内容,但你可以从高层次上看到,它看起来与其他你可能参与过的软件项目相似。
我们将逐章构建编写量子程序所需的概念;图 1.6 展示了一个路线图。在下一章中,我们将通过学习构成量子计算机的基本构建块以及使用它们来编写我们的第一个量子程序来开始。

图 1.6 本书构建了我们编写量子程序所需的概念。我们从第一部分开始,通过在 Python 中构建自己的模拟器,来介绍较低级别的模拟器和固有操作(想想硬件 API)。第二部分检查 Q# 语言和量子开发技术,这些技术将帮助我们开发自己的应用程序。第三部分展示了量子计算的一些已知应用,以及我们在向前推进这项技术时所面临的挑战和机遇。
摘要
-
量子计算之所以重要,是因为量子计算机有可能使我们能够解决用传统计算机难以解决的问题。
-
量子计算机在某些类型的问题上可以提供比经典计算机的优势,例如分解大数。
-
量子计算机是利用量子物理处理数据的设备。
-
程序是由可以被经典计算机解释以执行任务的指令序列。
-
量子程序是通过向量子设备发送指令来执行计算的程序。
2 量子位:构建块
本章涵盖
-
为什么随机数是一个重要的资源
-
什么是量子位?
-
我们可以在量子位上执行哪些基本操作?
-
在 Python 中编程量子随机数生成器
在本章中,我们将开始接触一些量子编程概念。我们将探索的主要概念是量子位,它是经典比特的量子对应物。我们将使用量子位作为抽象或模型来描述量子物理可能实现的新类型的计算。图 2.1 展示了使用量子计算机的模型以及我们在本书中使用的模拟器设置。真实或模拟的量子位将存在于目标机器上,并与我们将要编写的量子程序交互!这些量子程序可以通过各种主机程序发送,然后等待从量子程序接收结果。

图 2.1 我们如何使用量子计算机的心理模型。图的上半部分是量子计算机的一般模型。我们将在这本书中使用本地模拟器,下半部分代表我们将要构建和使用的内容。
为了帮助了解量子位是什么以及我们如何与之交互,我们将使用一个例子来说明它们是如何被使用的:随机数生成。虽然我们可以从量子位构建出更多有趣的设备,但一个简单的量子随机数生成器(QRNG)的例子是一个熟悉量子位的好方法。
2.1 为什么我们需要随机数?
人类喜欢确定性。我们喜欢按下键盘上的键每次都做同样的事情。然而,在某些情况下,我们希望有随机性:
-
玩游戏
-
模拟复杂系统(例如股市)
-
选择安全的秘密(例如,密码和加密密钥)
在所有这些需要随机性的情况下,我们可以描述每个结果的概率。对于随机事件,描述概率是我们对这种情况所能说的所有内容,直到骰子掷出(或硬币翻转或密码重用)。当我们描述每个例子的概率时,我们可能会说类似这样的话:
-
如果我掷这个骰子,那么我会以 1/6 的概率得到六。
-
如果我抛这个硬币,那么我会以 1/2 的概率得到正面。
我们也可以描述那些每个结果概率不相同的情况。在《幸运轮盘》(图 2.2)中,如果我们转动轮盘,得到$1,000,000 奖金的概率远小于我们转动轮盘破产的概率。

图 2.2 在《幸运轮盘》中赢得$1,000,000 和破产的概率。在转动轮盘之前,我们不知道它将停在哪里,但我们通过观察轮盘知道破产的概率远大于赢得大奖的概率。
就像游戏节目一样,在计算机科学中有很多场景,随机性至关重要,尤其是在需要安全性的情况下。如果我们想保持某些信息私密,密码学通过以不同的方式将我们的数据与随机数结合,让我们能够做到这一点。如果我们的随机数生成器不够好——也就是说,如果攻击者可以预测我们用来保护私人数据的数字——那么密码学对我们帮助不大。我们还可以想象使用一个差的随机数生成器来运行抽奖或彩票;如果攻击者能够弄清楚我们的随机数是如何生成的,他就可以直接把我们带到银行。
有多大几率?
使用我们的对手可以预测的随机数,我们可能会损失大量金钱。只需问问《压你的运气!》节目的制作人,这是一个在 20 世纪 80 年代流行的游戏节目。
一名参赛者发现他可以预测游戏的新电子“轮盘”会停在哪个位置,这使他赢得了超过 25 万美元(按今天的货币计算)。想了解更多信息,请阅读 Zachary Crockett 所著的《那个没有 Whammies 的人》一文,priceonomics.com/the-man-who-got-no-whammies。
事实上,量子力学让我们能够构建一些真正独特的随机数源。如果我们构建得当,结果的不确定性是由物理定律保证的,而不是基于对计算机解决困难问题所需时间的假设。这意味着黑客或对手必须违反物理定律才能破坏安全性!这并不意味着我们应该在所有事情上都使用量子随机数;在安全基础设施中,人类仍然是最薄弱的环节
。
深入探讨:计算安全性和信息论安全性
一些保护私人信息的方法依赖于对攻击者解决某些问题难易程度的假设。例如,RSA 算法是一种常用的加密算法,它基于寻找大数质因数的难度。RSA 在互联网和其他环境中用于保护用户数据,假设对手不能轻易分解非常大的数。到目前为止,这个假设已经被证明是正确的,但完全有可能发现一种新的分解算法,从而破坏 RSA 的安全性。新的计算模型,如量子计算,也改变了做出“分解是困难的”这样的计算假设是否合理或不合理。正如我们在第十一章中将会看到的,一种名为Shor 算法的量子算法允许以比经典计算机快得多地解决某些类型的密码学问题,挑战了通常用于保证计算安全性的假设。
相比之下,如果对手只能随机猜测秘密,即使拥有非常大量的计算能力,那么安全系统就能提供更好的保证,关于其保护私人信息的能力。这样的系统被称为信息安全的。在本章的后面部分,我们将看到以难以预测的方式生成随机数允许我们实现一个名为一次性密码的信息安全程序。
这使我们有了信心,我们可以使用量子随机数来完成重要任务,例如保护私人数据、运行彩票和玩龙与地下城。模拟量子随机数生成器的工作原理让我们能够学习量子力学背后的许多基本概念,所以让我们直接跳进去开始吧!
如前所述,一种很好的入门方式是查看一个生成随机数的量子程序示例:量子随机数生成器(QRNG)。如果以下算法(也显示在图 2.3 中)现在看起来不太明白,请不要担心——我们将在本章的其余部分解释不同的部分:
-
请求量子设备分配一个量子比特。
-
对量子比特应用一个名为Hadamard 指令的指令;我们将在本章后面部分了解这一点。
-
测量量子比特,并返回结果。

图 2.3 量子随机数生成器算法。要使用量子计算机采样随机数,我们的程序将准备一个新鲜的量子比特,然后使用 Hadamard 指令来准备所需的叠加。最后,我们可以测量并返回我们在最后得到的随机结果。
在本章的其余部分,我们将开发一个名为QuantumDevice的 Python 类,以便我们可以编写实现此类算法的程序。一旦我们有了QuantumDevice类,我们就能像编写我们习惯的经典程序一样编写 QRNG 作为 Python 程序。
注意:请参阅附录 A,了解如何在您的设备上设置 Python 以运行量子程序。
注意,以下示例将在您在本章中编写模拟器之前无法运行
。
列表 2.1 qrng.py:一个生成随机数的量子程序
def qrng(device : QuantumDevice) -> bool: ❶
with device.using_qubit() as q: ❷
q.h() ❸
return q.measure() ❹
❶量子程序编写方式与经典程序相同。在这种情况下,我们使用 Python,因此我们的量子程序是一个名为 qrng 的 Python 函数,它实现了 QRNG。
❷量子程序通过请求量子计算硬件提供量子比特来工作:我们可以用来执行计算的位量子模拟器。
❸一旦我们有一个量子比特,我们就可以向该量子比特发出指令。类似于汇编语言,这些指令通常用简短的缩写表示;我们将在本章后面部分看到 h()代表什么。
❹要从我们的量子比特中获取数据,我们可以测量它们。在这种情况下,一半的时间,我们的测量将返回 True,另一半的时间,我们将得到 False。
就这样!四步,我们就已经创建了我们的第一个量子程序。这个量子随机数生成器返回真或假。用 Python 的话说,这意味着每次我们运行qrng时,我们都会得到一个 1 或一个 0。它不是一个非常复杂的随机数生成器,但它返回的数字确实是随机的。
要运行qrng程序,我们需要给函数提供一个QuantumDevice,该设备提供对量子比特的访问并实现我们可以发送给量子比特的不同指令。虽然我们一开始只需要一个量子比特,但我们将构建自己的量子计算机模拟器。现有的硬件可以用于这项任务,但我们将要看的将超出现有硬件的范围。 它将在笔记本电脑或台式机上本地运行,并像实际的量子硬件一样工作。在整个章节的其余部分,我们将构建编写我们自己的模拟器和运行qrng所需的不同组件。
2.2 经典比特是什么?
当学习量子力学的概念时,退一步重新审视经典概念通常有助于理解它们在量子计算中的表达方式。考虑到这一点,让我们再看看比特是什么。
假设我们想向我们亲爱的朋友 Eve 发送一条重要的消息,比如“
”。我们如何以易于发送的方式表示我们的消息?
我们可能首先会列出我们可能用来写消息的每一个字母和符号。幸运的是,Unicode 联盟(unicode.org)已经为我们做了这件事,并为世界上广泛使用的各种字符分配了代码。例如,I被分配了代码 0049,而
被表示为 A66E,
为 2E0E,
为 1F496。这些代码一开始可能看起来没有帮助,但它们是发送每个符号作为消息的有用方法。如果我们知道如何发送两条消息(让我们称它们为“0”和“1”),这些方法让我们能够构建更复杂的消息,如“
”、“
”和“
”作为“0”和“1”消息的序列:
| 0 | 0000 | 8 | 1000 |
|---|---|---|---|
| 1 | 0001 | 9 | 1001 |
| 2 | 0010 | A | 1010 |
| 3 | 0011 | B | 1011 |
| 4 | 0100 | C | 1100 |
| 5 | 0101 | D | 1101 |
| 6 | 0110 | E | 1110 |
| 7 | 0111 | F | 1111 |
现在我们知道如何只向 Eve 发送两条消息:一条“0”消息和一条“1”消息,我们就可以发送任何我们想要的东西。使用这些方法,我们的消息“
”变成了“0001 1111 0100 1001 0110”或 Unicode 1F496。
小贴士:不要错误地发送“0001 1111 0100 1001 0100”,否则 Eve 会收到你发来的
!
我们把每个消息“0”和“1”称为一个比特。
注意 为了区分比特和本书其余部分将看到的量子比特,我们通常会强调我们在谈论的是经典比特。
当我们使用“比特”这个词时,我们通常指的是以下两种情况之一:
-
任何可以通过回答一个真/假问题来完全描述的物理系统
-
存储在这样的物理系统中的信息
例如,挂锁、开关、晶体管、曲线球上的左旋或右旋,以及酒杯中的酒都可以被视为比特,因为我们都可以使用它们来发送或记录信息(见表 2.1)。
表 2.1 比特示例
| 标签 | 挂锁 | 开关 | 晶体管 | 酒杯 | 棒球 |
|---|---|---|---|---|---|
| 0 | 未锁定 | 关闭 | 低压 | 含白葡萄酒 | 向左旋转 |
| 1 | 锁定 | 开启 | 高压 | 含红葡萄酒 | 向右旋转 |
这些例子都是比特,因为我们可以通过回答一个单一的真/假问题来完全描述它们。换句话说,每个例子都允许我们发送 0 或 1 的信息。像所有概念模型一样,比特也有局限性——例如,我们如何描述桃红酒呢?
话虽如此,比特是一个有用的工具,因为我们可以描述与比特交互的方式,这些方式与我们如何实际构建比特无关。
2.2.1 我们可以用经典比特做什么?
现在我们有了描述和发送经典信息的方法,我们可以做些什么来处理和修改它呢?我们用操作来描述我们可以处理信息的方式,我们将其定义为描述模型如何被改变或作用的方式。
为了可视化 NOT 操作,让我们想象将两个点标记为 0 和 1,如图 2.4 所示。NOT 操作就是任何将 0 比特转换为 1 比特,反之亦然的变换。在经典存储设备如硬盘驱动器中,NOT 门翻转存储我们比特值的磁场。如图 2.5 所示,我们可以将 NOT 视为在图 2.4 中绘制的 0 和 1 点之间实现 180°旋转。

图 2.4 一个经典比特可以处于两种不同的状态之一,通常称为 0 和 1。我们可以将经典比特表示为在 0 或 1 位置上的一个黑点。

图 2.5 经典的 NOT 操作在 0 和 1 之间翻转一个经典比特。例如,如果一个比特最初处于 0 状态,NOT 操作会将其翻转至 1 状态。
以这种方式可视化经典比特也让我们稍微扩展了比特的概念,包括描述随机比特的方法(这将在以后很有帮助)。如果我们有一枚公平的硬币(即,一枚一半时间落在正面,另一半时间落在反面的硬币),那么称那枚硬币为 0 或 1 是不正确的。如果我们把硬币的一侧放在表面上,我们才能知道硬币比特的值;我们也可以翻转它以获得随机的比特值。每次我们翻转硬币,我们都知道它最终会落下,我们会得到正面或反面。它落在正面或反面是由称为硬币偏差的概率所决定的。我们必须选择硬币的一侧来描述偏差,这可以很容易地用一个问题来表达,比如“硬币落在正面的概率是多少?”因此,一枚公平的硬币的偏差是 50%,因为它一半时间落在正面,这在图 2.6 中被映射到比特值 0。

图 2.6 我们可以使用之前的相同图片来扩展我们的比特概念,以描述一枚硬币。与比特不同,硬币每次被掷出时都有可能是 0 或 1。我们用 0 和 1 之间的点来图形化地表示这个概率。
使用这种可视化,我们可以将表示比特值 0 和 1 的前两个点用一条线连接起来,在这条线上我们可以绘制硬币的偏差。这样我们更容易看到,NOT 操作(它仍然适用于我们新的概率比特)对一枚公平的硬币没有任何作用。如果 0 和 1 出现的概率相同,那么我们旋转 0 到 1 或 1 到 0 都没有关系:我们最终还是会得到 0 和 1 具有相同的概率。
如果我们的偏差不在中间呢?如果我们知道有人试图通过使用几乎总是落在正面的一枚加权或修改过的硬币来作弊,我们可以说硬币的偏差是 90%,并在我们的线上画一个点,这个点比 1 更接近 0。
定义 在线上我们绘制每个经典比特的点是该比特的状态。
让我们考虑一个场景。比如说,我想通过密码锁发送给你一些比特。我能以最便宜的方式做到这一点吗?
一种方法是通过邮寄一个包含许多开或关的密码锁的盒子,并希望它们以与我发送时相同的状态到达。另一方面,我们可以同意所有密码锁都从 0(未锁定)状态开始,我可以给你发送关于哪些密码锁需要锁上的指令。这样,你可以购买自己的密码锁,而我只需要发送一个描述,说明如何使用经典 NOT 门来准备这些密码锁。发送一张纸或一封电子邮件比邮寄一个密码锁盒子便宜得多!
这说明了我们在整本书中都将依赖的一个原则:物理系统的状态也可以用如何准备该状态的指令来描述。因此,允许在物理系统上进行的操作也定义了可能的状态。
虽然这听起来可能完全微不足道,但我们还可以用经典比特做一件事,这对我们理解量子计算将变得至关重要:我们可以观察它们。如果我观察一个锁头并得出结论,“啊哈!这个锁头是开着的,”那么我现在可以把我的大脑想象成一种特别柔软的比特。0 消息通过我思考“啊哈!这个锁头是开着的”存储在我的大脑中,而 1 消息将通过我思考“啊,好吧,这个锁头是锁着的!”存储。实际上,通过观察一个经典比特,我已经复制它到我的大脑中。我们说测量经典比特的行为是复制那个比特。
更普遍地说,现代生活是围绕我们通过观察来复制经典比特的便利性构建的。我们复制经典比特时毫无顾忌,每秒测量数十亿个经典比特,从我们的游戏机复制数据到我们的电视。
另一方面,如果比特以硬币的形式存储,那么测量过程就涉及翻转它。测量并不完全复制硬币,因为我下一次翻转时可能会得到不同的测量结果。如果我只有一个硬币的测量结果,我就不能得出得到正面或反面的概率。我们没有在锁头比特上遇到这种歧义,因为我们知道锁头的状态要么是 0 要么是 1。如果我测量一个锁头并发现它处于 0 状态,我就知道除非我对锁头做了什么,否则它将始终处于 0 状态。
在量子计算中,情况并不完全相同,正如我们在本章后面将看到的。虽然测量经典信息足够便宜,以至于我们抱怨 5 美元的电缆能让我们测量多少亿比特,但我们必须对量子测量的方法更加小心。
2.2.2 抽象是我们的朋友
无论我们如何物理地构建比特,我们都可以(幸运的是)以相同的方式在数学和代码中表示它们。例如,Python 提供了bool类型(简称布尔,以纪念逻辑学家乔治·布尔),它有两个有效值:True和False。我们可以将 NOT 和 OR 等比特变换表示为作用于bool变量的操作。重要的是,我们可以通过描述该操作如何变换每个可能的输入来指定一个经典操作,这通常被称为真值表。
定义一个真值表是一个描述经典运算对于每个可能的输入组合的输出的表格。例如,图 2.7 显示了 AND 运算的真值表。

图 2.7 逻辑运算 AND 的真值表。如果我们知道一个逻辑运算的整个真值表,那么我们就知道该运算对任何可能的输入会做什么。
我们可以通过迭代True和False的组合在 Python 中找到 NAND(代表 NOT-AND)运算的真值表。
列表 2.2 使用 Python 打印出 NAND 的真值表
>>> from itertools import product
>>> for inputs in product([False, True], repeat=2):
... output = not (inputs[0] and inputs[1])
... print(f"{inputs[0]}\t{inputs[1]}\t->\t{output}")
False False -> True
False True -> True
True False -> True
True True -> False
注意:将操作描述为真值表适用于更复杂的操作。原则上,即使是两个 64 位整数之间的加法操作也可以写成真值表。但这并不实用,因为两个 64 位输入的真值表将有 2¹²⁸ ≈ × 10³⁸个条目,需要 10⁴⁰位来编写。相比之下,最近的估计将整个互联网的大小接近于 10²⁷位。
经典逻辑和硬件设计的大部分艺术在于制作电路,这些电路可以提供经典操作的非常紧凑的表示,而不是依赖于可能巨大的真值表。在量子计算中,我们使用幺正算子这个名字来表示量子比特的类似真值表,随着我们继续前进,我们将详细说明这一点。
总结:
-
经典比特是处于两种不同状态之一的物理系统。
-
经典比特可以通过操作来处理信息。
-
测量经典比特的行为会复制该状态中包含的信息。
注意:在下一节中,我们将使用线性代数来了解量子比特,这是量子计算机中的基本信息单元。如果您需要线性代数的复习,现在是去附录 C 走一趟的好时机。我们将在整本书中引用这个附录中的类比,我们将把向量看作地图上的方向。当您回来时,我们在这里等你!
2.3 量子比特:状态和操作
正如经典比特是经典计算机中最基本的信息单元,量子比特是量子计算机中的基本信息单元。它们可以通过具有两种状态的系统物理实现,就像经典比特一样,但它们的行为遵循量子力学定律,这允许一些经典比特无法实现的行为。让我们像对待任何其他有趣的新计算机部件一样对待量子比特:插上它,看看会发生什么!
模拟量子比特
在整本书中,我们不会使用实际的量子比特。相反,我们将使用量子比特的经典模拟。这让我们能够了解量子计算机的工作原理,并开始编写量子计算机可以解决的问题类型的小实例的编程,即使我们还没有访问解决实际问题的所需量子硬件。
这种方法的麻烦在于,在经典计算机上模拟量子比特需要指数级数量的经典资源。最强大的经典计算服务在必须简化或减少正在运行的量子程序类型之前,可以模拟大约 40 个量子比特。相比之下,截至写作时,当前商用硬件的最大容量约为 70 个量子比特。具有这么多量子比特的设备用经典计算机模拟起来非常困难,但目前可用的设备仍然太嘈杂,无法完成大多数有用的计算任务。
想象一下,如果只能用 40 个经典比特来编写经典程序!虽然与我们在经典编程中习惯使用的千兆字节相比,40 比特相当小,但我们仍然可以用仅 40 个量子比特做一些非常有趣的事情,这有助于我们原型化实际量子优势可能的样子。
2.3.1 量子位的状态
要实现我们的量子随机数生成器(QRNG),我们需要弄清楚如何描述我们的量子位。我们曾使用锁、棒球和其他经典系统来表示经典比特值 0 或 1。我们可以使用许多物理系统来充当我们的量子位,而状态是量子位可以具有的“值”。
类似于经典比特的 0 和 1 状态,我们可以为量子状态编写标签。与经典 0 和 1 最相似的量子位状态是|0〉和|1〉,如图 2.8 所示。这些分别被称为基 0和基 1。

图 2.8 使用狄拉克(括号-基)符号表示量子位,我们可以以与表示经典比特的 0 和 1 状态相同的方式图形化地表示量子位的|0〉和|1〉状态。特别是,我们将|0〉和|1〉状态绘制为轴上的相反点,如图所示。
基?
术语基来自量子计算中的一种风趣的命名方式,其历史源于一个特别愚蠢的双关语。当我们查看测量时,还有一种称为括号的另一种对象,写作〈0|。当我们把括号和基放在一起时,我们得到一对括号:〈〉。
使用括号和基表示量子力学的数学通常被称为狄拉克符号,以纪念保罗·狄拉克,他发明了这种符号以及我们现在不得不忍受的真正令人沮丧的双关语。在整个书中,我们将看到更多这种风格的风趣。
然而,需要注意的一点是,状态是一个方便的模型,用于预测量子位的行为,而不是量子位的固有属性。当我们考虑本章后面的测量时,这种区别变得尤为重要——正如我们将看到的,我们无法直接测量量子位的状态。
警告 在实际系统中,我们永远无法从有限数量的副本中提取或完美地学习量子位的状态。
不要担心现在这一切还没有完全理解;随着我们阅读这本书,我们会看到很多例子。现在需要记住的重要一点是,量子位不是状态。
如果我们想要模拟投掷后棒球的运动,我们可能会先写下它的当前位置、它的速度和方向、它的旋转方式等等。这个数字列表帮助我们代表一张纸上的棒球或计算机中的棒球,以预测那个棒球会做什么,但我们不会说棒球就是那个数字列表。为了开始我们的模拟,我们必须拿一个我们感兴趣的棒球,并测量它的位置、它的速度等等。
我们说,为了准确模拟棒球的行为,我们需要的数据的完整集合是那个棒球的状态。同样,量子比特的状态是我们模拟它和预测当我们测量它时会得到什么结果的完整数据集合。正如我们需要在模拟过程中更新棒球的模拟状态一样,当我们对量子比特应用操作时,我们也会更新量子比特的状态。
小贴士 一种记住这种微妙区别的方法是,量子比特被一个状态所描述,但量子比特不是一个状态。
当事情变得稍微微妙一些时,虽然我们可以通过复制一些经典信息而不对棒球做任何其他事情来测量棒球,但正如我们将在本书的其余部分看到的那样,我们无法完美地复制存储在量子比特中的量子信息——当我们测量量子比特时,我们会对它的状态产生影响。这可能会令人困惑,因为我们记录了量子比特的完整状态,以便我们可以在模拟器中随时查看内存。我们用实际量子比特能做的任何事情都无法让我们查看它们的状态,所以如果我们“作弊”查看模拟器的内存,我们就无法在真实硬件上运行我们的程序。
换句话说,虽然直接查看状态在构建过程中对调试经典模拟器可能很有用,但我们必须确保我们只编写基于从真实硬件中可能学到的信息编写的算法。
眼睛一闭就作弊
当我们使用量子模拟器时,模拟器必须内部存储我们的量子比特的状态——这就是为什么模拟量子系统如此困难的原因。原则上,每个量子比特都可能与其他每个量子比特相关联,因此我们需要指数级资源来在我们的模拟器中写下状态(我们将在第四章中了解更多关于这一点)。
如果我们通过直接查看模拟器存储的状态来“作弊”,那么我们只能在模拟器上运行我们的程序,而不能在实际硬件上运行。我们将在后面的章节中看到如何通过使用断言和使作弊不可见来更安全地“作弊”!。
2.3.2 操作游戏
现在我们已经为这些状态起了名字,让我们来看看如何表示它们所包含的信息。对于经典比特,我们可以简单地记录比特在任何时间包含的信息,即一条线上的值:0 或 1。这是因为我们能够进行的唯一操作就是在这条线上进行翻转(或 180° 旋转)。量子力学允许我们对量子比特应用更多种类的操作,包括小于 180° 的旋转。也就是说,量子比特与经典比特的不同之处在于我们可以对它们进行哪些操作。
注意 当前的操作是可以通过以不同的方式组合 NOT、AND 和 OR 来进行的逻辑操作,而量子操作则由旋转组成。
例如,如果我们想将量子比特的状态从 |0〉 转换为 |1〉,反之亦然,即 NOT 操作的量子对应物,我们顺时针旋转量子比特 180°,如图 2.9 所示。

图 2.9 NOT 操作在 |0〉 状态的量子比特上操作的视觉表示,将量子比特留在 |1〉 状态。我们可以将这个操作视为围绕连接 |0〉 和 |1〉 状态的线中心的 180° 旋转。
我们已经看到 180° 旋转是 NOT 门的对应物,但我们还能进行哪些其他旋转?
可逆性
当我们旋转量子状态时,我们总是可以通过反向旋转回到我们开始的状态。这种被称为 可逆性 的特性,最终证明是量子计算的基础。除了测量(我们将在本章后面更多地了解测量)之外,所有量子操作都必须是可逆的。
虽然我们习惯的所有经典操作并非都是可逆的。像 AND 和 OR 这样的操作在通常的写法中是不可逆的,因此没有一点额外的努力就无法作为量子操作实现。当我们介绍将其他经典操作表示为旋转的“反计算”技巧时,我们将在第八章中看到如何做到这一点。
另一方面,像 XOR 这样的经典操作很容易变得可逆,因此我们可以用称为 controlled NOT 的量子操作来表示它们作为旋转,正如我们在第八章中将要看到的。
如果我们将处于 |0〉 状态的量子比特顺时针旋转 90° 而不是 180°,我们得到一个量子操作,我们可以将其视为 NOT 操作的平方根,如图 2.10 所示。

图 2.10 我们也可以旋转状态小于 180 度。这样做,我们得到一个既不是 |0〉 也不是 |1〉,但位于它们之间圆周中点的状态。
正如我们之前定义一个数的平方根 √x 为一个数 y,使得 y² = x,我们可以定义量子操作的平方根。如果我们应用两次 90° 旋转,我们就会得到 NOT 操作,因此我们可以将 90° 旋转视为 NOT 的平方根。
半数和半非数
每个领域都有其绊脚石。例如,问一个图形程序员正 y 是否意味着“向上”或“向下”。在量子计算中,该领域的丰富历史和跨学科性质有时像一把双刃剑,因为对量子计算的不同思考方式都伴随着约定和符号。
这种表现方式之一是,我们很容易弄错将因子 2 放在何处。在这本书中,我们选择了遵循微软 Q# 语言使用的约定。
现在我们有一个新的状态,它既不是 |0〉 也不是 |1〉,而是它们两者的等量组合。在完全相同的意义上,我们可以通过将“北”和“东”两个方向相加来描述“东北”,我们可以将这个新状态写成如图 2.11 所示的形式。

图 2.11 我们可以通过将|0〉和|1〉状态视为方向来写出当我们旋转 90°时得到的状态。这样做,并使用一些三角学知识,我们可以得到旋转|0〉状态 90°会得到一个新的状态,即(|0〉 + |1〉) / √2。关于如何写出这类旋转的数学表达式的更多细节,请查看附录 B,以复习线性代数。
|+〉, |−〉和叠加
我们称|0〉和|1〉等量组合的状态为|+〉 = (|0〉 + |1〉) / √2 状态(由于项之间的符号)。我们说|+〉状态是|0〉和|1〉的叠加。
如果旋转是逆时针 90°,那么我们称得到的状态为|−〉 = (|0〉 − |1〉) / √2。尝试使用-90°写出这些旋转,看看我们是否得到|−〉!
量子比特的状态可以表示为一个圆上的点,该圆有两个标记的状态在两极:|0〉和|1〉。更普遍地说,我们将使用任意角度θ在量子比特状态之间进行旋转,如图 2.12 所示。

图 2.12 如果我们将|0〉状态旋转的角度不是 90°或 180°,那么得到的状态可以表示为一个圆上的点,该圆有两个标记的状态在两极:|0〉和|1〉。这为我们提供了一种可视化单个量子比特可能处于的状态的方法。
从数学上讲,我们可以将代表我们的量子比特的圆上的任何点的状态写成 cos(θ / 2) |0〉 + sin(θ / 2) |1〉,其中|0〉和|1〉是分别表示向量[[1], [0]]和[[0], [1]]的不同方式。
小贴士 一种思考基矢量表示法的方式是它为常用的向量赋予名称。当我们写|0〉 = [[1], [0]]时,我们是在说[[1], [0]]足够重要,以至于我们用 0 来命名它。同样,当我们写|+〉 = [[1], [1]] / √2 时,我们为将贯穿本书的状态的向量表示赋予了一个名称。
另一种说法是,一个量子比特通常是|0〉和|1〉向量的线性组合,其系数描述了|0〉需要旋转到该状态的角度。为了在编程中更有用,我们可以写出旋转状态如何影响|0〉和|1〉状态,如图 2.13 所示。

图 2.13 使用线性代数,我们可以将单个量子比特的状态描述为一个二维向量。在这个方程中,我们展示了这种关于量子比特状态的想法如何与我们之前使用的狄拉克(内积-外积)表示法相关联。特别是,我们展示了使用向量表示法和狄拉克表示法旋转|0〉状态任意角度θ后的最终状态;这两种方法将在我们的量子之旅的不同阶段都有所帮助。
小贴士 这与我们之前使用向量基表示线性函数为矩阵的情况完全相同。
我们将在本书中学习其他量子操作,但这些操作最容易通过旋转来可视化。表 2.2 总结了我们可以从这些旋转中创建的状态。
表 2.2 状态标签、狄拉克表示法的展开和作为向量的表示
| 状态标签 | 狄拉克表示法 | 向量表示 |
|---|---|---|
| |0〉 | |0〉 | [[1], [0]] |
| |1〉 | |1〉 | [[0], [1]] |
| |+〉 | (|0〉 + |1〉) / √2 | [[1 / √2], [1 / √2]] |
| |−〉 | (|0〉 − |1〉) / √2 | [[1 / √2], [–1 / √2]] |
一大堆数学
初看之下,像 |+〉 = (|0〉 + |1〉) / √2 这样的东西大声说出来会很糟糕,使得它在对话中变得毫无用处。然而,在实践中,量子程序员在口头表达或在白板上草图时通常会采取一些捷径。
例如,“√2”部分总是必须存在的,因为表示量子状态的向量必须总是长度为 1;这意味着我们有时可以稍微随意一些,写出 “|+〉 = |0〉 + |1〉”,并依赖我们的听众记住除以 √2。如果我们正在做演讲或在喝茶时讨论量子计算,我们可能会说,“ket plus 是 ket 0 加 ket 1”,但没有 bra 和 ket 的帮助,这个词 plus 的重复使用会有些令人困惑。为了通过口头强调加法可以让我们表示叠加,我们可能会说,“plus 状态是 0 和 1 的等价叠加”,而不是。
2.3.3 测量量子比特
当我们想要检索存储在量子比特中的信息时,我们需要测量量子比特。理想情况下,我们希望有一个测量设备能够让我们一次性直接读取关于状态的所有信息。然而,根据量子力学的定律,这不可能,正如我们在第三章和第四章中将会看到的。尽管如此,测量可以让我们了解相对于系统特定方向的状态信息。例如,如果我们有一个处于 |0〉 状态的量子比特,并且我们检查它是否处于 |0〉 状态,我们总是会得到它是的答案。另一方面,如果我们有一个处于 |+〉 状态的量子比特,并且我们检查它是否处于 |0〉 状态,我们会有 50% 的概率得到 0 的结果。如图 2.14 所示,这是因为 |+〉 状态与 |0〉 和 |1〉 状态重叠相同,这样我们就会以相同的概率得到两个结果。

图 2.14 |+〉 状态与 |0〉 和 |1〉 状态重叠相同,因为其“影子”正好位于中间。因此,当我们观察我们的量子比特以确定其处于 |0〉 或 |1〉 状态时,如果我们的量子比特最初处于 |+〉 状态,我们将以相同的概率获得两个结果。我们可以将 |+〉 状态在 |0〉 和 |1〉 状态之间的“影子”视为一种硬币。
小贴士量子比特的测量结果总是是经典比特值!换句话说,无论我们测量的是经典比特还是量子比特,我们的结果总是经典比特。
大多数时候,我们会选择测量我们是否有|0〉或|1〉;也就是说,我们希望沿着|0〉和|1〉之间的线进行测量。为了方便起见,我们给这个轴起了一个名字:Z-轴。我们可以通过使用内积将我们的状态向量投影到Z-轴上(见图 2.15)来直观地表示这一点。

图 2.15 展示了量子测量的可视化,这可以被视为将状态投影到特定方向。例如,如果一个量子比特被旋转,使其状态接近|1〉状态,那么测量它更有可能返回 1 而不是 0。
提示:如果您需要复习内积,请参阅附录 B。
想象一下,从我们绘制量子比特状态的点向Z-轴照射手电筒;得到 0 或 1 结果的可能性由状态在Z-轴上留下的影子决定。
深入探讨:为什么测量不是线性的?
在大谈量子力学的线性之后,我们立即引入测量作为非线性可能看起来有些奇怪。如果我们允许非线性操作如测量,我们也能实现其他非线性操作,如克隆量子比特吗?
简而言之,虽然每个人都同意测量背后的数学,但关于如何最好地理解量子测量为何如此运作的哲学讨论仍然很多。这些讨论属于量子基础的范畴,并试图通过理解为什么来做到更多,而不仅仅是理解量子力学是什么以及它预测了什么。在理解经典概率时,我们可以通过考虑诸如游戏节目策略或赌场如何从看似会输钱的游戏中赢钱这样的反直觉思维实验,量子基础通过进行小型的思维实验来探索量子力学的不同方面,从而发展新的解释。幸运的是,量子基础的一些结果可以帮助我们理解测量。
尤其值得注意的是,我们可以通过将测量装置的状态包含在我们的描述中来使量子测量线性化;我们将在第四章和第六章中看到实现这一目标所需的数学工具。当这一观察被推向极致时,它会导致诸如多世界解释这样的解释。多世界解释通过坚持只考虑包含测量设备的状态来解决测量的解释问题,这样测量的明显非线性实际上并不存在。
在另一个极端,我们可以通过注意到量子测量中的非线性与统计学中一个称为贝叶斯推理的分支中的非线性完全相同来解释测量。因此,量子力学只有在我们忘记包括一个代理在进行测量并从每个结果中学习时才显得非线性。这一观察导致我们将量子力学视为对世界的描述,而不是对我们所了解的世界进行描述。
尽管这两种解释在哲学层面上存在分歧,但它们都提供了不同的方法来解决线性理论如量子力学有时看似非线性的问题。无论哪种解释能帮助你理解测量与量子力学其他部分之间的相互作用,你都可以安心,因为测量结果总是由相同的数学和模拟来描述。实际上,依赖于模拟(有时被讽刺地称为“闭嘴计算”解释)是所有解释中最古老且最被推崇的。
每个投影的平方长度代表我们测量到的状态将沿着该方向被找到的概率。如果我们有一个处于|0〉状态的量子比特并尝试沿着|1〉状态的方向测量它,我们将得到概率为零,因为当我们把它们画在圆上时,状态是相反的。从图像的角度思考,|0〉状态在|1〉状态上没有投影——在图 2.15 的意义上,|0〉没有在|1〉上留下阴影。
提示:如果某事发生的概率为 1,那么该事件总是会发生。如果某事发生的概率为 0,那么该事件是不可能的。例如,一个典型的六面骰子掷出 7 的概率为零,因为这种情况是不可能发生的。同样,如果一个量子比特处于|0〉状态,从Z轴测量得到 1 的结果是不可能的,因为|0〉没有在|1〉上的投影。
然而,如果我们有一个|0〉并尝试沿着|0〉方向测量它,我们将得到概率为 1,因为状态是平行的(并且根据定义长度为 1)。让我们来分析一个既不平行也不垂直的状态的测量。
示例
假设我们有一个处于状态(|0〉 + |1〉) / √2(与表 2.1 中的|+〉相同)的量子比特,我们想要测量它或沿着Z轴投影它。然后我们可以通过将|+〉投影到|1〉上来找到经典结果为 1 的概率。
我们可以通过使用它们向量表示之间的内积来找到一个状态到另一个状态的投影。在这种情况下,我们写出|+〉和|1〉的内积为〈1 | +〉,其中〈1|是|1〉的转置,并且将两个竖线并排放置表示取内积。稍后,我们将看到〈1|是|1〉的共轭转置,被称为“bra”。
我们可以这样写出:

为了将这个投影转换为概率,我们将其平方,得到当我们制备 |+〉 状态时观察到 1 的结果的概率是 1/2。
我们通常投影到 Z-轴,因为它在许多实际实验中很方便,但我们也可以沿 X-轴测量,以查看我们是否有 |+〉 或 |−〉 状态。沿 X-轴测量,我们肯定得到 |+〉,永远不会得到 |−〉,如图 2.16 所示。

图 2.16 沿 X-轴测量 |+〉 总是得到 |+〉。为了理解这一点,请注意 |+〉 状态在 X-轴上留下的“阴影”(即 |−〉 和 |+〉 状态之间的线)正是 |+〉 状态本身。
注意:我们之所以能够得到一个完全确定的测量结果,仅仅是因为我们事先知道了“正确的”测量方向;如果我们只是被 handed 一个没有任何关于“正确的”测量方向的信息的状态,我们就无法完美地预测任何测量结果。
2.3.4 测量泛化:基独立
有时候我们可能不知道我们的量子比特是如何制备的,因此我们不知道如何正确地测量比特。更普遍地说,任何不重叠(即相对极点)的状态对都定义了相同的测量。测量的实际结果是一个经典比特值,它指示我们在进行测量时状态与哪个极点对齐。
更广泛的测量
量子力学允许进行更多种类的测量——随着我们的深入,我们将看到其中的一些,但本书主要关注的是检查两个相反极点的情况。这种选择是控制大多数量子设备的一种相当方便的方式,并且可以用于目前几乎所有可用的商业量子计算平台。
从数学上讲,我们使用像 〈measurement | state〉 这样的符号来表示测量一个量子比特。左边的部分,〈measurement|,被称为 bra,我们已经看到了右边的 ket 部分。它们合在一起被称为 braket!
Bra 与 ket 非常相似,但要从其中一个切换到另一个,我们必须取 bra 或 ket 的转置(将行转换为列,反之亦然):

另一种思考方式是,取转置将列向量(ket)转换为行向量(bra)。
注意:由于我们现在只处理实数,我们不需要在 ket 和 bra 之间做任何其他事情。但当我们下一章处理复数时,我们还需要复共轭。
夸克让我们可以写下测量。但要看到测量实际上做了什么,我们需要更多一样东西:一个规则,用来如何将一个态的克和另一个测量的克结合使用,以得到看到该测量结果的概率。在量子力学中,测量概率是通过观察态的克在测量的克上留下的投影或阴影的长度来找到的。我们从经验中知道,我们可以使用内积来找到投影和长度。在狄拉克符号中,一个克和一个态的内积写作 〈测量 | 态〉,这正好是我们需要的规则。
例如,如果我们准备了一个|+〉态,并且想知道当我们在这个z基中进行测量时观察到 1 的概率,那么按照图 2.15 所示进行投影,我们可以找到所需的长度。|+〉在〈1|上的投影告诉我们,我们以概率 Pr(1|+) = |〈1|+〉|² = |〈1|0〉 + 〈1|1〉|² / 2 = |0 + 1|² / 2 = 1 / 2 看到 1 的结果。因此,50%的时间我们会得到 1 的结果。其他 50%的时间,我们会得到 0 的结果。
玻恩规则
如果我们有一个量子态|state〉并且沿着〈测量|方向进行测量,我们可以写出我们观察到测量作为结果的概率为
Pr(测量|态) = |〈测量 | 态〉|²
换句话说,概率是测量克和态克的内积的模长的平方。
这个表达式被称为玻恩规则。
在表 2.3 中,我们列出了使用玻恩规则预测当我们测量量子比特时会得到哪些经典比特的几个其他例子。
表 2.3 使用玻恩规则寻找测量概率的例子
| 如果我们准备... | ...并且我们测量... | ...那么我们以这个概率看到这个结果。 |
|---|---|---|
| 〈0 | 〈0 | |
| 〈0 | 〈1 | |
| 〈0 | 〈+ | |
| 〈+ | 〈+ | |
| 〈+ | 〈– | |
| –〈0 | 〈0 | |
| – | +〉 | 〈– |
小贴士:在表 2.3 中,我们使用了〈0|0〉 = 〈1|1〉 = 1 和〈0|1〉 = 〈1|0〉 = 0 的事实。(试着亲自检查一下!)当两个态的内积为零时,我们说它们是正交的(或垂直的)。|0〉和|1〉的正交性使得许多计算变得容易快速完成。
我们现在已经涵盖了关于量子比特所需了解的一切,以便能够模拟它们!让我们回顾一下我们需要满足的要求,以确保我们有工作的量子比特。
量子比特
量子比特是任何满足三个特性的物理系统:
-
在了解数字向量(状态)的情况下,系统可以被完美模拟。
-
系统可以使用量子操作(例如,旋转)进行转换。
-
任何对系统的测量都会产生一个遵循波恩规则的经典比特信息。
任何时候我们有一个量子比特(具有前三个特性的系统),我们都可以使用相同的数学或模拟代码来描述它,而无需进一步参考我们正在处理的是哪种系统。这类似于我们不需要知道一个比特是由弹球运动的方向还是晶体管的电压定义的,就可以编写 NOT 和 AND 门,或者编写使用这些门进行有趣计算软件。
注意与我们将单词比特用于表示存储信息的物理系统和存储在比特中的信息类似,我们也将单词量子比特用于表示量子设备和存储在该设备中的量子信息。
相位
在表 2.3 的最后两行中,我们看到了将状态乘以-1 的相位不会影响测量概率。这不是巧合,而是指向量子比特更有趣的特性之一。因为波恩规则只关心状态与测量之间内积的平方绝对值,乘以一个数(-1)不会影响它的绝对值。我们称绝对值等于 1 的数,如+1 或-1,为相位。在下一章中,我们将更多地了解相位,当我们更多地使用复数时。
现在,我们说将整个向量乘以-1 是应用全局相位的一个例子,而从|+〉变为|−〉是应用|0〉和|1〉之间的相对相位的一个例子。虽然全局相位永远不会影响测量结果,但|+〉 = (|0〉 + |1〉) / √2 和|−〉 = (|0〉 − |1〉) / √2 这两个状态之间有很大的不同:|+〉和|−〉前面的系数是相同的,而在|−〉中它们相差一个相位(-1)。我们将在第三章、第四章、第六章和第七章中看到这两个概念之间更多的差异。
2.3.5 在代码中模拟量子比特
现在我们已经了解了如何描述量子比特的状态、操作和测量,是时候看看如何用代码来表示所有这些概念了。我们将使用我们的朋友伊夫的场景来激发我们编写的代码。
假设我们想要保守伊夫的**
的秘密,以免其他人发现。我们如何打乱我们的信息,以便只有伊夫能阅读它?
我们将在下一章中更深入地探讨这个应用,但任何好的加密算法最基本的一步是需要一个难以预测的随机数源。让我们写下确切地如何将我们的秘密和随机比特结合起来,以发送给 Eve 的安全信息。图 2.17 显示,如果你和 Eve 都知道相同的随机经典比特序列,我们可以使用这个序列来安全地通信。在章节开始时,我们看到了如何将我们想要发送给 Eve 的消息(在这种情况下,“
”)作为经典比特的字符串来编写。一次性密码是一系列随机经典比特,它充当打乱或加密我们消息的方式。这种打乱是通过在每个位置上对消息和一次性密码比特进行位异或操作来完成的。这然后产生了一个称为密文的经典比特序列。对于任何试图读取我们消息的人来说,密文看起来像随机比特。例如,无法判断密文中的比特是 1 是因为明文还是一次性密码。

图 2.17 如何使用随机比特加密秘密的示例,即使是在互联网或另一个不受信任的网络中。在这里,我们试图安全地发送消息“
”。如果我们和 Eve 以共享的秘密“
”开始,那么我们可以使用它作为一次性密码来保护我们的消息。
你可能会问我们如何得到一次性密码的随机比特字符串。我们可以用量子比特来制作自己的量子随机数发生器!这听起来可能有些奇怪,但我们将使用经典比特来模拟量子比特以制作我们的量子随机数发生器。它生成的随机数不会比我们用来进行模拟的计算机更安全,但这种方法让我们对量子比特的工作原理有了良好的理解。
让我们向 Eve 发送我们的信息!就像经典比特可以通过代码中的True和False值来表示一样,我们已经看到我们可以将两个量子比特状态|0〉和|1〉表示为向量。也就是说,量子比特状态在代码中以数字的列表形式表示。
列表 2.3 使用 NumPy 在代码中表示量子比特
>>> import numpy as np ❶
>>> ket0 = np.array( ❷
... [[1], [0]]
... )
>>> ket0
array([[1], ❸
[0]])
>>> ket1 = np.array(
... [[0], [1]]
... )
>>> ket1
array([[0],
[1]])
❶ 我们使用 Python 的 NumPy 库来表示向量,因为 NumPy 高度优化,会使我们的工作更加容易。
❷ 我们将我们的变量 ket0 命名为|0〉的表示法,其中我们通过〈〉括号的 ket 半部分来标记量子比特状态。
❸ NumPy 将以列的形式打印 2 × 1 向量。
正如我们之前看到的,我们可以通过使用|0〉和|1〉的线性组合来构造其他状态,如|+〉。在完全相同的意义上,我们可以使用 NumPy 将|0〉和|1〉的向量表示相加来构造|+〉的向量表示。
列表 2.4 |+〉的向量表示
>>> ket_plus = (ket0 + ket1) / np.sqrt(2) ❶
>>> ket_plus
array([[0.70710678+0.j], ❷
[0.70710678+0.j]])
❶ NumPy 使用向量来存储|+〉状态,这是|0〉和|1〉的线性组合。
❷ 在这本书中,我们会经常看到数字 0.70710678,因为它是对√2,即向量[[1], [1]]长度的良好近似。
在经典逻辑中,如果我们想模拟一个运算如何转换一个比特列表,我们可以使用真值表。同样,由于量子运算(除了测量)总是线性的,为了模拟一个运算如何转换量子比特的状态,我们可以使用一个矩阵,它告诉我们每个状态是如何转换的。
线性算子和量子运算
将量子运算描述为线性算子是一个好的开始,但并非所有线性算子都是有效的量子运算!如果我们能够实现一个由线性算子(如 2 × 𝟙,即两次单位算子)描述的运算,那么我们就能违反概率总是介于零和一之间的规则。我们还要求所有量子运算(除了测量)都是 可逆的,因为这是量子力学的一个基本属性。
事实上,量子力学中可实现的运算可以用矩阵 U 来描述,其逆矩阵 U^(–1) 可以通过取共轭转置来计算,U^(–1) = U^+. 这样的矩阵被称为 单位矩阵。

可视化有效量子运算的类型。所有单位算子都是线性的,但并非所有线性算子都是单位算子。可逆量子运算(即除了测量之外)由不是仅仅线性而是单位算子表示。
一个特别重要的量子运算就是 Hadamard 运算,它将 |0〉 转换为 |+〉,将 |1〉 转换为 |−〉。正如我们之前看到的,沿着 Z-轴测量 |+〉 会以相等的概率给出 0 或 1 的结果。由于我们想要随机比特来发送秘密消息,Hadamard 运算对于制作我们的量子随机数生成器非常有用。
使用矢量和矩阵,我们可以通过制作一个表格来定义 Hadamard 运算,该表格显示了它在 |0〉 和 |1〉 状态上的作用,如表 2.4 所示。
表 2.4 将 Hadamard 运算表示为表格
| 输入状态 | 输出状态 |
|---|---|
| |0〉 | |+〉 = (|0〉 + |1〉) / √2 |
| |1〉 | |−〉 = (|0〉 − |1〉) / √2 |
由于量子力学是线性的,这是 Hadamard 运算的完整描述!
以矩阵形式,我们将表 2.4 写为 H = np.array([[1, 1], [1, -1]]) / np .sqrt(2).
列表 2.5 定义 Hadamard 运算
>>> H = np.array([[1, 1], [1, -1]]) / np.sqrt(2) ❶
>>> H @ ket0
array([[0.70710678],
[0.70710678]])
>>> H @ ket1
array([[ 0.70710678],
[-0.70710678]])
❶ 我们定义一个变量 H 来保存我们在表 2.4 中看到的 Hadamard 运算的矩阵表示 H。在整个本章的其余部分,我们都需要 H,所以在这里定义它是很有帮助的。
Hadamard 运算
Hadamard 运算是一种量子运算,可以通过以下线性变换来模拟:

任何量子数据的运算都可以用这种方式写成矩阵。如果我们想将 |0〉 转换为 |1〉 并反之亦然(这是我们在之前看到的经典非运算的量子推广,对应于 180°旋转),我们就做与定义 Hadamard 运算时相同的事情。
列表 2.6 表示量子非门
>>> X = np.array([[0, 1], [1, 0]]) ❶
>>> X @ ket0
array([[0],
[1]])
>>> (X @ ket0 == ket1).all() ❷
True
>>> X @ *H* @ ket0 ❸
array([[0.70710678],
[0.70710678]])
❶ 对应于经典非操作的经典量子操作通常称为 x 操作;我们用 Python 变量 X 表示 x 的矩阵。
❷ 我们可以确认 X 将 |0〉 转换为 |1〉。NumPy 方法 all() 如果 X @ ket0 == ket1 的每个元素都为 True,则返回 True:也就是说,如果 X @ ket0 数组的每个元素都等于 ket1 的相应元素。
❸ 乘法算子对 H|0〉没有作用。我们可以再次使用 @ 算子来乘以代表状态 |+〉 = H|0〉 的 Python 值。我们可以将这个值表示为 H @ ket0。
乘法算子对最后一个输入没有作用,因为乘法算子交换了 |0〉 和 |1〉。H|0〉 状态,也称为 |+〉,已经是两个基矢的和:(|0〉 + |1〉) / √2 = (|1〉 + |0〉) / √2,所以乘法算子的交换操作没有作用。
回顾附录 C 中的映射类比,我们可以将矩阵 H 视为关于
方向的 反射,如图 2.18 所示。

图 2.18 h 操作作为关于
的反射或翻转。与 90°旋转不同,应用两次 h 将量子比特返回到初始状态。另一种思考 h 的方式是,关于
的反射交换了 X 轴和 Z 轴的作用。
第三维等待着!
对于量子比特,附录 C 中的映射类比帮助我们理解如何编写和操作单个量子比特的状态。然而,到目前为止,我们只看了可以用实数表示的状态。通常,量子状态可以使用复数。如果我们调整我们的映射并使其成为三维的,我们就可以毫无问题地包含复数。这种关于量子比特的思考方式被称为 布洛赫球面,在考虑量子操作,如旋转和反射时非常有用,正如我们在第六章中将要看到的。

更一般地,我们可以将量子比特状态可视化为球面上的点,而不仅仅是圆上的点。这样做需要使用复数,我们将在第六章中了解更多。
深入探讨:无限多个状态?
从上一侧栏的图中可能看起来,一个量子比特有无限多种不同的状态。对于球面上的任意两个不同点,我们总能找到一个位于它们“之间”的点。虽然这是真的,但也可能有些误导。暂时考虑一下经典情况,一个 90%时间落地正面的硬币与一个 90.0000000001%时间落地正面的硬币是不同的。实际上,我们总能制造出一个其偏差“位于”两个其他硬币偏差之间的硬币。抛硬币只能给我们一个经典比特的信息。平均来说,需要抛大约 1023 次才能区分一个 90%时间落地正面的硬币和一个 90.0000000001%时间落地正面的硬币。我们可以将这两个硬币视为相同,因为我们无法进行一个可靠地区分它们的实验。同样,对于量子计算,我们区分从 Bloch 球面图中识别出的无限多种不同量子状态的能力是有限的。
一个量子比特有无限多种状态并不是它独特的地方。有时人们会说一个量子系统可以“同时处于无限多种状态”,这就是为什么他们会说量子计算机可以提供加速。这是错误的! 如前所述,我们无法区分非常接近的状态,所以“无限多种”这一说法并不能给量子计算带来优势。我们将在接下来的章节中更多地讨论“同时”这一部分,但可以简单地说,并不是我们的量子比特可以处于的状态数量使得量子计算机变得酷!
2.4 编程一个工作用的量子随机数发生器
现在我们有几个量子概念可以玩,让我们应用我们所学到的知识来编程一个量子随机数发生器(QRNG),这样我们就可以无忧无虑地发送![../Images/emoji-sparklingheart.png]了。我们将构建一个返回 0 或 1 的 QRNG。
随机比特或随机数?
可能看起来我们的 QRNG 只能输出两个数字中的一个,0 或 1。相反:这足以生成 0 到N范围内的随机数,其中N是任何正整数。最容易看到这一点是从特殊情况开始,即N是某个正整数n的 2^(n) − 1,在这种情况下,我们只需将我们的随机数写成n-比特字符串。例如,我们可以通过生成三个随机比特r[0]、r[1]和r[2],然后返回 4r[2] + 2r[1] + r[0]来生成 0 到 7 之间的随机数。
如果N不是 2 的幂,情况会稍微复杂一些,因为我们有“剩余”的可能性需要处理。例如,如果我们需要掷一个六面的骰子,但我们只有八面的骰子(也许我们在最新的 RPG 之夜玩了一个德鲁伊),那么当骰子掷出 7 或 8 时,我们需要决定怎么办。如果我们想要一个公平的六面骰子,最好的办法就是在那种情况下重新掷骰子。使用这种方法,我们可以从抛硬币中构建任意公平的骰子——这对于我们想要玩的游戏来说非常方便。简而言之,我们并不受 QRNG 只有两种结果的限制!
与任何量子程序一样,我们的 QRNG 程序将是一系列指令,指示设备对一个量子比特执行操作(见图 2.19)。在伪代码中,实现 QRNG 的量子程序由三个指令组成:
-
准备一个处于|0〉状态的量子比特。
-
将 Hadamard 操作应用于我们的量子比特,使其处于状态|+〉 = H|0〉。
-
测量量子比特以获得 0 或 1 的结果,概率各为 50%。

图 2.19 我们想要测试的 QRNG 程序步骤。回顾图 2.3,我们可以使用我们迄今为止所学到的知识来编写 QRNG 算法中每一步后的量子比特状态。
即,我们希望程序看起来像以下这样。
列表 2.7 QRNG 程序的示例伪代码
def qrng():
q = Qubit()
H(q)
return measure(q)
使用矩阵乘法,我们可以使用像笔记本电脑这样的经典计算机来模拟qrng()在理想量子设备上的行为。我们的qrng程序调用一个软件栈(见图 2.20),该软件栈抽象化了我们是使用经典模拟器还是实际量子设备。

图 2.20 一个量子程序软件栈可能的样子
栈有很多部分,但不用担心——我们将随着进展讨论它们。现在,我们专注于图中的顶部部分(标记为“经典计算机”),并将从编写 Python 中的量子程序以及模拟器后端代码开始。
注意:在第七章中,我们将转向使用微软量子开发工具包提供的模拟器后端。
在心中有了对软件栈的这种看法之后,我们可以首先编写一个QuantumDevice类,其中包含用于分配量子比特、执行操作和测量量子比特的抽象方法。然后,我们可以用模拟器实现这个类,并在qrng()中调用该模拟器。
为了使我们的模拟器接口看起来像图 2.20 所示,让我们列出我们需要我们的量子设备执行的操作。首先,用户必须能够分配和返回量子比特。
列表 2.8 interface.py:量子设备的接口作为抽象方法
class QuantumDevice(metaclass=ABCMeta):
@abstractmethod
def allocate_qubit(self) -> Qubit: ❶
pass
@abstractmethod
def deallocate_qubit(self, qubit: Qubit): ❷
pass
@contextmanager
def using_qubit(self): ❸
qubit = self.allocate_qubit()
try:
yield qubit
finally:
qubit.reset() ❹
self.deallocate_qubit(qubit)
❶ 任何量子设备的实现都必须实现此方法,使用户能够获取量子比特。
❷ 当用户完成对一个量子比特的使用后,deallocate_qubit的实现将允许用户将量子比特返回到设备。
❸ 我们可以提供一个 Python 上下文管理器,以便安全地分配和释放量子比特。
❹ 上下文管理器确保无论抛出什么异常,每个量子比特在返回到经典计算机之前都会被重置和释放。
量子比特本身可以暴露我们需要的实际转换:
-
用户必须能够在量子比特上执行哈达玛操作。
-
用户必须能够测量量子比特以获取经典数据。
列表 2.9 interface.py:量子设备上量子比特的接口
from abc import ABCMeta, abstractmethod
from contextlib import contextmanager
class Qubit(metaclass=ABCMeta):
@abstractmethod
def h(self): pass ❶
@abstractmethod ❷
def measure(self) -> bool: pass
@abstractmethod
def reset(self): pass ❸
❶ H 方法使用哈达玛操作 np.array([[1, 1], [1, –1]]) / np.sqrt(2) 在原地转换量子比特(不创建副本)。
❷ 测量方法允许用户测量量子比特并提取经典数据。
❸ 重置方法使用户能够从头开始再次准备量子比特。
在此基础上,我们可以使用这些新类返回到 qrng 的定义。
列表 2.10 qrng.py:qrng 设备的定义
def qrng(device: QuantumDevice) -> bool:
with device.using_qubit() as q:
q.h()
return q.measure()
如果我们使用名为 SingleQubitSimulator 的类实现 QuantumDevice 接口,那么我们可以将其传递给 qrng 以在模拟器上运行我们的 QRNG 实现。
列表 2.11 qrng.py:qrng.py 的 main 定义
if __name__ == "__main__":
qsim = SingleQubitSimulator()
for idx_sample in range(10):
random_sample = qrng(qsim)
print(f"Our QRNG returned {random_sample}.")
现在我们已经拥有了编写 SingleQubitSimulator 所需的一切。我们首先为向量 |0〉 和哈达玛操作的矩阵表示定义几个常量。
列表 2.12 simulator.py:定义有用的常量
KET_0 = np.array([
[1],
[0]
], dtype=complex) ❶
H = np.array([
[1, 1],
[1, -1]
], dtype=complex) / np.sqrt(2) ❷
❶ 由于我们将在模拟器中大量使用 |0〉,因此我们为它定义了一个常量。
❷ 类似地,我们将使用哈达玛矩阵 H 来定义哈达玛操作如何转换状态,因此我们也为它定义一个常量。
接下来,我们定义模拟量子比特的外观。从模拟器的角度来看,量子比特封装了一个存储量子比特当前状态的向量。我们使用 NumPy 数组来表示量子比特的状态。
列表 2.13 simulator.py:定义表示设备中量子比特的类
class SimulatedQubit(Qubit):
def __init__(self): ❶
self.reset()
def h(self): ❷
self.state = H @ self.state
def measure(self) -> bool:
pr0 = np.abs(self.state[0, 0]) ** 2 ❸
sample = np.random.random() <= pr0 ❹
return bool(0 if sample else 1) ❺
def reset(self):
self.state = KET_0.copy()
❶ 作为 Qubit 接口的一部分,我们确保重置方法将我们的量子比特准备在 |0〉 状态。我们可以使用它来创建量子比特,以确保量子比特始终从正确的状态开始。
❷ 通过将矩阵表示 H 应用到我们当前存储的状态,然后更新到新状态,可以模拟哈达玛操作。
❸ 我们将量子比特的状态存储为向量,因此我们知道与 |0〉 的内积只是该向量的第一个元素。例如,如果状态是 np.array([[a], [b]]) 对于某些数字 a 和 b,那么观察到 0 的概率是 |a|²。我们可以使用 np.abs(a) ** 2 来找到这个概率。这给出了量子比特测量结果为 0 的概率。
❹ 为了将得到 0 的概率转换为测量结果,我们使用 np.random.random 生成一个介于 0 和 1 之间的随机数,并检查它是否小于 pr0。
❺ 最后,如果我们得到 0,则返回 0 给调用者,如果得到 1,则返回 1。
哪个随机数先出现:0 还是 1?
在制作这个量子随机数生成器(QRNG)时,我们必须调用一个经典的随机数生成器。这可能会感觉有点循环,但这是因为我们的经典模拟只是那样:一个模拟。对 QRNG 的模拟并不比我们用来实现该模拟器的硬件和软件更随机。
话虽如此,量子程序 qrng.py 本身不需要调用经典 RNG,而是调用模拟器。如果我们要在实际的量子设备上运行 qrng.py,模拟器和经典 RNG 将被实际比特的操作所取代。到那时,由于量子力学的定律,我们将得到一个无法预测的随机数流。
运行我们的程序,我们现在得到了我们预期的随机数!
$ python qrng.py
Our QRNG returned False.
Our QRNG returned True.
Our QRNG returned True.
Our QRNG returned False.
Our QRNG returned False.
Our QRNG returned True.
Our QRNG returned False.
Our QRNG returned False.
Our QRNG returned False.
Our QRNG returned True.
恭喜!我们不仅编写了我们的第一个量子程序,还编写了模拟后端,并使用它以与实际量子计算机相同的方式运行我们的量子程序。
深入了解:薛定谔的猫
你可能已经以一个非常不同的名字看到或听说过这个量子项目。QRNG 项目通常用薛定谔的猫思想实验来描述。假设一只猫在一个封闭的盒子里,盒子里有一个装有毒药的瓶子,如果某个特定的随机粒子发生衰变,毒药就会被释放。在我们打开盒子检查之前,我们如何知道它是活的还是死的?
整个系统的[状态]将通过在其中包含活猫和死猫(请原谅这种表达)以相等部分混合或扩散来表示。
——欧文·薛定谔
从历史上看,薛定谔在 1935 年提出了这种描述,通过一个强调这些含义如何反直觉的思想实验来表达他的观点,即量子力学的某些含义是“荒谬的”。这类思想实验,被称为思想实验,是物理学中的一种著名传统,通过将理论推向极端或荒谬的极限,可以帮助我们理解或批判不同的理论。
然而,近一个世纪后阅读关于薛定谔的猫,记住在这期间发生的一切是有帮助的。自从他的原始信件以来,世界已经看到了
-
想象不到规模的战争
-
人类探索我们星球之外的第一次步骤
-
商业喷气旅行的兴起
-
人类活动引起的气候变化的理解和首次影响
-
我们沟通方式的根本转变(从电视一直到互联网)
-
便宜的计算设备的广泛可用性
-
发现了各种亚原子粒子的奇妙多样性
我们生活的世界与薛定谔试图理解量子力学时的世界并不相同!我们在试图理解方面有很多优势,其中最显著的是,我们可以通过使用经典计算机编程模拟来快速掌握量子力学。例如,我们之前看到的h指令将我们的量子比特置于类似于思想实验中猫的状态,但优势在于,与思想实验相比,实验我们的程序要容易得多。在本书的其余部分,我们将使用量子程序来学习编写量子算法所需的量子力学部分。
摘要
-
随机数在广泛的应用中都有帮助,例如玩游戏、模拟复杂系统和保护数据。
-
经典比特可以处于两种状态之一,我们传统上将其称为 0 和 1。
-
量子力学中的经典比特的类似物,称为量子比特,可以处于|0〉或|1〉状态,或者处于|0〉和|1〉的叠加状态;例如,|+〉 = 1 / √2 (|0〉 + |1〉)。
-
通过使用 Hadamard 运算,我们可以将量子比特准备在|+〉状态;然后我们可以测量量子比特以生成由量子力学定律保证的随机数。
3 使用量子密钥分发共享秘密
本章涵盖
-
认识量子资源对安全性的影响
-
编程 Python 模拟量子密钥分发协议
-
实现量子非操作
在上一章中,我们开始尝试使用量子位,并用它们在 Python 中构建了一个量子随机数生成器。在这一章中,我们看到量子位可以通过让我们安全地分发密钥来帮助我们进行加密(或其他加密任务)。有经典的方法来共享随机密钥(例如,RSA),但它们对共享安全性的保证各不相同。
3.1 爱情和加密无所不能
我们在第二章中有一个量子随机数生成器,但这只是我们与朋友共享秘密所需的一半。如果我们想使用量子随机数与朋友安全通信,我们需要与他们共享这些随机数。这些随机数(通常称为密钥)可以与加密算法结合使用,该算法将密钥的随机性与人们想要保密的信息结合起来,这样只有拥有密钥的人才能看到信息。我们可以在图 3.1 中看到两个人如何使用密钥(这里是一个随机二进制字符串)来加密和解密他们之间的消息。

图 3.1 我们和 Eve 可能如何使用加密来秘密通信的思维模型,即使是在互联网或其他不受信任的网络中
如果我们想涉及量子位,我们可以证明使用量子密钥分发(QKD)是可证明安全的,而经典密钥分发方法通常是计算安全的。
定义 量子密钥分发(QKD)是一种通信协议,允许用户通过交换量子位和经过身份验证的经典信息来共享量子随机数。
这种差异对于大多数用例来说并不重要。但如果我们是一个政府、活动团体、银行、记者、间谍或其他任何信息安全是生死攸关的实体,这可就大不相同了。
计算安全与可证明安全
加密协议的可证明安全性是梦想。如果一个加密任务的某个方法或协议可以通过不假设任何关于对手的证明来证明其安全性:即,他们可以拥有宇宙中所有的时间和计算能力,而我们的协议仍然安全!我们当前的加密基础设施大多是计算安全的,这保证了在对手能力合理假设下的方法或协议的安全性。协议的设计者或使用者可以选择阈值,例如(例如,最大的当前超级计算机或地球上所有的计算机)以及什么是一个合理的时间(100 年,10000 年,宇宙的年龄)。
当我们使用 QKD 共享密钥时,并不能保证密钥会到达对方。这是因为有人总是可以进行拒绝服务攻击(例如切断发送者和接收者之间的光纤),这对任何其他经典协议都是一样的。QKD 可以承诺的一个好类比是食品产品上的防篡改封条。当花生酱制造商想要确保当我们打开罐子时,它正好是我们离开工厂时的样子,公司就会在容器上贴上防篡改封条。公司承诺,如果封条完好无损地到达我们(消费者),那么花生酱就是好的,没有第三方对其进行过任何篡改。使用 QKD 协议传输加密密钥就像在传输中的比特上贴上防篡改封条。如果有人试图在传输中破坏密钥,接收者会知道,并且不会使用那个密钥。然而,在传输中封印比特并不能保证比特会到达接收者。
我们可以使用许多协议来实现通用的 QKD 方案。在本章中,我们将使用最常用的 QKD 协议之一——BB84,但还有许多其他协议我们没有时间深入探讨。在本章中,我们将逐步构建到这一点,但图 3.2 显示了 BB84 协议的步骤。


QKD 是一个使用单个量子比特和量子计算衍生技术的量子程序示例。它吸引我们去开发的原因是我们今天已经有硬件来实现它了!一些公司已经商业销售 QKD 硬件大约 15 年了,但技术的重要下一步涉及对这些系统的硬件和软件安全审查。
警告:本书中我们实现和使用的是模拟可证明安全协议的例子。鉴于我们没有在量子设备上运行这些例子,它们不是可证明安全的。即使使用真实的量子硬件实现这些协议,这些安全证明也无法阻止侧信道攻击或社会工程学使我们与我们的密钥分离
。我们将在本章后面讨论无克隆定理时更多地讨论这些证明。
让我们深入了解 QKD 是如何工作的!为了我们的目的,让我们假设我们和伊芙是上一章中想要交换密钥以便发送秘密信息的两个人!
。场景如下:
我们希望向我们的朋友发送一个秘密信息。使用第二章中的量子随机数生成器、BB84 量子密钥分发协议和一次性密码加密,设计一个程序来发送可以证明是安全的消息。
我们可以将场景可视化为一种时序图,就像图 3.3 所示。

图 3.3 本章场景:使用 BB84 和一次性密码加密向 Eve 发送秘密信息。我们首先必须与 Eve 交换一个秘密密钥,以便我们可以用它来加密我们想要发送的消息。我们可以使用量子比特和叠加态来帮助进行密钥交换步骤!
注意,我们需要发送的密钥是一串经典比特。我们如何使用量子比特来发送这些经典比特?我们首先看看如何将经典信息编码在量子比特中,然后学习 BB84 协议的具体步骤。在下一节中,我们将探讨一个新的量子操作,它将帮助我们使用量子比特编码经典比特。
3.1.1 量子非操作
如果我们有某些经典信息,比如说一个单一的二进制比特,我们如何使用量子资源(如量子比特)来编码它?看看以下算法,用于发送编码在量子比特中的随机经典比特字符串:
-
使用量子随机数生成器生成要发送的随机密钥比特。
-
从一个|0〉状态的量子比特开始,然后将其准备成表示第 1 步中比特值的态。在这里,如果经典比特是 0,则使用|0〉;如果经典比特是 1,则使用|1〉。
-
那个准备好的量子比特被发送给 Eve,然后她测量它并记录经典比特值。
-
重复步骤 1-3,直到我们和 Eve 拥有我们想要的密钥量(通常由我们想要使用的加密协议决定)。
图 3.4 显示了此算法的时序图。

图 3.4 展示了使用量子比特发送经典比特字符串的算法。我们首先使用我们的量子随机数生成器生成一个经典比特值,将其编码在一个新的量子比特上,然后将其发送给 Eve。她可以测量它并记录经典测量结果。
现在,为了将量子比特从|0〉切换到|1〉,我们需要工具箱中的另一个量子操作。在第 2 步中,我们可以使用一个量子非操作——它类似于经典非操作——将量子比特从|0〉旋转到|1〉(见图 2.9)。
我们将此量子非操作称为x操作。
定义:x操作或量子非将|0〉状态的量子比特转换到|1〉状态,反之亦然。

图 3.5 展示了在|0〉状态的量子比特上操作的非操作的量子等效,使量子比特处于|1〉状态。
第 2 步可以重写如下:
- 如果第 1 步中的经典比特是 0,则不执行任何操作。如果它是 1,则对我们的量子比特应用量子非操作(也称为
x操作)。
此算法 100%有效,因为当 Eve 测量她收到的量子比特时,|0〉和|1〉状态可以通过在Z-轴上的测量完美地区分。这似乎意味着我们和 Eve 做了很多工作,只是为了分享一些随机的经典比特,但我们将看到如何添加一些量子行为到这个基本协议中,使其更有用!让我们看看我们如何在代码中实现这一点。
列表 3.1 qkd.py:通过量子比特交换经典比特
def prepare_classical_message(bit: bool, q: Qubit) -> None: ❶
if bit:
q.x() ❷
def eve_measure(q: Qubit) -> bool:
return q.measure() ❸
def send_classical_bit(device: QuantumDevice, bit: bool) -> None:
with device.using_qubit() as q:
prepare_classical_message(bit, q)
result = eve_measure(q)
q.reset()
assert result == bit ❹
❶ 为了用我们想要发送的经典比特来准备我们的量子比特,我们需要输入比特值和要使用的量子比特。这个函数不返回任何内容,因为我们应用到的量子比特的操作后果是在单量子比特模拟器中跟踪的。
❷ 如果我们发送一个 1,我们可以使用 NOT 操作 x 来准备处于 |1〉 状态的量子比特,因为 x 操作将 |0〉 旋转到 |1〉,反之亦然。
❸ 将测量作为另一个函数分开似乎有点愚蠢,因为它的代码只有一行。但我们将改变 Eve 未来如何测量量子比特,所以这是一个有用的设置。
❹ 我们可以检查测量 q 是否给出了我们发送的相同经典比特。
我们在上一章编写的模拟器几乎已经具备了实现这一功能所需的一切。我们只需要添加一个对应于x操作的指令。x指令可以用矩阵 x 来表示,就像我们用矩阵 H 表示h指令一样。同样地,我们在第二章中写H的方式,我们可以写出矩阵 X 如下:

练习 3.1:真值表和矩阵
在第二章中,我们看到了单位矩阵在量子计算中扮演的角色与真值表在经典计算中扮演的角色相同。我们可以利用这一点来找出矩阵 X 应该是什么样子才能表示量子 NOT 操作,x。让我们首先制作一个表格,说明矩阵 X 需要对每个输入状态做什么,以表示x指令的作用。
| 输入 | 输出 |
|---|---|
| |0〉 | |1〉 |
| |1〉 | |0〉 |
这个表格告诉我们,如果我们用矩阵 x 乘以向量 |0〉,我们需要得到 |1〉,同样地,X |1〉 = |0〉。
要么使用 NumPy,要么手动检查矩阵

与之前真值表中的内容相匹配。
练习解答
本书所有练习题的解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,然后打开提及练习解答的 Jupyter 笔记本。
让我们继续添加我们需要的功能到我们的模拟器,以便运行列表 3.1。我们将使用上一章中编写的模拟器,但如果你需要复习,可以在本书的 GitHub 仓库中找到代码:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。首先,我们需要通过添加一个新方法来更新我们的量子设备接口,这个新方法我们的量子比特必须具备。
列表 3.2 interface.py:向量子比特接口添加x
class Qubit(metaclass=ABCMeta):
@abstractmethod
def h(self): pass
@abstractmethod
def x(self): pass ❶
@abstractmethod
def measure(self) -> bool: pass
@abstractmethod
def reset(self): pass
❶ 我们可以模仿第一章中的h操作来实现量子 NOT 操作。
现在我们量子比特的接口知道我们想要实现x操作,让我们添加这个实现!
列表 3.3 simulator.py:向量子比特模拟器添加x
KET_0 = np.array([
[1],
[0]
], dtype=complex)
H = np.array([
[1, 1],
[1, -1]
], dtype=complex) / np.sqrt(2)
X = np.array([ ❶
[0, 1],
[1, 0]
], dtype=complex) / np.sqrt(2)
class SimulatedQubit(Qubit):
def __init__(self):
self.reset()
def h(self):
self.state = H @ self.state
def x(self): ❷
self.state = X @ self.state
def measure(self) -> bool:
pr0 = np.abs(self.state[0, 0]) ** 2
sample = np.random.random() <= pr0
return bool(0 if sample else 1)
def reset(self):
self.state = KET_0.copy()
❶ 添加一个变量 X 来存储我们需要表示 x 操作的矩阵X
❷ 正如 h 函数一样,我们希望通过应用存储在 X 中的矩阵来执行量子操作 x。
3.1.2 与量子比特共享经典比特
太棒了!让我们尝试使用我们升级的 Python 量子比特模拟器来使用量子比特共享一个秘密经典比特。
注意:在我们将要编写的本章代码中,我们和爱娃共享的量子比特生活在同一个模拟设备中。如果我们都使用同一个设备,那么考虑互相“发送”量子比特可能会有些尴尬!在现实中,我们的设备将使用光子(光的单个粒子)作为量子比特,这些光子通过光纤或通过望远镜在空中发送是非常容易的。
这还不完全等同于量子密钥分发协议,但它为我们最终目标 QKD 协议中的函数和步骤提供了一个良好的基础。
打开一个 IPython 会话,我们在终端中运行ipython以保存模拟器的代码。在导入 Python 文件后,创建一个单量子比特模拟器的实例,并生成一个随机比特作为要发送的经典比特。 (幸好我们有量子随机数发生器!) 使用一个新鲜的量子比特,根据我们想要发送给爱娃的经典比特值来准备它。然后爱娃测量量子比特,我们可以看到我们是否具有相同的经典比特值。
列表 3.4 使用单量子比特模拟器发送经典比特
>>> qrng_simulator = SingleQubitSimulator() ❶
>>> key_bit = int(qrng(qrng_simulator)) ❷
>>> qkd_simulator = SingleQubitSimulator() ❸
>>> with qkd_simulator.using_qubit() as q:
... prepare_classical_message(key_bit, q) ❹
... print(f"You prepared the classical key bit: {key_bit}")
... eve_measurement = int(eve_measure(q)) ❺
... print(f"Eve measured the classical key bit: {eve_measurement}")
...
You prepared the classical key bit: 1
Eve measured the classical key bit: 1
❶ 我们需要一个模拟量子比特来用于我们的 QRNG。
❷ 重新使用我们在第二章中编写的 qrng 函数,我们可以生成一个随机经典比特作为我们的密钥。
❸ 在这里,我们使用一个新的量子比特模拟器实例来进行密钥交换,但严格来说,我们并不需要这样做。在第四章中,我们将看到如何扩展模拟器以支持多个量子比特。
❹ 我们将我们的经典比特编码在 qkd_simulator 提供的量子比特中。如果经典比特是 0,我们对 qkd_simulator 不做任何操作;如果经典比特是 1,我们使用 x 方法将量子比特改变到|1〉状态。
❺ 爱娃测量 qkd_simulator 中的量子比特,然后将比特值存储为 eve_measurement。
我们使用量子比特进行秘密共享的示例应该是确定性的,也就是说,每次我们准备和发送一个比特时,爱娃都会正确地测量相同的值。这是安全的吗?如果你怀疑它不安全,你肯定抓住了问题的关键。在下一节中,我们将讨论我们原型秘密共享方案的安全性,并探讨改进它的方法。
3.2 两个基的故事
我们和爱娃现在有了使用量子比特发送经典比特的方法,但如果一个对手掌握了那个量子比特怎么办?他们可以使用measure指令获取与爱娃相同的经典数据。这是一个大问题,合理地让我们质疑为什么有人一开始会用量子比特来共享密钥。
幸运的是,量子力学提供了一种使这种交换更加安全的方法!我们可以在我们的协议中做出哪些修改?例如,我们可以用一个处于|+〉状态的量子比特来表示经典“0”消息,用一个处于|−〉状态的量子比特来表示“1”消息。
列表 3.5 qkd.py:使用|+〉 / |−〉状态编码消息
def prepare_classical_message_plusminus(bit: bool, q: Qubit) -> None:
if bit:
q.x()
q.h() ❶
def eve_measure_plusminus(q: Qubit) -> bool:
q.h() ❷
return q.measure()
def send_classical_bit_plusminus(device: QuantumDevice, bit: bool) -> None:
with device.using_qubit() as q:
prepare_classical_message_plusminus(bit, q)
result = eve_measure_plusminus(q)
assert result == bit
❶ 在此行prepare_classical_message_plusminus之前的所有内容与prepare_classical_message相同。在此处应用 Hadamard 门将|0〉 / |1〉状态旋转到|+〉 / |−〉状态。
❷ 使用 h 操作将我们的|+〉 / |−〉状态旋转回|0〉 / |1〉状态,因为我们的测量操作只定义了正确测量|0〉 / |1〉状态。
提示:关于基的复习,请参阅附录 C。
现在我们有两种不同的发送量子比特的方式,我们和 Eve 可以在发送量子比特时使用(见表 3.1 的总结)。我们称这两种不同的消息发送方式为基,每个都包含两个完全可区分(正交)的状态。这与附录 C 中我们查看的地图方向(如北和西)类似,这些方向定义了一个方便的基来描述方向。
表 3.1 我们想要发送的不同经典消息以及如何在Z**-和X**-基中编码它们
| “0”消息 | “1”消息 | |
|---|---|---|
| “0” (或 Z ) 基 | |0〉 | |1〉 = X |0〉 |
| “1” (或 X) 基 | |+〉 = H |0〉 | |−〉 = H |1〉 = HX|0〉 |
提示:关于基的复习,请参阅附录 C。
我们已经使用|0〉和|1〉状态作为一组基(称为Z基),使用|+〉和|−〉作为另一组(称为X基)。这些基的名称指的是我们可以完美区分状态的轴(见图 3.6)。

图 3.6 现在,除了使用Z基在量子比特上编码经典比特外,我们还可以使用X基。
注意:在量子计算中,实际上并没有真正的正确基,更多的是我们根据惯例选择使用的方便基。
如果我们和 Eve 都不知道我们使用的是哪种发送方式,我们都会遇到问题。如果我们混合在Z基和X基中发送消息会发生什么?好消息是,我们可以使用我们的模拟器来尝试并看看会发生什么。
列表 3.6 交换比特但不使用相同的基
def prepare_classical_message(bit: bool, q: Qubit) -> None: ❶
if bit:
q.x()
def eve_measure_plusminus(q: Qubit) -> bool:
q.h() ❷
return q.measure()
def prepare_classical_message(bit: bool, q: Qubit) -> None: ❶
if bit:
q.x()
def eve_measure_plusminus(q: Qubit) -> bool:
q.h() ❷
return q.measure()
def send_classical_bit_wrong_basis(device: QuantumDevice, bit: bool) -> None:
with device.using_qubit() as q:
prepare_classical_message(bit, q)
result = eve_measure_plusminus(q)
assert result == bit, "Two parties do not have the same bit value" ❸
❶ 使用我们之前看到的方法,通过使用 hmethod 在我们的量子比特上准备Z基。
❷ Eve 在X基下测量,因为她测量之前在她自己的量子比特上做了 Hadamard 门。
❸ 函数不返回任何内容,所以如果我们和 Eve 最终得到不匹配的密钥比特,它将引发错误。
运行前面的代码,我们可以看到,如果我们发送的是Z基,而 Eve 测量的是X基,我们最终可能不会得到匹配的经典比特。
列表 3.7 在Z基上发送;在X基上测量
>>> qsim = SingleQubitSimulator()
>>> send_classical_bit_wrong_basis(qsim, 0) ❶
AssertionError: Two parties do not have the same bit value
❶ 我们将比特值设为 0。你可能需要运行这条命令几次才能得到错误信息。
你可以尝试通过实验来验证这一点。你会发现大约一半的时间你会得到AssertionError(密钥交换失败)。为什么会这样?首先,爱娃是在X-基上测量的,因此她可以完美地区分|+〉和|−〉。如果她没有得到一个可以完美区分的基状态(就像在这个例子中,她得到了|0〉),她会测量什么?我们可以将|0〉状态在X-基上表示为

回想一下,在第二章中,我们以类似的方式定义了|+〉,即通过将|0〉和|1〉相加。|+〉状态也被称为|0〉和|1〉状态的叠加。
注意:任何可以写成这种状态线性组合的状态,都被认为是所加状态的叠加。
练习 3.2:验证|0〉是|+〉和|−〉的叠加
尝试使用你在上一章学到的关于向量的知识来验证|0〉 = (|+〉 + |−〉) / √2,无论是手动还是使用 Python。提示:回想一下|+〉 = (|0〉 + |1〉) / √2 以及|−〉 = (|0〉 − |1〉) / √2。
现在要使用第二章中的 Born 规则来计算实际的测量。回想一下,我们可以通过以下表达式计算测量结果的概率:
Pr(测量|状态) = |〈测量 | 状态〉|²
将|0〉状态在X基上的测量写出来,我们可以看到我们将有一半的时间得到 0(或|+〉),另一半的时间得到 1(或|−〉):

练习 3.3:在不同基上测量量子比特
以之前的例子为指南,
-
计算在|−〉方向测量|0〉状态时得到测量结果|−〉的概率。
-
还要计算输入状态为|1〉时得到|−〉测量结果的概率。
这告诉我们,如果爱娃不知道正确的测量基,那么她所做的测量就像随机猜测一样。这是因为,在错误的基上,量子比特处于定义基的两个状态的叠加中。QKD 工作原理的一个“关键”是,没有正确的附加信息(量子比特编码的基),任何对量子比特的测量基本上都是无用的。为了确保我们的安全,我们必须让对手难以获得额外的信息,以便知道正确的测量基。我们接下来要讨论的 QKD 协议为此提供了一个解决方案,以及一个证明(此处不涉及)来描述攻击者对密钥有任何信息的可能性!
3.3 量子密钥分发:BB84
我们现在已经看到了如何在两个不同的基中共享密钥,以及如果我们和 Eve 没有使用相同的基会发生什么。再次,你可能会问为什么我们使用这种方法来使共享我们的秘密密钥更加安全。有各种各样的不同 QKD 协议,每个都有其特定的优点和用例(与 RPG 角色职业类似)。QKD 最常见的协议被称为 BB84,以两位作者首字母的适当隐晦编码和协议发布的年份(Bennet 和 Brassard 1984)命名。
BB84 与我们迄今为止为共享密钥所做的工作非常相似,但在我们和 Eve 选择基的方式上有关键的区别。在 BB84 中,双方都随机(且独立)选择他们的基,这意味着他们将有 50%的时间使用相同的基。图 3.7 显示了 BB84 协议的步骤。
由于随机选择基,我们和 Eve 还必须在认证的经典信道(如互联网)上通信,以便各自将我们的密钥转换成我们相信与我们的合作伙伴相同的密钥。这是因为这是现实生活,当量子比特交换时,环境和第三方个人都有可能操纵或修改量子比特的状态。
密钥扩展
在我们描述我们和 Eve 使用的经典通信通道的细节中,我们忽略了一个细节:它必须认证。也就是说,当我们作为运行 BB84 的一部分向 Eve 发送经典消息时,如果其他人能读到它们是可以的,但我们需要确保我们确实是在和 Eve 交谈。为了证明有人编写并发送了特定的消息,我们实际上已经需要某种形式的共享秘密,我们可以用它来验证对方的身份。因此,在 BB84 中,我们必须已经与对方有一个共享秘密。这个秘密可以比我们试图发送的消息小,所以 BB84 在技术上更是一种密钥扩展协议。

图 3.7 BB84 协议的步骤,一种特定的 QKD 协议
BB84 协议的步骤如下:
-
通过采样我们的 QRNG 选择一个随机的一位消息发送。
-
我们和 Eve 各自使用各自的量子随机数发生器(QRNG)选择一个随机基(它们之间没有通信)。
-
在随机选择的基中准备一个量子比特,代表我们随机选择的消息(见表 3.2)。
-
将我们准备好的量子比特通过量子信道发送给 Eve。
-
Eve 在量子比特到达时对其进行测量,在随机选择的基中进行测量,并记录经典比特结果。
-
在认证的经典信道上与 Eve 通信,并共享我们用于准备和测量量子比特的基。如果它们匹配,保留比特并将其添加到密钥中。重复步骤 1-6,直到我们拥有所需的密钥量。
表 3.2 对于每个随机消息和基选择我们应该发送什么状态
| “0”消息 | “1”消息 | |
|---|---|---|
| “0” (或 Z) 基底 | |0〉 | |1〉 = X |0〉 |
| “1” (或 X) 基底 | |+〉 = H |0〉 | |−〉 = H |1〉 = H X |0〉 |
一个无错误的世界
由于我们正在模拟 BB84 协议,我们知道伊芙收到的量子位将与我们发送的完全相同。BB84 更现实的做法是分批进行,首先交换 n 个量子位,然后进行一轮共享基底值(错误纠正发生)。最后,我们必须使用隐私放大算法进一步缩小密钥,以考虑到窃听者可能已经从我们检测到的错误中获得了部分信息。我们在 BB84 的实现中省略了这些步骤以保持简单,但它们对于现实世界的安全性是至关重要的
。
让我们开始实现 Python 中的 BB84 QKD 协议!我们将从编写一个函数开始,该函数将运行 BB84 协议(假设无损传输)以进行单比特传输。这并不保证我们从这次运行中获得一个密钥比特。然而,如果我们和伊芙选择不同的基底,那次交换将不得不被丢弃。
首先,设置一些函数有助于简化我们编写完整 BB84 协议的方式。我们和伊芙需要做一些像采样随机比特和准备以及测量信息量子位的事情,这里分开来以增加清晰度。
列表 3.8 bb84.py:密钥交换之前的辅助函数
def sample_random_bit(device: QuantumDevice) -> bool:
with device.using_qubit() as q:
q.h()
result = q.measure()
q.reset() ❶
return result
def prepare_message_qubit(message: bool, basis: bool, q: Qubit) -> None: ❷
if message:
q.x()
if basis:
q.h()
def measure_message_qubit(basis: bool, q: Qubit) -> bool:
if basis:
q.h()
result = q.measure()
q.reset() ❸
return result
def convert_to_hex(bits: List[bool]) -> str: ❹
return hex(int(
"".join(["1" if bit else "0" for bit in bits]),
2
))
❶ sample_random_bit 函数几乎与我们的 qrng 函数相同,只是在这里,我们在测量后会重置量子位,因为我们知道我们希望能够多次使用它。
❷ 量子位被编码在随机选择的基底中的密钥比特值。
❸ 与 Eve 测量信息量子位后的 sample_random_bit 类似,她应该重置它,因为在模拟器中,我们将重用它进行下一次交换。
❹ 为了帮助压缩长二进制密钥的显示,一个辅助函数将表示转换为更短的十六进制字符串。
列表 3.9 bb84.py:发送经典比特的 BB84 协议
def send_single_bit_with_bb84(
your_device: QuantumDevice,
eve_device: QuantumDevice
) -> tuple:
[your_message, your_basis] = [
sample_random_bit(your_device) for _ in range(2) ❶
]
eve_basis = sample_random_bit(eve_device) ❷
with your_device.using_qubit() as q:
prepare_message_qubit(your_message, your_basis, q) ❸
# QUBIT SENDING... ❹
eve_result = measure_message_qubit(eve_basis, q) ❺
return ((your_message, your_basis), (eve_result, eve_basis)) ❻
❶ 我们可以使用之前修改过的 QRNG 随机选择比特值和基底:这里,sample_random_bit 函数。
❷ 伊芙需要随机选择一个与她的量子位相对应的基,这就是她为什么使用一个单独的 QuantumDevice 的原因。
❸ 准备好所有准备工作后,准备发送给伊芙的量子位。
❹ 由于所有计算都在我们计算机上的模拟器内部进行,因此不需要对我们从我们到伊芙“发送”量子位进行任何操作。
❺ 现在,伊芙有了我们的量子位,并按照她之前选择的随机基底对其进行测量。
❻ 返回我们和伊芙在这一轮结束时将拥有的密钥比特值和基底。
量子位和无克隆定理
从我们迄今为止所看到的情况来看,我们的对手似乎可以通过监听量子通道中的量子位并复制它们来进行欺骗。以下是方法:窃听者(在这里称为 Bob)首先需要(不被发现):
-
在我们和伊芙之间发送量子位时复制量子位,然后存储它们。
-
当我和 Eve 完成协议的经典部分时,要听我们双方宣布的基,并跟踪我们双方选择相同的基。
-
对于我们和 Eve 使用相同基的比特对应的量子比特,也要测量这些量子比特的副本,使用相同的基。
哇!我们、Eve 和 Bob 都会拥有相同的密钥!如果你认为这好像是个问题,你是对的。不过,不用担心;量子力学有解决方案。结果是 Bob 的计划问题出在第一步,他需要制作相同的量子比特副本。好消息是,在不知道量子比特状态的情况下,量子力学禁止制作精确的副本。量子比特不能在没有先验知识的情况下被完全复制的规则被称为不可克隆定理,其表述如下:
没有任何量子操作可以完美地将任意量子比特的状态复制到另一个量子比特上。

可视化的不可克隆定理
我们将在下一章学习如何描述多个量子比特的状态后,能够简单地证明这一点!
。
作为对不可克隆定理的另一种思考方式,如果 Bob 能够在不干扰量子比特的情况下测量它,他就可以绕过需要复制他截获的量子比特的需求。这是不可能的,因为一旦我们测量一个量子比特,它就会“坍缩”或以 Eve 能够检测到的方式改变,这会作为额外的噪声出现在她从她的量子比特收集的测量结果中。因此,在传输过程中测量是不可能的,Bob 无法在不被发现的情况下进行窃听,所以他的窃听注定会失败。
交换一个经典密钥比特不足以发送整个密钥,因此现在我们需要使用之前的技术来发送多个比特。
列表 3.10 bb84.py:与 Eve 交换密钥的 BB84 协议
def simulate_bb84(n_bits: int) -> tuple:
your_device = SingleQubitSimulator()
eve_device = SingleQubitSimulator()
key = []
n_rounds = 0
while len(key) < n_bits:
n_rounds += 1
((your_message, your_basis), (eve_result, eve_basis)) =
➥ send_single_bit_with_bb84(your_device, eve_device)
if your_basis == eve_basis: ❶
assert your_message == eve_result
key.append(your_message)
print(f"Took {n_rounds} rounds to generate a {n_bits}-bit key.")
return key
❶ 在这一点上,我和 Eve 可以公开宣布我们各自用来测量这个比特的基。如果一切正常,我们的结果应该在基相同时一致。我们在这里使用 assert 来检查。
现在密钥已经到手,我们可以继续使用密钥和一次性密码加密算法来发送秘密消息!
3.4 使用密钥发送秘密消息
我们和 Eve 已经解决了如何使用 BB84 协议来共享由 QRNG 生成的随机二进制密钥。最后一步是使用这个密钥与 Eve 共享秘密消息。我们和 Eve 之前决定使用最好的加密协议是一次性密码来发送我们的秘密消息。这实际上是最安全的加密协议之一,鉴于我们是以可能的最安全方式共享密钥,保持这一标准是有意义的!
例如,要告诉爱娃我们喜欢 Python,我们想要发送的消息是“
”。由于我们使用的是二进制密钥,我们需要将我们的 Unicode 消息表示转换为二进制,下面是一长串的比特列表:
"1101100000111101 1101110010010110 1101100000111101 1101110000001101 1101100000111101 1101110010111011"
这条消息的二进制表示是我们的消息文本,现在我们想要将其与密钥结合,以获得一个安全发送到网络上的密文。一旦我们从 BB84 协议中获得密钥(至少与我们的消息一样长),我们需要使用一次性密码加密方案来编码我们的消息。我们在第二章中看到了这种加密技术;见图 3.8 以快速复习。

图 3.8 使用随机比特加密秘密消息的一次性密码加密示例
为了实现这一点,我们需要使用经典的按位异或(Python 中的 ^ 运算符)将消息和我们的密钥结合,以创建我们可以安全发送给爱娃的密文。为了解密我们的消息,爱娃将使用相同的按位异或操作对密文和她的密钥(应该是你的密钥)进行操作。这将使她恢复消息,因为每次我们将比特字符串与另一个字符串进行两次异或操作时,我们都会得到原始的比特字符串。以下是 Python 中的示例。
列表 3.11 bb84.py:与爱娃交换密钥的 BB84 协议
def apply_one_time_pad(message: List[bool], key: List[bool]) -> List[bool]:
return [
message_bit ^ key_bit ❶
for (message_bit, key_bit) in zip(message, key)
]
❶ 在 Python 中,^ 运算符是按位异或。这会将我们的密钥的单个比特作为一次性密码应用于我们的消息文本。
练习 3.4:一次性密码加密
如果我们有密文 10100101 和密钥 00100110,原始发送的消息是什么?
让我们把这些放在一起,通过运行我们一直在构建的 bb84.py 文件,与爱娃分享消息(“
”)。
列表 3.12 bb84.py:使用 BB84 和一次性密码加密
if __name__ == "__main__":
print("Generating a 96-bit key by simulating BB84...")
key = simulate_bb84(96)
print(f"Got key {convert_to_hex(key)}.")
message = [
1, 1, 0, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 1,
1, 1, 0, 1, 1, 1, 0, 0,
1, 0, 0, 1, 0, 1, 1, 0,
1, 1, 0, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 1,
1, 1, 0, 1, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 0, 1,
1, 1, 0, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 0, 1,
1, 1, 0, 1, 1, 1, 0, 0,
1, 0, 1, 1, 1, 0, 1, 1
]
print(f"Using key to send secret message: {convert_to_hex(message)}.")
encrypted_message = apply_one_time_pad(message, key)
print(f"Encrypted message: {convert_to_hex(encrypted_message)}.")
decrypted_message = apply_one_time_pad(encrypted_message, key)
print(f"Eve decrypted to get: {convert_to_hex(decrypted_message)}.")
列表 3.13 运行章节场景的完整解决方案
$ python bb84.py
Generating a 96-bit key by simulating BB84...
Took 170 rounds to generate a 96-bit key. ❶
Got key: 0xb35e061b873f799c61ad8fad. ❷
Using key to send secret message: 0xd83ddc96d83ddc0dd83ddcbb. ❸
Encrypted message: 0x6b63da8d5f02a591b9905316\. ❹
Eve decrypted to get: 0xd83ddc96d83ddc0dd83ddcbb. ❺
❶ 由于我们的基和爱娃的基大约有一半的时间会一致,因此生成每个密钥位大约需要两轮 BB84。
❷ 每次运行 BB84 模拟时生成的确切密钥都会不同——这实际上是协议的一个很大部分!
❸ 通过写下“
”的每个 Unicode 代码点得到的消息
❹ 当我们将我们的秘密消息与之前获得的密钥结合,使用密钥作为一次性密码时,我们的消息会被打乱。
❺ 当爱娃使用相同的密钥时,她会收到我们原始的秘密消息。
量子密钥分发是量子计算最重要的衍生技术之一,并有可能对我们的安全基础设施产生巨大影响。虽然目前对于相对较近的各方(大约 200 公里或更短距离)来说,设置 QKD 相当容易,但在部署全球 QKD 系统方面存在重大挑战。通常,QKD 中使用的物理系统是光子,在没有丢失的情况下很难发送单光子长距离。
现在我们已经建立了一个单量子比特模拟器并编写了一些单量子比特应用程序,我们准备开始与多个量子比特进行实验。在下一章中,我们将对我们构建的模拟器添加功能,以模拟多个量子比特的状态,并与爱娃玩非局域游戏
。
摘要
-
量子密钥分发是一种协议,它允许我们随机生成共享密钥,我们可以使用这些密钥来安全且私密地进行通信。
-
在测量量子比特时,我们可以在不同的基中进行测量;如果我们测量与我们准备量子比特相同的基,结果将是确定的,而如果我们测量不同的基,结果将是随机的。
-
不可克隆定理保证了窃听者无法在不使密钥分发协议失败的情况下猜测正确的测量基。
-
一旦我们使用量子密钥分发(QKD)来共享密钥,我们就可以使用这个密钥与一个称为一次性密码的经典算法来安全地发送数据。
4 非局域游戏:与多个量子比特一起工作
本章涵盖
-
使用非局域游戏来验证量子力学与宇宙运作方式的一致性
-
模拟多个量子比特的状态准备、操作和测量结果
-
识别纠缠态的特征
在上一章中,我们使用量子比特与 Eve 安全通信,探讨了如何利用量子设备进行密码学。一次只处理一个量子比特很有趣,但处理多个量子比特将会——嗯,更有趣!在本章中,我们将学习如何模拟多个量子比特的状态以及它们“纠缠”的含义。我们还将再次与 Eve 玩游戏,但这次我们需要一个裁判!
4.1 非局域游戏
到目前为止,我们已经看到如何编程单量子比特设备来完成有用的任务,例如随机数生成和量子密钥分发。然而,最激动人心的计算任务需要使用多个量子比特一起完成。在本章中,我们将了解非局域游戏:一种使用多量子比特系统与朋友验证我们对宇宙量子力学描述的方法。
4.1.1 什么是非局域游戏?
我们都玩过各种各样的游戏,无论是体育、棋类、电子游戏还是角色扮演游戏。游戏是探索新世界和测试我们力量、耐力和理解极限的最好方式之一。事实证明,Eve 喜欢玩游戏,她最新的加密信息如下:
“嗨玩家!我非常想玩一个叫做 CHSH 的游戏。这是一个非局域游戏,其中我们与裁判一起玩。我将在下一条消息中发送指令。停止”
Eve 提出的游戏之所以是非局域的,是因为玩家在玩游戏时(遗憾的是)不在同一个地方。玩家通过向中心裁判发送和接收消息来参与游戏,但在玩游戏时没有机会互相交谈。真正酷的是,通过玩游戏,我们可以证明经典物理学根本无法描述我们在这些游戏中使用特定策略得到的结果。我们将在这里查看的特定获胜策略涉及玩家在游戏开始前共享一对量子比特。随着我们进入本章,我们将深入了解纠缠两个量子比特的含义,但让我们先描述我们的非局域游戏的完整规则。
注意:裁判在非局域游戏中进行裁决可以确保玩家之间没有通过足够大的距离进行通信,这样任何一个玩家的光线在游戏结束前都无法到达另一个玩家。
4.1.2 测试量子物理:CHSH 游戏
Eve 建议玩的游戏被称为CHSH 游戏,如图 4.1 所示.^(1)
CHSH 游戏由两个玩家和一个裁判组成。我们可以玩任意多轮游戏,每一轮有三个步骤。正如伊芙在她的第一条信息中提到的,一旦一轮开始,玩家们就不能进行交流,必须自己(可能是预先计划的)做出决定。

图 4.1 CHSH 游戏,一个有两个玩家和一个裁判的非局域性游戏。裁判以比特值的形式向每个玩家提出问题,然后每个玩家必须弄清楚如何回应裁判。如果玩家们的响应的布尔 AND 与裁判问题的经典 XOR 相同,则玩家们获胜。
CHSH 游戏一轮的步骤如下:
-
裁判通过给我们和伊芙各自一个经典比特来开始一轮。裁判独立且均匀随机地选择这些比特,所以我们可能会得到 0 或 1,每种情况各有 50% 的概率,伊芙也是如此。这意味着裁判可以以四种可能的方式开始游戏(我们的比特,伊芙的比特):(0,0),(0,1),(1,0),或 (1,1)。
-
我们和伊芙必须各自 独立地 决定一个经典比特作为回应给裁判。
-
裁判随后计算我们和伊芙的经典比特响应的奇偶性(XOR)。
如表 4.1 所列,在四种情况中的三种情况下,我们和伊芙必须以 偶数 奇偶性(我们的答案必须相等)来响应以获胜,而在第四种情况下,我们的答案必须不同。这些规则确实 不同寻常,但与一些多日棋盘游戏相比,还算不错。
表 4.1 CHSH 游戏的获胜条件
| 我们的输入 | 伊芙的输入 | 获胜的响应奇偶性 |
|---|---|---|
| 0 | 0 | 偶数 |
| 0 | 1 | 偶数 |
| 1 | 0 | 偶数 |
| 1 | 1 | 奇数 |
我们可以扩展表 4.1 来获取游戏的所有可能结果;见表 4.2。
表 4.2 具有获胜条件的 CHSH 游戏的所有可能状态。输入比特来自裁判,两位玩家都对裁判做出回应。
| 我们的输入 | 伊芙的输入 | 我们的响应 | 伊芙的响应 | 奇偶性 | 获胜? |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 偶数 | 是 |
| 0 | 0 | 0 | 1 | 奇数 | 否 |
| 0 | 0 | 1 | 0 | 奇数 | 否 |
| 0 | 0 | 1 | 1 | 偶数 | 是 |
| 0 | 1 | 0 | 0 | 偶数 | 是 |
| 0 | 1 | 0 | 1 | 奇数 | 否 |
| 0 | 1 | 1 | 0 | 奇数 | 否 |
| 0 | 1 | 1 | 1 | 偶数 | 是 |
| 1 | 0 | 0 | 0 | 偶数 | 是 |
| 1 | 0 | 0 | 1 | 奇数 | 否 |
| 1 | 0 | 1 | 0 | 奇数 | 否 |
| 1 | 0 | 1 | 1 | 偶数 | 是 |
| 1 | 1 | 0 | 0 | 偶数 | 否 |
| 1 | 1 | 0 | 1 | 奇数 | 是 |
| 1 | 1 | 1 | 0 | 奇数 | 是 |
| 1 | 1 | 1 | 1 | 偶数 | 否 |
让我们看看一些 Python 代码来模拟这个游戏。由于我们和伊芙对裁判的响应可以依赖于裁判给出的信息,我们可以将每个玩家的行动表示为裁判调用的“函数”。
练习 4.1:裁判心态
由于裁判完全是经典的,我们将它们建模为使用经典随机数生成器。然而,这留下了我们和 Eve 通过猜测裁判的问题来作弊的可能性。一个可能的改进可能是使用第二章中的 QRNG。修改列表 4.1 中的代码示例,以便裁判可以通过测量一个从 |+〉 状态开始的量子比特来向我们和 Eve 提问。
练习解答
本书所有练习的解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需转到您所在的章节文件夹,并打开提及练习解答的 Jupyter notebook。
如列表 4.1 所示,我们正在声明一个新的类型 Strategy 来定义表示我们和 Eve 的单个位函数的函数元组。我们可以将这些函数视为代表我们和 Eve 各自如何处理裁判给我们的位。
列表 4.1 chsh.py:CHSH 游戏的 Python 实现
import random
from functools import partial
from typing import Tuple, Callable
import numpy as np
from interface import QuantumDevice, Qubit
from simulator import Simulator
Strategy = Tuple[Callable[[int], int], Callable[[int], int]] ❶
def random_bit() -> int:
return random.randint(0, 1) ❷
def referee(strategy: Callable[[], Strategy]) -> bool:
you, eve = strategy() ❸
your_input, eve_input = random_bit(), random_bit() ❹
parity = 0 if you(your_input) == eve(eve_input) else 1 ❺
return parity == (your_input and eve_input) ❻
def est_win_probability(strategy: Callable[[], Strategy], ❼
n_games: int = 1000) -> float:
return sum(
referee(strategy)
for idx_game in range(n_games)
) / n_games ❽
❶ 使用 Python 的 typing 模块,我们可以记录一个类型为 Strategy 的值是一个包含两个函数的元组,每个函数都接受一个 int 并返回一个 int。
❷ 裁判将使用的经典随机数生成器
❸ “策略”函数将根据我们的输入分配代表“你”和“eve”将做什么的单个位函数。
❹ 裁判选择两个随机位,一个给每个玩家。
❺ 给每个玩家他们的随机位,然后计算他们响应的奇偶性
❻ 检查表 4.1 以查看玩家是否获胜。
❼ 我们可以使用 Python 的内置 sum 函数来计算裁判对特定策略返回 True 的次数——换句话说,我们赢得游戏的次数。
❽ 将游戏次数除以,然后估计我们的和 Eve 的策略赢得 CHSH 游戏的概率。
注意,在列表 4.1 中,我们还没有为裁判的输入 strategy 提供定义。现在我们已经用 Python 实现了游戏的规则,让我们谈谈 策略 并开始实现 CHSH 游戏的经典策略。
提示:选择变量命名约定,使代码中每个变量的作用显而易见是有帮助的。我们选择在变量 n_games 中使用前缀 n_ 来表示该变量表示一个数字或大小,并使用前缀 idx_ 来指代每个单独游戏的索引。就像开车一样,如果我们的代码可预测,那就很好。
4.1.3 经典策略
对于我们和 Eve 来说,最简单的策略是完全忽略我们的输入。查看表 4.3,如果我们俩在游戏开始前就同意我们永远不会改变我们的输出(即,总是返回 0),我们将有 75% 的胜率(这假设裁判为每个玩家均匀地选择随机位)。
表 4.3 CHSH 游戏的最佳经典策略,我们双方在满足条件时总是响应 0
| 我们的输入 | 爱娃的输入 | 我们的响应 | 爱娃的响应 | 奇偶性 | 胜? |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 偶数 | 是 |
| 0 | 1 | 0 | 0 | 偶数 | 是 |
| 1 | 0 | 0 | 0 | 偶数 | 是 |
| 1 | 1 | 0 | 0 | 偶数 | 否 |
如果我们将这个策略写成 Python 函数,代码如下。
列表 4.2 chsh.py:CHSH 游戏的简单、恒定策略
def constant_strategy() -> Strategy:
return (
lambda your_input: 0,
lambda eve_input: 0
)
现在我们可以测试我们预期在 CHSH 游戏中赢得一局的频率,使用constant_strategy:
>>> est_win_probability(constant_strategy) ❶
0.771
❶ 注意,当你尝试这个时,你可能得到略多或略少的 75%。这是因为胜率是通过有限轮次(在统计学中称为二项分布)估计的。对于这个例子,我们预计误差条约为 1.5%。
好吧,这是一个简单的策略,但我们能做得更聪明吗?鉴于我们和爱娃只有经典资源,遗憾的是,这已经被证明是我们能做的最好的。除非作弊(例如,与爱娃通信或猜测裁判的输入),否则我们平均无法在 75%的时间内赢得这场比赛。
所有这些都引出了一个明显的问题:如果我们和爱娃能够使用量子比特,那会怎么样?那时我们最佳的战略是什么,我们会有多高的胜率?如果我们有证据表明我们无法在超过 75%的时间内赢得 CHSH 游戏,然后我们找到了一种可以提高胜率的方法,这又说明了我们对宇宙的理解是什么?正如你可能猜到的,如果我们玩 CHSH 游戏时玩家共享量子资源,即拥有量子比特,我们就能比 75%的胜率做得更好。在章节的后面,我们将探讨 CHSH 的量子策略,但剧透一下:我们需要模拟多个量子比特。
4.2 与多个量子比特状态一起工作
到目前为止,在这本书中,我们一次只处理一个量子比特。例如,要玩一个非局域游戏,每个玩家将需要他们自己的量子比特。这引发了一个问题,当我们考虑的系统有多个量子比特时,事情会有什么变化?主要区别是我们不能单独描述每个量子比特,而必须从描述整个系统的状态的角度来思考。
注意:在描述一组或寄存器时,我们通常不能单独描述每个量子比特。最有用的量子行为只有在描述一组或寄存器的状态时才能看到。
下一个部分将帮助将这个系统级视图与类似经典编程概念的寄存器联系起来。
4.2.1 寄存器
假设我们有一个经典比特的寄存器:也就是说,许多经典比特的集合。我们可以独立地索引寄存器中的每个比特并查看其值,尽管它仍然是寄存器的一部分。寄存器的内容可以表示更复杂的值,比如一起表示一个 Unicode 字符的比特(正如我们在第三章中看到的),但这种高级解释不是必要的。
当我们在经典寄存器中存储信息时,随着我们添加更多的比特,该寄存器的不同状态数量会迅速增长。例如,如果我们有三个比特,我们的寄存器可以处于八种不同的状态;参见列表 4.3 中的示例。我们说对于经典寄存器状态 101,零比特是 1,第一个比特是 0,第二个比特是 1。当这些值连接在一起时,它们给出了字符串 101。
列表 4.3 列出经典三比特寄存器的所有状态
>>> from itertools import product
>>> ", ".join(map("".join, product("01", repeat=3)))
'000, 001, 010, 011, 100, 101, 110, 111'
如果我们有四个比特,我们可以存储 16 种不同的状态;如果我们有 N 个比特,我们可以存储 2N 种状态。我们说经典寄存器的不同可能状态的数量 以指数方式增长,与比特的数量成正比。列表 4.3 输出的比特字符串显示了寄存器每个状态的实际数据。它们还作为方便的标签,用于我们可以用三个经典比特编码的八种可能消息之一。
所有这些与量子比特有什么关系?我们在第二章和第三章中看到,任何经典比特的状态也描述了一个量子比特状态。对于量子比特寄存器也是如此。例如,三比特状态 “010” 描述了三量子比特状态 |010〉。
小贴士 在第二章中,我们了解到用这种方式用经典比特描述的量子比特状态被称为 计算基态;在这里,我们用相同的术语来描述由经典比特字符串描述的多个量子比特的状态。
然而,与单个量子比特一样,多个量子比特寄存器的状态也可以通过将不同的量子比特状态相加来构成。正如我们可以将 |+〉 写作 (|0〉 + |1〉) / √2 来得到另一个有效的量子比特状态一样,我们的三量子比特寄存器可以处于多种不同的状态:
-
(|010〉 + |111〉) / √2
-
(|001〉 + |010〉 + |100〉) / √3
小贴士 随着我们的深入,我们将看到更多内容,但正如我们需要 2 的平方根来使 |+〉 = (|0〉 + |1〉) / √2 的测量概率成立一样,我们需要在我们的例子中除以 √2 和 √3 来确保每个测量的所有概率都是现实的:即总和为 1。
这个量子寄存器线性的例子被称为 叠加原理。
定义 叠加原理 告诉我们,我们可以将量子寄存器的两种不同状态相加,得到另一个有效状态。我们之前看到的 |+〉 状态是这种情况的一个好例子,但每个寄存器只有一个量子比特。
要在计算机上写出量子寄存器的状态,我们再次使用向量,就像我们在第二章中做的那样。主要区别是我们每个向量中列出的数字数量。让我们看看在计算机上写出两量子比特寄存器状态的样子。例如,两量子比特状态(|00〉 + |11〉) / √2 也可以写成状态(1 × |00〉 + 0 × |01〉 + 0 × |10〉 + 1 × |11〉) / √2。如果我们列出为了得到我们想要的状态而必须乘以每个计算基态的值,我们就有精确的信息可以写入我们的向量。在列表 4.4 中,我们将(|00〉 + |11〉) / √2 写成向量形式。
列表 4.4:使用 Python 编写一个两量子比特状态的示例
>>> import numpy as np
>>> two_qubit_state = np.array([[ ❶
...
... 1, ❷
...
... 0, ❸
...
... 0,
... 1
... ]]) / np.sqrt(2) ❹
❶ 我们以相同的方式开始,使用 np.array 函数创建一个新的向量。
❷ 这个向量中的每个条目描述了一个不同的计算基态。这个条目告诉我们,我们需要将|00〉乘以 1。
❸ 同样,这个条目告诉我们需要添加多少|01〉状态才能得到我们想要的状态。
❹ 最后,我们除以√2,以确保所有测量概率都正确,就像我们在第二章和第三章中处理|+〉状态时做的那样。
列表 4.4 中的向量中的数字是我们要乘以每个计算基态的系数,然后我们将它们相加以得到一个新的状态。这些系数也被称为每个基态的振幅。
思考方向
另一种思考这个示例的方法是考虑地图上的方向,类似于附录 C 中讨论向量时的方式。每个不同的计算基态告诉我们一个量子比特状态可以指向的方向。我们可以将两量子比特状态视为四维空间中的一个方向,而不是我们在附录 C 中看到的二维地图。由于这本书是二维的而不是四维的,我们很遗憾不能在这里画图,但有时将向量视为方向比将向量视为数字列表更有帮助。
4.2.2 为什么模拟量子计算机很难?
我们已经看到,随着比特数的增加,寄存器可以处于的不同状态数量呈指数增长。虽然在我们仅用两个量子比特玩非局域游戏时这不会成为问题,但随着我们继续阅读本书,我们将需要使用超过两个量子比特。
当我们这样做时,我们也将会有指数级多的不同计算基态用于我们的量子寄存器,这意味着随着我们量子寄存器大小的增加,我们向量中需要指数级多的不同振幅。为了写出 10 量子比特寄存器的状态,我们需要使用一个包含 2¹⁰ = 1024 个不同振幅的向量。对于一个 20 量子比特寄存器,我们向量中需要大约 100 万个振幅。当我们达到 60 个量子比特时,我们向量中需要大约 1.15 × 10¹⁸ 个数字。这大约是地球上每粒沙子的一个振幅。
在表 4.4 中,我们总结了当我们尝试使用经典计算机(如手机、笔记本电脑、集群和云环境)模拟量子计算机时,这种指数级增长意味着什么。这张表显示,尽管使用经典计算机推理量子计算机非常具有挑战性,但我们可以相当容易地推理小型示例。使用笔记本电脑或台式机,我们可以模拟大约 30 个量子比特,而不会遇到太多麻烦。正如本书的其余部分将展示的那样,这已经足够理解量子程序的工作原理以及量子计算机如何用于解决有趣的问题。
表 4.4 存储量子状态需要多少内存?
| 量子比特数量 | 振幅数量 | 内存 | 大小比较 |
|---|---|---|---|
| 1 | 2 | 128 bits | |
| 2 | 4 | 256 比特 | |
| 3 | 8 | 512 比特 | |
| 4 | 16 | 1 千比特 | |
| 8 | 256 | 4 KB | 滑动式信用卡 |
| 10 | 1024 | 16 KB | |
| 20 | 1,048,576 | 16 MB | |
| 26 | 67,108,864 | 1 GB | 树莓派 RAM |
| 28 | 268,435,456 | 4 GB | iPhone XS Max RAM |
| 30 | 1,073,741,824 | 16 GB | 笔记本或台式机 RAM |
| 40 | 1,099,511,627,776 | 16 TB | |
| 50 | 1,125,899,906,842,624 | 16 PB | |
| 60 | 1,152,921,504,606,846,976 | 16 EB | |
| 80 | 1,208,925,819,614,629,174,706,176 | 16 亿字节 | 大约的互联网大小 |
| 410 | 2.6 × 10¹²³ | 4.2 × 10¹²⁴ bytes | 宇宙大小的计算机 |
深入探讨:量子计算机的指数级强大吗?
你可能听说过,为了使用经典计算机模拟量子计算机,我们需要追踪的不同数字的数量,这就是量子计算机更强大的原因,或者量子计算机可以存储如此多的信息。这并不完全正确。一个被称为 霍勒沃定理 的数学定理告诉我们,由 410 个量子比特组成的量子计算机最多可以存储 410 个经典比特的信息,即使需要整个宇宙大小的经典计算机来写入该量子计算机的状态。
换句话说,仅仅因为模拟量子计算机很难并不意味着它有什么有用的功能。在本书的其余部分,我们将看到,要使用经典计算机(如手机、笔记本电脑、集群和云环境)模拟量子计算机需要一点艺术性,才能弄清楚如何使用量子计算机解决有用的问题。
4.2.3 状态准备的张量积
将量子寄存器描述为描述计算基态的向量是很好,但即使我们知道我们想要达到的状态,我们也需要知道如何准备它。例如,在一个非局域游戏中,如果一个玩家有一个处于 |0〉 状态的量子比特,而另一个玩家有一个处于 |1〉 状态的量子比特,我们可以直接将这两个单量子比特状态结合起来,以描述游戏的状态为 |01〉。将两个(或更多)量子比特的状态“结合”是什么意思?我们可以通过向我们的数学工具箱中添加一个名为 张量积 的概念来实现这一点。
与我们使用列表 4.3 中的 product 函数来组合三个经典比特寄存器的标签一样,我们可以使用张量积的概念(表示为 ⊗),将每个量子比特的量子状态结合起来,形成一个描述多个量子比特的状态。product 的输出是那些三个经典比特的所有可能状态的一个列表。同样,张量积的输出是一个列出量子寄存器所有计算基态的状态。我们可以使用 NumPy 来计算张量积;NumPy 通过 np.kron 函数提供了张量积的实现,如图 4.5 所示。
为什么是 kron?
np.kron 这个名字对于一个实现张量积的函数来说可能看起来有些奇怪,但这个名字是与其相关的数学概念——克罗内克积(Kronecker product)的简称。NumPy 使用 kron 作为克罗内克积的简称,遵循了 MATLAB、R、Julia 和其他科学计算平台使用的约定。
列表 4.5 使用 NumPy 和张量积表示一个双量子比特状态
>>> import numpy as np
>>> ket0 = np.array([[1], [0]]) ❶
>>> ket1 = np.array([[0], [1]])
>>> np.kron(ket0, ket1) ❷
array([[0],
[1], ❸
[0],
[0]])
❶ 定义了单量子比特状态 |0〉 和 |1〉 的向量,就像在第二章和第三章中一样。
❷ 我们可以通过调用 NumPy 的张量积实现来构建 |0〉 ⊗ |1〉 的向量。由于历史原因,NumPy 中的张量积是通过 kron 函数表示的。
❸ np.kron 返回的向量在对应于 |01〉计算基态的条目处为 1,其他地方为 0,因此我们识别这个向量是状态 |01〉。
这个例子向我们展示了 |0〉 ⊗ |1〉 = |01〉。也就是说,如果我们单独拥有每个量子比特的状态,我们可以通过使用张量积来描述整个寄存器的状态,从而将它们结合起来。
我们可以通过这种方式组合任意数量的量子比特。比如说,我们有了四个量子比特,它们都处于 |0〉 状态。所有四个量子比特的寄存器可以描述为 |0〉 ⊗ |0〉 ⊗ |0〉 ⊗ |0〉。
列表 4.6 将 |0000〉 作为 4 个 |0〉 的张量积
>>> import numpy as np
>>> ket0 = np.array([[1], [0]])
>>> from functools import reduce
>>> reduce(np.kron, [ket0] * 4) ❶
array([[1], ❷
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0],
[0]])
❶ Python 标准库提供的 reduce 函数允许我们在列表中的每个元素之间应用一个双参数函数,如 kron。在这里,我们使用 reduce 而不是 np.kron(ket0, np.kron(ket0, np.kron(ket0, ket0)))。
❷ 我们得到了一个表示 |0〉 ⊗ |0〉 ⊗ |0〉 ⊗ |0〉 = |0000〉 的四量子比特状态向量。
注意:我们之前看到的二量子比特状态 (|00〉 + |11〉) / √2 不能写成两个单量子比特状态的张量积。不能表示为张量积的多量子比特状态称为 纠缠。在本章的其余部分和本书的其余部分,我们将看到更多关于纠缠的内容。
4.2.4 寄存器上的量子比特操作的张量积
现在我们知道了如何使用张量积来组合量子状态,np.kron 实际上在做什么呢?本质上,张量积是一个表格,列出了将其两个参数以不同方式组合的所有可能方式,如图 4.2 所示。

图 4.2 逐步展示两个矩阵的张量积。我们首先将 a 中的每个项与 b 的完整副本相乘。张量积的矩阵维度是输入矩阵维度的乘积。
图 4.2 中显示的两个矩阵的张量积在 Python 中的表示也显示在下面的列表中。
列表 4.7 使用 NumPy 求解 A 和 B 的张量积
>>> import numpy as np
>>> A = np.array([[1, 3], [5, 7]]) ❶
>>> B = np.array([[2, 4], [6, 8]])
>>> np.kron(A, B) ❷
array([[ 2, 4, 6, 12],
[ 6, 8, 18, 24],
[10, 20, 14, 28],
[30, 40, 42, 56]])
>>> np.kron(B, A) ❸
array([[ 2, 6, 4, 12],
[10, 14, 20, 28],
[ 6, 18, 8, 24],
[30, 42, 40, 56]])
❶ 矩阵 A 和 B 是这里用作示例的任意 2 × 2 矩阵。
❷ 如我们之前所见,np.kron 是 NumPy 对张量积的实现。
❸ 注意张量积参数的顺序很重要。虽然 np.kron(A, B) 和 np.kron(B, A) 包含相同的信息,但每个中的条目顺序相当不同!
通过两个矩阵之间的张量积,我们可以找到不同的量子操作如何转换量子寄存器的状态。我们也可以通过取两个矩阵的张量积来理解量子操作如何转换多个量子比特的状态,这样我们可以理解我们在非局域游戏中的移动如何影响我们的共享状态。
例如,我们知道可以将 |1〉 写作 X|0〉(即对初始化的量子比特应用 x 指令的结果)。这也为我们提供了另一种方式来写出像我们之前看到的 |01〉 状态这样的多量子比特状态。在这种情况下,我们只需对两个量子比特寄存器的第二个量子比特应用一个 x 指令即可得到 |01〉。使用张量积,我们可以找到一个单位矩阵来表示这一点。
列表 4.8 计算两个矩阵的张量积
>>> import numpy as np
>>> I = np.array([[1, 0], [0, 1]]) ❶
>>> X = np.array([[0, 1], [1, 0]]) ❷
>>> IX = np.kron(I, X) ❸
>>> IX
array([[0, 1, 0, 0], ❹
[1, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0]])
>>> ket0 = np.array([[1], [0]]) ❺
>>> ket00 = np.kron(ket0, ket0)
>>> ket00
array([[1],
[0],
[0],
[0]])
>>> IX @ ket00
array([[0],
[1], ❻
[0],
[0]])
❶ 定义了一个表示对第一个量子比特不做任何操作的矩阵,称为单位矩阵 𝟙。由于 𝟙 在 Python 中难以书写,我们使用 I 来代替
❷ 定义了允许我们模拟 x 指令的单位矩阵 X
❸ 使用张量积 I ⊗ X 将它们结合起来
❹ 矩阵 𝟙 ⊗ X 包含两个 X 的副本,表示对于第一个量子比特的每个可能状态,第二个量子比特会发生什么。
❺ 让我们看看当我们使用 𝟙 ⊗ X 来模拟 x 指令如何将两个量子比特寄存器中的第二个量子比特的状态进行转换时会发生什么。我们将从这个处于 |00〉 = |0〉 ⊗ |0〉 状态的寄存器开始。
❻ 我们认出我们得到的状态是本节前面提到的 |01〉 状态。正如预期的那样,这是通过将第二个量子比特从 |0〉 翻转到 |1〉 得到的状态。
练习 4.2:对两个量子比特寄存器进行哈达玛操作
你会如何准备一个 |+0〉 状态?首先,你将使用什么向量来表示两个量子比特状态 |+0〉 = |+〉 ⊗ |0〉?你有一个初始处于 |00〉 状态的两个量子比特寄存器。你应该应用什么操作来得到你想要的状态?
提示:如果你卡住了,试试 (H ⊗ 𝟙)!
深入探讨:最终证明无克隆定理
学习到多个量子位的操作也由单位矩阵表示,让我们最终证明了我们之前几次看到的不可克隆定理。关键洞察是,克隆一个状态不是线性的,因此不能写成矩阵的形式。
与数学中的许多证明一样,不可克隆定理的证明是通过矛盾来进行的。也就是说,我们假设定理的相反,然后表明由于这个假设,我们得到了一些错误的东西。
那么,我们不再犹豫,首先假设我们有一些神奇的clone指令,可以完美地复制其量子位的状态。例如,如果我们有一个量子位q1,其状态从|1〉开始,以及一个量子位q2,其状态从|0〉开始,那么在调用q1.clone(q2)之后,我们会得到寄存器|11〉。
类似地,如果q1从|+〉开始,那么q1.clone(q2)应该给我们一个处于|++〉 = |+〉 ⊗ |+〉状态的寄存器。问题在于调和q1.clone(q2)在这两种情况下应该做什么。我们知道,除了测量之外,任何量子操作都必须是线性的,所以让我们给让我们模拟clone的矩阵起个名字:C看起来相当合理。
使用C,我们可以将我们要克隆|+〉的情况分解为我们要克隆|0〉的情况加上我们要克隆|1〉的情况。我们知道C|+0〉 = |++〉,但我们还知道C|+0〉 = C(|00〉 + |10〉) / √2 = (C|00〉 + C|10〉) / √2。由于clone需要克隆|0〉、|1〉以及|+〉,我们知道C|00〉 = |00〉和C|10〉 = |11〉。这给了我们(C|00〉 + C|10〉) / √2 = (|00〉 + |11〉) / √2,但我们之前得出结论,C|+0〉 = |++〉 = (|00〉 + |01〉 + |10〉 + |11〉) / 2。
因此,我们遇到了矛盾,可以得出结论,我们在第一步就犯了错误,那时我们假设clone可以存在!因此,我们证明了不可克隆定理。
从这个论点中需要注意的一个重要问题是,如果我们知道正确的基,我们总能从一个量子位复制信息到另一个量子位。问题在于当我们不知道我们应该复制关于|0〉与|1〉还是|+〉与|−〉的信息时,因为我们可以复制|0〉或|+〉,但不能两者同时复制。在经典情况下这不是问题,因为我们只有计算基可以工作。
到目前为止,我们一直使用 NumPy 来编写我们的量子位模拟器SingleQubitSimulator()。这非常有帮助,因为没有 NumPy,我们就需要编写自己的矩阵分析函数和方法。然而,通常方便依赖于具有量子概念特殊支持的 Python 包,这些包建立在 NumPy 和 SciPy(NumPy 数值能力的扩展)提供的优秀数值支持之上。在下一章中,我们将探讨一个专为量子计算设计的 Python 包,称为 QuTiP(Python 中的量子工具箱),它将帮助我们完成模拟器的升级,以模拟我们的 CHSH 游戏。
摘要
-
类似于 CHSH 游戏这样的非局域游戏是实验,可以用来验证量子力学与我们宇宙实际运作方式的一致性。
-
要用经典比特来表示量子寄存器的状态需要指数级数量的比特,这使得使用传统计算机模拟超过大约 30 个量子比特变得非常困难。
-
我们可以使用张量积将单量子比特状态组合在一起,从而能够描述多量子比特状态。
-
无法通过组合单量子比特状态来表示的多量子比特状态代表着相互纠缠的量子比特。
^(1.)CHSH 这个名字来源于最初创建这个游戏的四位研究者的首字母:Clauser, Horne, Shimony, 和 Holt。如果您感兴趣,可以找到原始文献:journals.aps.org/prl/abstract/10.1103/PhysRevLett.23.880。
5 非局域游戏:实现多量子比特模拟器
本章涵盖
-
使用 QuTiP Python 包和张量积编程多量子比特模拟器
-
通过模拟实验结果来识别量子力学与我们对宇宙观察的一致性证明
在上一章中,我们学习了非局域游戏以及我们如何利用它们来验证我们对量子力学的理解。我们还学习了如何表示多个量子比特的状态以及什么是纠缠。
在本章中,我们将深入研究一个新的 Python 包 QuTiP,它将使我们能够更快地编程量子系统,并为模拟量子力学提供一些内置的酷特性。然后我们将学习如何使用 QuTiP 来编程多量子比特模拟器,并看看这如何(或没有!)改变我们量子比特的三个主要任务:状态制备、操作和测量。这将使我们能够完成第四章中 CHSH 游戏的实现!
5.1 QuTiP 中的量子对象
QuTiP(Python 中的量子工具箱,www.qutip.org)是一个特别有用的包,它提供了内置支持,用于将状态和测量分别表示为 bra 和 ket,并构建矩阵来表示量子操作。正如np.array是 NumPy 的核心一样,我们所有的 QuTiP 使用都将围绕Qobj类(简称量子对象)展开。这个类封装了矢量和矩阵,提供了额外的元数据和有用的方法,这将使我们的模拟器更容易改进。图 5.1 展示了从矢量创建Qobj的示例,其中它跟踪了一些元数据:
-
data保存表示Qobj的数组。 -
dims是我们量子寄存器的大小。我们可以将其视为跟踪我们处理量子比特的方式。 -
shape保存了我们用来创建Qobj的原对象的维度。它类似于np.shape属性。 -
type是Qobj所表示的内容(状态=ket,测量=bra,或算子=oper)。

图 5.1 QuTiP Python 包中Qobj类的属性。在这里,我们可以看到诸如type和dims属性,这些属性帮助我们和包跟踪关于我们的量子对象的元数据。
让我们尝试导入 QuTiP 并请求它执行 Hadamard 运算;请参见列表 5.1。
注意:确保你在正确的conda env中运行程序;更多信息请参阅附录 A。
列表 5.1 QuTiP 对 Hadamard 运算的表示
>>> from qutip.qip.operations import hadamard_transform
>>> H = hadamard_transform()
>>> H
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
Qobj data =
[[ 0.70710678 0.70710678]
[ 0.70710678 -0.70710678]]
注意,QuTiP 会打印出有关每个 Qobj 实例的诊断信息,包括数据本身。例如,这里的 type = oper 告诉我们 H 代表一个算子(我们之前看到的矩阵的更正式的术语)以及 H 所表示算子的维度信息。最后,isherm = True 的输出告诉我们 H 是一种特殊矩阵的例子,称为 厄米算子。
我们可以通过传递 Python 列表到 Qobj 初始化器中,以与创建 NumPy 数组相同的方式创建新的 Qobj 实例。
列表 5.2 从表示量子比特状态的向量创建 Qobj
>>> import qutip as qt
>>> ket0 = qt.Qobj([[1], [0]]) ❶
>>> ket0
Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket ❷
Qobj data =
[[1.]
[0.]]
❶ 创建 Qobj 实例和数组之间的一个关键区别是,当我们创建 Qobj 实例时,我们总是需要两层列表。外层列表是新的 Qobj 实例中的行列表。
❷ QuTiP 打印出有关新量子对象的大小和形状的元数据,以及新对象中包含的数据。在这种情况下,新 Qobj 的数据有两行,每行有一列。我们将其识别为用于编写 |0〉 状态的向量或 kets。
练习 5.1:创建其他状态的 Qobj
你会如何创建一个表示 |1〉 状态的 Qobj?对于 |+〉 或 |−〉 状态呢?如果你需要,可以回查第 2.3.5 节,了解这些状态所代表的向量。
练习解答
本书所有练习的解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,并打开名为练习解答的 Jupyter 笔记本。
QuTiP 通过提供大量简写来帮助我们处理量子计算中需要用到的对象。例如,我们也可以通过使用 QuTiP 的 basis 函数来创建前一个示例中的 ket0;参见列表 5.3。basis 函数接受两个参数。第一个参数告诉 QuTiP 我们想要一个量子比特状态:对于单个量子比特是 2,因为表示它所需的向量的长度。第二个参数告诉 QuTiP 我们想要哪个基态。
列表 5.3 使用 QuTiP 简单创建 |0〉 和 |1〉
>>> import qutip as qt
>>> ket0 = qt.basis(2, 0) ❶
>>> ket0
Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket ❷
Qobj data =
[[1.]
[0.]]
>>> ket1 = qt.basis(2, 1) ❸
>>> ket1
Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket
Qobj data =
[[0.]
[1.]]
❶ 将 2 作为第一个参数传递,表示我们想要一个单个量子比特,并将 0 作为第二个参数传递,因为我们想要 |0〉
❷ 注意,这里的输出与上一个示例完全相同。
❸ 我们也可以通过传递 1 而不是 0 来构造表示 |1〉 的量子对象。
...基?
正如我们之前所看到的,状态 |0〉 和 |1〉 构成了一个单量子比特的 计算基。QuTiP 函数 basis 的名字来源于这个定义,因为它使得量子对象可以表示计算基状态。
天地之间还有比我们的量子比特更多的事情。
我们可能觉得必须告诉 QuTiP 我们想要一个量子比特有点奇怪。毕竟,我们还能想要什么?实际上,相当多(是的,双关语非常有意)!
除了比特之外,还有许多其他方式可以表示经典信息,例如 三进制,它有三个可能值。然而,当我们编写程序时,我们通常不会看到使用除比特之外的其他方式来表示经典信息,因为选择一个约定并坚持下去非常有用。尽管如此,在电信系统等特定领域,除了比特之外的事物仍然有其用途。
以完全相同的方式,量子系统可以具有任何数量的不同状态:我们可以有量子三比特、量子四比特、量子五比特、量子十七比特等等,统称为量子 dits。虽然在某些情况下,使用量子 dits 而不是量子比特来表示量子信息可能很有用,并且可以具有一些非常有趣的数学属性,但量子比特为我们提供了进入量子编程所需的一切。
练习 5.2:使用 qt.basis 对多个量子比特进行操作
你如何使用 qt.basis 函数创建一个处于 |10〉 状态的两个量子比特寄存器?如何创建 |001〉 状态?请记住,qt.basis 的第二个参数是我们之前看到的计算基态的索引。
QuTiP 还提供了一些不同的函数来创建量子对象以表示幺正矩阵。例如,我们可以通过使用 sigmax 函数来创建 X 矩阵的量子对象。
列表 5.4 使用 QuTiP 创建 X 矩阵的对象
>>> import qutip as qt
>>> qt.sigmax()
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
Qobj data =
[[0\. 1.]
[1\. 0.]]
正如我们在第二章中看到的,sigmax 的矩阵表示 180° 的旋转(图 5.2)。

图 5.2 在 |0〉 状态下对量子比特执行 NOT 操作的量子等效可视化,使量子比特处于 |1〉 状态
QuTiP 还提供了一个名为 ry 的函数来表示旋转任意角度,而不是像 x 操作那样旋转 180°。我们在第二章中考虑旋转 |0〉 的任意角度 θ 时看到了 ry 所表示的操作。参见图 5.3 以复习我们现在所知道的 ry 操作。

图 5.3 QuTiP 函数 ry 的可视化,它对应于围绕量子比特的 Y-轴(指向页面直接外侧)的变量旋转 θ
现在我们已经掌握了一些单量子比特操作,我们如何在 QuTiP 中轻松模拟多量子比特操作?我们可以使用 QuTiP 的 tensor 函数快速使用张量积来创建我们的多量子比特寄存器和操作,正如我们在列表 5.5 中所展示的。
注意:由于单位矩阵通常用字母 I 表示,许多科学计算包使用 eye 这个名字作为双关语来指代单位矩阵。
列表 5.5 QuTiP 中的张量积
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> psi = qt.basis(2, 0) ❶
>>> phi = qt.basis(2, 1) ❷
>>> qt.tensor(psi, phi)
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[ 0.] ❸
[ 1.]
[ 0.]
[ 0.]]
>>> H = hadamard_transform() ❹
>>> I = qt.qeye(2) ❺
>>> qt.tensor(H, I) ❻
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4),
➥ type = oper, isherm = True
Qobj data =
[[ 0.70710678 0\. 0.70710678 0\. ]
[ 0\. 0.70710678 0\. 0.70710678]
[ 0.70710678 0\. -0.70710678 0\. ]
[ 0\. 0.70710678 0\. -0.70710678]]
❶ 将 psi 设置为表示 |Ψ〉 = |0〉
❷ 将 phi 设置为表示 |ϕ〉 = |1〉
❸ 在调用张量积之后,QuTiP 告诉我们|Ψ〉 ⊗ |ϕ〉 = |0〉 ⊗ |1〉 = |01〉中每个经典标签的振幅,使用与列表 4.3 相同的顺序。
❹ 将 H 设置为表示之前讨论过的 Hadamard 算符
❺ 我们可以使用 QuTiP 提供的 qeye 函数来获取一个表示单位矩阵的 Qobj 实例的副本,这是我们第一次在列表 4.8 中看到的。
❻ 表示量子操作的幺正矩阵通过张量积以与状态和测量相同的方式组合。
我们可以使用一个常见的数学技巧来证明应用状态和操作的张量积是如何工作的。比如说我们想要证明这个陈述:
如果我们对一个状态应用一个幺正算符,然后取张量积,我们将得到与应用张量积然后幺正算符相同的答案。
在数学上,我们会说对于任何幺正算符U和V以及任何状态|Ψ〉和|ϕ〉,(U|Ψ〉) ⊗ (V|ϕ〉) = (U ⊗ V) (|Ψ〉 ⊗ |ϕ〉)。我们可以使用的数学技巧是从左侧减去右侧。我们应该得到 0。我们将在以下列表中尝试这样做。
列表 5.6 在 QuTiP 中验证张量积
>>> (
... qt.tensor(H, I) * qt.tensor(psi, phi) - ❶
... qt.tensor(H * psi, I * phi) ❷
... )
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[ 0.] ❸
[ 0.]
[ 0.]
[ 0.]]
❶ 我们试图证明的语句的右侧,其中我们使用H和I作为U和V:(U ⊗ V) (|Ψ〉 ⊗ |ϕ〉)。
❷ 我们试图证明的语句的左侧,其中我们使用H和I作为U和V:(U|Ψ〉) ⊗ (V|ϕ〉)。
❸ 哈哈!如果它们的差为 0,则等式的两边是相等的。
注意:有关 QuTiP 中所有内置状态和操作的列表,请参阅qutip.org/docs/latest/guide/guide-basics.html#states-and-operators。
5.1.1 升级模拟器
目前的目标是使用 QuTiP 将我们的单量子位模拟器升级为具有 QuTiP 一些特性的多量子位模拟器。我们将通过向第二章和第三章的单量子位模拟器添加一些功能来实现这一点。
我们需要对模拟器从之前章节所做的最显著的改变是,我们不能再为每个量子位分配一个状态。相反,我们必须为设备中的整个寄存器分配一个状态,因为一些量子位可能彼此纠缠。让我们开始进行必要的修改,以将状态的概念分离到设备级别。
注意:要查看我们之前编写的代码以及本章的示例,请参阅本书的 GitHub 仓库:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。
为了复习,我们有两个用于我们的模拟器的文件:接口(interface.py)和模拟器本身(simulator.py)。设备接口(QuantumDevice)定义了与实际或模拟量子设备交互的方式,在 Python 中,它是一个对象,允许我们分配和释放量子位。
在接口中,我们不需要为 QuantumDevice 类添加任何新功能来模拟我们的 CHSH 游戏,因为我们仍然需要分配和释放量子比特。我们可以在 Qubit 类中添加功能,这个类与我们的 SingleQubitSimulator 一起在 simulator.py 中提供。
现在我们需要考虑,如果我们从 QuantumDevice 分配一个 Qubit,我们的接口中是否需要做出任何改变。在第二章中,我们看到了 Hadamard 操作对于在 QRNG 中旋转量子比特到不同基是有用的。让我们在此基础上添加一个新的方法到 Qubit,以便量子程序可以发送一种新的旋转指令,我们将需要使用量子策略来执行 CHSH。
列表 5.7 interface.py:添加新的 ry 操作
class Qubit(metaclass=ABCMeta):
@abstractmethod
def h(self): pass
@abstractmethod
def x(self): pass
@abstractmethod
def ry(self, angle: float): pass ❶
@abstractmethod
def measure(self) -> bool: pass
@abstractmethod
def reset(self): pass
❶ 抽象方法 ry,它通过一个角度参数来指定量子比特绕 Y- 轴旋转多远
这样就应该涵盖了我们需要对 Qubit 和 QuantumDevice 接口进行的所有更改,以便与 Eve 进行 CHSH 游戏。我们需要解决我们需要对 simulator.py 进行哪些更改,以便它能够分配、操作和测量多量子比特状态。
我们实现 QuantumDevice 的 Simulator 类的主要变化是需要属性来跟踪它有多少个量子比特以及寄存器的整体状态。下一个列表显示了这些变化以及分配和释放方法的更新。
列表 5.8 simulator.py:多量子比特 Simulator
class Simulator(QuantumDevice): ❶
capacity: int ❷
available_qubits: List[SimulatedQubit] ❸
register_state: qt.Qobj ❹
def __init__(self, capacity=3):
self.capacity = capacity
self.available_qubits = [ ❺
SimulatedQubit(self, idx)
for idx in range(capacity)
]
self.register_state = qt.tensor( ❻
*[
qt.basis(2, 0)
for _ in range(capacity)
]
)
def allocate_qubit(self) -> SimulatedQubit: ❼
if self.available_qubits:
return self.available_qubits.pop()
def deallocate_qubit(self, qubit: SimulatedQubit):
self.available_qubits.append(qubit)
❶ 我们将名称从 SingleQubitSimulator 更改为 Simulator,以表明它更通用。这意味着我们可以用它来模拟多个量子比特。
❷ 更通用的 Simulator 类需要一些属性,首先是容量,它表示它可以模拟的量子比特数量。
❸ available_qubits 是一个包含 Simulator 正在使用的量子比特的列表。
❹ register_state 使用新的 QuTiP Qobj 来表示整个模拟器的状态。
❺ 列表推导式允许我们通过调用具有容量范围的索引的 SimulatedQubit 来创建一个可用量子比特的列表。
❻ register_state 通过取与模拟器容量相等的 |0〉 状态的多个复制品的张量积来初始化。The *[...] notation 将生成的列表转换为 qt.tensor 的参数序列。
❼ allocate_qubit 和 deallocate_qubit 方法与第三章中的相同。
不要窥视盒子,凡人!
正如我们使用 NumPy 来表示模拟器的状态一样,我们新升级的模拟器的 register_state 属性使用 QuTiP 来预测每个指令如何转换了我们的寄存器状态。然而,当我们编写量子程序时,我们是在列表 5.7 中给出的接口上进行操作的,这个接口没有提供任何让我们访问 register_state 的方式。
我们可以将模拟器视为一种封装了状态概念的 黑盒。如果我们的量子程序能够查看这个盒子内部,它们将能够通过复制信息的方式作弊,这是由不可克隆定理所禁止的。这意味着为了使量子程序正确,我们无法查看模拟器以查看其状态。
在本章中,我们将稍微作弊一下;但在下一章中,我们将修复这个问题,以确保我们的程序可以在实际的量子硬件上运行。
我们还将在我们的 Simulator 中添加一个新的 私有 方法,允许我们对设备中的特定量子比特应用操作。这将使我们能够在量子比特上编写方法,将操作发送回模拟器以应用于整个量子比特寄存器的状态。
提示:Python 对保持方法或属性为私有并不严格,但我们将在这个方法名前加上下划线来表示它仅用于类内部。
列表 5.9 simulator.py:为 Simulator 添加一个额外的方法
def _apply(self, unitary: qt.Qobj, ids: List[int]): ❶
if len(ids) == 1: ❷
matrix = qt.circuit.gate_expand_1toN(
unitary, self.capacity, ids[0]
)
else:
raise ValueError("Only single-qubit unitary matrices are supported.")
self.register_state = matrix * self.register_state ❸
❶ 私有方法 _apply 接收一个表示要应用的可逆操作的 Qobj 类型的输入单元,以及一个表示我们想要应用操作的 available_qubits 列表中索引的整数列表。目前,这个列表将只包含一个元素,因为我们目前只在我们的模拟器中实现单量子比特操作。我们将在下一章中放宽这个限制。
❷ 如果我们想要对一个寄存器中的量子比特应用单量子比特操作,我们可以使用 QuTiP 生成所需的矩阵。QuTiP 通过将我们的单量子比特操作的矩阵应用于正确的量子比特,并在其他地方应用 𝟙 来完成这项工作。这是由 gate_expand_1toN 函数自动完成的。
❸ 现在我们有了要乘以整个寄存器状态的正确矩阵,我们可以相应地更新寄存器的值。
让我们来看看 SimulatedQubit 的实现,这是一个表示我们如何模拟单个量子比特的类,前提是我们知道它是一个具有多个量子比特的设备的一部分。单量子比特和多量子比特版本的 SimulatedQubit 之间的主要区别在于,我们需要每个量子比特记住其“父”设备和在该设备中的位置或 id,这样我们才能将状态与寄存器关联起来,而不是与每个量子比特关联。这很重要,正如我们将在下一节中看到的,当我们想要测量多量子比特设备中的量子比特时。
列表 5.10 simulator.py:在多量子比特设备上执行单量子比特操作
class SimulatedQubit(Qubit):
qubit_id: int
parent: "Simulator"
def __init__(self, parent_simulator: "Simulator", id: int): ❶
self.qubit_id = id
self.parent = parent_simulator
def h(self) -> None:
self.parent._apply(H, [self.qubit_id]) ❷
def ry(self, angle: float) -> None:
self.parent._apply(qt.ry(angle), [self.qubit_id]) ❸
def x(self) -> None:
self.parent._apply(qt.sigmax(), [self.qubit_id])
❶ 要初始化一个量子比特,我们需要父模拟器的名称(以便我们能够轻松关联)和量子比特在模拟器寄存器中的索引。__init__ 然后设置这些属性并将量子比特重置到 |0〉 状态。
❷ 要实现 H 操作,我们要求 SimulatedQubit 的父对象(它是一个 Simulator 实例)使用 _apply 方法生成表示对整个寄存器操作的矩阵,然后更新寄存器状态。
❸ 我们还可以将 QuTiP 中的参数化 qt.ry 操作传递给 _apply 以旋转我们的量子比特绕 Y 轴旋转一个角度“angle”。
太好了!我们几乎完成了升级我们的模拟器以使用 QuTiP 并支持多个量子比特的工作。我们将在下一节中处理在多量子比特状态上模拟测量的工作。
5.1.2 测量:我们如何测量多个量子比特?
提示:本节是本书中最具挑战性的部分之一。如果第一次阅读时它没有太多意义,请不要担心。
在某种程度上,测量多个量子比特的工作方式与我们习惯于测量单量子比特系统的方式相同。我们仍然可以使用 Born 定律来预测任何特定测量结果的可能性。例如,让我们回到我们已经看到几次的 (|00〉 + |11〉) / √2 状态。如果我们测量处于该状态的一对量子比特,我们会以相等的概率得到“00”或“11”作为我们的经典结果,因为它们都有相同的振幅:1 / √2。
同样,我们仍然要求如果我们连续两次测量相同的寄存器,我们得到相同的答案。例如,如果我们得到“00”的结果,我们知道量子比特被留在 |00〉 = |0〉 ⊗ |0〉 状态。
然而,如果我们不测量整个量子寄存器而只测量其一部分,这会变得有点复杂。让我们看看几个例子,看看这是如何工作的。再次以 (|00〉 + |11〉) / √2 为例,如果我们只测量第一个量子比特并且得到“0”,我们知道我们下次测量时需要得到相同的答案。这种情况只能通过观察第一个量子比特上的“0”导致状态转换为 |00〉 来实现。
另一方面,如果我们测量处于 |+〉 状态的一对量子比特中的第一个量子比特会发生什么?首先,回顾一下当以向量形式表示时 |+〉 看起来是什么样子是有帮助的。
列表 5.11 使用 QuTiP 表示 |++〉 状态
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> ket_plus = hadamard_transform() * qt.basis(2, 0) ❶
>>> ket_plus
Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket ❷
Qobj data =
[[0.70710678]
[0.70710678]]
>>> register_state = qt.tensor(ket_plus, ket_plus) ❸
>>> register_state
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data = ❹
[[0.5]
[0.5]
[0.5]
[0.5]]
❶ 首先将 |+〉 写作 H|0〉。在 QuTiP 中,我们使用 hadamard _ 变换函数 _ 来获取一个 Qobj 实例以表示 H,我们使用 basis(2, 0) 来获取一个表示 |0〉 的 Qobj。
❷ 我们可以打印出 ket_plus 来获取该向量中的元素列表;就像之前一样,我们称这些元素中的每一个为一个振幅。
❸ 为了表示状态 |+〉,我们使用 |+〉 = |+〉 ⊗ |+〉。
❹ 这个输出告诉我们 |++〉 对于四个计算基态 |00〉、|01〉、|10〉 和 |11〉 都有相同的振幅,正如 ket_plus 对于计算基态 |0〉 和 |1〉 的振幅相同。
假设我们测量第一个量子比特并得到“1”的结果。为了确保我们下次测量时得到相同的结果,测量后的状态不能在 |00〉 或 |01〉 上有任何振幅。如果我们只保留 |10〉 和 |11〉(我们之前计算的向量的第三行和第四行)上的振幅,那么我们得到的状态就变成了 (|10〉 + |11〉) / √2。
√2 从哪里来的?
我们包括了一个√2,以确保当我们测量第二个量子比特时,所有测量概率的总和仍然为 1。为了使 Born 规则有意义,我们总是需要每个振幅平方的总和加起来为 1。
尽管如此,我们还可以用另一种方式来表示这个状态,我们可以使用 QuTiP 来检查这一点。
列表 5.12 使用 QuTiP 表示|1+〉状态
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> ket_0 = qt.basis(2, 0)
>>> ket_1 = qt.basis(2, 1)
>>> ket_plus = hadamard_transform() * ket_0 ❶
>>> qt.tensor(ket_1, ket_plus)
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[0\. ]
[0\. ]
[0.70710678]
[0.70710678]]
❶ 回想一下,我们可以将|+〉写成H|0〉。
这告诉我们,如果我们只保留与测量第一个量子比特得到“1”结果一致的状态|+〉的部分,那么我们得到|1〉 = |1〉 ⊗ |+〉。也就是说,在这种情况下,第二个量子比特根本没有任何变化!
练习 5.3:测量另一个量子比特
在我们的两个量子比特从|++〉状态开始的例子中,假设我们测量了第二个量子比特。检查一下,无论我们得到什么结果,第一个量子比特的状态都不会发生变化。
为了更普遍地弄清楚测量寄存器的一部分意味着什么,我们可以使用线性代数中的另一个概念,即投影器。
定义 A 投影器是状态向量(括号中的“ket”或|+〉部分)和测量(括号中的“bra”或〈+|部分)的乘积,它代表我们的要求:如果发生某种测量结果,那么我们必须转换到一个与该测量一致的状态。
见图 5.4,了解单量子比特投影器的快速示例。在多个量子比特上定义投影器的工作方式完全相同。

图 5.4 单量子比特状态的投影器作用示例
在 QuTiP 中,我们通过使用.dag()方法(简称为dagger,这是对图 5.4 中看到的数学符号的回溯)来写出与基相对应的共轭。幸运的是,即使数学运算不是那么直接,在 Python 中编写起来并不那么糟糕,正如我们接下来可以看到的。
列表 5.13 simulator.py:测量寄存器中的单个量子比特
def measure(self) -> bool:
projectors = [ ❶
qt.circuit.gate_expand_1toN( ❷
qt.basis(2, outcome) * qt.basis(2, outcome).dag(),
self.parent.capacity,
self.qubit_id
)
for outcome in (0, 1)
]
post_measurement_states = [
projector * self.parent.register_state ❸
for projector in projectors
]
probabilities = [ ❹
post_measurement_state.norm() ** 2
for post_measurement_state in post_measurement_states
]
sample = np.random.choice([0, 1], p=probabilities) ❺
self.parent.register_state = post_measurement_states[sample].unit()❻
return bool(sample)
def reset(self) -> None: ❼
if self.measure(): self.x()
❶ 使用 QuTiP 制作一个投影器列表,每个可能的测量结果一个。
❷ 如列表 5.9 所示,使用 gate_expand_1toN 函数将每个单量子比特投影器扩展为作用于整个寄存器的投影器
❸ 使用每个投影器来挑选出与每个测量结果一致的状态部分
❹ 每个投影器选择的长度(在 QuTiP 中用.unit()方法表示)告诉我们每个测量结果的可能性。
❺ 一旦我们得到了每个结果的概率,我们可以使用 NumPy 来选择一个结果。
❻ 使用 QuTiP 内置的.unit()方法确保测量概率的总和仍然为 1
❼ 如果测量的结果是|1〉,那么使用 x 指令翻转会将其重置回|0〉。
5.2 CHSH:量子策略
现在我们已经扩展了我们的模拟器以处理多个量子比特,让我们看看我们如何模拟一个基于量子的策略,这将使玩家的获胜概率高于任何经典策略!见图 5.5,以提醒如何玩 CHSH 游戏。

图 5.5 CHSH 游戏,一个有两个玩家和一个裁判的非定域游戏。裁判以比特值的形式向每个玩家提出问题,然后每个玩家必须想出如何回应裁判。如果玩家们的响应的布尔异或与裁判问题的经典与相同,则玩家获胜。
现在,你和 Eve 有了量子资源,让我们从最简单的方法开始:你们每个人从同一个设备中分配到一个量子比特。我们将使用我们的模拟器来实现这个策略,所以这并不是真正测试量子力学,更多的是测试我们的模拟器是否与量子力学一致。
注意:我们无法模拟玩家真正非定域性,因为模拟器部分需要通信来模拟量子力学。以这种方式忠实模拟量子游戏和量子网络协议暴露了许多有趣的经典网络拓扑问题,这些问题远远超出了本书的范围。如果您对更多用于量子网络而不是量子计算的模拟器感兴趣,我们建议查看 SimulaQron 项目(www.simulaqron.org)以获取更多信息。
让我们看看如果我们每个人从一个单量子比特开始,并且这些量子比特处于(|00〉 + |11〉)/ √2 状态,这是我们在这章中看到几次的状态,我们和 Eve 能有多经常获胜。不用担心如何准备这种状态;我们将在第六章中学习如何做到这一点。现在,让我们看看一旦我们有了这些量子比特,我们能用它们做什么。
使用这些量子比特,我们可以为我们在本章开头看到的 CHSH 游戏形成一个新的量子策略。窍门是我们和 Eve 可以在从裁判那里得到各自的消息后对各自的每个量子比特应用操作。
事实上,ry对于这种策略非常有用。它让我们和 Eve 在裁判要求我们输出相同答案(00、01 和 10 情况)的频率上稍作权衡,以便在需要输出不同答案(11 情况)时表现得更好,如图 5.6 所示。

图 5.6 通过旋转量子比特赢得 CHSH 游戏。如果我们从裁判那里得到一个0,我们应该将我们的量子比特旋转 45°;如果我们得到一个1,我们应该将我们的量子比特旋转 135°。
从这个策略中我们可以看到,我们和 Eve 在测量之前对各自的量子比特要做什么都有一个相当简单、直接的规定。如果我们从裁判那里得到一个0,我们应该将我们的量子比特旋转 45°;如果我们得到一个1,我们应该将我们的量子比特旋转 135°。如果您喜欢表格方法来处理这个策略,表 5.1 展示了总结。
表 5.1 我们和 Eve 根据从裁判那里接收到的输入比特对我们量子比特进行的旋转。注意,它们都是ry旋转,只是角度不同(转换为弧度用于ry)。
| 裁判的输入 | 我们的旋转 | Eve 的旋转 |
|---|---|---|
| 0 | ry(90 * np.pi / 180) |
ry(45 * np.pi / 180) |
| 1 | ry(0) |
ry(135 * np.pi / 180) |
如果这些角度看起来是随机的,请不要担心。我们可以使用我们新的模拟器来检查它们是否有效!下一个列表使用我们添加到模拟器中的新功能来编写量子策略。
列表 5.14 chsh.py:一个使用两个量子比特的量子 CHSH 策略
import qutip as qt
def quantum_strategy(initial_state: qt.Qobj) -> Strategy:
shared_system = Simulator(capacity=2) ❶
shared_system.register_state = initial_state
your_qubit = shared_system.allocate_qubit() ❷
eve_qubit = shared_system.allocate_qubit()
shared_system.register_state = qt.bell_state() ❸
your_angles = [90 * np.pi / 180, 0] ❹
eve_angles = [45 * np.pi / 180, 135 * np.pi / 180]
def you(your_input: int) -> int:
your_qubit.ry(your_angles[your_input]) ❺
return your_qubit.measure() ❻
def eve(eve_input: int) -> int: ❼
eve_qubit.ry(eve_angles[eve_input])
return eve_qubit.measure()
return you, eve ❽
❶ 要开始量子策略,我们需要创建一个 QuantumDevice 实例,我们将在这个实例中模拟我们的量子比特。
❷ 我们可以将标签分配给每个量子比特,就像我们将它们分配给共享系统一样。
❸ 我们稍微作弊一下,将我们的量子比特的状态设置为纠缠态(|00〉 + |11〉)/ √2。我们将在第六章中看到如何从头开始制备这个状态,以及为什么制备这个状态的函数被称为 bell_state。
❹ 我们和欧娃需要根据裁判的输入进行的旋转角度
❺ 玩 CHSH 游戏的策略始于我们根据裁判输入的经典比特旋转我们的量子比特。
❻ 我们策略返回的经典比特值是我们测量量子比特时得到的比特值。
❼ 欧娃的策略与我们的类似;她只是对初始旋转使用了不同的角度。
❽ 就像我们的经典策略一样,quantum_strategy 返回一个表示我们和欧娃各自行动的函数元组。
现在我们已经实现了 quantum_strategy 的 Python 版本,让我们看看我们使用 CHSH 游戏的 est_win_probability 函数可以赢多少次。
列表 5.15 运行 CHSH 使用我们的新 quantum_strategy
>>> est_win_probability(quantum_strategy) ❶
0.832
❶ 当你尝试这个时,你可能得到略多于或少于 85%,因为获胜概率是在幕后使用二项分布估计的。对于这个例子,我们预计误差条约为 1.5%。
列表 5.15 中估计的 83.2% 的获胜概率高于我们使用任何经典策略所能获得的概率。这意味着我们和欧娃可以比任何其他经典玩家更频繁地赢得 CHSH 游戏——太棒了!然而,这个策略展示的是像(|00〉 + |11〉)/ √2 这样的态是量子力学提供的重要资源的一个例子。
注意,像(|00〉 + |11〉)/ √2 这样的态被称为 纠缠,因为它们不能写成单个量子比特态的张量积。随着我们的深入,我们将看到更多纠缠的例子,但纠缠是我们能够在编写量子程序时使用的最神奇和有趣的事情之一。
正如我们在本例中所见,纠缠使我们能够在我们想要从量子系统中获取有用信息时,创建可以利用的数据相关性。
光速仍然是一个东西
如果你读过关于相对论的内容(如果你没有,没关系),你可能听说过不可能以超过光速的速度发送信息。从我们目前对纠缠的了解来看,量子力学似乎违反了这一点,但事实上,纠缠永远不能单独用来发送我们选择的消息。我们总是需要在使用纠缠的同时发送其他东西。这意味着光速仍然限制了信息在宇宙中传播的速度——谢天谢地!
纠缠远非奇怪或奇怪,它是我们关于量子计算已经学到的内容的直接结果:它是量子力学线性的直接后果。如果我们能够准备一个处于|00〉状态和|11〉状态的二量子比特寄存器,那么我们也可以准备一个这两个状态的线性组合状态,例如(|00〉 + |11〉)/ √2。
由于纠缠是量子力学线性的直接结果,CHSH 游戏也为我们提供了一个很好的方法来检查量子力学是否真的正确(或者我们的数据所能显示的最好)。让我们回到列表 5.15 中的胜率。如果我们进行一个实验,并且看到大约 83.2%的胜率,这告诉我们我们的实验不可能完全是经典的,因为我们知道经典策略最多只能赢 75%的时间。这个实验在历史上已经进行了很多次,也是我们了解我们的宇宙不仅仅是经典的原因之一——我们需要量子力学来描述它。
注意:在 2015 年,CHSH 游戏中的两个玩家相隔超过一公里!
自测试:非局域游戏的应用
这暗示了非局域游戏的一个应用:如果我们能够与 Eve 一起玩并赢得一个非局域游戏,那么在这个过程中,我们必须已经构建了一些我们可以用来发送量子数据的东西。这种洞察力导致了一些被称为量子自测试的想法,其中我们让设备的一部分与其他部分玩非局域游戏,以确保设备正常工作。
本章中我们编写的模拟器为我们提供了观察那些实验如何工作的所有必要信息。现在我们可以继续使用量子力学和量子比特来做一些很酷的事情,同时我们知道量子力学确实是我们的宇宙运作的方式。
摘要
-
我们可以使用 QuTiP 包来帮助我们处理张量积和其他我们需要在 Python 中编写多量子比特模拟器的计算。
-
QuTiP 中的
Qobj类跟踪了我们想要模拟的状态和算符的许多有用属性。 -
我们和 Eve 可以使用量子策略来玩 CHSH 游戏,在我们开始玩游戏之前,我们共享一对纠缠量子比特。
-
通过将 CHSH 游戏编写成一个量子程序,我们可以证明使用纠缠量子比特对的玩家比只使用经典计算机的玩家赢的次数更多,这与我们对量子力学的理解是一致的。
6 量子数据传输与纠缠:量子数据的移动
本章涵盖
-
使用经典和量子控制移动量子计算机中的数据
-
使用布洛赫球可视化单量子比特操作
-
预测双量子比特操作和泡利操作的结果
在上一章中,我们借助 QuTiP 包为我们的量子设备模拟器添加了对多个量子比特的支持。这使得我们能够玩 CHSH 游戏,并表明我们对量子力学的理解与我们在现实世界中的观察一致。
在本章中,我们将看到如何在量子设备的不同人或寄存器之间移动数据。我们将探讨诸如不可克隆定理之类的理论如何影响我们在量子设备上管理数据的方式。我们还将检查量子设备可以执行的一种独特量子协议,称为量子传输,它移动数据(而不是复制)。
6.1 移动量子数据
就像在经典计算中一样,有时在量子计算机中,我们有一些数据在这里,我们非常希望它在那里。在经典计算中,通过复制数据可以轻松解决这个问题;但正如我们在第三章和第四章中看到的,不可克隆定理意味着在一般情况下,我们不能复制存储在量子比特中的数据。
经典地移动数据
在经典计算的一些部分,我们会遇到无法复制信息的问题,但原因不同。在多线程应用程序中复制数据可能会引入微妙的竞态条件,而性能考虑可能会促使我们减少复制的数据量。
许多经典语言(例如 C++11 和 Rust)采用的解决方案侧重于移动数据。从移动数据的角度思考对量子计算很有帮助,尽管我们将以非常不同的方式实现移动。
那么,如果我们想在量子设备中移动数据,我们能做什么呢?幸运的是,有几种不同的方法可以移动量子数据而不是复制它。在本章中,我们将看到这些方法中的几种,并将最后几个特性添加到我们的模拟器中以实现它们。让我们开始分享量子信息吧!
假设爱娃有一些量子比特,这些量子比特编码了她想要与我们分享的数据:
嘿玩家!我有一些量子信息想要与你分享。我能把它发给你吗?
在这里,爱娃指的是swap指令——它与之前我们看到的指令有一点不同,因为它一次操作两个量子比特。相比之下,我们之前看到的每个操作一次只操作一个量子比特。
观察swap操作的作用,其名称相当描述性:它实际上交换了同一寄存器中两个量子比特的状态。例如,假设我们有两个处于|01〉状态的量子比特。如果我们在这两个量子比特上使用swap指令,我们的寄存器现在将处于|10〉状态。让我们看看使用 QuTiP 内置的swap矩阵的一个例子。
列表 6.1 使用 QuTiP 的swap操作在|+0〉上得到|0+〉状态
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> ket_0 = qt.basis(2, 0)
>>> ket_plus = hadamard_transform() * ket_0
>>> initial_state = qt.tensor(ket_plus, ket_0)
>>> initial_state ❶
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[0.70710678] ❷
[0\. ]
[0.70710678]
[0\. ]]
>>> swap_matrix = qt.swap() ❸
>>> swap_matrix * initial_state ❹
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[0.70710678] ❺
[0.70710678]
[0\. ]
[0\. ]]
>>> qt.tensor(ket_0, ket_plus) ❻
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[0.70710678]
[0.70710678]
[0\. ]
[0\. ]]
❶ 使用 qt.basis、hadamard_transform 和 qt.tensor 定义一个变量,用于第五章中的老朋友:状态向量 |+0〉
❷ 正如我们在第四章中看到的,这个状态在 |00〉 和 |10〉 计算基态上有相等的振幅。
❸ 通过调用 qt.swap 获取交换指令的单位矩阵副本
❹ 与我们模拟单量子比特操作的方式相同,我们可以通过乘以交换操作的单位矩阵来找到在应用交换指令后我们的双量子比特寄存器的状态。
❺ 当我们这样做时,我们最终处于 |00〉 和 |01〉 之间的叠加态,而不是 |00〉 和 |10〉 之间的叠加态。
❻ 快速检查对从 |+0〉 状态开始的两个量子比特寄存器使用交换指令的结果是 |0+〉
查看列表 6.1,我们可以看到 swap 指令基本上做了它名字所暗示的事情。特别是,swap 将处于 |+0〉 状态的两个量子比特转换到了 |0+〉 状态。更普遍地说,我们可以通过查看我们用来模拟它的单位矩阵来了解 swap 指令做了什么。
列表 6.2 swap 指令的单位矩阵
>>> import qutip as qt
>>> qt.swap()
Quantum object: dims = [[2, 2], [2, 2]],
➥ shape = (4, 4), type = oper, isherm = True ❶
Qobj data =
[[1\. 0\. 0\. 0.] ❷
[0\. 0\. 1\. 0.] ❸
[0\. 1\. 0\. 0.]
[0\. 0\. 0\. 1.]] ❹
❶ 我们用来模拟交换指令的单位矩阵是一个 4 × 4 矩阵,因为它作用于双量子比特状态。
❷ 这个单位矩阵的每一列都告诉我们计算基态中的一个状态会发生什么;在这里,交换指令对处于 |00〉 状态的量子比特不做任何操作。
❸ 另一方面,交换指令交换了 |01〉 和 |10〉 状态。
❹ 交换指令也不会改变 |11〉 状态。
这个单位矩阵的每一列都告诉我们计算基态中的一个状态会发生什么。例如,第一列告诉我们 |00〉 状态被映射到向量 [[1], [0], [0], [0]],这是我们认识的 |00〉。
注意:swap 指令的单位矩阵不能写成任何两个单量子比特单位矩阵的张量积。也就是说,我们不能通过一次考虑一个量子比特来理解 swap 做了什么——我们需要弄清楚它对 swap 指令作用的两个量子比特对的态做了什么。
图 6.1 显示,我们可以看到 swap 指令在一般情况下会做什么,无论我们的两个量子比特最初处于什么状态。

图 6.1 双量子比特操作 swap 交换寄存器中两个量子比特的状态。我们可以在这里显示的通用示例中看到这一点,因为描述 |01〉 和 |10〉 状态的项被交换了。其他两个没有被交换,因为我们无法区分两个量子比特处于相同状态时的情况。
记得在第二章中,我们看到了单位矩阵非常类似于真值表。也就是说,像我们从 qt.swap 得到的单位矩阵这样的单位矩阵是有用的,因为它们帮助我们模拟 swap 指令做了什么。然而,就像经典加法器不是它的真值表一样,记住这些单位矩阵不是量子程序,而是我们用来模拟量子程序如何工作的工具是有帮助的。
练习 6.1:交换寄存器中的第二个和第三个量子比特
假设您有一个包含三个量子比特且处于|01+〉状态的寄存器。使用 QuTiP,写出这个状态,然后交换第二个和第三个量子比特,使您的寄存器处于|0+1〉状态。
提示:由于第一个量子比特不会发生任何变化,请确保使用单位矩阵和qt.swap的张量积来构建寄存器中正确的操作。
练习题解答
本书中的所有练习题解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入您所在章节的文件夹,然后打开名为练习题解答的 Jupyter 笔记本。
到目前为止,Eve 已经迫不及待地想要发送她的量子比特了。让我们添加我们需要的到我们的模拟器中,以便不再让她等待!
6.1.1 替换模拟器
我们在第四章中开发的模拟器只需要进行一些调整,就可以使用像swap这样的双量子比特操作。我们需要做的更改如下:
-
修改
_apply以支持双量子比特操作。 -
添加
swap和其他双量子比特指令。 -
添加剩余的单量子比特旋转指令。
正如我们在第四章中看到的,如果一个矩阵作用于单量子比特寄存器,我们可以使用 QuTiP 通过使用gate_expand_1toN函数将其应用于任意数量的量子比特寄存器。这个函数将除了我们正在处理的量子比特之外的所有量子比特的单位算符的张量积。
同样,我们可以调用 QuTiP 的gate_expand_2toN函数,将双量子比特单位矩阵转换为我们可以用来模拟像swap这样的双量子比特操作如何转换整个寄存器状态的矩阵。现在让我们将其添加到我们的模拟器中(列表 6.3)。
提示:我们对本章中的代码进行了一些小的修改,以帮助使打印的输出看起来更美观。这些更改以及本书和其他章节的所有示例,都可以在 GitHub 仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。
列表 6.3 simulator.py:应用双量子比特单位矩阵
def _apply(self, unitary: qt.Qobj, ids: List[int]):
if len(ids) == 1:
matrix = qt.circuit.gate_expand_1toN(unitary,
self.capacity, ids[0])
elif len(ids) == 2: ❶
matrix = qt.circuit.gate_expand_2toN(unitary, ❷
self.capacity, *ids)
else:
raise ValueError("Only one- or two-qubit unitary matrices supported.")
self.register_state = matrix * self.register_state
❶ 要模拟双量子比特操作,我们需要寄存器中量子比特的两个索引:一个用于我们的指令作用的每个量子比特。
❷ gate_expand_2toN的调用看起来与我们的gate_expand_1toN调用非常相似,只是我们传递的是一个 4×4 矩阵而不是 2×2 矩阵。
我们看到 QuTiP 提供了swap函数来为我们提供一个模拟swap指令的单位矩阵的副本。这可以使用我们对Simulator._apply所做的更改快速地将swap指令添加到我们的模拟器中。
列表 6.4 simulator.py:添加swap指令
def swap(self, target: Qubit) -> None:
self.parent._apply(
qt.swap(), ❶
[self.qubit_id, target.qubit_id] ❷
)
❶ 要获取 4×4 单位矩阵,我们需要传递给“apply”,我们只需使用本章前面几次看到的qt.swap函数。
❷ 我们需要确保传递我们想要交换的两个量子比特的索引,以便 gate_expand_2toN 能够正确地将我们新swap指令的单位矩阵应用于整个寄存器的状态。
当我们在模拟器上工作时,让我们添加一个额外的指令来更轻松地打印其状态,而无需访问其内部。
列表 6.5 simulator.py:添加dump指令
def dump(self) -> None:
print(self.register_state)
这样,我们可以要求模拟器帮助我们调试量子程序,但以一种可以在不支持它的设备(例如,实际量子硬件)上安全移除的方式。
小贴士:记住,量子比特不是一个状态。状态只是表示量子系统如何行为的便捷方式。
在实施这两个更改后,我们就准备好使用swap指令了。让我们用它来重复实验,在这个实验中,我们从|0+〉状态开始交换两个量子比特,将它们转换到|+0〉状态。
小贴士:一如既往,对于完整的示例文件,请参阅本书的 GitHub 仓库:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。
列表 6.6 在|0+〉状态下测试swap指令
>>> from simulator import Simulator
>>> sim = Simulator(capacity=2)
>>> with sim.using_register(n_qubits=2) as (you, eve): ❶
... eve.h()
... sim.dump()
... you.swap(eve)
... sim.dump()
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket ❷
Qobj data =
[[0.70710678]
[0.70710678]
[0\. ]
[0\. ]]
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket ❸
Qobj data =
[[0.70710678]
[0\. ]
[0.70710678]
[0\. ]]
❶ 由于在本章中我们将大量使用多量子比特寄存器,我们添加了一个新的便利方法,允许我们一次性分配多个量子比特。
❷ 第一个dump来自我们对 sim.dump 的第一个调用,并确认 eve.h()已将量子比特准备在|0+〉状态。
❸ 在调用 you.swap(eve)之后,我们的量子比特最终处于|+〉状态,而 Eve 的量子比特则以我们的量子比特开始的方式结束:处于|0〉状态。
太棒了:我们现在有了一种与 Eve 共享量子数据的方法!嗯,至少在我们共享单个量子设备的情况下是这样,这样我们才能同时应用swap指令到我们的量子比特上。
如果我们想在设备之间共享量子信息会发生什么?幸运的是,量子计算为我们提供了一种通过仅通信经典数据来发送量子比特的方法,只要我们双方在量子比特之间开始时有一些纠缠。像量子计算中的许多事物一样,这种技术有一个异想天开的名字:量子隐形传态。然而,不要被这个名字欺骗。当我们深入探讨时,隐形传态使用我们在第四章中学到的知识,以有用的方式共享量子数据。图 6.2 显示了量子隐形传态程序中的步骤列表。

图 6.2 量子隐形传态程序中的步骤。我们准备并纠缠一个量子比特寄存器,然后 Eve 可以准备并传态她的状态给我们。请注意,她将不会在传态后拥有该状态。
量子隐形传态真正酷的地方在于,虽然我们和 Eve 仍然需要在各自的量子比特之间进行一些双量子比特操作,但 Eve 可以在我们完成这些操作后决定她想要发送给我们的数据。这意味着我们可以在需要交换量子数据之前准备纠缠量子比特,然后按需使用它们。
使用我们在过去几章中开发的模拟器,我们可能会编写一个量子程序,如下所示:
列表 6.7 Python 中的量子传输程序
def teleport(msg : Qubit, here : Qubit, there : Qubit) -> None:
here.h()
here.cnot(there)
msg.cnot(here)
msg.h()
if msg.measure(): there.z()
if here.measure(): there.x()
msg.reset()
here.reset()
虽然这个程序中有一些新的指令。在本章的其余部分,我们将看到运行量子传输所需的其余组件。
6.1.2 还有哪些其他双量子比特门?
如你所猜,swap 并不是唯一的双量子比特操作。实际上,正如列表 6.7 所示,为了使量子传输工作,我们需要在模拟器中添加另一个名为 cnot 的双量子比特指令。cnot 指令执行的功能与 swap 类似,但它交换的是 |10〉 和 |11〉 计算基态,而不是 |01〉 和 |10〉 状态。另一种思考方式是,cnot 会根据第一个量子比特处于 |1〉 状态来 控制 第二个量子比特。这个名字 cnot 的由来就是:它是“受控非”的缩写。
小贴士:我们通常将传递给 cnot 指令的第一个量子比特称为 控制量子比特,第二个量子比特称为 目标量子比特。然而,正如我们将在第七章中看到的,这些名称有一些微妙之处。
让我们直接看看 cnot 指令是如何工作的,通过将其应用于那个可爱的例子,即 |+0〉 状态:
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> ket_0 = qt.basis(2, 0)
>>> ket_plus = hadamard_transform() * ket_0
>>> initial_state = qt.tensor(ket_plus, ket_0) ❶
>>> qt.cnot() * initial_state ❷
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket ❸
Qobj data =
[[0.70710678]
[0\. ]
[0\. ]
[0.70710678]]
>>> qt.cnot()
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper, isherm = True ❹
Qobj data =
[[1\. 0\. 0\. 0.]
[0\. 1\. 0\. 0.]
[0\. 0\. 0\. 1.]
[0\. 0\. 1\. 0.]]
❶ 初始化两个量子比特,使它们从 |+0〉 = (|00〉 + |10〉) / √2 状态开始
❷ QuTiP 提供了 cnot 指令的单位矩阵作为 qt.cnot 函数。
❸ cnot 指令将我们的量子比特留在 (|00〉 + |11〉) / √2 状态。
❹ cnot 的矩阵将 |10〉 计算基态映射到 |11〉,反之亦然,正如我们所期望的那样。
注意:cnot 指令与经典编程语言中的 if 语句不同,因为 cnot 指令保留了叠加态。如果我们想使用 if 语句,我们必须测量控制量子比特,这将导致控制量子比特上的任何叠加态坍缩。实际上,在本书末尾编写我们的量子传输程序时,我们将使用 cnot 指令和基于测量结果的 if 语句——两者都很有用!在第八章和第九章中,我们将看到控制操作与 if 语句的不同之处。
图 6.3 展示了 cnot 指令在一般情况下对双量子比特状态的作用。然而,现在我们认识到,通过在 |0〉 状态的两个量子比特上应用 cnot 而得到的输出状态,是我们第四章中玩 CHSH 游戏所需的 entangled 状态,即 (|00〉+|11〉)/ √2。

图 6.3 双量子比特操作 cnot 在控制量子比特的状态下执行 not 操作。
这意味着我们拥有了编写量子程序所需的一切,该程序可以将处于 |00〉 状态的两个量子比特纠缠在一起。我们所需做的只是将 cnot 指令添加到我们的模拟器中,就像我们之前添加 swap 一样。
列表 6.8 simulator.py:添加cnot指令
def cnot(self, target: Qubit) -> None:
self.parent._apply(
qt.cnot(),
[self.qubit_id, target.qubit_id]
)
现在我们可以编写一个程序来准备一对纠缠的量子比特:
>>> from simulator import Simulator
>>> sim = Simulator(capacity=2)
>>> with sim.using_register(2) as (you, eve):
... eve.h()
... eve.cnot(you)
... sim.dump()
...
Quantum object: dims = [[2, 2], [1, 1]],
➥ shape = (4, 1), type = ket
Qobj data =
[[0.70710678]
[0\. ]
[0\. ]
[0.70710678]]
在这一点上,暂停片刻(抱歉,伊芙!)并反思我们刚刚所做的是很有帮助的。在第四章中,当我们模拟与伊芙玩 CHSH 游戏时,我们必须“作弊”,假设我们和伊芙可以访问两个在纠缠态(|00〉 + |11〉)/ √2 下神奇地开始的量子比特。但现在,我们看到我们和伊芙如何通过在玩 CHSH 之前运行另一个量子程序来准备这种纠缠。h指令准备我们需要的叠加态,而新的cnot指令使我们能够与伊芙准备纠缠。这种纠缠“共享”了我们的两个量子比特之间的叠加态。(毕竟,分享就是关爱。)
就像我们在第四章中为 CHSH 游戏做准备与伊芙之间的纠缠一样,这是伊芙将她的量子数据传输到我们的第一步。这使得cnot指令在未来的操作中变得非常重要。
然而,回到伊芙,她将数据传输到我们的下一步是使用四种不同的单个量子比特操作来解码她发送给我们的量子数据(回想一下图 6.2)。让我们看看下一个。
6.2 所有单个(量子比特)旋转
我们需要编程量子传输的最后一件事是根据伊芙发送给我们的某些经典数据进行校正。为此,我们需要一些新的单个量子比特指令。为此,回顾我们用来描绘量子指令为旋转的图片是有帮助的,因为我们可能有点“作弊”。到目前为止,我们一直将量子比特描绘为圆上的任何位置,但在现实中,我们的量子比特模型缺少一个维度。单个量子比特的状态由球面上的任何点表示,通常称为布洛赫球。
单个量子比特仅限!
这种(以及之前的)可视化量子比特状态的方法仅在量子比特未与其他量子比特纠缠时才有效。另一种说法是,我们无法轻易地可视化多量子比特状态。甚至尝试可视化具有纠缠的两个量子比特寄存器的状态将涉及在七维空间中绘制图片。虽然 7D 可能适合尼亚加拉大瀑布的游乐广告,但用这种方式绘制有用的图片要困难得多。
我们所熟悉的圆实际上是球的一个切片,而我们所做的所有旋转都导致状态仍然在那个圆上。图 6.4 显示了前一个量子比特模型与布洛赫球的比较。

图 6.4 比较了我们的前一个量子比特模型(圆上的一个点)和布洛赫球。布洛赫球是单个量子比特状态的更一般模型。我们需要另一个维度来捕捉由复数向量表示的量子比特状态,但它仅适用于单个量子比特。
提示:你可能已经从我们展示了Z-轴和X-轴的事实中推断出Y-轴可能隐藏在某个地方!实际上,当我们从圆移动到球体时,从页面中伸出的轴通常被称为“Y-轴。”
当我们在第二章首次介绍量子比特状态的矢量表示时,你可能还记得每个向量中的振幅是复数。在本章的其余部分,我们将看到,当我们使用旋转指令来转换单个量子比特的状态时,我们通常会得到复数。复数是跟踪旋转的极其有用的工具,因此在量子计算中起着重要作用。主要来说,它们帮助我们理解不同量子状态之间的角度和相位。如果你对复数有些生疏,不要担心,因为在本书的其余部分,你将有很多机会练习它们。
6.2.1 将旋转与坐标相关联:泡利算符
在图 6.5 中,让我们花一点时间快速回顾一下我们迄今为止看到的几个单量子比特操作:x和ry。

图 6.5 x和ry对量子比特的影响示意图。我们已经在之前的章节中看到了这两个操作。ry指令通过角度 theta 旋转我们的量子比特状态,而x将|0〉状态的量子比特转换到|1〉状态,反之亦然。
现在我们知道我们的量子比特状态可以在球面上旋转,那么还有哪些旋转可以帮助我们将状态旋转出平面呢?我们可以在|+〉和|−〉状态之间的线上添加一个旋转。这条线通常被称为X-轴,以区别于连接|0〉和|1〉状态的Z-轴。QuTiP 中的rx函数为我们提供了一个封装了X-轴旋转矩阵的Qobj。
列表 6.9 使用 QuTiP 内置函数qt.sigmaz
>>> import qutip as qt
>>> import numpy as np
>>> qt.rx(np.pi).tidyup() ❶
Quantum object: dims = [[2], [2]], shape = (2, 2),
➥ type = oper, isherm = False
Qobj data =
[[ 0.+0.j 0.-1.j] ❷
[ 0.-1.j 0.+0.j]]
❶ QuTiP Qobj 实例有一个 tidyup 方法,可以帮助使矩阵更易于阅读,因为经典计算机上的浮点运算可能会导致小的误差。
❷ 在系数–i(在 Python 中表示为–1j)的范围内,围绕X-轴旋转 180°会导致我们在第二章首次看到的x(NOT)指令。
提示:在 Python 中,复数i表示为1.0j:1 乘以j,这在其他领域有时被称为虚数i。
这个片段说明了非常重要的一点:x操作正是我们通过围绕X-轴旋转 180°(π)所得到的结果。
定义:正如列表 6.1 的说明中提到的,我们可以检查qt.rx(np.pi)实际上比qt.sigmax()少了一个–i因子。这个因子是一个全局相位的例子。正如我们很快就会看到的,全局相位不能影响测量的结果。因此,qt.rx(np.pi)和qt.sigmax()是不同的幺正矩阵,它们表示相同的操作。在第七章和第八章中,我们将更多地练习全局和局部相位。
通过类比,我们将绕 Z-轴旋转 180°称为 z 操作。在第三章中,QuTiP 提供了 qt.sigmax 函数来模拟 x 指令。同样,qt.sigmaz 提供了我们需要来模拟 z 指令的单位矩阵。下面的列表展示了使用 qt.sigmaz 的一个示例。请注意,我们立即通过乘以 i 来包括系数(即全局相位);这之所以有效,是因为 –i × i = –(–1) = 1。
列表 6.10 使用 QuTiP 函数 qt.rz 和 qt.sigmaz
>>> import qutip as qt
>>> import numpy as np
>>> 1j * qt.rz(np.pi).tidyup() ❶
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
Qobj data =
[[ 1\. 0.]
[ 0\. -1.]]
>>> qt.sigmaz() ❷
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
Qobj data =
[[ 1\. 0.]
[ 0\. -1.]]
❶ 以这种方式消除全局相位使得读取输出更加容易。
❷ 如承诺的那样,直到系数为 –i,z 指令在 Z-轴上应用了 180° 旋转。
与 X 操作在 |0〉 和 |1〉 之间翻转,同时保持 |+〉 和 |−〉 不变的方式相同,z 操作在 |+〉 和 |−〉 之间翻转,同时保持 |0〉 和 |1〉 不变。
表 6.1 显示了一个类似于我们在第二章中为哈达玛操作(Hadamard operation)制作的真值表。通过查看真值表,我们可以确认,如果我们对任何输入状态执行两次 z 操作,我们将回到起点。换句话说,z 的平方等于恒等操作 𝟙,就像 X² = 𝟙 一样。
表 6.1 将 z 指令表示为表格
| 输入状态 | 输出状态 |
|---|---|
| |0〉 | |0〉 |
| |1〉 | –|1〉 |
| |+〉 | |−〉 |
| |−〉 | |+〉 |
注意 我们在表 6.1 中列出了四行,但我们只需要两行就可以完全指定 Z 对任何输入的作用。其他两行是为了强调我们可以选择通过 Z 在 |0〉 和 |1〉 上的作用来定义 Z,或者通过 Z 在 |+〉 和 |−〉 上的作用来定义 Z。
练习 6.2:练习使用 rz 和 z
假设你准备了一个处于 |−〉 状态的量子比特,并应用了一个 z 旋转。如果你沿着 X-轴进行测量,你会得到什么?如果你应用两个 z 旋转,你会测量到什么?如果你用 rz 实现相同的两个旋转,你应该使用什么角度?
我们可以用相同的方式定义一个额外的旋转:绕着从页面“出来”的轴旋转。这个轴连接着状态 (|0〉 + i|1〉) / √2 = R[x] (π/2)|0〉 和 (|0〉 − i |1〉) / √2 = R[x] (π/2)|1〉,并且传统上被称为 Y-轴。绕 Y-轴旋转 180°会翻转比特标签(|0〉
|1〉)和相位(|+〉
|−〉),但会保留沿 Y-轴的两个状态。
练习 6.3:sigmay 的真值表
使用 qt.sigmay() 函数制作一个类似于表 6.1 的表格,但用于 y 指令。
定义 一起,代表 x、y 和 z 操作的三个矩阵 X、Y 和 Z 被称为保罗矩阵(Pauli matrices),以纪念物理学家沃尔夫冈·泡利(Wolfgang Pauli)。单位矩阵 𝟙 有时也被包括在内,代表“不做任何事情”或恒等操作。
与保罗矩阵玩剪刀石头布
泊松矩阵具有许多有用的性质,我们将在本书的其余部分使用这些性质。许多这些性质使得使用泡利算符的不同方程变得容易。例如,如果我们乘以X和Y,我们得到iZ,但如果我们乘以YX,我们得到–iZ*。
QuTiP 可以帮助探索当我们相乘泡利矩阵时会发生什么:例如,以两种可能的顺序将X和Y相乘:
>>> import qutip as qt
>>> qt.sigmax() * qt.sigmay()
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = False
Qobj data =
[[0.+1.j 0.+0.j]
[0.+0.j 0.-1.j]]
>>> qt.sigmay() * qt.sigmax()
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = False
Qobj data =
[[0.-1.j 0.+0.j]
[0.+0.j 0.+1.j]]
同样,YZ = iX和ZX = iY,但ZY = –iX和XZ* = –iY。一种记住这个的方法是将X、Y和Z想象成在玩一个小游戏:X“打败”Y,Y“打败”Z,然后Z“打败”X*。
我们可以将这些矩阵视为为量子比特态建立一种坐标系统,称为布洛赫球面。如图 6.6 所示,X轴和Z轴形成了我们在书中迄今为止看到的圆,而Y轴则从页面上伸出。

图 6.6 布洛赫球面,展示其球面之美。在这里,每个轴都标有表示围绕该轴旋转的泡利算符。
使用泡利测量描述态
任何单个量子比特态都可以通过X、Y和Z测量的测量概率完全指定,直到全局相位。也就是说,如果我们告诉你我们能够执行的三种泡利测量中每种得到“1”结果的概率,你可以使用这些信息来编写一个与我们完全相同(除了全局相位)的状态向量。这使得将三维空间中的点与单个量子比特态的类比对于思考单个量子比特态是有用的。
i有优势
Y 轴末端的态通常标记为|i 〉和|−i 〉,但它们通常不单独使用。我们将继续使用之前标记的状态:|0〉、|1〉、|+〉和|−〉。
在这个图景下,更容易理解为什么某些旋转不会影响测量的结果。例如,如图 6.7 所示,布洛赫球面图帮助我们理解如果我们围绕Z轴旋转|0〉会发生什么。

图 6.7 布洛赫球面展示了rz旋转如何使|0〉态保持不变
就像北极无论我们如何旋转地球都保持在同一位置一样,如果我们围绕与态平行的轴旋转态,那么我们的量子比特上不会有可观察到的效果。我们也可以从数学中看到这种效果。
列表 6.11 |0〉态不受rz旋转的影响
>>> ket0 = qt.basis(2, 0) ❶
>>> ket_psi = qt.rz(np.pi / 3) * ket0 ❷
>>> ket_psi ❸
Quantum object: dims = [[2], [1]], shape = (2, 1),
➥ type = ket
Qobj data =
[[ 0.8660254-0.5j]
[ 0.0000000+0.j ]]
>>> bra0 = ket0.dag() ❹
>>> bra0
Quantum object: dims = [[1], [2]], shape = (1, 2),
➥ type = bra
Qobj data = ❺
[[ 1\. 0.]]
>>> np.abs((bra0 * ket_psi)[0, 0]) ** 2 ❻
1.0
❶ 定义一个变量来表示态|0〉
❷ 引入一个新的态|Ψ〉,它是围绕 Z 轴旋转 60°(以弧度计为π / 3)的|0〉态。
❸ 结果态是|Ψ〉 = [cos(60° / 2) − i sin(60° / 2)] |0〉 = [√3 / 2 − i / 2] |0〉。
❹ 记住,在 QuTiP 中,我们通过调用 Qobj 实例的.dag 方法来写出“共轭”算符⁺,在这种情况下,它给出了〈0〉的行向量。
❺ 通过取内积 〈0|Ψ〉,我们可以计算 Born 规则 Pr(0|Ψ) = |〈0|Ψ〉|²。请注意,我们需要用 [0, 0] 来索引,因为 QuTiP 将 |0〉 与 |Ψ〉 的内积表示为一个 1 × 1 矩阵。
❻ 如前所述,沿 Z 轴测量的“0”观察概率没有改变。
小贴士:在写下状态时,|Ψ〉 常常被用作一个任意名称,类似于在代数中 x 常常被用来表示一个任意变量。
练习 6.4:验证应用 rz 不改变 |0〉
我们只检查了一个测量概率仍然相同,但也许 x 或 y 测量的概率已经改变。为了完全检查全局相位不会改变任何东西,准备与列表 6.11 中相同的状态和旋转,并检查应用 rz 指令后沿 X**- 或 Y-轴测量状态的概率是否改变。
通常,我们可以总是乘以一个绝对值为 1 的复数,而不会改变任何测量的概率。任何复数 z = a + bi 都可以写成 z = re^(iθ) 的形式,其中 r 和 θ 是实数,r 是 z 的绝对值,θ 是一个角度。当 r = 1 时,我们有一个形式为 e^(iθ) 的数,我们称之为 相位。然后我们说,乘以一个相位对该状态应用了一个 全局相位。
注意:任何全局相位都无法通过任何测量检测到。
状态 |Ψ〉 和 e^(iθ) |Ψ〉 以任何可以想象的方式都是描述完全相同状态的两种不同方法。我们甚至无法进行任何测量(即使是原则上)来了解全局相位。另一方面,我们已经看到我们可以区分像 |+〉 = (|0〉 + |1〉) / √2 和 |−〉 = (|0〉 − |1〉) / √2 这样的状态,它们仅在 |1〉 计算基态的 局部 相位上有所不同。
回顾一下,让我们总结一下到目前为止我们关于 x、y 和 z 指令以及我们用来模拟这些指令的泡利矩阵所了解到的内容。我们已经看到,x 指令在 |0〉 和 |1〉 之间翻转,而 z 指令在 |+〉 和 |−〉 状态之间翻转。换句话说,x 指令翻转比特,而 z 指令翻转相位。
观察布洛赫球,我们可以看到绕 Y-轴旋转应该做这两件事。我们也可以通过 Y = –iXZ 来看到这一点,因为使用 QuTiP 检查这一点很简单。我们在表 6.2 中总结了每个泡利指令的作用。
表 6.2 泡利矩阵作为比特和相位翻转
| 指令 | 泡利矩阵 | 翻转比特 (|0?〉 |1〉) |
翻转相位 (|+〉 |−〉)? |
|---|---|---|---|
| (无指令) | 𝟙 | 否 | 否 |
x |
X | 是 | 否 |
y |
Y | 是 | 是 |
z |
Z | 否 | 是 |
练习 6.5:嗯,这让我想起了什么
我们已经多次看到的(|00〉 + |11〉)/ √2 状态并不是纠缠态的唯一例子。实际上,如果我们随机选择一个两量子比特状态,它几乎肯定是一个纠缠态。就像计算基{|00〉、|01〉、|10〉、|11〉}是一个特别有用的非纠缠态集合一样,还有一个由物理学家约翰·斯图尔特·贝尔命名的四个特定纠缠态集合,称为贝尔基:
| 名称 | 计算基展开 |
|---|---|
| |β[00]〉 | (|00〉 + |11〉) / √2 |
| |β[01]〉 | (|00〉 – |11〉) / √2 |
| |β[10]〉 | (|01〉 + |10〉) / √2 |
| |β[11]〉 | (|01〉 – |10〉) / √2 |
使用你关于cnot指令和 Pauli 指令(x、y和z)的知识,编写程序来准备表中的每个四个贝尔态。
提示:表 6.2 在这个练习中应该非常有帮助。
我们通过向我们的Qubit接口和模拟器添加x、y和z操作的指令来完成对单量子比特操作的讨论。为此,我们可以使用相应的 QuTiP 函数qt.rx、qt.ry和qt.rz来实现旋转指令rx、ry和rz,以获取模拟每个指令所需的单位矩阵的副本。以下是我们在模拟器中如何实现这一点的示例。
列表 6.12 simulator.py:添加所有 Pauli 旋转
def rx(self, theta: float) -> None:
self.parent._apply(qt.rx(theta), [self.qubit_id])
def ry(self, theta: float) -> None:
self.parent._apply(qt.ry(theta), [self.qubit_id])
def rz(self, theta: float) -> None:
self.parent._apply(qt.rz(theta), [self.qubit_id])
def x(self) -> None:
self.parent._apply(qt.sigmax(), [self.qubit_id])
def y(self) -> None:
self.parent._apply(qt.sigmay(), [self.qubit_id])
def z(self) -> None:
self.parent._apply(qt.sigmaz(), [self.qubit_id])
QuTiP 使用σ[x]而不是X来表示 Pauli 矩阵。使用这种表示法,函数sigmax()返回一个表示 Pauli 矩阵X的新Qobj。这样,我们可以实现对应于每个 Pauli 矩阵的x、y和z指令。
没有人能告诉你什么是矩阵;你必须亲自去体验
我们在本书的第一部分已经讨论了很多关于矩阵的内容。很多。人们可能会说量子编程就是关于矩阵的,而量子比特实际上只是向量。然而,实际上,矩阵是我们模拟量子设备所做事情的方式。在第二部分中,我们将看到更多,但量子程序并不操作矩阵和向量——它们操作经典数据,例如要发送到量子设备的指令以及如何处理从设备返回的数据。例如,如果我们在一个设备上运行一个指令,没有简单的方法可以看到我们应该使用哪个矩阵来模拟该指令——相反,我们必须使用称为过程全息术的技术从多次重复测量中重建该矩阵。
当我们编写矩阵时,无论是在代码中还是在纸上,我们都在隐式地模拟一个量子系统。如果这让你感到困惑,不要担心;随着你阅读本书的其余部分,这将会更加清晰。
6.3 量子态传输
好的,现在我们拥有了编写量子程序来描述量子态传输所需的一切。作为一个快速回顾,图 6.8 展示了我们希望这个程序执行的操作。

图 6.8 回顾量子态传输程序的步骤。
我们将假设你们可以在它们位于同一设备上时准备一些纠缠量子比特,并且我们和爱娃有一种我们可以用来发出正确校正信号的经典通信方式。现在我们可以使用我们在本章中添加到我们的模拟器中的功能来实现量子传输程序。
列表 6.13 teleport.py:几行代码实现量子传输
from interface import QuantumDevice, Qubit
from simulator import Simulator
def teleport(msg: Qubit, here: Qubit, there: Qubit) -> None: ❶
here.h() ❷
here.cnot(there) ❸
# ... ❹
msg.cnot(here) ❺
msg.h()
if msg.measure(): there.z() ❻
if here.measure(): there.x()
msg.reset() ❼
here.reset()
❶ 传输函数接受两个量子比特作为输入:我们想要移动的量子比特(msg)以及我们想要它移动到的位置(“那里”)。我们还需要一个临时量子比特,我们称之为“这里”。我们按照惯例假设“这里”和“那里”都从|0〉状态开始。
❷ 我们需要从“这里”和“那里”之间开始一些纠缠。我们可以使用我们的老朋友,h指令,以及我们的新朋友,cnot指令。
❸ 这个程序中唯一需要同时作用于“这里”和“那里”的指令。运行这个指令后,我们可以将我们的量子比特发送给爱娃,然后我们两个人都可以只用经典通信来运行程序的其余部分。
❹ 在程序的这个阶段,“这里”和“那里”处于(|00〉 + |11〉)/ √2 状态,这是我们第一次在第四章中看到的。
❺ 运行我们用来准备(|00〉 + |11〉)/ √2 状态的程序的反向,但是在我们设备上完全存在的 msg 和“这里”量子比特上。我们可以把运行准备的反向看作是一种测量,这样这些步骤就为我们设置好了,以便在一个纠缠基下测量我们试图发送给爱娃的量子消息。
❻ 当我们实际进行这个测量时,我们会得到可以发送给爱娃的经典数据。一旦她有了这些数据,她就可以使用x和z指令来解码量子消息。
❷ 现在我们已经处理完我们的量子比特,把它们放回|0〉状态是个好主意,这样它们就可以再次使用了。然而,这不会影响“那里”的状态,因为我们只是重置了我们的量子比特,而不是我们给爱娃的那个!
c 我们在那里做了什么?
假设我们不需要将经典测量结果作为量子传输的一部分发送给爱娃。在这种情况下,我们可以使用量子传输以超过光速的速度发送经典和量子数据。正如我们在第五章中玩 CHSH 游戏时不能与爱娃通信一样,光速意味着我们需要以经典方式与爱娃通信,以使用纠缠来发送量子数据。在这两种情况下,纠缠可以帮助我们通信,但它并不让我们单独通信:我们始终需要其他类型的通信。
为了看到这实际上是如何工作的,我们可以在我们的量子比特上准备一些东西,并将其发送给爱娃,然后她可以在她的量子比特上撤销我们的准备。到目前为止,我们和爱娃发送的消息都是经典的,但在这里,消息是量子的。我们可以并且会测量量子消息以获得一个经典比特,但我们也可以像使用其他任何量子数据一样使用我们从爱娃那里得到的量子消息。例如,我们可以应用我们喜欢的任何旋转和其他指令。
这有什么好处?
发送量子数据可能看起来并不比发送经典数据更有用;毕竟,发送经典数据已经让我们得到了很多有趣的东西。相比之下,发送量子数据的应用目前似乎更具有针对性。
话虽如此,移动量子数据是一个非常有用的例子,可以帮助我们理解量子计算机是如何工作的。本章中开发的思想并不总是直接有用,但将有助于我们未来构建伟大事物。
假设我们通过使用操作msg.ry(0.123)来准备一个量子消息。以下列表显示了我们可以如何将此消息传送给 Eve。
列表 6.14 teleport.py:使用量子隐形传态移动量子数据
if __name__ == "__main__":
sim = Simulator(capacity=3)
with sim.using_register(3) as (msg, here, there): ❶
msg.ry(0.123) ❷
teleport(msg, here, there) ❸
there.ry(-0.123) ❹
sim.dump()
❶ 如前所述,分配一个量子比特寄存器,并为每个量子比特命名
❷ 准备一个要发送给 Eve 的消息。在这里,我们使用特定的角度作为消息,但它可以是任何东西。
❸ 调用我们之前编写的量子隐形传态程序,将我们准备的消息移动到 Eve 的量子比特上
❹ 检查dump指令的输出,以确认我们分配的寄存器已回到|000〉状态
如果 Eve 随后通过旋转相反的角度撤销我们的旋转,我们可以检查我们分配的寄存器是否回到了|000〉状态。这表明我们的量子隐形传态是有效的!当你运行这个程序时,你会得到类似于以下输出的结果:
Quantum object: dims = [[2, 2, 2], [1, 1, 1]],
➥ shape = (8, 1), type = ket
Qobj data =
[[1.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]]
注意 你的输出可能因全局相位不同而有所不同,具体取决于你得到了哪些测量结果。
为了验证量子隐形传态是否成功,如果 Eve 撤销我们在她的量子比特上执行的指令(there.ry(0.123)),她应该得到我们开始时的|0〉状态。通过量子隐形传态,我们可以使用纠缠和经典通信来发送量子信息,正如你在练习 6.6 中可以证明的那样。
练习 6.6:如果它不起作用怎么办?
尝试更改你的操作或 Eve 的操作,以确信只有当你撤销 Eve 对其量子比特应用的操作时,你才能在最后得到|000〉状态。
现在,我们可以向所有朋友吹嘘(做出我们想要的任何科幻参考)我们能够进行量子隐形传态。希望你能理解这不是从轨道上被传送到一个星球上,当你进行量子隐形传态时,信息并没有以超过光速的速度传递。
摘要
-
量子不可克隆定理阻止我们复制量子寄存器存储的任意数据,但我们可以通过
swap操作和像量子隐形传态这样的算法来移动数据。 -
当一个量子比特没有与任何其他量子比特纠缠时,我们可以将其状态可视化为球面上的一个点,称为布洛赫球。
-
通常,我们可以围绕x、y或Z轴旋转单个量子比特的状态。围绕这些轴旋转 180°的操作称为泡利操作,描述了翻转一个或两个比特及其相位。
-
在量子传输中,纠缠与泡利旋转一起使用,将量子数据从一比特传输到另一比特,而不复制它。
第一部分:结论
我们已经到达了本书第一部分的结尾,但遗憾的是,我们的量子比特在另一个城堡!
。通过这一部分并不容易,因为我们一边学习大量新的量子概念,一边构建量子设备的模拟器。你可能对某些问题或主题感到有些不稳定,这是正常的。
提示附录 B 包含一些快速参考(术语表和定义),在你继续阅读本书并从事后续的量子发展工作时可能会很有帮助。
我们将使用并练习这些技能来开发更复杂的量子程序,用于酷炫的应用,如化学和密码学。在我们继续前进之前,先给自己鼓掌:你已经做得很多了!让我们总结一下你取得的成就:
-
温习了线性代数和复数技能
-
学习了量子比特是什么以及你可以用它做什么
-
使用 Python 构建了一个多比特模拟器
-
编写了量子程序,用于量子密钥分发(QKD)、玩非局域游戏,甚至量子传输
-
学习了量子系统状态的 braket 符号
你的 Python 模拟器将继续是尝试理解我们在更大应用中发生的事情的有用工具。在第二部分,我们将主要使用 Q#作为编写量子程序的首选工具。我们将在下一章讨论为什么我们将更多地使用 Q#而不是 Python 来编写更高级的量子程序,但主要原因还是速度和可扩展性。此外,你可以从 Python 或使用 Q#内核与 Jupyter 一起使用 Q#,这样你就可以使用你最喜爱的开发环境进行工作。
在第二部分再见!
第二部分:在 Q#中编程量子算法
当作为开发者工作时,拥有适合正确工作的工具非常有帮助——量子计算也不例外。量子计算软件堆栈通常非常多样化。在第二部分,我们将我们的堆栈扩展到包括 Q#,这是一种用于与量子设备一起工作的特定领域编程语言。正如特定领域语言可以帮助处理像 GPU 和 FPGA 这样的专用经典硬件一样,Q#通过提供正确的工具来实现和应用量子算法,帮助我们最大限度地利用量子计算。与其他专用语言一样,当与 Python 和.NET 等更通用语言和平台协同工作时,Q#表现得非常好。
考虑到所有这些,我们开始 Q-锐化我们的技能,学习编写量子算法的基础。在第七章中,我们回顾量子随机数生成器,以学习 Q#语言的 basics,这是基于我们在第二章中开发的技能。接下来,在第八章中,我们通过学习相位回弹、预言机和其他新技巧来扩展我们的量子编程技术工具箱,并使用它们来玩一些有趣的新游戏。最后,在第九章中,我们学习相位估计技术,为我们在第三部分开始解决更实际问题时所需的技能打下基础。
7 改变概率:Q#简介
本章涵盖
-
使用量子开发工具包用 Q#编写量子程序
-
使用 Jupyter Notebook 与 Q#一起工作
-
使用经典模拟器运行 Q#程序
到目前为止,我们使用 Python 来实现自己的软件栈来模拟量子程序。如果你还记得,图 2.1(再次出现作为图 7.1),是一个很好的模型,说明了我们编写的程序如何与量子模拟器和作为量子开发者使用和构建的设备交互。

图 7.1 我们如何使用量子计算机的心理模型。图的上半部分是量子计算机的一般模型。鉴于我们在这本书中使用本地模拟器,下半部分代表我们正在构建和使用的部分。
在接下来的内容中,我们将编写更复杂的量子程序,这些程序将受益于通过在 Python 中嵌入我们的软件栈难以实现的专业语言特性。特别是当我们探索量子算法时,拥有一种专门为量子编程量身定制的语言是非常有帮助的。在本章中,我们将开始使用 Q#,这是量子开发工具包中包含的微软为量子编程定制的领域特定语言。
7.1 介绍量子开发工具包
量子开发工具包提供了一种新的语言 Q#,用于编写量子程序并使用经典资源进行模拟。用 Q#编写的量子程序通过将量子设备视为一种加速器来运行,类似于我们可能在图形卡上运行代码的方式。
提示:如果你曾经使用过 CUDA 或 OpenCL 这样的图形卡编程框架,这个模型非常相似。
让我们来看看图 7.2 中的 Q#软件栈。

图 7.2 在经典计算机上的 Microsoft 量子开发工具包软件栈。我们可以编写一个由函数和操作组成的 Q#程序,引用我们想要包含的任何 Q#库。然后,宿主程序可以协调我们的 Q#程序与目标机器(例如,在我们的计算机上本地运行的模拟器)之间的通信。
一个 Q#程序由操作和函数组成,这些操作和函数指导量子和经典硬件执行某些操作。Q#还提供了许多库,其中包含许多有用的预定义操作和函数,可以在我们的程序中使用。
一旦编写了 Q#程序,我们需要一种方法让它将指令传递给硬件。一个经典程序有时被称为驱动程序或宿主程序,它负责分配目标机器并在该机器上运行 Q#操作。
量子开发工具包包括一个名为 IQ#的 Jupyter Notebook 插件,它通过自动提供宿主程序来简化 Q#的入门。在第九章中,我们将看到如何使用 Python 编写宿主程序,但在此阶段,我们将专注于 Q#。有关设置 Q#环境以与 Jupyter Notebook 一起使用的说明,请参阅附录 A。
使用 Jupyter Notebook 的 IQ#插件,我们可以使用两种不同的目标机器来运行 Q#代码。第一个是QuantumSimulator目标机器,它与我们所开发的 Python 模拟器非常相似。它将比我们的模拟量子比特的 Python 代码快得多。
第二个是ResourcesEstimator目标机器,它将允许我们估计运行它需要多少量子比特和量子指令,而无需完全模拟它。这在我们查看本书后面的较大 Q#程序时,将特别有用,因为它可以帮助我们了解运行 Q#程序所需资源的情况。
为了了解一切是如何工作的,让我们先写一个纯经典的 Q#“hello, world”应用程序。首先,通过在终端中运行以下命令来启动 Jupyter Notebook:
jupyter notebook
这将在我们的浏览器中自动打开一个新标签页,显示我们的 Jupyter Notebook 会话的主页。从“新建”↓菜单中选择 Q#来创建一个新的 Q#笔记本。将以下内容输入笔记本的第一个空单元格中,然后按 Ctrl-Enter 或⌘-Enter 来运行:
function HelloWorld() : Unit { ❶
Message("Hello, classical world!"); ❷
}
❶ 定义了一个新的函数,该函数不接受任何参数,并返回空元组,其类型写为 Unit。
❷ 告诉目标机器收集诊断信息。QuantumSimulator 目标机器将所有诊断信息打印到屏幕上,因此我们可以使用类似于 Python 中的 print 的 Message。
你应该得到一个类似于图 7.3 的图形。

图 7.3 使用 IQ#和 Jupyter Notebook 入门。在这里,一个名为HelloWorld的新 Q#函数被定义为 Jupyter Notebook 的第一个单元格,该函数的模拟结果在第二个单元格中。
提示:与 Python 不同,Q#使用分号而不是换行符来结束语句。如果你遇到了很多编译错误,请确保你记得你的分号。
你应该得到一个响应,表明HelloWorld函数已成功编译。要运行新函数,我们可以在新的单元格中使用%simulate命令。
列表 7.1 在 Jupyter 中使用%simulate魔法命令
In [2]: %simulate HelloWorld
Hello, classical world!
一点经典的魔法
%simulate命令是一个魔法命令的例子,因为它实际上不是 Q#的一部分,而是对 Jupyter Notebook 环境的指令。如果你熟悉 Jupyter 的 IPython 插件,你可能已经使用过类似的魔法命令来告诉 Jupyter 如何处理 Python 绘图功能。
在这本书中,我们使用的所有魔法命令都以%开头,以便于与 Q#代码区分开来。
在这个例子中,%simulate为我们分配了一个目标机器,并将一个 Q#函数或操作发送到那个新目标机器。在第九章中,我们将看到如何使用 Python 主机程序而不是使用 Jupyter Notebook 来完成类似的事情。
Q#程序被发送到模拟器,但在这个情况下,模拟器只是运行经典逻辑,因为还没有量子指令需要担心。
练习 7.1:更改问候语
将HelloWorld的定义改为包含你的名字而不是“经典世界”,然后再次使用%simulate运行你的新定义。
练习题解
本书中的所有练习题解都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,然后打开提及练习题解的 Jupyter 笔记本。
7.2 Q#中的函数和操作
现在我们已经用 Jupyter Notebook 启动了量子开发工具包,让我们用 Q#编写一些量子程序。回到第二章,我们看到了如何使用量子比特一次生成一个经典比特的随机数。回顾这个应用是开始使用 Q#的好地方,尤其是随机数在玩游戏时很有用。
7.2.1 在 Q#中使用量子随机数生成器玩游戏
很久以前,在卡美洛,莫甘娜分享了我们玩游戏的爱。作为一个聪明且技能远超她那个时代的数学家,莫甘娜甚至有时会把她游戏中的一部分用到量子比特上。有一天,当兰斯洛特爵士在树下睡觉时,莫甘娜把他困住,并挑战他玩一个小游戏:他们每个人都必须尝试猜测测量莫甘娜量子比特的结果。
同一个量子比特的两个方面?
在第二章中,我们看到了如何通过准备和测量量子比特一次生成一个经典比特的随机数。也就是说,量子比特可以用来实现硬币。我们将在本章使用同样的想法,把硬币看作是一种允许用户“抛掷”它并获得一个随机比特的接口。也就是说,我们可以通过准备和测量量子比特来实现硬币接口。
如果沿着Z-轴测量的结果是 0,那么兰斯洛特赢得游戏并可以回到桂妮薇尔那里。但如果结果是 1,那么莫甘娜就赢了,兰斯洛特必须留下来再玩一次。注意这与我们之前的 QRNG 程序有相似之处。就像在第二章中一样,我们将测量一个量子比特来生成随机数,这次是为了玩游戏。当然,莫甘娜和兰斯洛特可以抛一个更传统的硬币,但那样有什么乐趣呢?
这里是莫甘娜小游戏步骤:
-
准备一个处于|0〉状态的量子比特。
-
应用 Hadamard 操作(回想一下,单位算子H将|0〉转换为|+〉,反之亦然)。
-
在Z-轴上测量量子比特。如果测量结果是 0,那么兰斯洛特可以回家。否则,他必须留下来再玩一次!
坐在咖啡馆里看着世界过去,我们可以用我们的笔记本电脑通过在 Q#中编写量子程序来预测莫甘娜与兰斯洛特的游戏会发生什么。与之前我们写的HelloWorld函数不同,我们的新程序需要与量子比特一起工作,所以让我们花点时间看看如何使用量子开发工具包来做这件事。
我们在 Q#中与量子比特交互的主要方式是通过调用代表量子指令的操作。例如,Q#中的H操作代表我们在第二章中看到的 Hadamard 指令。为了了解这些操作是如何工作的,了解 Q#操作和我们在HelloWorld示例中看到的函数之间的区别是有帮助的:
-
Q#中的函数代表可预测的经典逻辑:像数学函数(
Sin、Log)这样的东西。函数在给定相同的输入时总是返回相同的输出。 -
Q#中的操作代表可能具有副作用的代码,例如采样随机数或发出修改一个或多个量子比特状态的量子指令。
这种分离有助于编译器确定如何自动将我们的代码作为更大量子程序的一部分进行转换;我们将在后面了解更多关于这一点。
函数与操作的区别的另一种视角
另一种思考函数和操作之间区别的方式是,函数计算事物但不能导致任何事物发生。无论我们多少次调用平方根函数Sqrt,我们的 Q#程序都不会有任何改变。相比之下,如果我们运行X操作,那么一个X指令就会被发送到我们的量子设备,这会导致设备状态的改变。根据X指令应用的初始量子比特状态,我们可以通过测量量子比特来知道X指令已被应用。因为在这种意义上函数不做任何事情,所以我们可以根据相同的输入精确预测它们的输出。
一个重要的后果是,函数不能调用操作,但操作可以调用函数。这是因为我们可以有一个不一定可预测的操作调用一个可预测的函数,我们仍然可能有一个可能或不可能可预测的东西。然而,一个可预测的函数不能调用一个可能不可预测的操作并仍然保持可预测。
随着我们在整本书中使用 Q#函数和操作,我们将更多地了解它们之间的区别。
由于我们希望量子指令影响我们的量子设备(以及 Lancelot 的命运),Q#中的所有量子操作都被定义为操作(因此得名)。例如,假设 Morgana 和 Lancelot 使用 Hadamard 指令将他们的量子比特准备在|+〉状态。然后我们可以通过将第二章中的量子随机数生成器(QRNG)示例作为 Q#操作编写来预测他们游戏的结局。
这个操作可能会有副作用...
当我们想要向目标机器发送指令以对我们的量子位执行某些操作时,我们必须从操作中这样做,因为发送指令是一种 副作用。也就是说,当我们运行一个操作时,我们不仅仅是计算某物:我们是在 做 某事。运行一个操作两次并不等同于运行一次,即使两次都得到了相同的输出。副作用不是确定性的或可预测的,因此我们不能使用函数来发送操纵我们的量子位的指令。
在列表 7.2 中,我们正是这样做的,从编写一个名为 GetNextRandomBit 的操作开始,以模拟 Morgana 游戏的每一轮。请注意,由于 GetNextRandomBit 需要与量子位一起工作,它必须是一个操作而不是一个函数。我们可以使用 use 语句向目标机器请求一个或多个新的量子位。
在 Q# 中分配量子位
use 语句是请求目标机器提供量子位的唯一两种方式之一。在 Q# 程序中,我们可以有的 use 语句数量没有限制,除了每个目标机器可以分配的量子位数量。
在包含 use 语句的块的末尾(即 operation、for 或 if 体)中,量子位会返回到目标机器。通常,量子位在操作体的开始时以这种方式分配,因此一种思考 use 语句的方式是确保每个分配的量子位都“属于”特定的操作。这使得在 Q# 程序中“泄漏”量子位成为不可能,这对于量子位可能是实际量子硬件上非常昂贵的资源来说非常有帮助。
如果我们需要对量子位的释放有更多的控制,use 语句也可以可选地后面跟一个块,用 { 和 } 表示。在这种情况下,量子位在块的末尾而不是在操作的末尾释放。
Q# 还提供了一种分配量子位的方式,称为 借用。与使用 use 语句分配量子位不同,borrow 语句让我们可以借用不同操作拥有的量子位,而无需知道它们开始时的状态。在这本书中,我们不会看到很多借用,但 borrow 语句与 use 语句非常相似,它使得我们不可能忘记我们已经借用了量子位。
按照惯例,所有量子位在我们得到它们后立即处于 |0〉 状态,我们向目标机器承诺,在块的末尾将它们放回 |0〉 状态,以便它们可以为需要它们的下一个操作做好准备。
列表 7.2 Operation.qs:Q# 模拟 Morgana 游戏的一轮
operation GetNextRandomBit() : Result { ❶
use qubit = Qubit(); ❷
H(qubit); ❸
return M(qubit); ❹
}
❶ 声明一个操作,因为我们想使用量子位并返回一个测量结果给调用者
❷ Q# 中的 use 关键字请求目标机器提供一个或多个量子位。在这里,我们请求一个类型为 Qubit 的单个值,并将其存储在新的变量 qubit 中。
❸ 在调用 H 之后,量子位处于 H|0〉 = |+〉 状态。
❹ 使用 M 操作在 Z-基下测量我们的量子比特。结果将是零或一,概率相等。测量后,我们可以将经典数据返回给调用者。
在这里,我们使用 M 操作在 z-基下测量我们的量子比特,并将结果保存到我们之前声明的 result 变量中。由于量子比特的状态是 |0〉 和 |1〉 的等概率叠加,result 将是 Zero 或 One,概率相等。
独立 Q# 应用程序
我们也可以像 QRNG 一样编写 Q# 应用程序作为独立应用程序,而不是从 Python 或 IQ# 笔记本中调用它们。为此,我们可以指定量子应用程序中的一个 Q# 操作作为 入口点,然后当我们从命令行运行应用程序时,它会自动调用。
对于这本书,我们将坚持使用 Jupyter 和 Python 来与 Q# 一起工作。但请查看github.com/crazy4pi314/learn-qc-with-python-and-qsharp/tree/master/ch07/Qrng 中的示例,了解如何编写独立的 Q# 应用程序。
在列表 7.2 中,我们还看到了 Q# 中打开 命名空间 的第一个例子。与 C++ 和 C# 中的命名空间或 Java 中的包类似,Q# 命名空间有助于保持函数和操作的有序。例如,在列表 7.2 中调用的 H 操作是在 Q# 标准库中定义的 Microsoft.Quantum.Intrinsic.H;也就是说,它位于 Microsoft.Quantum.Intrinsic 命名空间中。要使用 H,我们可以使用它的完整名称 Microsoft.Quantum.Intrinsic.H,或者使用 open 语句使一个命名空间中的所有操作和函数可用:
open Microsoft.Quantum.Intrinsic; ❶
❶ 使 Microsoft.Quantum.Intrinsic 中提供的所有函数和操作在 Q# 笔记本或源文件中使用时可用,无需指定它们的完整名称。
小贴士:在 Jupyter Notebook 中编写 Q# 时,Q# 标准库中的 Microsoft.Quantum.Intrinsic 和 Microsoft.Quantum.Canon 命名空间会自动为我们打开,因为它们在大多数 Q# 代码中都会用到。例如,当我们之前调用 Message 函数时,该函数是由 Q# 内核自动打开的 Microsoft.Quantum.Intrinsic 命名空间提供的。要检查 Q# 笔记本中打开了哪些命名空间,我们可以使用 %lsopen 魔法命令。
练习 7.2:生成更多比特
使用 %simulate 魔法命令运行 GetNextRandomBit 操作几次。你得到的是预期的结果吗?
接下来,我们将看到兰斯洛特需要多少轮才能得到他回家的 Zero。让我们编写一个操作来玩直到我们得到一个 Zero。由于这个操作模拟了玩 Morgana 的游戏,我们将称之为 PlayMorganasGame。
小贴士:所有 Q# 变量默认都是不可变的。
列表 7.3 Operations.qs:使用 Q# 模拟 Morgana 游戏的多轮游戏
operation PlayMorganasGame() : Unit {
mutable nRounds = 0; ❶
mutable done = false;
repeat { ❶
set nRounds = nRounds + 1;
set done =
(GetNextRandomBit() == Zero); ❸
}
until done; ❹
Message($"It took Lancelot {nRounds}
➥ turns to get home."); ❺
}
❶ 使用可变关键字声明一个变量来指示已通过多少轮。我们可以稍后使用“设置”关键字来更改此变量的值。
❷ Q# 允许操作使用重复直到成功(RUS)循环。
❸ 在我们的循环内部,我们调用我们之前编写的 QRNG 作为 GetNextRandomBit 操作。我们检查结果是否为零,如果是,则将“完成”设置为 true。
❹ 如果我们得到一个零,那么我们可以停止循环。
❺ 我们再次使用消息来将轮数打印到屏幕上。为此,我们使用 $"" 字符串,它允许我们通过在字符串内部使用 {} 占位符将变量包含在诊断消息中。
小贴士 Q# 中用 $"" 标记的字符串称为插值字符串,其工作方式与 Python 中的 f"" 字符串非常相似。
列表 7.3 包含一个名为重复直到成功(RUS)循环的 Q# 控制流。与while循环不同,RUS 循环还允许我们指定一个“修复”操作,如果退出循环的条件未满足,则运行该操作。
我们何时需要重置量子比特?
在 Q# 中,当我们使用 use 分配一个新的量子比特时,我们向目标机器承诺在释放之前将其放回 |0〉 状态。乍一看,这似乎是不必要的,因为目标机器可以在量子比特释放时重置其状态——毕竟,我们经常在操作或 use 块的末尾调用 Reset 操作。确实,当量子比特被测量时,这会自动发生,就在它被释放之前!
重要的是要注意,重置操作是通过在Z基中进行测量,并在测量返回One时使用X操作翻转量子比特来工作的。在许多量子设备中,测量比其他操作要昂贵得多,因此如果我们能避免调用重置,我们就可以降低量子程序的成本。特别是考虑到中期设备的限制,这种优化对于使量子程序具有实际用途至关重要。
在本章的后面部分,我们将看到一些示例,其中我们知道量子比特在需要释放时所处的状态,这样我们就可以“取消准备”量子比特而不是对其进行测量。在这些情况下,我们没有最终的测量,因此 Q# 编译器不会为我们添加自动重置,从而避免了可能昂贵的测量操作。
我们可以使用%simulate命令运行这个新操作,其方式与HelloWorld示例非常相似。当我们这样做时,我们可以看到兰斯洛特需要停留多长时间:
In []: %simulate PlayMorganasGame
It took Lancelot 1 turns to get home.
Out[]: ()
看起来兰斯洛特那次运气不错!或者也许是不幸,如果他无聊地在卡美洛的桌子旁闲逛。
深入探讨:打开与导入
初看起来,Q# 中的 open 语句可能非常类似于 Python、JavaScript 或 TypeScript 中的 import 语句。open 和 import 都使库中的代码可用于我们的程序和应用程序。主要区别在于,使用 open 语句打开命名空间只是这样做:它打开一个命名空间并使其可用,但它不会导致任何代码运行,也不会改变我们的代码编译和查找库的方式。
从原则上讲,我们可以不使用单个 open 语句编写每个 Q# 程序,通过显式地使用每个函数和操作的完整名称(包括其命名空间)来引用它们(即调用 Microsoft.Quantum.Intrinsic.H 而不是仅仅 H)。从这个意义上讲,open 语句类似于 C++ 或 C# 中的 using 语句,F# 中的 open 语句,以及 Java 中的 import 语句。
相比之下,Python、JavaScript 和 TypeScript 中的 import 不仅使库中的名称可用于我们的代码,还会导致那些库的部分运行。当一个 Python 模块首次导入时,Python 解释器使用模块的名称来查找该模块的定义位置,然后运行该模块及其初始化代码。通常,为了使用这些模块,我们需要首先使用 pip 或 conda 等工具安装一个或多个 包,对于 Python 包或 npm 对于 JavaScript 和 TypeScript。
同样的概念可以用来将新的库添加到我们的 Q# 程序中,使用 NuGet 包管理器。在一个 Q# 笔记本中,%package 命令指示 Q# 内核下载一个给定的 Q# 库并将其添加到我们的会话中。例如,当我们打开一个新的笔记本时,Q# 标准库会自动从 nuget.org 上的 Microsoft.Quantum.Standard 包下载并安装(www.nuget.org/packages/Microsoft.Quantum.Standard)。同样,当编写命令行 Q# 应用程序时,运行 dotnet add package 会将所需的元数据添加到我们的项目 Q# 编译器中,以便找到我们需要的包。
更多详情,请查看量子开发工具包文档,docs.microsoft.com/azure/quantum/user-guide/libraries/additional-libraries。
7.3 将操作作为参数传递
假设在我们对 Morgana 的游戏中,我们感兴趣的是以非均匀概率采样随机比特。毕竟,Morgana 没有承诺 Lancelot 他们测量的量子比特是如何准备的;如果她用带有他们量子比特的偏硬币而不是公平硬币来让他玩得更久,她可以让他玩得更久。
修改 Morgana 的游戏最简单的方法是,而不是直接调用 H,将表示 Morgana 为游戏做准备的操作作为输入。要接受一个操作作为输入,我们需要编写输入的 类型,就像我们可以编写 qubit : Qubit 来声明类型为 Qubit 的输入 qubit 一样。操作类型由从输入类型到输出类型的粗箭头 (=>) 表示。例如,H 的类型是 Qubit => Unit,因为 H 接受单个量子比特作为输入,并返回一个空元组作为其输出。
提示:在 Q# 中,函数用细箭头 (->) 表示,而操作用粗箭头 (=>) 表示。
列表 7.4 使用操作作为输入预测 Morgana 的游戏
operation PrepareFairCoin(qubit : Qubit) : Unit {
H(qubit);
}
operation GetNextRandomBit(
statePreparation : (Qubit => Unit) ❶
) : Result {
use qubit = Qubit();
statePreparation(qubit); ❷
return Microsoft.Quantum.Measurement.MResetZ(qubit); ❸
}
❶ 我们为 GetNextRandomBit 添加了一个新的输入,称为 statePreparation,表示我们想要用来准备作为硬币使用的状态的运算。在这种情况下,Qubit => Unit 是任何接受单个量子比特并返回空元组类型 Unit 的运算的类型。
❷ 在 GetNextRandomBit 中,作为 statePreparation 传递的运算可以像任何其他运算一样调用。
❸ Q# 标准库提供了 Microsoft.Quantum.Measurement.MResetZ,以便在一步中测量和重置量子比特。在这种情况下,MResetZ 操作与上一个例子中的 return M(qubit); 语句做的是同样的事情。区别在于 MResetZ 总是重置其输入量子比特,而不仅仅是当它在释放量子比特之前使用时。我们将在本章后面更多地了解这个操作,以及如何调用此操作时使用更短的名字。
练习 7.3:GetNextRandomBit 的类型
我们新定义的 GetNextRandomBit 的类型是什么?
输入元组,输出元组
Q# 中的所有函数和操作都接受单个 元组 作为输入,并返回单个 元组 作为输出。例如,声明为 function Pow(x : Double, y : Double) : Double {...} 的函数接受一个 (Double, Double) 类型的元组作为输入,并返回一个 (Double) 类型的元组作为其输出。这是因为一个称为 单元素元组等价性 的属性。对于任何类型 'T',包含单个 'T' 的元组 ('T) 与 'T' 本身等价。在 Pow 的例子中,我们可以将输出视为一个等价于 Double 的元组 (Double)。

使用单个输入和单个输出的操作表示
考虑到这一点,返回无输出的函数或操作可以被视为返回一个没有元素的元组,()。这种元组的类型称为 Unit,类似于其他基于元组的语言,如 F#。如果我们将元组视为一种盒子,那么这与 C、C++ 或 C# 中的 void 不同,因为那里仍然有 一些东西:一个空盒。
在 Q# 中,我们总是返回一个盒子,即使那个盒子是空的。在 Q# 中没有“无”的意义。有关更多详细信息,请参阅 Isaac Abraham 所著的 Get Programming with F#(Manning,2018)的第 7.2 节。

Unit与void
在列表 7.4 中,我们看到GetNextRandomBit将其输入statePreparation视为一个“黑盒”。了解 Morgana 的态准备策略的唯一方法就是运行它。
换句话说,我们不想对statePreparation做任何表明我们知道它做什么或它是什么的事情。GetNextRandomBit与statePreparation交互的唯一方式是通过调用它,并传递一个Qubit来对其操作。
这允许我们重用GetNextRandomBit中的逻辑,以处理 Morgana 可能用来给 Lancelot 制造一些麻烦的许多不同类型的态准备程序。例如,假设她想要一个三分之四返回Zero,四分之一返回One的偏硬币。我们可能会运行以下内容来预测这种新策略。
列表 7.5 将不同的态准备策略传递给PlayMorganasGame
open Microsoft.Quantum.Math; ❶
operation PrepareQuarterCoin(qubit : Qubit) : Unit {
Ry(2.0 * PI() / 3.0, qubit); ❷
}
❶ Microsoft.Quantum.Math 命名空间提供了如 Sin、Cos、Sqrt 和 ArcCos 等经典数学函数以及 PI()等常量。
❷ Ry 操作实现了 Y 轴旋转。Q#使用弧度而不是度来表示旋转,因此这是一个围绕 Y 轴的 120°旋转。
Ry 操作实现了我们在第二章中看到的 Y 轴旋转。因此,当我们在列表 7.5 中将量子比特的状态旋转 120°时,如果量子比特从|0〉状态开始,那么它将量子比特准备在状态Ry(120°)|0〉 = √3/4 |0〉 + √1/4 |1〉。结果,当我们测量量子比特时观察到 1 的概率是√1/4² = 1 / 4。
我们可以使这个例子更加通用,允许 Morgana 为她(通过共享量子比特实现的)的硬币指定任意偏置。
列表 7.6 将操作传递以实现任意的硬币偏置
operation PrepareBiasedCoin(
morganaWinProbability : Double, qubit : Qubit
) : Unit {
let rotationAngle = -2.0 * Arccos(
Sqrt(1.0 - morganaWinProbability)); ❶
Ry(rotationAngle, qubit);
}
operation PrepareMorganasCoin(qubit : Qubit)
: Unit { ❷
PrepareBiasedCoin(0.62, qubit);
}
❶ 确定旋转输入量子比特的角度,以获得看到 One 作为结果的正确概率。这需要一点三角学知识;有关详细信息,请参阅下一侧边栏。
❷ 这个操作具有正确的类型签名(Qubit => Unit),我们可以看到 Morgana 在每个回合获胜的概率是 62%。
解析三角函数
正如我们多次看到的,量子计算广泛地处理旋转。为了确定我们旋转所需的角,我们依赖于三角学(字面上,“三角形的研究”):这是描述旋转角度的数学分支。例如,正如我们在第二章中看到的,绕 Y 轴旋转|0〉角度θ会导致状态 cos(–θ / 2) |0〉 + sin(–θ / 2) |1〉。我们知道我们需要选择θ,使得 cos(–θ / 2) =
,这样我们就能得到 62%的 One 结果概率。这意味着我们需要“反转”余弦函数来确定θ需要是多少。
在三角学中,余弦函数的倒数是反余弦函数,写作 arccos。对 cos(–θ / 2) =
的两边取反余弦,我们得到 arccos(cos(–θ / 2)) = arccos(
)。我们可以消去 arccos 和 cos,找到一个旋转角度,得到我们需要的值,–θ / 2 = arccos(
)。
最后,我们将两边乘以 –2 来得到列表 7.6 第 4 行使用的方程。我们可以在以下图中直观地看到这个论点。

Morgana 如何选择 θ 来控制游戏。她选择的 θ 越接近 π,Morgana 就能越长时间让可怜的 Lancelot 玩下去。
然而,这多少有些令人不满意,因为操作 PrepareMorganasCoin 引入了很多样板代码,只是为了锁定输入参数 morganaWinProbability 传递给 PrepareBiasedCoin 的 0.62 值。如果 Morgana 改变她的策略,使其具有不同的偏差,那么使用这种方法,我们需要另一个样板操作来表示它。退一步来看,让我们看看 PrepareMorganasCoin 实际上 做什么。它从一个操作 PrepareBiasedCoin : (Double, Qubit) => Unit 开始,并通过锁定 Double 参数为 0.62 来将其包装成一个类型为 Qubit => Unit 的操作。也就是说,它通过将输入值固定为 0.62 来移除了 PrepareBiasedCoin 的一个参数。
幸运的是,Q# 提供了一种方便的缩写方式来创建新的函数和操作,通过锁定一些(但不是全部!)输入。使用这种缩写,称为 偏应用,我们可以将列表 7.6 中的 PrepareMorganasCoin 重写为一个更易读的形式:
let flip = GetNextRandomBit(PrepareBiasedCoin(0.62, _));
_ 表示 PrepareBiasedCoin 的输入部分 缺失。我们说 PrepareBiasedCoin 已经被部分应用。而 PrepareBiasedCoin 的类型是 (Double, Qubit) => Unit,因为我们填充了输入的 Double 部分,PrepareBiasedCoin(0.62, _) 的类型变为 Qubit => Unit,使其与我们对 GetNextRandomBit 的修改兼容。
提示:Q# 中的偏应用与 Python 中的 functools.partial 和 Scala 中的 _ 关键字类似。
另一种思考偏应用的方式是将其视为通过特化现有函数和操作来创建新函数和操作的方法。
列表 7.7 使用偏应用来特化一个操作
function BiasedPreparation(headsProbability : Double)
: (Qubit => Unit) { ❶
return PrepareBiasedCoin(
headsProbability, _); ❷
}
❶ 输出类型是一个操作,它接受一个量子比特并返回空元组。也就是说,BiasedPreparation 是一个创建新操作的功能!
❷ 通过传递 headsProbability 但为目标量子比特留空(_),创建新的操作。这给我们一个操作,它接受单个量子比特并填充空白。
提示:在列表 7.7 中,我们将 PrepareBiasedCoin(headsProbability, _) 作为其自身的值返回,就像我们可能从具有 Int 输出类型的函数或操作中返回 42 一样。在 Q# 中,函数和操作与 42、true 和 (3.14, "hello") 都是 Q# 中的值,就像 Python 函数 (lambda x: x ** 2) 是 Python 中的值一样。正式地说,我们在 Q# 中说函数和操作是 一等 值。
可能会有些令人困惑,BiasedPreparation 从一个函数返回一个操作,但这与之前描述的函数和操作之间的划分完全一致,因为 BiasedPreparation 仍然是可预测的。特别是,BiasedPreparation(p) 对于给定的 p 总是返回相同的操作,无论我们调用函数多少次。我们可以通过注意到 BiasedPreparation 只部分应用操作但从不调用它们来确保这一点。
练习 7.4:部分应用
部分应用适用于函数和操作!尝试编写一个函数 Plus,它将两个整数 n 和 m 相加,并编写另一个函数 PartialPlus,它接受一个输入 n 并返回一个函数,该函数将 n 加到其输入上。
提示:您可以使用以下代码片段作为模板开始:
function Plus(n : Int, m : Int) : Int {
// fill in this part
}
function PartialPlus(n : Int) : (Int -> Int) {
// fill in this part
7.4 在 Q# 中玩 Morgana 的游戏
准备好一等操作和部分应用后,我们现在可以制作一个更完整的 Morgana 的游戏版本。
Q# 标准库
量子开发工具包附带各种标准库,我们将在本书的其余部分看到这些库。例如,在列表 7.8 中,我们使用了一个 MResetZ 操作,它既测量量子比特(类似于 M),又将其重置(类似于 Reset)。此操作由 Microsoft.Quantum.Measurement 命名空间提供,这是量子开发工具包附带的主要标准库之一。
可以在该命名空间中找到所有操作和函数的完整列表,请参阅 docs.microsoft.com/qsharp/api/qsharp/microsoft.quantum.measurement。不过,现在不必过于担心它;随着我们的深入,我们将看到更多的 Q# 标准库。
列表 7.8 完整的 Q# 列表,用于偏置的 PlayMorganasGame
open Microsoft.Quantum.Math; ❶
open Microsoft.Quantum.Measurement;
operation PrepareBiasedCoin(winProbability : Double, qubit : Qubit)
: Unit {
let rotationAngle = 2.0 * Arccos(
Sqrt(1.0 - winProbability)); ❷
Ry(rotationAngle, qubit);
}
operation GetNextRandomBit(statePreparation : (Qubit => Unit))
: Result {
use qubit = Qubit();
statePreparation(qubit); ❸
return MResetZ(qubit); ❹
}
operation PlayMorganasGame(winProbability : Double) : Unit {
mutable nRounds = 0;
mutable done = false;
let prep = PrepareBiasedCoin(
winProbability, _); ❺
repeat {
set nRounds = nRounds + 1;
set done = (GetNextRandomBit(prep) == Zero);
}
until done;
Message($"It took Lancelot {nRounds} turns to get home.");
}
❶ 打开 Q# 标准库中的命名空间,以帮助进行经典数学和测量量子比特
❷ 旋转角度选择硬币的偏差。
❸ 使用我们传递的操作作为状态准备,并将其应用于量子比特。
❹ MResetZ 操作定义在我们在示例开头打开的 Microsoft.Quantum.Measurement 命名空间中。它测量在 Z-基下的量子比特,然后应用将量子比特返回到 |0〉 状态所需的操作。
❺ 使用部分应用来指定状态准备过程中的偏差,但不指定目标量子比特。虽然 PrepareBiasedCoin 有类型 (Double, Qubit) => Unit,但 PrepareBiasedCoin(0.2, _) “填充”了两个输入中的一个,留下一个类型为 Qubit => Unit 的操作,正如 EstimateBias 所期望的那样。
为 Q# 函数和操作提供文档
可以通过在函数或操作声明之前编写小型的、特别格式化的文本文档(在 /// 注释中)来为 Q# 函数和操作提供文档。这些文档使用 Markdown 编写,Markdown 是一种简单的文本格式化语言,在 GitHub、Azure DevOps、Reddit 和 Stack Exchange 等网站上使用,并由 Jekyll 等网站生成器使用。当我们将鼠标悬停在函数或操作的调用上时,会显示 /// 注释中的信息,并且可以使用这些信息来创建类似于 docs.microsoft.com/qsharp/api/ 中的 API 参考。
/// 注释的不同部分通过如 /// # Summary 这样的部分标题来指示。例如,我们可以使用以下内容来记录 7.8 列表中的 PrepareBiasedCoin 操作:
/// # Summary
/// Prepares a state representing a coin with a given bias.
///
/// # Description
/// Given a qubit initially in the |0〉 state, applies operations
/// to that qubit such that it has the state √p |0〉 + √(1 - p) |1〉,
/// where p is provided as an input.
/// Measurement of this state returns a One Result with probability p.
///
/// # Input
/// ## winProbability
/// The probability with which a measurement of the qubit should return One.
/// ## qubit
/// The qubit on which to prepare the state √p |0〉 + √(1 - p) |1〉.
operation PrepareBiasedCoin(
winProbability : Double, qubit : Qubit
) : Unit {
let rotationAngle = 2.0 * Arccos(Sqrt(1.0 - winProbability));
Ry(rotationAngle, qubit);
}
当使用 IQ# 时,我们可以通过使用 ? 命令来查找文档注释。例如,我们可以在输入单元格中运行 X? 来查找 X 操作的文档。
对于完整的参考,请参阅
docs.microsoft.com/azure/quantum/user-guide/language/statements/iterations#documentation-comments。
为了估计特定状态准备操作的偏差,我们可以重复运行 PlayMorganasGame 操作,并计算我们得到多少次 Zero。让我们选择一个 winProbability 值,并使用该值运行 PlayMorganasGame 操作,看看兰斯洛特会卡住多久:
In []: %simulate PlayMorganasGame winProbability=0.9 ❶
It took Lancelot 5 turns to get home.
❶ 我们可以通过指定在要模拟的操作名称之后来使用 %simulate 命令将输入传递给操作。
尝试使用不同的 winProbability 值进行实验。注意,如果 Morgana 真的改变了天平,我们可以确认兰斯洛特将需要相当长的时间才能回到圭尼维尔:
In []: %simulate PlayMorganasGame winProbability=0.999
It took Lancelot 3255 turns to get home.
在下一章中,我们将通过回到卡美洛来找到我们第一个量子算法的例子:Deutsch–Jozsa 算法。
摘要
-
Q# 是由微软提供的开源量子开发工具包的一部分量子编程语言。
-
Q# 中的量子程序被分解为表示经典和确定逻辑的 函数,以及可能产生副作用(如向量子设备发送指令)的 操作。
-
函数和操作是 Q# 中的第一类值,可以作为输入传递给其他函数和操作。我们可以利用这一点来组合量子程序的不同部分。
-
通过在我们的程序中传递 Q# 操作,我们可以将第二章中的 QRNG 示例扩展,允许传递准备其他状态的操作,除了 |+〉;这反过来又允许生成有偏的随机数。
8 什么是量子算法?
本章涵盖
-
理解量子算法是什么
-
设计或 acles 以在量子程序中表示经典函数
-
使用有用的量子编程技术
量子算法的一个重要应用是加速解决需要搜索我们试图学习的函数输入的问题。这些函数可能是模糊的(例如哈希函数)或计算上难以评估(在研究数学问题时很常见)。在两种情况下,将量子计算机应用于此类问题需要我们理解如何编程以及如何向量子算法提供输入。为了学习如何这样做,我们将编写并运行Deutsch–Jozsa 算法的实现,这将使我们能够快速使用量子设备学习未知函数的性质。
8.1 经典和量子算法
算法(名词):解决问题或达到某个目的的逐步过程。
—Merriam-Webster 词典
当我们谈论经典编程时,我们有时会说一个程序实现了 算法:也就是说,一系列可以用来解决问题的步骤。例如,如果我们想对一个列表进行排序,我们可以独立于我们使用的语言或操作系统来谈论快速排序算法。我们经常在高级别指定这些步骤。在快速排序的例子中,我们可能会列出以下步骤:
-
如果要排序的列表为空或只有一个元素,则按原样返回。
-
选择列表中要排序的元素,称为 枢轴。
-
将列表中的其他所有元素分为小于枢轴和大于枢轴的元素。
-
递归地对每个新列表进行快速排序。
-
返回第一个列表,然后是枢轴,最后是第二个列表。
这些步骤作为在特定感兴趣的语言中编写实现的指南。比如说我们想在 Python 中编写快速排序算法。
列表 8.1 快速排序的一个示例实现
def quicksort(xs):
if len(xs) > 1: ❶
pivot = xs[0] ❷
left = [x in xs[1:] if x <= pivot] ❸
right = [x in xs[1:] if x > pivot]
return quicksort(left) +
➥ [pivot] + quicksort(right) ❹
else:
return xs
❶ 通过查看列表中是否至少有两个元素来检查基本情况
❷ 为步骤 2 选择第一个元素作为枢轴
❸ 按照第 3 步描述构建两个新列表的 Python 代码
❹ 按照步骤 4 和 5 描述将所有内容重新连接在一起
一个编写良好的算法可以帮助指导如何编写实现,因为它清楚地说明了必须执行的步骤。在这一点上,量子算法是相同的:它们列出了在任何实现中我们需要执行的步骤。
定义 量子程序 是量子算法的实现,由一个 经典程序 组成,该程序向 量子设备 发送指令以准备特定的状态或测量结果。
正如我们在第七章中看到的,当我们编写一个 Q#程序时,我们实际上是在编写一个经典程序,该程序代表我们向我们的多个不同目标机器之一发送指令,如图 8.1 所示,将测量结果返回给我们的经典程序。

图 8.1 在经典计算机上的 Microsoft Quantum Development Kit 软件堆栈。我们可以编写一个包含函数和操作并引用我们想要包含的任何 Q# 库的 Q# 程序。然后,主机程序可以协调我们的 Q# 程序和目标机器(例如,在我们的计算机上本地运行的模拟器)之间的通信。
量子编程的艺术
我们无法复制量子状态,但如果它们是运行程序的结果,我们可以告诉其他人他们需要采取哪些步骤来准备相同的状态。正如我们之前所看到的,量子程序是一种特殊的经典程序,因此我们可以随意复制它们。正如我们将在本书的其余部分看到的那样,任何量子状态都可以通过从只有 |0〉 状态的副本开始的量子程序的输出来近似或精确写出。例如,在第二章中,我们使用一个由单个 H 指令组成的程序来准备 QRNG 的初始状态 |+〉。
换句话说,我们可以将程序视为如何准备量子比特的配方。给定一个量子比特,我们无法确定用于准备它的配方是什么,但我们可以随意复制配方本身。
与执行 quicksort 的步骤指导 Python 解释器比较值并在内存中移动值不同,Q# 程序中的步骤指导我们的目标机器对设备中的量子比特应用旋转和测量。如图 8.1 所示,我们可以使用主机程序将 Q# 应用程序发送到每个不同的目标机器以运行。目前,我们将继续使用 IQ# 插件作为 Jupyter Notebook 的主机程序;在下一章中,我们将看到如何使用 C# 编写我们自己的主机程序。
在本书的大部分时间里,我们感兴趣的是模拟量子程序,所以我们使用 QuantumSimulator 目标机器。这个模拟器的工作方式与我们在第二章和第四章中开发的模拟器非常相似,因为它通过乘以单位算符如 H 来执行像 H 这样的指令。
小贴士:与前面的章节一样,我们使用字体来区分像 H 这样的指令与用于模拟这些指令的单位矩阵 H。
ResourcesEstimator 目标机器允许我们不仅运行量子程序,还可以得到运行该程序所需的量子比特数量的估计。这对于无法用经典方法模拟或运行在现有硬件上的大型程序非常有用,帮助我们了解需要多少量子比特;我们将在稍后了解更多关于这个目标机器的信息。
由于 Q#应用程序将指令发送到我们用于运行它们的目标机器,因此很容易在具有相同指令集的不同目标机器上重用 Q#代码。例如,QuantumSimulator目标机器使用我们期望实际量子硬件采取的相同指令,一旦它可用;因此,我们现在可以在模拟器上使用小规模的问题实例测试 Q#程序,然后稍后在这些量子硬件上运行相同的程序。
在这些不同的目标机器和应用中,共同的是我们需要编写一个程序,向目标机器发送指令以实现某个目标。因此,作为量子程序员,我们的任务是确保这些指令具有解决有用问题的效果。
提示:我们使用模拟器来测试 Q#程序的方式与使用模拟器测试其他专用硬件(如现场可编程门阵列(FPGAs))的程序或使用模拟器测试桌面和笔记本电脑上的移动设备应用程序的方式有些相似。主要区别在于,我们只能使用经典计算机来模拟一个非常小的量子比特数或受限制的程序类型的量子计算机。
当我们有算法引导我们组织在经典和量子设备中需要发生的步骤时,这样做要容易得多。在开发新的量子算法时,我们可以使用量子效应,例如纠缠,这在第四章中我们已经看到。
提示:要获得量子硬件的任何优势,我们必须使用硬件的独特量子属性。否则,我们只是拥有一个更昂贵、速度更慢的经典计算机。
8.2 德尔布希-约萨算法:搜索的适度改进
那么,什么可能是一个好的例子,一个利用我们崭新的量子硬件的量子算法呢?我们在第四章和第七章中学到,思考游戏往往有帮助,这也不例外。为了找到本章的游戏,让我们回到卡美洛,梅林在那里发现自己面临着一个考验。
8.2.1 量子湖的女神
著名的智慧巫师梅林刚刚遇到了尼姆,湖之女神。尼姆,寻求为下一任英格兰国王寻找一位有能力的导师,已经决定测试梅林,看看他是否胜任这项任务。两个 bitter rivals,亚瑟和莫德雷德,正在争夺王位,如果梅林接受尼姆的任务,他必须选择谁作为国王的导师。
对于尼姆,她不关心谁成为国王,只要梅林能给他们提供明智的建议。尼姆关心的是,作为新国王指定的指导者,梅林在领导力上是否会可靠和一致。
由于 Nimue 和我们一样喜欢玩游戏,她决定和梅林玩游戏来测试他是否会成为一个好导师。Nimue 的游戏,国王制造者,测试梅林作为国王顾问的角色是否一致。要玩国王制造者,Nimue 给梅林两个激烈对手中的一个的名字,梅林必须回答 Nimue 的候选人是否应该是真正的王位继承人。以下是规则:
-
在每一轮中,Nimue 问梅林一个形式为“潜在继承人应该成为国王吗?”的问题。
-
梅林必须回答“是”或“不”,不提供任何额外信息。
每一轮都给 Nimue 更多关于凡人领域的信息,因此她希望尽可能少地问问题,以捕捉梅林的不诚实。她的目标是以下内容:
-
验证梅林是否会是新英格兰国王的好导师。
-
尽可能少地问问题来验证。
-
避免了解梅林会同意指导谁。
在这一点上,梅林有四种可能的策略:
-
当被问及亚瑟是否应该成为国王时说“是”,否则说“不”(好导师)。
-
当被问及莫德雷德是否应该成为国王时说“是”,否则说“不”(好导师)。
-
无论 Nimue 询问谁(不良导师)。
-
无论 Nimue 询问谁都说“不”(不良导师)。
我们可以通过再次使用真值表的概念来思考梅林的战略。例如,假设梅林决定特别不帮忙,并拒绝所有王位候选人。我们可能使用表 8.1 中的真值表来写下这一点。
表 8.1 一种可能的国王制造者策略的真值表:梅林总是说“不”。
| 输入(Nimue) | 输出(梅林) |
|---|---|
| “莫德雷德应该成为国王吗?” | “不。” |
| “亚瑟应该成为国王吗?” | “不。” |
在这一点上,Nimue 有理由抱怨梅林作为导师的智慧!梅林在让亚瑟和莫德雷德之间做出选择的责任上并不一致。虽然 Nimue 可能不在乎梅林选择谁,但他肯定必须选择某人来指导并准备王位。
Nimue 需要一种策略,以尽可能少的游戏回合来确定梅林是否有策略 1 或 2(好导师)或梅林是否按照 3 或 4(不良导师)进行游戏。她可以简单地问两个问题:“莫德雷德应该成为国王吗?”和“亚瑟应该成为国王吗?”然后比较他的答案,但这样会导致 Nimue 确定他选择了谁作为国王。每个问题,Nimue 都会更多地了解王国的凡人事务——多么令人不快!
虽然看起来 Nimue 的游戏注定要迫使她了解梅林的选择继承人,但她很幸运。这是一个量子湖,我们将在本章的其余部分看到,Nimue 可以问一个单一的问题,这将告诉她只有梅林是否致力于他的导师角色,而不是他选择了谁。
由于我们没有可用的量子湖,让我们尝试用 Q#在经典计算机上模拟尼缪使用量子指令的行为,然后进行模拟。让我们用梅林的战略来表示一个经典函数f,它将尼缪的问题作为输入x。也就是说,我们将写f(亚瑟)来表示“当被问及亚瑟是否应该成为国王时,梅林会怎么回答。”请注意,由于尼缪只会问两个问题中的一个,她问的问题是一个比特的例子。有时使用标签“0”和“1”来写这个比特是方便的,有时使用布尔值“False”和“True”来标记尼缪的输入比特是有帮助的。毕竟,“1”对于一个像“莫德雷德应该成为国王吗?”这样的问题来说是一个非常奇怪的答案。
使用比特,我们写f(0) = 0 来表示,如果尼缪问梅林,“莫德雷德应该成为国王吗?”他的回答是否定的。表 8.2 显示了我们可以如何将尼缪的问题映射到布尔值。
表 8.2 将尼缪的问题编码为比特
| 尼缪的问题 | 作为比特的表示 | 作为布尔值的表示 |
|---|---|---|
| “莫德雷德应该成为国王吗?” | 0 | 否 |
| “亚瑟应该成为国王吗?” | 1 | 是 |
如果她没有量子资源,为了确保知道梅林的战略,尼缪必须尝试f的所有输入;也就是说,她必须问梅林两个问题。尝试所有输入将给出梅林的全部战略;如前所述,尼缪并不真正对此感兴趣。
我们不需要询问梅林关于莫德雷德和亚瑟的情况,我们可以在 Q#中实现一个量子算法,通过只问梅林一个问题来学习他是否是一个好的导师。使用 Quantum Development Kit 提供的模拟器,我们甚至可以在我们的笔记本电脑或台式机上运行我们的新 Q#程序!在本章的其余部分,我们将探讨如何编写这个量子算法的示例,称为 Deutsch–Jozsa 算法(见图 8.2)。

图 8.2 本章我们将使用 Microsoft Quantum Development Kit 软件栈,编写在模拟目标机器上的 Jupyter Notebook 主机上运行的 Q#程序。
让我们勾勒出我们的量子程序将是什么样子。函数f(梅林的战略)的可能输入和输出是True和False。我们可以使用调用f时得到的输入和输出为f编写一个真值表。例如,如果f是经典 NOT 操作(通常表示为¬),那么我们会观察到f(True)是False,反之亦然。如表 8.3 所示,在我们的游戏中使用经典 NOT 操作作为策略对应于选择莫德雷德成为国王。
表 8.3 经典 NOT 操作的真值表
| 输入 | 输出 |
|---|---|
True(“亚瑟应该成为国王吗?”) |
False(“不。”) |
False(“莫德雷德应该成为国王吗?”) |
True(“是的。”) |
我们函数f的定义有四种可能的选择,每种选择代表梅林可用的四种策略之一,如图 8.3 所示。

图 8.3 从一位到一位的四个不同函数。我们称顶行中的两个函数为平衡的,因为映射到 0 的输入和映射到 1 的输入数量相等。我们称底行中的两个函数为常函数,因为所有输入都映射到单个输出。
其中两个函数,为了方便起见标记为id和not,将每个0和1输入映射到不同的输出;我们称这些函数为平衡的。在我们的小游戏中,它们代表梅林恰好选择一个人成为国王的情况。所有情况都在表 8.4 中列出。
表 8.4 将梅林的战略分类为常函数或平衡函数
| 梅林的战略 | 函数 | 类型 | 是否通过尼缪的挑战? |
|---|---|---|---|
| 选择亚瑟 | id |
平衡的 (f(0) ≠ f(1)) | 是 |
| 选择莫德雷德 | not |
平衡的 (f(0) ≠ f(1)) | 是 |
| 选择两者都不 | zero |
常函数 (f(0) = f(1)) | 否 |
| 选择两者 | one |
常函数 (f(0) = f(1)) | 否 |
另一方面,我们标记为zero和one的函数是常函数,因为它们将两个输入都映射到相同的输出。常函数代表梅林毫无用处的策略,因为他要么选择了两者都成为国王(开启一场糟糕战争的糟糕方式),要么两者都不选择。
经典地,要确定一个函数是否是常函数或平衡函数(分别对应梅林是好导师还是坏导师),我们必须通过构建其真值表来学习整个函数。记住,尼缪想要确保梅林是一个可靠的导师。如果梅林遵循由常函数表示的策略,他将不是一个好的导师。通过查看id和one函数的真值表,即表 8.5 和表 8.6,我们可以看到它们如何描述梅林遵循的策略将使他成为一个好导师或坏导师。
表 8.5 id函数的真值表,一个平衡函数的例子
| 输入 | 输出 |
|---|---|
True(“亚瑟应该成为国王吗?”) |
True(“是”) |
False(“莫德雷德应该成为国王吗?”) |
False(“否”) |
表 8.6 one函数的真值表,一个常函数的例子
| 输入 | 输出 |
|---|---|
True(“亚瑟应该成为国王吗?”) |
True(“是”) |
False(“莫德雷德应该成为国王吗?”) |
True(“是”) |
尼缪试图学习梅林是好导师还是坏导师(即,f是否平衡或常函数)的困难在于,梅林导师的质量是他策略的一种全局属性。无法通过查看f的单一输出来得出关于f对不同输入输出的任何结论。如果我们只能访问f,那么尼缪就陷入了困境:她必须重建整个真值表来决定梅林的策略是否是常函数或平衡函数。
另一方面,如果我们能将梅林的战略表示为量子程序的一部分,我们就可以使用本书中我们已经了解到的量子效应。利用量子计算,尼姆只有在梅林的战略是恒定或平衡的情况下才能学习,而不必确切地了解他正在使用哪种策略。由于我们并不关心真值表提供的信息是否超出了梅林是好导师还是坏导师,使用量子效应可以帮助我们更直接地学习我们所关心的内容。凭借我们的量子算法,我们可以通过一次函数调用就完成这项工作,而无需学习任何我们不感兴趣的信息。通过不要求所有真值表的细节,而只是寻找我们函数的更一般性质,我们可以最大限度地利用我们的量子资源。
量子计算的力量
如果我们想使用经典计算机来学习一个函数是否恒定或平衡,我们首先必须解决一个更难的问题:确定我们确切拥有哪个函数。相比之下,量子力学只让我们解决我们关心的问题(恒定与平衡),而不必解决经典计算机必须解决的更具挑战性的问题。
这本书中我们将看到的一个模式示例,其中量子力学让我们能够指定比经典情况下更弱的算法。
为了做到这一点,我们将使用 Deutsch-Jozsa 算法,该算法使用对梅林策略的量子表示的单次查询来学习他是否是一个好导师或坏导师。这种优势并不非常实用(仅节省一个问题),但没关系;我们将在本书的后面部分看到更多实用的算法。现在,Deutsch-Jozsa 算法是一个学习如何实现量子算法和,更重要的是,我们可以使用哪些工具来理解量子算法做什么的绝佳起点。
8.3 奥秘:在量子算法中表示经典函数
让我们看看从尼姆的量子湖中看事物是什么样子。当我们跳入湖中游泳时,我们面临一个相当直接的问题:我们如何使用量子位来实现代表梅林策略的函数 f?从上一节中,我们看到经典函数 f 是我们对梅林在每一轮国王制造者游戏中使用的策略的描述。由于 f 是经典的,因此很容易将其翻译成梅林将采取的一系列行动:尼姆给梅林一个单独的经典比特(她的问题),梅林给尼姆一个经典比特作为回答(他的答案)。
为了避免干涉凡人的事务,尼姆现在想使用 Deutsch-Jozsa 算法,而不是其他方法。由于她生活在量子湖中,尼姆可以轻松地分配一个量子位给梅林。幸运的是,梅林知道如何与量子位进行通信,但我们仍然需要弄清楚梅林会如何使用尼姆的量子位来执行他的策略。
问题在于,我们不能将量子比特传递给用于表示梅林策略的函数f:f接受并返回经典比特,而不是量子比特。为了使梅林能够使用他的策略来指导他对尼穆量子比特的处理,我们希望将梅林的策略f转化为一种称为预言机的量子程序。对我们来说,幸运的是,梅林非常适合扮演预言机的角色。
注意:从 T. H. White 对梅林的描述中,我们了解到他生活在时间倒流中。我们将通过确保梅林所做的一切都是可逆的来表示这一点。正如我们在第二章中看到的,一个结果是梅林无法测量尼穆的量子比特,因为测量是不可逆的。这项特权仅属于尼穆。
要理解我们将梅林的行为建模为预言机需要做什么,我们必须弄清楚两件事:
-
基于他的策略,梅林应该对尼穆的量子比特应用什么变换?
-
梅林需要应用哪些量子操作来实现这种变换?
单位矩阵和真值表
另一种说法是,我们在步骤 1 中需要做的事情是找到表示梅林行为的单位矩阵,类似于我们如何使用经典函数f来表示梅林在尼穆给他经典比特时所做的行为。正如我们在第二章中看到的,单位矩阵对量子计算的作用就像真值表对经典计算的作用:它们告诉我们对于每个可能的输入,量子操作的效果是什么。一旦我们找到了正确的单位矩阵,在步骤 2 中,我们将确定可以由该单位矩阵描述的量子操作的序列。
8.3.1 梅林的变换
要完成步骤 1,我们需要将像f这样的函数转换为单位矩阵,所以让我们首先回顾一下f可以是什么。梅林可能使用的策略由函数id、not、zero和one(见图 8.4)表示。

图 8.4 从一位到一位的四个不同函数
对于图 8.4 中的两个平衡函数id和not,我们可以轻松回答问题 1。id和not的量子程序可以作为旋转操作实现,这使得它们很容易转化为量子操作。例如,量子非操作是在X-轴上旋转 180°,交换|0〉和|1〉状态。
提示:回顾第三章,量子操作X,由单位矩阵
表示,在X轴上旋转 180°。这个操作实现了量子非操作:因为X|0〉 = |1〉和X|1〉 = |0〉,我们可以用第二章中的¬(非)操作符来表示它,即X|x〉 = |¬x〉。
虽然任何旋转都可以通过向相反方向旋转相同的角度来撤销,但在常数函数zero和one上我们会遇到更多问题。zero和one都不能直接作为旋转来实现,所以我们还有更多工作要做。例如,如果f是zero,那么输出f(0)和f(1)都是0。如果我们只有输出0,我们就无法判断我们是将0还是1作为输入给f得到的(见图 8.5)。

图 8.5 为什么不能撤销常数zero或one函数?基本上,如果所有输入都映射到单个输出,我们就失去了关于我们最初输入的信息。
注意:一旦我们应用zero或one,我们就失去了关于输入的任何信息。
由于one和zero都是不可逆的,而有效的量子比特操作是可逆的,因此梅林需要另一种方法来在尼缪挑战等量子算法中代表像f这样的函数。另一方面,如果我们可以用可逆的经典函数而不是f来表示梅林的战略,那么编写他战略的量子表示将容易得多。以下是我们将经典函数表示为量子或 acles 的策略:
-
找到一种方法来用可逆的经典函数表示我们的不可逆经典函数。
-
使用我们可逆的经典函数对量子态进行变换。
-
确定我们可以进行哪些量子操作以产生那种变换。
让我们使用经过验证的方法,即猜测和检查,来看看我们是否可以设计一个有效的可逆经典函数。确定我们是否得到了0或1作为输入的最简单方法是将它记录下来。所以让我们创建一个新的函数,它返回两个比特而不是一个。
对于我们的第一次尝试,让我们记录并保留输入:
g(x) = (x, f(x*))
例如,如果梅林使用策略one(也就是说,无论尼缪问他什么,他都回答“是”),那么f(x) = 1,而g(x) = (x, f(x*)) = (x, 1)。
这使我们更接近目标,因为我们现在可以判断我们是开始于0还是1输入。但我们还没有完全达到目标,因为g有两个输出和一个输入(见图 8.6)。

图 8.6 第一次尝试:使用g(x)保留输入和输出。使用这种方法,一些输出组合无法从任何输入中得到(例如,没有输入产生输出(1,0))。因此,由于没有输入对应那些输出,无法撤销函数。
要使用g作为策略,梅林必须给尼缪回更多的量子比特,但她既是双剑也是量子比特的守护者。更技术地说,撤销g会破坏信息,因为它需要两个输入并返回一个输出!
再试一次,让我们定义一个新的经典函数 h,它接受两个输入并返回两个输出,即 h(x, y)。让我们再次考虑这样一个例子,我们用函数 f(x) = 1 来描述 Merlin 的策略。由于 g 几乎带我们到达了目的地,我们将选择 h 使得 h(x, 0) = g(x)。从我们的第一次尝试中,我们看到当 Merlin 使用策略 f(x) = 1 时,g(x) = (x, 1),因此我们有 h(x, 0) = (x, 1)。现在我们只需要定义当我们将 y = 1 传递给 h 时会发生什么。如果我们想让 h 是可逆的,我们需要 h(x, 1) 被分配给除了 (x, 1) 之外的其他值。一种方法是将 h(x, y) 定义为 (x, ¬y),这样 h(x, 1) = (x, 0) ≠ (x, 1)。这个选择特别方便,因为应用 H 两次会带我们回到原始输入,即 h(h(x, y)) = h(x, ¬y) = (x, ¬¬y) = (x, y)。如果这有点啰嗦,请查看图 8.7 中这个论点的视觉表示。

图 8.7 第二次尝试:h(x, y),这是一个可逆的,并且具有相同数量输入和输出的函数
现在我们知道了如何从每个策略制作一个可逆的经典函数,让我们通过制作一个量子程序来完成我们的可逆函数。在 one 的情况下,我们看到 h 翻转了它的第二个输入,h(x, y) = (x, ¬y)。因此,我们可以编写一个量子程序,它简单地通过翻转两个输入量子比特中的第二个来实现与我们的可逆经典函数相同的功能。正如我们在第四章中看到的,我们可以使用 X 指令来做这件事,因为 X|x〉 = |¬x〉。
8.3.2 推广我们的结果
更一般地,我们可以通过翻转不可逆函数 f 的输出位来精确地以我们构建可逆经典函数 h 的相同方式构建一个可逆的量子操作。我们可以为 f 的每个输入状态定义单位矩阵(即真值表的量子对应物)U[f]。图 8.8 显示了如何进行这种定义的比较。

图 8.8 从不可逆的经典函数构建可逆的经典函数和幺正矩阵。在左侧,我们可以看到,如果我们跟踪我们提供给不可逆函数的输入,我们可以从一个不可逆的经典函数构建一个可逆的经典函数。我们可以通过有两个量子比特寄存器来描述量子操作的单位矩阵,一个寄存器跟踪输入,另一个持有不可逆函数的输出。
以这种方式定义 U[f] 使得取消对 f 的调用变得容易,因为应用 U[f] 两次给出恒等矩阵 𝟙(即“无操作”指令的单位矩阵)。当我们通过将函数 f 有条件地应用于量子状态标签来以这种方式定义单位矩阵时,我们称这种新操作为 oracle。
定义:一个预言机是一个由单位矩阵U[f]表示的量子操作,它将其输入状态转换为

符号⊕代表常规布尔逻辑中的异或运算符。
剩下的就是确定我们需要发送什么序列的指令来实现每个单位操作U[f]。我们已经看到了实现one预言机的指令:对第二个量子比特执行一个X指令。现在让我们看看如何为其他可能的f函数编写预言机。这样,梅林就会知道无论他的策略是什么,他都应该做什么。
深入探讨:为什么叫它预言机?
到目前为止,我们已经看到了一些量子计算从其物理历史中继承的有趣命名例子,如 bra、ket 和传送。然而,喜欢玩点小乐趣的不仅仅是物理学家!理论计算机科学的一个分支,称为复杂性理论,探索了在给定不同类型的计算机器的情况下,理论上可以高效完成的事情。例如,你可能听说过“P与NP”问题,这是复杂性理论中的一个经典难题,它询问P类问题是否像NP类问题一样难以解决。复杂度类P是那些存在算法可以在多项式时间内回答的问题的集合。相比之下,NP是那些我们可以在多项式时间内检查一个潜在答案的集合,但我们不知道是否可以在多项式时间内从头开始提出一个答案。
复杂性理论中的许多其他问题都是通过引入小的游戏或故事来提出的,以帮助研究人员记住在哪里使用哪些定义。我们关于梅林和尼姆的这个小故事是对这一传统的致敬。事实上,量子计算中最受赞誉的故事之一被称为MA,代表“梅林-亚瑟”。在MA类中的问题被认为是一个故事,其中亚瑟可以向全能但不可信的巫师梅林提出一系列问题。如果一个 yes/no 决策问题在MA中,那么每当答案是“yes”时,都存在梅林可以给亚瑟的证明,亚瑟可以使用一个P机器和一个随机数生成器轻松检查。
“预言机”这个名字很适合这种讲故事的方式,因为任何复杂度类A都可以通过允许A机器在单步中解决一个B问题而转变为一个新的复杂度类A^B,就像它们在咨询一个预言机一样。德鲁什-约萨问题等类似问题的历史大部分源于试图理解量子计算如何影响计算复杂性,因此许多命名惯例和术语已被纳入量子计算。
更多关于复杂性理论及其与量子计算、黑洞、自由意志和希腊哲学的关系的信息,请参阅斯科特·阿伦森(Cambridge University Press,2013 年)所著的《自从德谟克利特以来的量子计算》。
通常,从单位矩阵开始找到一系列指令是一个数学上困难的问题,被称为 单位合成。尽管如此,在这种情况下,我们可以通过将 Merlin 的策略 f 中的每一个都代入我们的 U f 定义中,并确定哪些指令会产生那种效果来解决这个问题——我们可以像将 one 函数转换为预言机那样猜测和检查。让我们为 zero 函数尝试一下。
练习 8.1:尝试编写一个预言机!
如果 f 是 zero,预言机操作 (U[f]) 会是什么?
解决方案:让我们一步一步来解决这个问题:
-
从 U[f] 的定义中,我们知道 U[f] |xy〉 = |x〉 |y ⊕ f(x)〉。
-
将
zero替换为 f,f(x) = 0,我们得到 U[f] |xy〉 = |x〉|y ⊕ 0〉。 -
我们可以使用 y ⊕ 0 = y 来进一步简化这个表达式,得到 U[f] |xy〉 = |x〉|y〉。
-
到目前为止,我们注意到 U[f] 对其输入状态没有任何作用,因此我们可以通过——什么都不做来实现它。
练习解决方案
本书所有练习的解决方案都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,打开提及练习解决方案的 Jupyter 笔记本。
函数 f = id 比零和一的情况稍微复杂一些,因为 y ⊕ f(x) 不能简化为不依赖于 x。如表 8.7 总结所示,我们需要 U[f] |x〉 |y〉 = |x〉 |y ⊕ f(x)〉 = |x〉 |y ⊕ x〉。也就是说,我们需要预言机对输入状态(|x〉 |y〉)的作用是保持 x 不变,并用 x 和 y 的异或替换 y。
另一种思考这个输出的方式是回忆 y ⊕ 1 = ¬y,所以当 x = 1 时,我们需要翻转 y。这正是我们在第六章中定义的 CNOT(控制非)指令的方式,因此我们认识到当 f 是 id 时,U[f] 可以通过应用一个 CNOT 来实现。
这就剩下如何定义 f = not 的预言机了。正如 id 的预言机在输入(控制)量子比特处于 |1〉 状态时翻转输出(目标)量子比特一样,同样的论点告诉我们,我们需要我们的 not 预言机在输入量子比特处于 |0〉 时翻转第二个量子比特。最简单的方法是首先使用 X 指令翻转输入量子比特,然后应用一个 CNOT 指令,最后使用另一个 X 指令撤销第一次翻转。
提示:如果你想知道更多关于如何构建其他预言机和如何使用 QuTiP 来证明它们能完成你想要的功能,请查看附录 D。
为了回顾我们学习到的定义预言机的方法,我们将本节的所有工作收集在表 8.7 中。
表 8.7 每个一位函数 f 的预言机输出
| 函数名称 | 函数 | 预言机的输出 |
|---|---|---|
zero |
f(x) = 0 | |x〉|y ⊕ 0〉 = |x〉|y〉 |
one |
f(x) = 1 | |x〉|y ⊕ 1〉 = |x〉|¬y〉 |
id |
f(x) = x | |x〉|y ⊕ x〉 |
not |
f(x) = ¬x | |x〉|y ⊕ ¬x〉 |
uncompute trick:将函数转换为量子或然子
如此看来,似乎需要很多工作来为每个函数 f 设计 U[f]。幸运的是,有一个很好的技巧,它允许我们从相对简单的需求开始构建或然子。
回想一下,之前我们尝试定义一个可逆版本的 f,通过返回 (x, f(x)) 作为输出,当给定 (x, 0) 作为输入时。同样地,假设我们给定一个量子操作 V[f],它能够正确地将 |x〉|0〉 转换为 |x〉|f(x)〉。我们可以通过使用一个额外的量子比特并使用称为 uncompute trick 的技术,将 V[f] 调用两次来始终创建一个或然子 U[f],如图所示。

使用 uncompute trick 将仅在我们添加额外的 |0〉 输入量子比特时才工作的操作转换为可以作为一个或然子使用的操作
虽然这在 Deutsch–Jozsa 的情况下并不特别有帮助,但它表明或然子的概念非常通用,因为它通常更容易从一个形式为 V[f] 的操作开始。
注意:或然子构造也适用于多量子比特函数。作为一个思维练习,如果我们有一个函数 f(x[0], x[1]) = x[0] AND x[1],那么或然子 U[f] 将如何转换输入状态 |x[0] x[1]〉?我们将在后面的章节中看到如何编码这个或然子。
因此,我们使用了或然子表示来解决像 zero 和 one 这样的函数不能表示为旋转的问题。处理完这个问题后,我们可以继续编写 Nimue 用于挑战 Merlin 的其余算法。
深入探讨:将函数表示为或然子的其他方法
这并不是定义 U[f] 的唯一方法。Merlin 可以在 f(x) 为 one 时翻转 Nimue 的输入 x 的符号:

这种表示在某些情况下更有用,例如在梯度下降算法中。这些算法在机器学习中很常见,通过搜索函数变化最快的方向来最小化函数。更多信息,请参阅 Andrew Trask 的《Grokking Deep Learning》(Manning,2019)第 4.10 节。
在特定应用中选择正确的方法来表示经典信息,如量子算法中的子程序调用,是量子编程艺术的一部分。现在,我们将使用之前引入的 oracle 定义。
在手头有了 f 的或然子表示后,Deutsch–Jozsa 算法的最初几个步骤可以用我们之前用来编写 quicksort 的类似伪代码来编写:
-
准备两个标记为
control和target的量子比特,处于 |0〉 ⊗ |0〉 状态。 -
对
control和target量子比特应用操作,以准备以下状态:|+〉 ⊗ |−〉。 -
将或然子 U[f] 应用到输入状态 |+〉 ⊗ |−〉。回想一下,U[f] |x〉 |y〉 = |x〉 |y ⊕ f(x)〉。
-
在 X 基态测量
control量子比特。如果我们观察到 0,那么函数是常量的;否则,函数是平衡的。
小贴士测量基态的量子比特总是返回 0 或 1,就像我们在 Z 基态测量一样。回想第三章,如果量子比特的状态是|+〉,我们在 X 基态测量时总是得到 0,而如果量子比特是|−〉,我们总是得到 1。
图 8.9 展示了这些步骤。我们将在本章末尾看到这个算法为什么能工作,但现在让我们跳进去开始编程。为了做到这一点,我们将使用量子开发工具包提供的 Q#语言,因为这使得从源代码中看到量子算法的结构变得容易得多。

图 8.9 Deutsch–Jozsa 算法的步骤。我们首先制备|+−〉状态,然后查询或 acles(即我们向 Merlin 提问),然后测量控制量子比特以了解或 acles 代表的是常量函数还是平衡函数。
8.4 在 Q#中模拟 Deutsch–Jozsa 算法
在第七章中,我们尝试在 Q#程序中将操作作为参数传递。我们可以使用相同的方法,将操作作为或 acles 的输入来帮助预测 Nimue 的挑战结果。为此,回想一下,我们可以考虑这个问题的四种可能的函数,每个函数代表 Merlin 可能使用的策略(见表 8.8)。
表 8.8 将单比特函数表示为双比特或 acles
| 函数名称 | 函数 | 或 acles 的输出 | Q#操作 |
|---|---|---|---|
zero |
f(x) = 0 | |x〉|y ⊕ 0〉 = |x〉|y〉 | NoOp(control, target); |
one |
f(x) = 1 | |x〉|y ⊕ 1〉 = |x〉|¬y〉 | X(target); |
id |
f(x) = x | |x〉|y ⊕ x〉 | CNOT(control, target) |
not |
f(x) = ¬x | |x〉|y ⊕ ¬x〉 | X(control); CNOT(control, target); X(control); |
如果我们用将|x〉|y〉映射到|x〉|y ⊕ f(x)〉的量子操作(或 acles)来表示每个函数f(x),那么我们可以从图 8.3 中识别出每个函数zero、one、id和not。
四个或 acles 可以直接翻译成 Q#:
namespace DeutschJozsa {
open Microsoft.Quantum.Intrinsic;
operation ApplyZeroOracle(control : Qubit, target : Qubit) : Unit {
}
operation ApplyOneOracle(control : Qubit, target : Qubit) : Unit {
X(target);
}
operation ApplyIdOracle(control : Qubit, target : Qubit) : Unit {
CNOT(control, target);
}
operation ApplyNotOracle(control : Qubit, target : Qubit) : Unit {
X(control);
CNOT(control, target);
X(control);
}
}
我们就不能直接查看源代码吗?
在 Oracles.qs 中,我们为四个单量子比特或 acles ApplyZeroOracle、ApplyOneOracle、ApplyIdOracle和ApplyNotOracle编写了源代码。查看这些源代码,我们可以判断每个是常量还是平衡的,而无需调用它,那么我们为什么要担心 Deutsch–Jozsa 算法呢?从 Nimue 的角度思考,她不一定有 Merlin 用来对她量子比特执行操作的源代码。即使她有,Merlin 的方法是难以理解的,所以她可能无法轻易预测 Merlin 即使给出了他使用的源代码会做什么。
从实际角度来说,虽然很难对双量子比特的预言机进行太多模糊处理,但 Deutsch–Jozsa 算法展示了一种更通用的技术。例如,我们可能能够访问到一个操作的源代码,但提取关于该操作的问题答案可能是一个数学上或计算上困难的问题。所有加密哈希函数都通过设计具有这种属性,无论是用来确保文件正确下载,检查应用是否由开发者签名,还是作为通过挖掘冲突来扩展区块链的一部分。
在第十章中,我们将看到一个使用类似于在 Deutsch–Jozsa 算法中开发的技术来更快地询问此类函数的例子。
在 Q#中实现了这些预言机之后,我们可以编写整个 Deutsch–Jozsa 算法(以及 Nimue 的 Kingmaker 策略)!参见图 8.10 以刷新对 Deutsch-Jozsa 步骤的了解。

图 8.10 Deutsch–Jozsa 算法的步骤。我们首先准备|+−〉状态,然后查询预言机(即向 Merlin 提问),然后测量控制量子比特。
列表 8.2 Algorithm.qs:运行 Deutsch–Jozsa 的 Q#操作
operation CheckIfOracleIsBalanced(oracle : ((Qubit, Qubit) => Unit))
: Bool {
use control = Qubit();
use target = Qubit(); ❶
H(control); ❷
X(target);
H(target);
oracle(control, target); ❸
H(target); ❹
X(target);
return MResetX(control) == One; ❺
}
❶ 要求目标机器给我们两个量子比特,控制量子比特和目标量子比特,每个都从|0〉状态开始
❷ 在控制和目标量子比特上准备输入状态|+−〉 = (|00〉 − |01〉 + |10〉 − |11〉) / 2,如图 8.10 步骤 2 所示
❸ 调用作为输入参数给出的预言机。注意,预言机只调用一次!
❹ 我们知道目标量子比特仍然在|−〉状态,因此我们可以撤销 X(target); H(target);操作序列以重置目标量子比特。
❺ 测量控制量子比特是在|+〉还是|−〉状态,对应于X-基下的零或一的结果
与 Q#标准库提供的MResetZ操作一样,MResetX操作执行所需的X-基测量并将被测量的量子比特重置到|0〉状态。现在我们想要确保我们的实现是好的,所以让我们来测试它!
Q#中的测量结果
我们现在已经在 Q#中看到了MResetX和MResetZ操作,它们分别测量和重置量子比特在X-和Z-基下的状态。这两个操作都返回一个Result值,一开始可能会有些令人困惑。毕竟,X-基测量告诉我们我们是在|+〉或|−〉状态,那么为什么 Q#使用Zero和One这样的标签呢?
| 结果值 | X-基 | Z-基 |
|---|---|---|
| 零 | 〈+ | |
| 一 | 〈– |
我们将在稍后了解更多关于这一点,但简而言之,类型为Result的值告诉我们不同的指令对一个状态应用了(-1)的多少个相位。例如,Z|1〉 = –|1〉 = (–1)¹ |1〉,而X|–〉 = (–1)¹ |−〉。由于在这两种情况下,我们都是将(-1)提升到 1 次幂,当我们分别测量Z**-和X-基时,|1〉和|−〉被分配到One结果。同样,由于Z|0〉 = (–1)⁰|0〉,当我们测量Z时,我们将|0〉分配到Zero结果。
我们之前提到,尼姆乌希望尽可能少地了解人类事务。因此,她只让梅林对她的一次量子比特进行操作,我们称之为oracle(control, target)。尼姆乌从MResetX的调用中只得到一个经典比特的信息,这不足以让她区分id策略(梅林选择亚瑟作为国王的导师)和not策略(梅林选择莫德雷德作为导师)。
为了确保她仍然可以学习她真正关心的内容——梅林的战略是恒定还是平衡——我们可以使用 Q#标准库提供的Fact函数来测试我们的实现是否正常工作。Fact函数接受两个布尔变量作为前两个参数,检查它们是否相等,如果不相等,则发出一条消息。
提示稍后,我们将了解如何使用这些断言为量子库编写单元测试。
我们首先做的是将我们之前编写的ApplyZeroOracle操作作为zero函数的或然性。由于zero不是一个平衡函数,我们预计CheckIfOracleIsBalanced(ApplyZeroOracle)将返回false作为其输出;这个预期可以使用Fact函数来检查。
列表 8.3 Algorithm.qs:Q#操作测试 Deutsch–Jozsa
operation RunDeutschJozsaAlgorithm() : Unit {
Fact(not CheckIfOracleIsBalanced(ApplyZeroOracle),
"Test failed for zero oracle."); ❶
Fact(not CheckIfOracleIsBalanced(ApplyOneOracle),
"Test failed for one oracle."); ❷
Fact(CheckIfOracleIsBalanced(ApplyIdOracle),
"Test failed for id oracle.");
Fact(CheckIfOracleIsBalanced(ApplyNotOracle),
"Test failed for not oracle.");
Message("All tests passed!"); ❸
}
❶ 运行梅林使用零策略的情况下的 Deutsch–Jozsa 算法
❷ 对单一策略执行完全相同的事情,这次调用 CheckIfOracleIsBalanced(ApplyOneOracle)而不是
❸ 如果所有四个断言都通过了,那么我们可以确信,无论梅林使用哪种策略,我们的 Deutsch–Jozsa 算法的程序都是有效的。
如果我们使用%simulate魔法命令运行此程序,我们可以通过使用 Deutsch–Jozsa 算法来确认尼姆乌可以学习她想要了解的梅林战略的详细信息:
In [ ]: %simulate RunDeutschJozsaAlgorithm
All tests passed!
8.5 反思量子算法技术
呼——我们在这一步迈出了相当大的步伐:
-
我们已经使用经典可逆函数来模拟梅林的战略,以便我们可以将其编写为量子或然性。
-
我们已经使用 Q#和量子开发工具包实现了 Deutsch–Jozsa 算法,并测试了我们可以通过一次或然性调用来学习梅林的战略。
在这一点上,回顾我们在尼姆乌的量子湖中游泳时所学到的东西是有帮助的,因为我们在本章中使用的技巧将在本书的其余部分都有所帮助。
8.5.1 鞋子和袜子:应用和撤销量子操作
有助于反思的第一个模式可能是在Algorithm.qs中注意到的。让我们再次看看操作应用于target量子比特的顺序。
列表 8.4 来自 Deutsch–Jozsa 的target指令
// ...
X(target);
H(target);
oracle(control, target);
H(target);
X(target);
// ...
可以将这个序列视为X(target); H(target);指令将target准备在|−〉状态,而H(target); X(target);指令“取消准备”|−〉,将target返回到|0〉状态。我们必须反转顺序,因为通常所说的鞋子和袜子原则。如果我们想穿上鞋子和袜子,我们最好先穿上袜子;但如果我们想脱掉它们,我们需要先脱掉鞋子。参见图 8.11 说明此过程。

图 8.11 在脱鞋之前我们不能脱袜子。
Q#语言通过一个称为函子的功能使我们的代码进行鞋子和袜子之类的变换变得更容易。函子允许我们轻松地描述已经定义的操作的新变体。让我们直接进入一个示例,并介绍一个新的操作PrepareTargetQubit,它封装了X(target); H(target);序列。
列表 8.5 从列表 8.4 的状态准备
operation PrepareTargetQubit(target : Qubit)
: Unit is Adj { ❶
X(target);
H(target);
}
❶ 通过将“is Adj”作为签名的一部分来编写,我们告诉 Q#编译器自动计算此操作的逆操作——即伴随——。
我们可以使用编译器生成的逆操作,即 Q#提供的两个函子之一Adjoint(我们将在第九章看到另一个),来调用Adjoint。
列表 8.6 使用Adjoint关键字应用指令
PrepareTargetQubit(target);
oracle(control, target);
Adjoint PrepareTargetQubit(target); ❶
❶ Adjoint PrepareTargetQubit将Adjoint函子应用于PrepareTargetQubit,从而得到一个“撤销”PrepareTargetQubit的操作。按照鞋子和袜子思维,这个新操作首先调用Adjoint H(target);然后调用Adjoint X(target)。
自伴操作
在这种情况下,X和Adjoint X是相同的操作,因为翻转一个比特然后再翻转它总是让我们回到起点。换句话说,X撤销了X。同样,Adjoint H与H相同,所以前面的代码片段给出了H(target); X(target);的顺序。我们说指令X和H是self-adjoint的。
虽然不是所有操作都是其自身的伴随,但!例如,Adjoint Rz(theta, _)与Rz(-theta, _)是相同的操作。
在更实际的意义上,操作U上的Adjoint函子与反转或撤销U效果的运算相同。名称adjoint指的是单位矩阵U的共轭转置U^+。换句话说,U^+被称为U的adjoint。Q#中的Adjoint关键字保证,如果操作U由单位矩阵U描述,那么如果存在Adjoint U,它将由U^+描述。
执行指令的模式如此常用,以至于 Q# 标准库提供了 ApplyWith 操作来表达这种先执行后撤销操作的模式。
注意:ApplyWith 操作由 Microsoft.Quantum.Canon 命名空间在 Q# 标准库中提供。与其它语言的库类似,Q# 标准库提供了许多编写 Q# 程序所需的基本工具。随着你继续阅读本书,你会看到 Q# 标准库如何帮助使量子开发者的生活变得更轻松。
通过使用 ApplyWith 和部分应用,我们可以以紧凑的方式重写 CheckIfOracleIsBalanced 操作。
列表 8.7 ApplyWith 和部分应用帮助实现鞋子与袜子排序
H(control);
ApplyWith(PrepareTargetQubit, oracle(control, _), target);
set result = MResetX(control);
在这个示例中,ApplyWith 操作在 oracle(control, _ ) 完成后自动应用 PrepareTargetQubit 的伴随算符。注意 _ 被用来部分应用 oracle 到控制比特上。
让我们逐步展开列表 8.7,看看它是如何工作的。对 ApplyWith 的调用首先应用其第一个参数,然后应用其第二个参数,最后将第一个参数的伴随算符应用到最后一个参数提供的量子比特上。
列表 8.8 在列表 8.7 中展开 ApplyWith
H(control);
PrepareTargetQubit(target);
(oracle(control, _))(target);
Adjoint PrepareTargetQubit(target);
set result = MResetX(control);
第三行的部分应用可以通过将 target 替换为 _ 来替换。
列表 8.9 解决列表 8.8 中的部分应用
H(control);
PrepareTargetQubit(target);
oracle(control, target);
Adjoint PrepareTargetQubit(target);
set result = MResetX(control);
使用像 ApplyWith 这样的操作有助于在量子编程中重用常见的模式,尤其是在确保我们不会在大型量子程序中忘记取一个 Adjoint。
Q# 还提供了另一种使用语句块而不是传递操作来表示鞋子与袜子模式的方法。例如,我们可以使用 within 和 apply 关键字来编写列表 8.7,如下所示。
列表 8.10 使用 within 和 apply 进行鞋子与袜子排序
H(control);
within {
PrepareTargetQubit(target);
} apply {
oracle(control, target);
}
set result = MResetX(control);
两种形式在不同的上下文中都可能很有用,所以请随意使用最适合您的方法!
8.5.2 使用 Hadamard 指令翻转控制比特和目标比特
我们可以使用上一节中的鞋子与袜子般的思维方式来改变在 CNOT 等指令中扮演控制比特和目标比特角色的量子比特。为了理解这是如何工作的,重要的是要记住量子指令会转换它们所作用的寄存器的整个状态。在像 Deutsch–Jozsa 算法这样的情况下,控制比特可以通过对控制比特和目标比特同时应用门来受到影响——而不仅仅是目标比特。这是一个更一般模式的例子:当我们用 X-基而不是 Z-(计算)基应用 CNOT 指令时,CNOT 操作的控制比特和目标比特会交换角色。
为了看到这一点,让我们看看如果使用H指令转换到X-基,应用CNOT指令,然后使用更多的H指令回到z -基会发生什么单位算子(量子真值表的经典对应物)。
列表 8.11 检查H是否翻转了CNOT的控制和目标
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> H = hadamard_transform()
>>> HH = qt.tensor(H, H)
>>> HH ❶
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper,
➥ isherm = True
Qobj data = ❷
[[ 0.5 0.5 0.5 0.5]
[ 0.5 -0.5 0.5 -0.5]
[ 0.5 0.5 -0.5 -0.5]
[ 0.5 -0.5 -0.5 0.5]]
>>> HH * qt.cnot(2, 0, 1) * HH ❸
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper,
➥ isherm = True
Qobj data = ❹
[[1\. 0\. 0\. 0.]
[0\. 0\. 0\. 1.]
[0\. 0\. 1\. 0.]
[0\. 1\. 0\. 0.]]
>>> qt.cnot(2, 1, 0) ❺
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper,
➥ isherm = True
Qobj data =
[[1\. 0\. 0\. 0.]
[0\. 0\. 0\. 1.] ❻
[0\. 0\. 1\. 0.]
[0\. 1\. 0\. 0.]]
❶ 定义一个简写来表示模拟 H(control); H(target);序列的单位算子H ⊗ H是有帮助的。
❷ 观察单位算子H ⊗ H,我们看到|00〉被转换成(|00〉 + |01〉 + |10〉 + |11〉) / 2,这是对所有四个计算基状态的平均叠加。
❸ 给出表示 H(control); H(target); CNOT(control, target); H(control); H(target);的单位算子。我们可以将这个指令序列视为在Z-基而不是X-基上应用 CNOT。
❹ 这个序列的单位算子看起来有点像 CNOT,但有一些行被翻转了。发生了什么?
❺ 为了尝试弄清楚将H指令应用于每个量子比特对CNOT指令做了什么,让我们看看CNOT(target, control)的单位算子。
❻ 在调用 CNOT 指令时翻转控制量子比特和目标量子比特的角色,将给我们与使用H指令在X-基上应用 CNOT 指令时完全相同的单位算子。
图 8.12 展示了我们刚刚运行的 Python 代码的视觉表示。

图 8.12 使用 Hadamard 指令改变 CNOT 指令的控制和目标量子比特。通过在每个量子比特上应用 Hadamard,并在 CNOT 之前和之后应用,我们可以翻转控制量子比特和目标量子比特的角色。
从之前的计算中,我们可以得出结论,CNOT(target, control)与H(control); H(target); CNOT(control, target); H(control); H(target);完全相同。与H翻转了X-和Z-基的角色一样,H指令可以在使用量子比特作为控制或目标之间翻转。
8.6 相位回弹:我们成功的关键
考虑到这些技术,我们现在可以探索德-约萨算法是如何工作的:一种称为相位回弹的量子编程技术。这项技术允许我们编写CheckIfOracleIsBalanced操作,使其适用于几个不同的预言机,同时只揭示我们想要知道的一个比特(是否梅林扮演了一个好的导师)。
为了了解德-约萨算法如何使用相位回弹在一般情况下工作,让我们回到我们的三种思考方式,并使用数学来预测当我们调用任何预言机时会发生什么。回想一下,我们定义了预言机U[f],它是从每个经典函数f构建的,使得对于所有经典比特x和y,U[f] |xy〉 = |x〉|f(x) ⊕ y〉。
小贴士:在这里,我们使用x和y来表示经典比特,这些比特标记了两个量子比特的状态。这是使用计算基来推理量子程序行为的另一个例子。
让我们以与我们在 QuTiP 程序中相同的方式开始,将输入状态|+−〉 = |+〉 ⊗ |−〉在计算基中展开。首先展开控制量子比特的状态,我们有|+−〉 = |+〉 ⊗ |−〉 = (|0〉 + |1〉) / √2 ⊗ |−〉 = (|0−〉 + |1−〉) / √2。像之前一样,我们可以使用 QuTiP 来检查我们的数学。
列表 8.12 使用 QuTiP 检查(|0−〉 + |1−〉) / √2 = |+−〉
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> from numpy import sqrt
>>> H = hadamard_transform()
>>> ket_0 = qt.basis(2, 0) ❶
>>> ket_1 = qt.basis(2, 1)
>>> ket_plus = H * ket_0
>>> ket_minus = H * ket_1
>>> qt.tensor(ket_plus, ket_minus)
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[ 0.5]
[-0.5]
[ 0.5]
[-0.5]]
>>> (
... qt.tensor(ket_0, ket_minus) +
... qt.tensor(ket_1, ket_minus)
... ) / sqrt(2)
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[ 0.5] ❷
[-0.5]
[ 0.5]
[-0.5]]
❶ 从有用的缩写符号开始
❷ 两个向量相同,这告诉我们(|0−〉 + |1−〉) / √2 是|+−〉的另一种写法。
接下来,正如我们在第二章中看到的,我们可以使用线性来预测U[f]如何转换这个输入状态。
矩阵回顾
在本节中,我们隐式地使用了线性,当我们使用矩阵来模拟 Deutsch–Jozsa 算法的工作原理时。正如第二章所述,矩阵是线性函数的一种写法。
由于U[f]是一个幺正矩阵,我们知道对于任何状态|Ψ〉和|ϕ〉以及任何数α和β,U[f] (α|Ψ〉 + β|ϕ〉) = α U[f] |Ψ〉 + β U[f] |ϕ〉]。使用这个性质与计算基一起,我们有|+−〉和(|0−〉 + |1−〉) / √2 是相同的状态,同样U[f] |+−〉和U[f] (|0−〉 + |1−〉) / √2 也是相同的状态。
小贴士使用我们的多量子比特状态缩写,|+−〉 = |+〉 ⊗ |−〉,|0−〉 = |0〉 ⊗ |−〉,和|1−〉 = |1〉 ⊗ |−〉。
图 8.13 给出了线性的视觉表示。

图 8.13 应用线性来理解我们的预言机如何转换输入状态
这样写,我们并不立即清楚通过将U[f]应用于|0−〉和|1−〉我们获得了什么优势。让我们通过提取控制(第一个)量子比特来考虑对目标量子比特的影响,来看看预言机操作是如何应用于第一项的。
这样做,我们再次使用线性来通过一次传递一个状态到我们的预言机来理解U[f]的工作原理。正如我们在第二章中学到的,线性是一个非常强大的工具,它让我们能够将甚至相当复杂的量子算法分解成我们可以更容易理解和分析的部分。在这种情况下,我们可以通过线性(即,将|0−〉分解成|00〉和|01〉的叠加)来理解U[f]对|0−〉的作用。

例如,如果我们考虑zero函数,那么f(0) = 0。因此,|f(0)〉 = |0〉和|¬f(0)〉 = |1〉,所以U[f] |0−〉 = |0−〉。
另一方面,如果f(0) = 1,那么U[f] |0−〉 = |0〉 ⊗ (|1〉 − |0〉) / √2 = –|0–〉。也就是说,U[f]翻转了|0−〉的符号。
小贴士(|1〉 − |0〉) / √2 也可以写成–|–〉,或者写成*X|–〉。
注意,U[f]根据f(0)的值要么旋转目标量子比特的 X,要么不旋转:

如果控制量子比特处于|1〉状态,我们可以使用之前相同的技巧来理解U[f]的作用。这样做,我们得到一个相位为(–1)(*f*(1))而不是(–1)(f(0)),因此U[f] |1−〉 = (–1)^(f(1)) |1−〉。
再次使用线性来组合控制量子比特的两个状态,我们现在知道当控制量子比特处于|+〉状态时,U[f]如何转换两个量子比特的状态:

最后一步是注意,正如我们在第四章中看到的,我们不能观察到全局相位。因此,我们可以将(–1)^(f(0))提取出来,用f(0) ⊕ f(1)(这是我们最初感兴趣的问题)来表示输出状态,如图 8.14 所示。

图 8.14 计算 Deutsch–Jozsa 算法的最后几个步骤。通过写出算子对|+−〉状态的作用,我们可以看到在最后测量控制量子比特时,算子是否代表常数或平衡函数。
如果f(0) ⊕ f(1) = 0(常数),则输出状态是|+−〉;但如果f(0) ⊕ f(1) = 1(平衡),则输出状态是|–〉。通过一次调用U[f],我们可以了解f是否为常数或平衡,即使我们不知道对于任何特定的输入x,f(x)*是什么。
当我们将U[f]应用于输入量子比特处于|+〉状态时,我们可以这样思考发生的事情:输入量子比特的状态代表我们对f提出的问题。如果我们提出|0〉问题,我们得到答案是f(0),而如果我们提出|1〉问题,我们得到答案是f(1)。然后|+〉问题告诉我们关于f(0) ⊕ f(1)的信息,而不告诉我们关于f(0)或f(1)单独的信息。
然而,当我们以这种叠加方式提问时,"输入"和"输出"的角色并不像经典情况下那样立即明确。特别是,|0〉和|1〉输入都会使输出量子比特翻转,而|+〉输入会使输入量子比特翻转,前提是我们从|−〉状态开始输出量子比特。一般来说,像U[f]这样的双量子比特操作会改变它们所作用的量子比特的全部空间——我们将输入和输出分开是解释U[f]作用的一种方式。
输入量子比特的状态根据输出量子比特中定义的变换而改变的事实是相位回波的一个例子,这是 Deutsch–Jozsa 算法使用的量子效应。在接下来的两章中,我们将使用相位回波来探索新的算法,例如用于量子传感和量子化学模拟的算法。
深入研究:扩展 Deutsch–Jozsa
虽然在这里我们只考虑了一比特输入的函数,但 Deutsch–Jozsa 算法对于函数的任何大小的输入/输出都只需要一个查询。
要编码一个双量子比特函数f(x[0], x[1]),我们可以引入一个三量子比特预言机U[f] |x[0]x[1]y〉 = |x[0] x[1]〉 ⊗ |f(x[0], x[1]) ⊕ y〉。例如,考虑f(x[0], x[1]) = x[0] ⊕ x[1]。这个函数是平衡的,因为f(0, 0) = f(1, 1) = 0,但f(0, 1) = f(1, 0) = 1。当我们对三量子比特状态|++−〉 = (|00〉 + |01〉 + |10〉 + |11〉) ⊗ |−〉应用U[f]时,我们得到(|00〉 − |01〉 − |10〉 + |11〉) ⊗ |−〉 = |−−−〉。使用 X 基测量,我们可以从像f(x[0], x[1]) = 0 这样的常数函数中区分出来,这将给我们一个输出|−〉。
只要我们承诺f要么是常数要么是平衡的,无论f作为输入接受多少位,相同的模式都成立:我们可以通过一次调用U[f]来学习关于f行为的一个数据位。这简直是 O(1)!如果你不熟悉大 O 符号,请参阅 Aditya Bhargava 的《Grokking Algorithms》(Manning,2016 年)。
在下一章中,我们将通过研究相位估计算法如何使量子传感器等衍生技术成为可能,来构建我们在本章学到的技能。
摘要
-
量子算法是一系列步骤,我们可以遵循这些步骤来使用量子计算机解决问题。我们可以通过在 Q#中编写量子程序来实现量子算法。
-
Deutsch-Jozsa 算法是量子算法的一个例子,它使我们能够用比任何可能的经典算法更少的资源来解决计算问题。
-
如果我们要将经典函数嵌入到量子算法或程序中,我们需要以可逆的方式进行。我们可以构建一种特殊的量子操作,称为预言机,它允许我们表示应用于量子数据的经典函数。
-
Deutsch-Jozsa 算法允许我们通过仅调用一次该预言机来测试一个一位预言机的两个输出是否相同或不同;我们不是学习任何特定的输出,而是直接学习我们感兴趣的全球属性。
-
Deutsch-Jozsa 算法展示了在其他量子算法中也常见的“鞋袜”模式。我们通常需要应用外层操作,然后应用内层操作,最后撤销(或取外层操作的伴随算符)。
-
相位回弹技术使我们能够将量子操作施加的相位与控制量子比特而不是目标量子比特相关联。我们将在接下来学习的算法中看到更多这方面的内容。
9 量子传感:不仅仅是相位
本章涵盖
-
如何通过相位回弹学习关于未知操作的有用信息
-
在 Q# 中创建新类型
-
在 Python 主程序中运行 Q# 代码
-
识别本征态和相的重要性质和行为
-
在 Q# 中编程受控量子操作
在上一章中,我们在 Q# 中实现了我们的第一个量子算法,德-约萨算法。通过帮助 Nimue 和 Merlin 播演国王制造者,我们看到了量子编程技术,如相位回弹,如何帮助我们解决问题。在本章中,我们将探讨我们可以在量子程序中使用的 相位估计 算法来解决不同类型的问题。再次回到 Camelot;这次,我们将使用 Lancelot 和 Dagonet 之间的游戏来展示手头的任务。
9.1 相位估计:利用量子比特的有用性质进行测量
在整本书中,我们看到了游戏可以是有助于学习量子计算概念的。例如,在上一章中,Nimue 与 Merlin 的游戏让我们探索了我们的第一个量子算法:德-约萨算法。在本章中,我们将使用另一个游戏来发现如何使用 相位回弹 学习量子状态,这是德-约萨和许多其他量子算法使用的量子发展技术。
对于本章的游戏,让我们回顾一下 Lancelot 一直在做什么。当 Nimue 和 Merlin 决定国王的命运时,我们发现 Lancelot 和宫廷小丑 Dagonet 正在玩一个猜谜游戏。由于他们已经玩了一段时间,Dagonet 觉得无聊,并想“借用”Nimue 的量子工具,让他们的游戏更有趣。
9.1.1 部分应用
对于 Dagonet 的新游戏,而不是让 Lancelot 猜一个数字,Dagonet 让 Lancelot 通过用不同的输入调用它来猜测量子操作对一个单比特的作用。鉴于所有单比特操作都是旋转,这对游戏来说很适用。Dagonet 选择一个特定轴的旋转角度,Lancelot 可以输入一个数字到 Dagonet 的操作中,这将改变 Dagonet 应用的缩放旋转。Dagonet 选择哪个轴并不重要,因为游戏的目的是猜测旋转角度。为了方便起见,Dagonet 的旋转总是围绕 Z 轴。最后,Lancelot 可以测量量子比特,并使用他的测量结果来猜测 Dagonet 的原始旋转角度。参见图 9.1,以下步骤的流程图:
-
Dagonet 为单比特旋转操作选择一个秘密角度。
-
Dagonet 为 Lancelot 准备了一个操作,这个操作隐藏了秘密角度,并允许 Lancelot 多输入一个数字(我们将它称为比例),这个数字将与秘密角度相乘,以给出操作的总体旋转角度。
-
兰斯洛特在游戏中的最佳策略是选择许多尺度值,并估计每个值的测量
One的概率。为此,他需要为每个许多尺度值执行以下步骤多次:-
准备|+〉状态,并在达贡特的旋转中输入尺度值。他使用|+〉状态,因为他知道达贡特是围绕Z轴旋转的;并且对于这个状态,这些旋转将导致他可以测量的局部相位变化。
-
在准备好每个|+〉状态后,兰斯洛特可以使用秘密操作旋转它,测量量子比特,并记录测量结果。
-
-
兰斯洛特现在有了与他尺度因子相关的数据,以及他测量该尺度因子为
One的概率。他可以在脑海中拟合这些数据,并从拟合的参数中得到达贡特的角(他确实是这片土地上最伟大的骑士)。我们可以使用 Python 来帮助我们完成同样的工作!

图 9.1 达贡特和兰斯洛特游戏的步骤。达贡特在一个操作中隐藏一个秘密旋转角度,兰斯洛特必须找出这个角度。
注意,这确实是一个游戏,因为兰斯洛特无法仅通过一次测量直接测量这个旋转。如果他能够做到,这将违反不可克隆定理,他将超越物理定律。作为圆桌骑士团的骑士,兰斯洛特不仅受职责和荣誉的约束,还受物理定律的约束,所以他必须按照规则玩达贡特的这个游戏。
深入探讨:使用哈密顿学习学习轴
在本章中,我们关注的是已知达贡特旋转轴的情况,但我们需要学习他的角度。这种情况对应于物理学中一个常见的问题,即我们被要求学习磁场中量子比特的拉莫进动。学习拉莫进动不仅对构建量子比特有用;它还允许检测非常小的磁场并构建非常精确的传感器。
然而,更普遍地说,我们可以学习比单个旋转角度更多的内容。当轴也未知时的情况是一个称为哈密顿学习的一般类型问题的例子,这是量子计算中的一个研究领域。在哈密顿学习中,我们使用与本章中探索的游戏非常相似的游戏来重建量子比特或量子比特寄存器的物理模型。
让我们跳入使用 Q#原型化这个游戏。能够访问 Q#标准库的不同部分将很有帮助,因此我们可以从在 operations.qs 文件的顶部添加以下open语句开始。
列表 9.1 operations.qs:打开游戏中的 Q#命名空间
namespace PhaseEstimation { ❶
open Microsoft.Quantum.Intrinsic; ❷
open Microsoft.Quantum.Convert as Convert; ❸
open Microsoft.Quantum.Measurement as Meas; ❹
open Microsoft.Quantum.Arrays as Arrays; ❺
// ...
❶ Q#文件中的所有开放语句都紧跟在命名空间声明之后。
❷ 如前所述,打开 Microsoft.Quantum.Intrinsic 为我们提供了访问所有基本指令(R1、Rz、X 等等)的权限,我们可以将这些指令发送到量子设备。
❸ 在打开命名空间时,我们也可以给它们起别名,类似于我们在导入时可以给 Python 包和模块起别名。在这里,我们缩写 Microsoft.Quantum.Convert,以便以后我们可以通过在它们前面加上 Convert 前缀来使用该命名空间中的类型转换函数。
❹ 我们可以将上一章中的 MResetZ 操作作为 Meas.MResetZ 提供出来,以记录操作的来源。
❺ 在本章中我们需要打开的最后一个命名空间是 Microsoft.Quantum.Arrays,它提供了用于处理数组的实用函数和操作。
列表 9.2 展示了 Dagonet 需要实现旋转的量子操作的示例。与其他旋转一样,我们的新旋转操作返回Unit(空元组()的类型),表示操作没有有意义的返回值。对于操作的真正主体,我们可以通过将 Dagonet 隐藏的角度angle与兰斯洛特的尺度因子scale相乘来找到旋转的角度。
列表 9.2 operations.qs:设置游戏的操作
operation ApplyScaledRotation(
angle : Double, scale : Double, ❶
target : Qubit)
: Unit is Adj + Ctl { ❷
R1(angle * scale, target); ❸
}
❶ 要玩猜谜游戏,我们需要一个量子操作,它接受两个经典参数:一个是 Dagonet 传递的,另一个是兰斯洛特传递的。
❷ 签名中的 Adj + Ctl 部分表示此操作支持我们在第八章首次看到的伴随函子,以及我们将在本章后面看到的受控函子。
❸ 这里的旋转操作 R1 几乎与我们之前见过的 Rz 操作完全相同。当添加受控函子时,R1 和 Rz 之间的区别将变得重要。
注意:当在单独的文件中编写 Q#(即不是从 Jupyter Notebook 中)时,所有操作和函数都必须在命名空间内定义。这有助于保持我们的代码整洁,并使我们的代码与我们在量子应用程序中使用的各种库中的代码发生冲突的可能性降低。为了简洁,我们通常不显示命名空间声明,但完整的代码始终可以在本书的示例存储库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。
要查看 Dagonet 设置游戏的视觉表示,请参阅图 9.2。

图 9.2 使用部分应用隐藏秘密角度,在玩 Dagonet 的猜谜游戏。兰斯洛特得到的占卜者有一个用于他的尺度参数的输入,然后他可以选择使用该操作的目标机器,但不能“窥视”操作以查看秘密角度。
深入探讨:为什么不是直接测量角度?
看起来兰斯洛特必须跳过很多圈才能猜测达戈内特隐藏的角度。毕竟,除了职责和荣誉之外,还有什么能阻止兰斯洛特传递一个比例尺为 1.0 并直接从其量子比特上应用的相位中读取角度呢?结果证明,在这种情况下,不可克隆定理再次发挥作用,告诉我们兰斯洛特永远无法从单一测量中学习到一个相位。
最简单的方法是暂时假设兰斯洛特能够做到这一点,然后看看会发生什么。假设达戈内特隐藏了一个 π/2 的角度,而兰斯洛特将他的量子比特准备在 |+〉 = 1 / √2 (|0〉 + |1〉) 状态。如果兰斯洛特将 1.0 作为他的比例尺,他的量子比特最终会处于 1 / √2 (|0〉 + i |1〉) 状态。如果兰斯洛特能够直接测量相位 e^(iπ/2) = +i 来从单一测量中猜测达戈内特的角度,他就可以使用这一点来准备另一个 1 / √2 (|0〉 + i |1〉) 状态的副本,即使兰斯洛特不知道正确的基来测量。实际上,如果达戈内特隐藏的角度是 π,兰斯洛特的神奇测量设备也应该起作用,在这种情况下,兰斯洛特最终会得到一个处于 1 / √2 (|0〉 − |1〉) 状态的量子比特。
换一种说法,如果兰斯洛特能够通过直接测量相位来计算出达戈内特的角,他就能为任何角度 ϕ 制作形式为 1 / √2 (|0〉 + e^(iϕ) |1〉) 的任意状态的副本,而无需事先知道 ϕ。这相当严重地违反了不可克隆定理,因此我们可以安全地得出结论,兰斯洛特需要做更多的工作才能赢得达戈内特的比赛。
一旦我们以这种方式定义了一个操作,达戈内特就可以使用我们在第八章中首次看到的 Q# 的部分应用功能来隐藏他的输入。然后兰斯洛特得到一个可以应用于他的量子比特的操作,但不会让他直接看到他试图猜测的角度。
使用 ApplyScaledRotation,达戈内特可以轻松地为兰斯洛特创建一个可以调用的操作。例如,如果达戈内特选择了角度 0.123,他可以通过给兰斯洛特提供操作 ApplyScaledRotation(0.123, _, _) 来“隐藏”它。就像第七章中部分应用的例子一样,_ 表示未来输入的槽位。
如图 9.3 所示,由于 ApplyScaledRotation 的类型是 (Double, Double, Qubit) => Unit is Adj + Ctl,只提供第一个输入会导致一个类型为 (Double, Qubit) => Unit is Adj + Ctl 的操作。这意味着兰斯洛特可以提供一个类型为 Double 的输入,一个他想要对其应用操作的量子比特,并使用 Adjoint 和我们在第六章中看到的函子。

图 9.3 部分应用 ApplyScaledRotation 以为兰斯洛特创建一个操作
我们能在语法中看到角度的值并不意味着兰斯洛特可以看到。实际上,兰斯洛特对一个部分应用的操作或函数能做的事情只有调用它、进一步部分应用它,或者将其传递给另一个函数或操作。从兰斯洛特的角度来看,ApplyScaledRotation(0.123, _, _) 是一个黑盒。多亏了这个部分应用的小技巧,他只会得到一个接受他的缩放值并可用于旋转量子比特的操作。
我们可以通过给兰斯洛特的操作类型命名来简化我们的生活,这个命名比 (Double, Qubit) => Unit is Adj + Ctl 更容易阅读。在下一节中,我们将学习 Q# 如何让我们注释我们使用的类型签名,以便玩达贡特和兰斯洛特的猜谜游戏。
9.2 用户定义类型
我们已经看到类型在 Q# 中扮演的角色,尤其是在函数和操作的签名中。我们也看到函数和操作都是元组输入、元组输出。在本节中,我们将学习如何在 Q# 中构建自己的类型,以及为什么这可能很有用。
在 Q#(以及许多其他语言)中,一些类型被定义为语言本身的一部分:例如我们之前见过的 Int、Qubit 和 Result 类型。
提示:要获取这些基本类型的完整列表,请参阅 Q# 语言文档中的docs.microsoft.com/azure/quantum/user-guide/language/typesystem/#primitive-types。
从这些基本类型出发,我们可以在类型后添加 [] 来创建数组类型。例如,在本章的游戏中,我们可能需要输入一个表示兰斯洛特对达贡特的多次操作的数组,即 double 类型的数组。我们可以使用 Double[] 来表示 Double 值的数组。
列表 9.3 定义长度为 10 的 Double 数组类型
let scales = EmptyArray<Double>(10); ❶
❶ EmptyArray 来自于 Microsoft.Quantum.Arrays 命名空间。确保在运行此代码之前将其打开。
我们也可以使用 newtype 语句在 Q# 中定义自己的类型。这个语句允许我们声明新的用户定义类型(UDTs)。使用 UDTs 的两个主要原因是:
-
方便
-
传达意图
第一个原因是出于方便的考虑。有时函数或操作的类型签名可能会变得相当长,因此我们可以定义自己的类型作为简写。我们可能想要命名我们的类型的另一个原因是传达意图。比如说,我们的操作接受两个 Double 值的元组,代表一个复数。定义一个新的类型 Complex 可以提醒我们和我们的队友这个元组代表什么。量子开发工具包提供了几个不同的函数、操作和用户定义类型(UDTs),例如以下示例,它定义了类型 Complex。
列表 9.4 在 Q# 运行时中复数的定义
namespace Microsoft.Quantum.Math { ❶
newtype Complex = ( ❷
Real: Double,
Imag: Double
);
}
❶ Complex 数在 Microsoft.Quantum.Math 命名空间中作为 UDT 实现。我们可以通过在量子应用程序中包含语句“open Microsoft.Quantum.Math;”来使用此类型。
❷ Complex 类型定义为两个 Double 值的元组,其中第一个元素命名为 Real,第二个元素命名为 Imag。
小贴士:量子开发工具包是开源的,所以如果你好奇,你总是可以查找 Q# 语言、运行时、编译器和标准库的各个部分是如何工作的。例如,Complex UDT 的定义位于 Q# 运行时仓库中的 src/Simulation/QSharpFoundation/Math/Types.qs 文件,网址为 github.com/microsoft/qsharp-runtime。
如图 9.4 所示,有两种方式可以从 UDT 中获取不同的项。我们可以使用命名项与 :: 操作符一起,或者使用 ! 操作符“展开”UDT,以回到被 UDT 包装的原始类型:
function TakesComplex(complex : Complex) : Unit {
let realFromNamedItem = complex::Real; ❶
let (real, imag) = complex!; ❷
}
❶ 由于 Complex UDT 是使用名为 Real 的命名项定义的,我们可以通过 ::Real 访问该项,以获取输入的实部。
❷ 或者,由于 Complex 被定义为包装一个类型为 (Double, Double) 的元组,unwrap 操作符 ! 将我们带回到复数的实部和虚部,而不需要 UDT 包装器。

图 9.4 使用 :: 和 ! 操作符与 UDT。我们可以将 UDT 视为带标签的数据元组。:: 操作符允许我们通过名称访问数据,而 ! 操作符将 UDT 展开为其基础类型。
小贴士:使用 UDT 的两种方式在不同的场景中都很有用,但在这本书中,我们大多数时候会坚持使用命名项和 :: 操作符。
一旦定义了新的 UDT,它也可以作为实例化该类型新实例的一种方式。例如,Complex 类型作为一个函数,通过输入两个 Double 值的元组来创建一个新的复数。这与 Python 类似,在 Python 中,类型也是创建该类型实例的函数。
列表 9.5 使用 UDT Complex 创建复数
let imaginaryUnit = Complex(0.0, 1.0); ❶
❶ 使用 newtype 定义 UDT 也定义了一个与该类型同名的新函数,该函数返回我们新 UDT 的值。例如,我们可以将 Complex 作为具有两个 Double 输入的函数调用,这些输入代表新 Complex 值的实部和虚部。在这里,我们定义了一个表示 0 + 1.0i(Python 中的 0+1j)的 Complex 值,也称为虚数单位。
练习 9.1:策略的 UDT
在第四章中,我们使用 Python 类型注解来表示 CHSH 游戏中的策略概念。Q# 中的 UDT 可以以类似的方式使用。尝试定义一个新的 UDT 用于 CHSH 策略,然后使用我们新的 UDT 来包装第四章中的常量策略。
提示:我们的和 Eve 的策略部分可以分别表示为接受 Result 并输出 Result 的操作:即 Result => Result 类型的操作。
练习解答
本书所有练习题的解决方案都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,然后打开提及练习解决方案的 Jupyter 笔记本。
对于本章中的游戏,我们定义了一个新的 UDT,既是为了标记我们打算如何使用它,也是为了方便地表示 Lancelot 在猜测游戏中获得的操作类型。在下面的列表中,我们可以看到这个新类型ScalableOperation的定义,它是一个包含一个名为Apply的命名输入的元组。
列表 9.6 operations.qs:量子猜测游戏的设置
newtype ScalableOperation = ( ❶
Apply: ((Double, Qubit) => Unit is Adj + Ctl) ❷
);
function HiddenRotation(hiddenAngle : Double)
: ScalableOperation { ❸
return ScalableOperation(
ApplyScaledRotation(hiddenAngle, _, _) ❹
);
}
❶ 我们可以通过使用 newtype 语句并给出我们新类型的名称以及定义新类型基于的基本类型来声明一个新的 UDT(用户定义类型)。
❷ 我们可以使用与声明操作或函数签名相同的语法为 UDT 中的各种项命名。在这里,新的 UDT 有一个名为Apply的单个项,它允许调用被ScalableOperation包装的操作。
❸ 一旦定义,我们就可以像使用任何其他类型一样使用新的 UDT。在这里,我们定义了一个输出ScalableOperation类型值的函数。
❹ 我们可以通过调用 ScalableOperation 来轻松创建输出值,这个操作将被我们新的 UDT 包装。在这个例子中,我们可以使用我们在本章前面看到的相同部分应用ApplyScaledRotation来创建新的ScalableOperation实例。
提示:当我们定义 Q#中函数和操作的输入时,这些输入的名称以小写字母开头。然而,在列表 9.6 中,ScalableOperation中的命名项Apply以大写字母开头。这是因为函数或操作的输入只在其可调用范围内有意义,而命名项则具有更广泛的意义。我们可以使用输入和命名项的大小写来明确它们的定义位置。
列表 9.6 中定义的函数HiddenRotation帮助我们通过为 Dagonet 隐藏他的角度提供了一种方式来实现 Lancelot 和 Dagonet 的游戏。使用 Dagonet 的角度调用HiddenRotation会返回一个新的ScalableOperation,Lancelot 可以调用它来收集他猜测隐藏角度所需的数据。图 9.5 展示了 Dagonet 新的游戏设置。

图 9.5:使用部分应用和 UDT 玩 Dagonet 的猜测游戏。Dagonet 使用一个函数来构造他传递给 Lancelot 的 UDT,代表他的隐藏角度的旋转。
在有了新的类型和 Dagonet 隐藏他的角度的方法之后,让我们继续实现游戏的其余部分!我们已经为下一步做好了准备:估计 Lancelot 和 Dagonet 游戏中我们进行的每次测量的概率。这与估计抛硬币的概率非常相似;见图 9.6。

图 9.6 兰斯洛特的估计类似于估计抛硬币的结果。他可以通过多次抛硬币并计数“正面”来估计硬币的偏差。同样,兰斯洛特可以多次准备处于相同状态的相同量子比特,每次都对其进行测量,并计数测量结果。
让我们看看在达贡特的游戏中,这个抛硬币估计过程在代码中会是什么样子。注意,由于兰斯洛特和达贡特同意 Z- 轴应该是他们游戏的旋转轴,兰斯洛特可以准备他的目标量子比特处于 |+〉 状态,以便达贡特的旋转能够发挥作用。
列表 9.7 operations.qs:估计测量 |1〉 的概率
operation EstimateProbabilityAtScale(
scale : Double, ❶
nMeasurements : Int, ❷
op : ScalableOperation) ❸
: Double { ❹
mutable nOnes = 0; ❺
for idx in 0..nMeasurements - 1 {
use target = Qubit(); ❻
within { ❼
H(target); ❽
} apply {
op::Apply(scale, target); ❾
}
set nOnes += Meas.MResetZ(target) == One
? 1 | 0; ❿
}
return Convert.IntAsDouble(nOnes) / ⓫
Convert.IntAsDouble(nMeasurements);
}
❶ 兰斯洛特的操作需要接收一个 Double 类型的值,表示他选择的缩放值来运行达贡特给他的操作。
❷ 兰斯洛特选择测量他的量子比特的次数,以得到他对概率的估计。
❸ 最后一个输入是 ScalableOperation 类型,本章前面声明的 UDT。这个输入代表达贡特给兰斯洛特的操作。
❹ 作为输出,兰斯洛特希望返回一个估计的概率,因此我们可以将其声明为一个 Double 类型的输出。
❺ 为了跟踪到目前为止观察到的 |1〉 结果的数量,我们定义一个具有 Int 值 0 的可变变量。
❻ 对于每次测量,我们需要分配一个量子比特,它是达贡特操作的靶子。
❼ 使用我们在第八章中学到的“鞋子与袜子”模式中的“within”和“apply”关键字
❽ 通过使用 H 操作来准备处于 |+〉 状态的量子比特来实现兰斯洛特的策略
❾ 准备好达贡特操作的输入后,我们通过使用 ::Apply 来调用它,以展开 ScalableOperation UDT。
❿ ?| 三元运算符(类似于 Python 中的 if ... else 运算符或在 C、C++ 和 C# 中的 ?: 运算符)提供了一个方便的方式来增加 nOnes。
⓫ 为了得到测量 |1〉 的概率的最终估计,我们计算“1”的数量与总计数之比。函数 Convert.IntAsDouble 帮助我们返回一个浮点数。
在列表 9.7 中,within/apply 块确保兰斯洛特的量子比特回到了正确的轴。我们可以通过将 1 或 0 添加到 nOnes 中来计数最终测量返回 One 结果的次数。在这里,?| 三元运算符(类似于 Python 中的 if ... else 运算符或在 C、C++ 和 C# 中的 ?: 运算符)提供了一个方便的方式来增加 nOnes。
任何其他名称的操作
你可能已经注意到,操作通常使用动词命名,而函数通常使用名词命名。这有助于记住你在第七章中看到的区别:函数 是 某物,而操作 做 某事。与名称的一致性可以帮助你理解量子程序的工作方式,因此 Q# 在整个语言和库中使用了这样的约定。
在此基础上,我们现在可以编写一个运行整个游戏并返回 Lancelot 需要猜测 Dagonet 隐藏角度的所有内容的操作。
列表 9.8 operations.qs:运行整个游戏
operation RunGame(
hiddenAngle : Double, scales : Double[], nMeasurementsPerScale : Int
) : Double[] {
let hiddenRotation = HiddenRotation(hiddenAngle); ❶
return Arrays.ForEach( ❷
EstimateProbabilityAtScale( ❸
_,
nMeasurementsPerScale,
hiddenRotation
),
scales ❹
);
}
❶ 使用我们之前编写的 HiddenRotation 函数创建一个新的 ScalableOperation 值,以隐藏 Dagonet 的角度
❷ Microsoft.Quantum.Arrays 中的 ForEach 操作(我们给它简称为 Arrays)接收一个操作并将其应用于 scales 数组的每个元素。
❸ 要获取传递给 ForEach 的操作,我们使用部分应用来锁定 Lancelot 在每个不同尺度上进行的测量次数以及 Dagonet 给他的隐藏操作。
❹ 当我们将“scales”作为 ForEach 的第二个输入时,将“scales”的每个元素替换到部分应用的槽位 _ 中。
注意:当 Q# 也拥有 Microsoft.Quantum.Arrays.Mapped 时,ForEach 在 Python 和其他语言中表现得像 map,这可能会显得有些奇怪。关键的区别在于 ForEach 接收一个操作,而 Mapped 接收一个函数。
为了让 Lancelot 真正理解他从 Q# 程序中获取的所有数据,使用一些经典的数据科学技术可能会有所帮助。由于 Python 在这方面非常出色,从 Python 主程序运行我们的新 RunGame 操作可以是一个帮助 Lancelot 的绝佳方式。
9.3 运行,蛇行,跑:从 Python 运行 Q#
在前面的章节中,我们在带有 Q# 内核的 Jupyter Notebook 中运行了我们的 Q# 代码。在本章中,我们想探讨另一种运行 Q# 代码的方法:从 Python。从 Python 调用 Q# 在不同的场景中可能很有帮助,尤其是如果我们想在 Q# 中使用数据之前先预处理数据,或者如果我们想可视化量子程序的结果。
让我们开始编写实现 Dagonet 和 Lancelot 游戏的文件。为了尝试 Q# 和 Python 的互操作性,我们将使用一个 Python 宿主程序来运行 Q# 程序。这意味着我们将为游戏有两个文件:operations.qs 和一个 host.py 文件,我们将直接使用它来运行游戏。让我们深入 host.py 文件,看看我们如何从 Python 与 Q# 交互;见图 9.7。

图 9.7 使用 Python 编写的宿主程序来运行 Q#。像 Jupyter Notebook 宿主一样,Python 程序可以协调将 Q# 程序发送到特定的目标机器并收集结果。
在 Python 和 Q# 之间,我们需要的所有互操作性功能都由 qsharp Python 包提供。
提示:附录 A 包含 qsharp Python 包的完整安装说明。
一旦我们有了 qsharp 包,我们就可以像导入其他 Python 包一样导入它。让我们看看一个小型的 Python 文件,我们可以看到这个动作是如何发生的。
列表 9.9 qsharp-interop.py:直接在 Python 中使用 Q# 代码
import qsharp ❶
prepare_qubit = qsharp.compile(""" ❷
open Microsoft.Quantum.Diagnostics; ❸
operation PrepareQubit(): Unit { ❹
using (qubit = Qubit()) {
DumpMachine();
}
}
""")
if __name__ == "__main__":
prepare_qubit.simulate() ❺
❶ qsharp Python 包需要像其他 Python 包一样导入。
❷ 使用 qsharp.compile Python 函数将包含 Q# 代码的字符串编译成 Python 文件中使用的格式
❸ 正如常规的 Q# 文件一样,我们需要包含打开语句来使用 Q# 标准库的不同部分。
❹ 这个 Q# 代码字符串描述的操作只是将量子比特准备在 |0〉 状态,并使用 DumpMachine 显示目标机器对该量子比特的了解。
❺ 我们还希望使用定义为 prepare_qubit 的可调用函数,这样我们就可以使用 qsharp 包中的 simulate 方法,该方法运行之前编译的 Q# 代码片段。
让我们尝试运行 9.9 列表中的 qsharp-interop.py 脚本。
列表 9.10 运行 9.9 列表
$ python qsharp-interop.py
Preparing Q# environment...
# wave function for qubits with ids (least to most significant): 0
|0〉: 1.000000 + 0.000000 i == ******************** [ 1.000000 ]
➥ --- [ 0.00000 rad ]
|1〉: 0.000000 + 0.000000 i == [ 0.000000 ]
提示:如果您从 Q# Jupyter Notebook 运行代码,Q# 片段的输出将看起来不同。请参见本章后面的图 9.9 中的示例。
从 9.10 列表中的输出中,我们可以看到它确实准备了一个 |0〉 状态,因为输出中唯一具有系数 1.0 的项是 |0〉 状态。
qsharp Python 包也会在包含我们的 Python 程序的同一目录中查找定义在 *.qs 文件中的 Q# 操作或函数。在这种情况下,随着我们继续本章的其余部分,我们将向名为 operations.qs 的 Q# 文件中添加内容。这是一种开始游戏 host.py 文件相当方便的方法。加载的 qsharp 包然后允许我们从与 host.py 同一目录中的 Q# 文件命名空间导入操作和函数。我们之前看到了 RunGame,很快我们也会看到 RunGameUsingControlledRotations。
列表 9.11 host.py:相位估计游戏的开始
import qsharp ❶
from PhaseEstimation import RunGame, RunGameUsingControlledRotations ❷
from typing import Any ❸
import scipy.optimize as optimization
import numpy as np
BIGGEST_ANGLE = 2 * np.pi
❶ 导入 Q# Python 包。
❷ 从 operations.qs 中导入 RunGame 和 RunGameUsingControlledRotations 操作,以自动创建代表我们导入的每个 Q# 操作的 Python 对象。
❸ 其余的导入有助于 Python 中的类型提示、可视化 Q# 模拟的结果以及将测量数据拟合以获得 Lancelot 的最终猜测。
现在我们已经导入并设置了我们的 Python 文件,让我们编写 run_game_at_scales:调用 Q# 操作的函数。
列表 9.12 host.py:调用 Q# 操作的 Python 函数
def run_game_at_scales(scales: np.ndarray,
n_measurements_per_scale: int = 100,
control: bool = False
) -> Any: ❶
hidden_angle = np.random.random() * BIGGEST_ANGLE ❷
print(f"Pssst the hidden angle is {hidden_angle}, good luck!")
return ( ❸
RunGameUsingControlledRotations
if control else RunGame
).simulate( ❹
hiddenAngle=hidden_angle,
nMeasurementsPerScale=n_measurements_per_scale,
scales=list(scales)
)
❶ 将返回类型提示设置为 Any,这告诉 Python 不要担心检查此函数的返回值类型。
❷ Dagonet 选择了他希望 Lancelot 猜测的隐藏角度。
❸ run_game_at_scales 的返回值取决于控制条件,这允许我们在为这个游戏开发的两个模拟之间进行选择(我们现在使用 control=False)。
❹ 当 qsharp 导入这些操作时,它们的 Python 表示形式有一个名为“simulate”的方法,该方法接受所需的参数并将它们传递给 Q# 模拟器。
此 Python 文件应可作为脚本运行,因此我们还需要定义__main__。这就是我们可以在 Python 中使用宿主程序执行兰斯洛特在心中所做的事情,通过测量和刻度并拟合它们到 Dagonet 的旋转模型。旋转角度如何改变测量结果的最佳模型由以下公式给出,其中θ是 Dagonet 的隐藏角度,scale是 Dagonet 的缩放因子:

练习 9.2:重生
如果我们使用波恩规则,就可以找到这个模型!第二章的定义如下。看看你是否可以用 Python 绘制结果值作为兰斯洛特刻度的函数。你的图表看起来像三角函数吗?
测量概率|状态> = (〈测量|状态〉)|²
提示: 对于兰斯洛特的测量,波恩规则中的〈测量|部分由〈1|给出。在测量之前,他的量子比特处于状态HR[1](θ * scale)*H|0〉。你可以通过使用 Q#参考中的矩阵形式来模拟 QuTiP 中的R1操作,docs.microsoft.com/qsharp/api/qsharp/microsoft.quantum.intrinsic.r1。
一旦我们有了这个模型和数据,我们就可以使用 SciPy Python 包中的scipy.optimize函数将我们的数据拟合到模型。它找到的θ参数值是 Dagonet 的隐藏角度!下一个列表显示了如何将这些全部组合起来的示例。
列表 9.13 host.py:以脚本方式运行 host.py
if __name__ == "__main__":
import matplotlib.pyplot as plt ❶
scales = np.linspace(0, 2, 101) ❷
for control in (False, True): ❸
data = run_game_at_scales(scales, control=control) ❹
def rotation_model(scale, angle): ❺
return np.sin(angle * scale / 2) ** 2
angle_guess, est_error = optimization.curve_fit( ❻
rotation_model, scales, data, BIGGEST_ANGLE / 2,
bounds=[0, BIGGEST_ANGLE]
)
print(f"The hidden angle you think was {angle_guess}!")
plt.figure() ❼
plt.plot(scales, data, 'o')
plt.title("Probability of Lancelot measuring One at each scale")
plt.xlabel("Lancelot's input scale value")
plt.ylabel("Lancelot's probability of measuring a One")
plt.plot(scales, rotation_model(scales, angle_guess))
plt.show() ❽
❶ 此脚本绘制数据和拟合结果,因此我们需要导入友好的 matplotlib 库。
❷ 兰斯洛特的输入列表(即他的刻度)由 np.linspace 生成的等间距、顺序数字列表生成。
❸ 此脚本运行游戏模拟的两个版本,以便进行比较。现在不用担心 control = True 的情况;我们很快就会回到这个问题。
❹ 在 run_game_at_scales 中存储从 Python 运行的 Q#模拟的结果
❺ 表示对量子比特的操作。兰斯洛特可以获取数据,将其拟合到模型中,并提取角度的猜测。
❻ 标准的 scipy 函数 optimization.curve_fit 接受一个函数模型、输入、测量数据和初始猜测,以尝试拟合模型的全部参数。
❼ 验证由 optimization.curve_fit 找到的拟合结果非常重要,这样我们就可以绘制数据和拟合模型,看看它是否看起来正确。
❽ 在新窗口中显示数据和拟合的图表
现在我们已经有一个可以运行整个游戏的宿主程序,我们可以看到兰斯洛特在推断达戈内特在其 Q#操作中隐藏的角度方面做得相当合理。通过进行不同的测量并使用经典的数据科学技术,兰斯洛特可以估算达戈内特操作对其量子比特施加的相位。运行 host.py 应该会生成两个弹出窗口,显示测量概率作为兰斯洛特两个可用策略的缩放函数的图表(图 9.8)。第一个是我们已经概述的方法。我们将在本章的最后部分实现后者。

图 9.8 当我们运行 host.py 时应该弹出的两个图表之一
小贴士 由于 SciPy 拟合包并不完美,有时它找到的拟合参数可能不正确。运行几次,希望拟合算法在下一次会做得更好。如果你对绘图包matplotlib有任何疑问,请查看 Manning 出版的这些其他标题:数据科学训练营(第二章;由 Leonard Apeltsin 编写;即将出版),以及使用 Python 和 Dask 进行数据科学(第七章和第八章;由 Jesse C. Daniel 编写;2019 年)。
从这些图表中我们可以看出,我们能够相当好地拟合兰斯洛特的数据。这意味着我们在angle_guess中找到的拟合值是达戈内特隐藏角度的一个相当好的近似值!
尽管如此,兰斯洛特策略中还有一个令人烦恼的问题:每次他进行测量时,他都需要准备正确的输入传递给达戈内特的操作。在这个特定的游戏中,这可能不是什么大问题,但当我们探索下一章中这个游戏的更大应用时,每次都正确准备输入寄存器可能会很昂贵。幸运的是,我们可以使用控制操作一次又一次地重复使用相同的输入,正如我们将在本章的其余部分看到的那样。
你已经看到了控制操作的例子(如 CNOT),但结果证明,许多其他量子操作也可以条件性地应用,这可以非常有用。控制操作,以及我们需要的最后一个新量子计算概念(本征态),将帮助我们实现我们在第八章末看到的技巧:相位回弹。
小贴士 在接下来的几节中有很多关于局部和全局相位的讨论。回想一下,全局相位是一个可以从我们状态的所有项中提取的复系数,且无法观察到。如果你需要关于相位的复习,请查看第 4-6 章。
9.4 本征态和局部相位
到目前为止,我们已经看到 X 量子操作允许我们翻转比特(|0〉
|1〉),而 Z 操作允许我们翻转相位(|+〉
|−〉)。然而,这两个操作都只对某些输入状态应用全局相位。正如我们在前面的章节中看到的,我们实际上无法了解全局相位,因此了解每个操作留下哪些状态不变对于理解应用该操作可以学到什么至关重要。
例如,让我们回顾一下 Z 操作。在下面的列表中,我们可以看到当我们尝试使用 Z 来翻转处于 |+〉 和 |−〉 状态之间的 qubit,而不是在输入 qubit 处于 |0〉 状态时会发生什么。
列表 9.14 将 Z 应用于处于 |0〉 状态的 qubit
use qubit = Qubit(); ❶
Z(qubit); ❷
DumpRegister((), [qubit]); ❸
Reset(qubit); ❹
❶ 在 Q# 中,我们通常首先通过使用语句分配一个 Qubit。这提供了一个新的处于 |0〉 状态的 qubit。
❷ 应用一个 Z 操作,使得 qubit 的状态转换为 Z|0〉 = |0〉
❸ 为了确认 Z 操作没有做任何事情,我们使用 DumpRegister 函数来指示模拟器打印出完整的态矢量。
❹ 在释放 qubit 之前重置它。这不是严格必要的,因为我们事先知道 qubit 仍然处于 |0〉 状态。
在列表 9.14 中,我们可以通过使用 DumpRegister 函数来指示模拟器打印出所有诊断信息(在这种情况下,完整的态矢量)来确认 Z 操作没有做任何事情。图 9.9 显示了这种诊断打印输出的样子。
提示:如果我们在一个不是模拟器的目标机器上运行此操作,我们得到的是一个状态矢量,而不是机器提供的其他诊断信息(例如,硬件 ID)。

图 9.9 列表 9.14 的输出
注意,在列表 9.14 中,Z 对 qubit 没有任何作用,因为 Z|0〉 = |0〉。如果我们修改列表以通过在 Z 前使用 X 来准备 |1〉,我们会看到非常相似的情况。
列表 9.15 将 Z 应用于处于 |1〉 状态的 qubit
use qubit = Qubit();
X(qubit); ❶
Z(qubit); ❷
DumpRegister("1.txt", [qubit]); ❸
Reset(qubit);
❶ 如前所述,为了准备 |1〉 状态,我们可以使用 |1〉 = X|0〉。
❷ 重复我们上面的实验,但使用不同的输入
❸ 如前所述,我们可以将 qubit 的状态写入一个文本文件,因为我们正在运行一个内部保持状态的模拟器。
输出如下:
# wave function for qubits with ids (least to most significant): 0
|0〉: 0.000000 + 0.000000 i == [ 0.000000 ]
|1〉: -1.000000 + 0.000000 i == ******************** [ 1.000000 ]
➥ --- [ 3.14159 rad ] ❶
❶ 此文件表示向量 [[0], [–1]],或狄拉克表示法中的 –|1〉。
将 Z 操作应用于 |1〉 态的效果是翻转 qubit 的状态符号。这是另一个 全局相位 的例子,正如我们在第六章和第八章中看到的。
当两个状态 |Ψ〉 和 |ϕ〉 仅通过一个复数 e^(iθ) 不同时,|ϕ〉 = e^(iθ) |Ψ〉,我们说 |Ψ〉 和 |ϕ〉 通过一个 全局相位 变化。例如,|0〉 和 –|0〉 通过全局相位 –1 = e^(iπ) 不同。
状态的全局相位不会影响任何测量概率,因此我们永远无法检测到当输入处于 |0〉 或 |1〉 状态时是否应用了 Z 操作。我们可以通过使用 AssertQubit 操作来确认这一点,该操作检查特定测量结果的概率。
列表 9.16 使用 AssertQubit 检查测量结果
use qubit = Qubit();
AssertQubit(Zero, qubit); ❶
Z(qubit); ❷
AssertQubit(Zero, qubit); ❸
Message("Passed test!"); ❹
Reset(qubit);
❶ 检查测量量子比特返回的结果是否为零,如果不是,则终止程序
❷ 之后,量子比特处于 –|0〉 状态,而不是 |0〉 状态。也就是说,Z 操作对该量子比特的状态应用了一个全局相位。
❸ 再次调用 AssertQubit 以检查获得零结果的概率仍然是 1
❹ 打印一条消息以检查量子程序是否通过了这两个断言
运行此代码片段仅简单地打印 Passed test!,因为当断言成功时,对 AssertQubit 的调用并不做任何事情。使用这种断言,我们可以编写使用模拟器来确认我们对特定量子程序行为的理解的单元测试。在实际的量子硬件上,由于克隆定理,我们无法进行此类检查,因此可以安全地删除断言。
重要的断言可以是编写单元测试和检查量子程序正确性的非常有用的工具。尽管如此,重要的是要记住,当我们在实际的量子硬件上运行程序时,它们将被删除,所以我们不使用断言来 使 程序运行正确。
当然,这同样是良好的编程实践;在像 Python 这样的经典语言中,断言有时可以因为性能原因而被禁用,这样我们就不能依赖断言始终存在。
通过识别操作 U 分配给量子态的全局相位的量子态,我们获得了一种理解该量子操作行为的方法。我们称这些状态为操作 U 的 本征态。如果两个操作具有相同的本征态,并且对每个本征态应用相同的全局相位,那么我们无法区分这两个操作——就像两个经典函数具有相同的真值表一样,无论我们在应用每个操作时量子比特处于何种状态,我们都无法判断哪个是哪个。这意味着我们不仅可以通过它们的矩阵表示来理解操作,还可以通过理解它们的本征态以及操作应用于每个本征态的全局相位来理解操作。正如我们所见,我们无法直接了解量子比特的全局相位;因此,在下一节中,我们将学习如何使用操作的受控版本将全局相位转换为可测量的局部相位。然而,现在,让我们用一个更正式的定义来总结什么是本征态。
如果在应用操作 U 之后,量子比特寄存器 qs 的状态仅通过全局相变而修改,那么我们说该寄存器的状态是 U 的 本征态。例如,|0〉 和 |1〉 都是 Z 操作的本征态。同样,|+〉 和 |−〉 都是 X 的本征态。
尝试下一项练习来练习使用本征态。
练习 9.3:诊断练习
尝试编写使用 AssertQubit 和 DumpMachine 的 Q# 程序来验证以下内容:
-
|+〉 和 |−〉 都是
X操作的本征态。 -
|0〉 和 |1〉 都是
Rz操作的本征态,无论你选择旋转的角度是多少。
为了更多的练习,尝试找出 Y 和 CNOT 操作的本征态,并编写一个 Q# 程序来验证你的猜测!
提示: 你可以使用 QuTiP 找到幺正操作的本征态的向量形式。例如,Y 操作的本征态由 qt.sigmay() .eigenstates() 给出。从那里,你可以使用你在第 4-6 章中学到的关于旋转的知识来确定哪些 Q# 操作可以准备这些状态。
不要忘记,你总是可以通过在 Q# 中编写快速测试来测试一个特定的状态是否是操作的本征态!
本征态是一个非常有用的概念,可以在各种量子计算算法中使用。我们将在下一节中使用它们,结合受控操作来实现一种称为 相位回弹 的量子开发技术,这是我们在第七章末介绍的。
深入探讨:这是非常合适的
本征态的名字来源于线性代数中一个广泛使用的基本概念,称为 本征向量。正如本征态是量子操作不改变的状态(即,最多应用全局相变),本征向量是乘以矩阵后仅按缩放因子保留的向量。也就是说,对于矩阵 A,如果对于某个数 λ,A v = λ v,那么 v 是 A 的本征向量。我们说 λ 是相应的 本征值。
前缀“eigen-”,德语中意为“合适的”或“特征的”,表明本征向量和本征值帮助我们理解矩阵的性质或特征。特别是,如果一个矩阵 A 与其共轭转置矩阵交换(即,如果 AA^† = A^†A),那么它可以被 分解 为投影到本征向量上的投影算符,每个算符都按其本征值缩放:
![equation_9-3.png]
由于这个条件对于幺正矩阵始终成立,我们可以通过将量子操作分解为其本征态和每个本征态上应用到的相来理解量子操作。例如,Z = |0〉〈0| − |1〉〈1| 和 X = |+〉〈+| − |−〉〈–|。
以这种方式分解矩阵的一个显著后果是,如果两个矩阵 A 和 B 有相同的本征向量和本征值,它们就是同一个矩阵。同样,如果两个操作可以用相同的本征态和本征相位来表示,那么这两个操作是无法区分的。
这种关于状态和操作思考方式常常帮助我们理解不同的量子计算概念。另一种思考本章你所工作的相位估计游戏的方式是将其视为学习每个本征态相关相位的算法!在第十章,你将看到这特别适合某些应用,例如学习化学系统的性质。
9.5 控制应用:将全局相位转换为局部相位
从我们所看到和可以测试的来看,状态的全局相位是不可观测的,而状态的局部相位是可以测量的。例如,考虑状态 1 / √2 (–i |0〉 –i |1〉) = –i / √2 (|0〉 + |1〉)。我们无法通过任何测量来区分该状态与 (|0〉 + |1〉) / √2。然而,我们可以通过一个局部相位来区分这两个状态与 (|0〉 − |1〉) / √2;也就是说,一个状态在 |1〉 前面有一个 +,而另一个状态有一个 –。
提示:如果你想要复习相位以及如何将其视为旋转,请参阅第四章和第五章。当你使用模拟器作为目标机器时,DumpMachine 和 DumpRegister 的输出也可以帮助你了解状态相位。
在上一节中,我们围绕本征态进行了实验,并看到本征态的全局相位可以携带关于一个操作的信息:我们可以称之为 U。如果我们想学习本征态的这种全局相位信息,那么我们似乎陷入了困境。如果兰斯洛特只准备 Dagonet 操作的本征态,他将永远无法了解 Dagonet 隐藏的角度。
量子算法来拯救!有一个非常有用的技巧我们可以应用,将操作 U 应用到的全局相位转换为由一个与之密切相关操作应用的局部相位。为了了解这是如何工作的,让我们回到 CNOT 操作。回想第六章,我们可以使用一个幺正矩阵来模拟 CNOT 操作:

当我们在第六章首次遇到这个矩阵时,我们使用了幺正矩阵与经典真值表之间的类比,来推断 CNOT 操作交换 |10〉 和 |11〉 状态,但将 |00〉 和 |01〉 状态的量子位保持不变。也就是说,CNOT 操作翻转其第二个量子位的状态,受第一个量子位的状态控制。如图 9.10 所示,我们可以将 CNOT 操作的幺正矩阵读作描述一种“量子如果”语句:“如果控制量子位处于 |1〉 状态,那么对目标量子位应用 X 操作。”

图 9.10 控制操作的幺正矩阵
要在 Q# 中使用 CNOT 操作,我们可以尝试以下代码。
列表 9.17 在 Q# 中使用 CNOT 操作
use control = Qubit();
use target = Qubit();
H(control); ❶
X(target); ❷
CNOT(control, target); ❸
DumpMachine();
Reset(control);
Reset(target);
❶ 准备控制量子比特为 |+〉
❷ 准备目标量子比特为 |1〉
❸ 应用 CNOT 并打印出模拟器的状态
通过将 CNOT 视为量子条件语句的类似物,我们可以更直接地编写其幺正矩阵。特别是,我们可以将 CNOT 操作的幺正矩阵视为一种“分块矩阵”,我们可以使用第四章中看到的张量积构建它:

练习 9.4:验证 CNOT 矩阵
验证 |0〉 〈0| ⊗ 𝟙 + |1〉 〈1| ⊗ X 与前面的方程式相同。
提示: 您可以使用 NumPy 的 np.kron 函数或 QuTiP 的 qt.tensor 函数手动验证此操作。如果您需要复习,请查看第六章中如何模拟量子隐形传态,或者查看第八章中 Deutsch–Jozsa 算法的推导。
我们可以按照这种模式构造其他操作,例如 CZ (受控-Z) 操作:

与 CNOT 操作类似,它执行与 X 操作相同的功能(应用位翻转),但受控于另一个量子比特的状态,当其控制量子比特处于 |1〉 状态时,CZ 操作会翻转相位,就像 Z 操作一样。图 9.10 展示了这种操作的一个示例。让我们通过编写一些 Q# 代码来尝试 CZ 操作,看看如何在实际中控制 Z。
列表 9.18 测试 Q# 操作 CZ
use control = Qubit();
use target = Qubit();
H(control); ❶
X(target); ❷
CZ(control, target); ❸
DumpRegister("cz-output.txt", [control, target]);
Reset(control);
Reset(target);
❶ 准备控制量子比特为 |+〉
❷ 准备目标量子比特为 |1〉
❸ 应用 CZ 并保存结果状态
输出如下:
# wave function for qubits with ids (least to most significant): 0;1
|0〉: 0.000000 + 0.000000 i == [ 0.000000 ]
|1〉: 0.000000 + 0.000000 i == [ 0.000000 ]
|2〉: 0.707107 + 0.000000 i == *********** [ 0.500000 ]
➥ --- [ 0.00000 rad ]
|3〉: -0.707107 + 0.000000 i == *********** [ 0.500000 ]
➥--- [ 3.14159 rad ]
如果我们运行此代码,cz-output.txt 中的内容将显示 [control, target] 寄存器的最终状态为 UCZ |+1〉 = |−1〉。
练习 9.5:验证 CZ 输出
要么手动验证,要么使用 QuTiP,确保前面的输出与 |–1〉 = |−〉 ⊗ |1〉 相同。
如果量子比特的顺序被交换,但除此之外答案正确,请注意 DumpMachine 使用 小端 表示法来排序状态。在小端表示法中,|2〉是 |01〉的简写,而不是 |10〉。如果这看起来很令人困惑,请归咎于 x86 处理器架构。
即,基于 目标 的状态,控制 的状态发生了变化,就像我们在第八章中用 Deutsch–Jozsa 算法看到的那样!这是因为当 control 处于 |0〉 状态时,Z 应用到的相位与 control 处于 |1〉 状态时不同,这种现象称为 相位回弹。在第八章中,我们使用处于 |+−〉 状态的一对量子比特的相位回弹来判断 CNOT 操作是否已应用。在这里,我们已经看到我们可以使用 CZ 操作来了解 Z 操作施加的全局相位。
重要
尽管 |1〉 是 Z 操作的本征态,但 |+1〉 不是 CZ 操作的本征态。这意味着在 |+1〉 状态的寄存器上调用 CZ 会有可观察的效果!
相位回弹是一种常见的量子编程技术,因为它允许我们将本将是全局相位的部分转换为控制量子比特的 |0〉 和 |1〉 分支之间的相位。在 CZ 示例中,输入状态 |+〉|1〉 和输出状态 |–〉|1〉 都是乘积状态,这使得我们可以在不影响目标量子比特的情况下测量控制量子比特。
全局思考,局部学习相位
注意,|1〉 和 Z |1〉 = –|1〉 之间的全局相位差变成了 |1〉 和 U[CZ] |+1〉 = |−1〉 之间的局部相位差。也就是说,通过控制处于 |+〉 状态的量子比特上的 Z 指令,我们能够了解在没有控制的情况下本将是全局相位的情况。
使用 CZ 操作,我们可以实现相位回弹技术,将全局相位转换为局部相位,然后我们可以进行测量。
列表 9.19 使用 CZ 操作实现相位回弹
use control = Qubit();
use target = Qubit();
H(control); ❶
X(target);
CZ(control, target); ❷
if (M(control) == One) { X(control); } ❸
DumpRegister("cz-target-only.txt", [target]); ❹
Reset(target); ❺
❶ 准备控制量子比特在 |+〉 状态,目标量子比特在 |1〉 状态
❷ 应用 CZ 操作并保存结果状态。然而,在我们丢弃目标量子比特的状态之前,让我们先测量并重置控制量子比特。
❸ 有趣的事实:实际上,这就是 Q# 中重置操作是如何实现的。
❹ 现在让我们只丢弃目标状态。
❺ 我们已经重置了控制量子比特,所以在这里不需要再次重置。
这里是输出:
# wave function for qubits with ids (least to most significant): 1
|0〉: 0.000000 + 0.000000 i == [ 0.000000 ]
|1〉: -1.000000 + 0.000000 i == ******************** [ 1.000000 ]
➥--- [ 3.14159 rad ] ❶
❶ 如预期,目标量子比特保持在 |1〉 状态,准备好输入到另一个 CZ 操作中。
9.5.1 控制任何操作
回想一下兰斯洛特和达戈内特的比赛,如果我们能帮助兰斯洛特重用他传递给达戈内特操作的量子比特,这样他就不必每次都重新准备它,那将非常有用。幸运的是,使用受控操作实现相位回弹为我们提供了如何做到这一点的线索。特别是,当我们使用相位回弹在第七章和第八章中实现 Deutsch–Jozsa 算法时,目标量子比特在算法的开始和结束时都处于 |−〉 状态。这意味着兰斯洛特可以为他的每一轮游戏重用同一个量子比特,而不必每次都重新准备。这对 Deutsch–Jozsa 来说并不重要,因为我们只运行了一轮尼缪和梅林的游戏。但对于兰斯洛特在与达戈内特的游戏中获胜来说,这正是正确的技巧,所以让我们看看我们如何帮助他使用相位回弹。
问题在于,虽然相位回弹是我们作为量子开发者工具箱中的一个有用工具,但到目前为止,我们只看到了如何使用它与X和Z操作。我们知道在我们的游戏中,Dagonet 告诉兰斯洛特他将使用R1操作;我们能否用相位回弹来帮助这里?我们在上一节中实现相位回弹的模式只要求我们控制一个操作,因此我们需要一种控制 Dagonet 给兰斯洛特的op::Apply操作的方法。在 Q#中,这就像写Controlled op::Apply而不是op::Apply一样简单,多亏了Controlled函子。与第六章中的Adjoint函子类似,Controlled是一个 Q#关键字,它修改了操作的行为:在这种情况下,将其转换为它的受控版本。
提示:就像is Adj表示一个操作可以与Adjoint一起使用一样,操作类型中的is Ctl表示它可以与Controlled函子一起使用。为了表示一个操作支持两者,我们可以写is Adj + Ctl。例如,X操作的类型是(Qubit => Unit is Adj + Ctl),这让我们知道X既是可逆的也是可控制的。
因此,为了帮助兰斯洛特,我们可以将op::Apply(scale, target)行更改为Controlled op::Apply([control], (scale, target)),这样我们就有了R1的受控版本。
虽然这解决了兰斯洛特的问题,但更深入地了解底层发生的事情可能会有所帮助。任何幺正操作(即不分配、释放或测量量子位的量子操作)都可以被控制,就像我们控制Z操作得到CZ,以及我们控制X得到CNOT一样。例如,我们可以定义一个受控-受控-NOT(CCNOT,也称为 Toffoli)操作,它接受两个控制量子位,并且当两个控制都在|1〉状态时翻转其目标。从数学上讲,我们写出CCNOT操作将输入状态|x〉|y〉|z〉转换为输出|x〉|y〉|z XOR (y AND z)〉。我们还可以编写一个矩阵,让我们模拟CCNOT操作:

同样,受控-SWAP 操作(也称为 Fredkin 操作)将其输入状态从|1〉|y〉|z〉转换为|1〉|z〉|y〉,并且当第一个量子位处于|0〉状态时,其输入保持不变。
提示:我们可以用三个CCNOT操作制作一个受控-SWAP:CCNOT(a, b, c); CCNOT(a, c, b); CCNOT(a, b, c);等价于Controlled SWAP([*a*], (b, c));。为了看到这一点,请注意,我们也可以用三个CNOT操作制作不受控的SWAP操作,原因是我们可以用一系列三个经典 XOR 操作在原地交换两个经典寄存器。
我们可以将这种模式推广到任何幺正操作 U(即任何不分配、释放或测量其量子比特的操作)。在 Q# 中,使用 Controlled 函子执行的操作会给表示哪些量子比特用作控制器的操作添加一个新的输入。
提示:这正是 Q# 作为元组-元组语言的事实非常有用的地方。由于每个操作恰好需要一个输入,对于任何操作 U,Controlled U 将 U 的原始输入作为其第二个输入。
CNOT 和 CZ 操作只是对 Controlled 的适当调用的简写。表 9.1 展示了更多这种模式的示例。
提示:就像 Adjoint 在其类型中有 is Adj 的任何操作上工作一样(如我们在第八章中看到的),Controlled 函子在具有 is Ctl 的任何操作上工作。
表 9.1 Q# 中控制操作的几个示例
| 描述 | 速记 | 定义 |
|---|---|---|
| 控制非 | CNOT(control, target) |
Controlled X([control], target) |
| 控制控制-NOT (Toffoli) | CCNOT(control0, control1, target) |
Controlled X([control0, control1], target) |
| 控制交换 (Fredkin) | n/a | Controlled SWAP([control], (target1, target2)) |
| 控制 Y | CY(control, target) |
Controlled Y([control], target) |
| 控制相位 | CZ(control, target) |
Controlled Z([control], target) |
正如我们在 CZ 示例中所看到的,以这种方式控制操作使我们能够将全局相位(如应用于本征态的相位)转换为我们可以通过测量学习的相对相位。
更多的是,通过使用控制旋转将相位反弹到控制寄存器,我们还可以反复重用相同的靶量子比特。当我们对 Z 的本征态中的目标寄存器应用 CZ 时,尽管控制寄存器发生了变化,但目标寄存器仍然保持在相同的状态。在本章的其余部分,我们将看到如何利用这一事实来完成 Lancelot 与 Dagonet 小游戏的策略。
9.6 实现 Lancelot 在相位估计游戏中的最佳策略
现在我们已经拥有了编写一个略微不同的 Lancelot 策略所需的一切,这将允许他使用控制操作重用相同的量子比特。正如之前所提到的,这可能对 Dagonet 的游戏影响不大,但对量子计算的其它应用来说却很重要。
例如,在第十章中,我们将看到如何使用一个非常类似于 Dagonet 和 Lancelot 所玩的游戏来解决量子化学中的问题。然而,在那里,准备正确的输入状态可能需要调用很多不同的量子操作,因此如果我们能够保留目标量子比特以供以后使用,我们就可以获得相当多的性能提升。
让我们简要回顾一下游戏的步骤:
-
Dagonet 为单量子比特旋转操作选择一个秘密角度。
-
达戈内特为兰斯洛特准备了一个操作,该操作隐藏了秘密角度并允许兰斯洛特额外输入一个数字(我们将它称为尺度),该数字将与秘密角度相乘以给出操作的总体旋转角度。
-
兰斯洛特在游戏中的最佳战略是选择许多尺度值并估计每个值测量到
One的概率:-
准备|+〉状态,并在达戈内特的旋转中输入尺度值。他使用|+〉状态,因为他知道达戈内特是围绕Z轴旋转的;并且对于这个状态,这些旋转将导致他可以测量的局部相位变化。
-
在准备每个|+〉状态后,兰斯洛特可以使用秘密操作旋转它,测量量子比特,并记录测量结果。
-
-
兰斯洛特现在有了与他的尺度因子和他测量的该尺度因子的
One概率相关联的数据。他可以将这些数据拟合到脑海中,并从拟合参数中得到达戈内特的角(他确实是这个国家最伟大的骑士)。我们可以使用 Python 来帮助我们做到同样的事情!
需要改变以使用我们新发现的技能进行受控旋转的是第 3 步。对于第 3a 步,量子比特的分配将改变。兰斯洛特不再为每次测量分配、准备和测量一个量子比特,而是可以分配一个目标量子比特与达戈内特的黑盒旋转,并分配和测量控制量子比特。他仍然可以重复测量,但不必每次都测量或重新准备目标量子比特。
我们可以通过如下重写第 3 步来总结这些变化:
-
1
-
2
-
兰斯洛特在游戏中最佳的战略是选择许多尺度值并估计每个值测量到
One的概率。为此,他必须对每个许多尺度值多次执行以下步骤。他准备一个处于|1〉状态的量子比特作为所有测量的目标,因为它是对称旋转的本征态:-
准备一个处于|+〉状态的第二个
控制量子比特。 -
使用兰斯洛特的尺度值应用新的受控版本的秘密旋转,未准备
控制量子比特并测量它,然后记录测量结果。
-
在我们的代码中,这些变化可以通过修改之前的EstimateProbabilityAtScale操作来实现。由于旋转轴可以是达戈内特选择的任何东西(这里,为了方便,是Z轴),兰斯洛特需要知道如何控制任意旋转。我们可以通过在调用达戈内特传递的ScalableOperation之前使用Controlled函子来实现这一点。Controlled函子与Adjoint函子非常相似,因为它接受一个操作并返回一个新的操作。Controlled U(control, target)是允许我们将U应用于我们的目标量子比特,并通过一个或多个控制量子比特进行控制的语法示例。以下列表显示了如何修改EstimateProbabilityAtScale以使用Controlled函子。
列表 9.20 operations.qs:兰斯洛特的新战略
operation EstimateProbabilityAtScaleUsingControlledRotations(
target : Qubit,
scale : Double,
nMeasurements : Int,
op : ScalableOperation)
: Double {
mutable nOnes = 0;
for idx in 0..nMeasurements - 1 {
use control = Qubit();
within {
H(control); ❶
} apply {
Controlled op::Apply( ❷
[control],
(scale, target)
);
}
set nOnes += Meas.MResetZ(control) == One
? 1 | 0;
}
return Convert.IntAsDouble(nOnes) /
Convert.IntAsDouble(nMeasurements);
}
❶ 猜测操作现在将目标寄存器作为输入并重用它。因此,我们每次只需要分配和准备控制寄存器。
❷ 我们需要做的唯一其他更改是调用 Controlled op::Apply 而不是 op::Apply,并将新的控制量子比特连同原始输入一起传递。
我们必须进行的其他修改(步骤 5)是运行游戏的操作。由于使用受控操作允许 Lancelot 重用目标量子比特,它只需要在游戏开始时分配一次。请参阅下一列表了解我们如何实现这一点。
列表 9.21 operations.qs:实现RunGameUsingControlledRotations
operation RunGameUsingControlledRotations(
hiddenAngle : Double,
scales : Double[],
nMeasurementsPerScale : Int)
: Double[] {
let hiddenRotation = HiddenRotation(hiddenAngle);
use target = Qubit(); ❶
X(target); ❷
let measurements = Arrays.ForEach(
EstimateProbabilityAtScaleUsingControlledRotations(
target, _, nMeasurementsPerScale, hiddenRotation
),
scales
);
X(target);
return measurements;
}
❶ 使用 EstimateProbabilityAtScaleUsingControlledRotations,我们可以一次分配目标量子比特,因为我们反复使用它进行每个猜测。
❷ 使用 X 操作,我们可以将目标状态准备为|1〉状态,这是(未控制的)R1 操作的本征态,Dagonet 在其中隐藏了他的角度。
使用列表 9.21 中的X操作,我们可以将目标状态准备为|1〉状态,这是(未控制的)R1操作的本征态,Dagonet 在其中隐藏了他的角度。由于每次测量都使用相位回弹仅影响控制寄存器,这种准备可以在玩游戏之前一次性完成。
摘要
-
相位估计是一种量子算法,它允许通过给定的操作学习施加到量子比特寄存器上的相位。
-
在 Q#中,我们可以声明新的用户定义类型来标记给定类型在量子程序中的使用方式,或者为长类型提供缩写。
-
Q#中的量子程序可以独立运行,也可以从用 Python 编写的宿主程序中运行;这允许使用 Q#程序与数据科学工具(如 SciPy)一起使用。
-
当一个操作除了应用全局相位外,不修改给定状态的其他输入时,我们称该输入状态为本征态,相应的相位为本征相位。
-
使用
Controlled函子和相位回弹一起,我们可以将全局本征相位转换为我们可以观察和估计的局部相位。 -
将所有这些放在一起,我们可以使用经典数据拟合技术从运行相位估计 Q#程序返回的测量结果中学习本征相位。
第二部分:结论
在本书的这一部分,我们使用 Q#和量子计算帮助卡美洛的各个居民度过了很多乐趣。通过使用用 Q#编写的量子随机数生成器,我们帮助莫甘娜欺骗了可怜的兰斯洛特。同时,我们帮助梅林和尼姆乌分别扮演各自的角色,决定国王的命运,在此过程中学习了 Deutsch–Jozsa 算法和相位回弹。随着土地的和平与卡美洛城堡夜晚的火焰,我们看到了如何使用我们所学的一切来帮助兰斯洛特玩另一场游戏,这次通过猜测 Dagonet 隐藏的量子操作而获胜。
在我们的 Camelot 逃亡过程中,你学到了不少新技巧,这些技巧将帮助你成为一名量子开发者:
-
量子算法是什么,以及如何使用量子开发工具包和 Q#来实现它
-
如何从 Python 和 Jupyter Notebook 中使用 Q#
-
如何设计 预言机 来在量子程序中表示经典函数
-
用户定义类型
-
控制操作
-
相位回弹
向前看,是时候把你在 Camelot 学到的知识带回家,并将这些新技术应用到一些更实用的东西上了。在下一章中,你将看到量子计算如何帮助理解化学问题。如果你不记得元素周期表也不要担心;你将与一些了解化学方面的同事合作,他们希望你能利用这本书的这一部分所学到的所有知识,用量子技术来升级他们的工作流程。
第三部分:应用量子计算
到这本书的这一部分,我们已经建立了一个庞大的量子算法技术工具箱——在这一部分,我们将看到如何将这些技术应用于不同的实际问题。特别是,我们将实现并运行三个不同量子程序的小型示例,每个程序都针对量子计算可以应用的不同的领域。这些示例足够小,以至于我们可以用经典计算机来模拟它们,但它们展示了量子设备如何为具有实际兴趣的问题提供计算优势。
在第十章中,我们将运用我们的量子编程技能来实现一个帮助解决具有挑战性的化学问题的量子算法。在第十一章中,我们将在此基础上实现一个用于搜索非结构化数据的算法;我们将学习如何应用内置在 Q#和 QDK 中的功能来估算运行大规模量子应用所需的资源。最后,在第十二章中,我们将实现 Shor 算法来分解整数,这可能是最著名的量子算法之一,因为它在经典密码学中的应用。
10 使用量子计算机解决化学问题
本章涵盖
-
使用量子计算机解决化学模拟问题
-
实现 Exp 操作和 Trotter-Suzuki 方法
-
创建用于相位估计、分解等程序的代码
在第九章中,我们使用了许多新的 Q#特性,如用户定义类型(UDTs)和从 Python 宿主运行程序,帮助我们编写一个可以估计相位的量子程序。正如我们将在本章中看到的,相位估计在量子算法中通常用于构建更大、更复杂的程序。在本章中,我们将探讨我们的第一个实际应用领域:化学。
10.1 量子计算的真实化学应用
到目前为止,在这本书中,我们学习了如何使用量子设备从与我们的朋友伊夫聊天到帮助决定国王的命运。然而,在本章中,我们将有机会做一些更实用的事情。
注意:现在我们有了使用量子计算机解决更难问题的工具,本章的场景比我们之前的大部分游戏和场景都要复杂。如果一开始事情没有弄明白,请不要担心。花点时间慢慢阅读;我们保证这会值得你的付出!
事实证明,我们的量子化学家朋友玛丽已经达到了她的经典计算机所能帮助她模拟不同化学系统的极限。玛丽使用计算化学技术解决的问题可以帮助对抗气候变化、理解新材料以及改善整个行业的能源使用;如果我们能通过使用 Q#来帮助她,那么这可能会带来相当多的实际应用。幸运的是,通过使用我们在第九章中学到的关于估计相位的知识,我们能够做到这一点,所以让我们开始吧!
通过化学提升口感
任何糖果制造商都可以告诉你温度的影响:将糖煮至“软裂”阶段,我们得到太妃糖;但如果我们添加更多能量,我们可以制作从太妃糖到焦糖的各种美味甜点。关于糖的一切——它的味道、外观以及它的拉丝效果——都取决于我们通过锅子注入的能量。在很大程度上,如果我们理解了在甜的熔化锅中添加能量时糖分子形状的变化,我们就理解了糖本身。
我们不仅在糖果上看到这种效果,在我们的生活中也随处可见。水、蒸汽和冰通过理解 H2O 可以采取的形状——作为能量的函数,它可以排列成什么形状——来区分。在许多情况下,我们希望通过模拟而不是实验来理解分子如何根据能量排列。在本章中,我们将基于前几章的技术,展示我们如何模拟化学系统的能量,以便我们能够像糖果制造商理解他们的工艺一样敏锐地理解它们,并利用这些化学系统使我们的生活变得更好——也许甚至更甜一些。
为了了解这是如何工作的,我们与玛丽达成一致,我们将首先查看 分子氢,或 H2,因为它是一个足够简单的化学系统,我们可以将我们从量子程序中学到的知识与我们能够用经典建模工具模拟的知识进行比较。这样,当我们使用相同的技巧来研究大于经典模拟能力的分子时,我们有一个很好的测试案例可以回退以确保一切正确。
模拟中的模拟
在本章中,我们与玛丽合作的工作涉及两种不同类型的模拟:使用经典计算机模拟量子计算机,以及使用量子计算机模拟另一种类型的量子系统。我们经常需要同时进行这两种模拟,因为在构建量子化学应用时,使用经典计算机来模拟量子计算机如何模拟量子化学系统是有帮助的。这样,当我们运行我们的量子模拟在实际量子硬件上时,我们可以确信它是正确的。
如图 10.1 所示,玛丽将利用她在量子化学方面的专业知识,描述一个她希望用量子计算机解决的问题:在这种情况下,理解 H2 的结构。大部分这类问题都涉及学习一种特殊矩阵的性质,称为 哈密顿量。一旦我们从玛丽那里得到哈密顿量,我们就可以编写一个类似于列表 10.1 中所示的量子操作来模拟它,并了解玛丽可以用它来理解不同化学物质的行为。在本章的其余部分,我们将开发我们需要实现图 10.1 中步骤的概念和理解。

图 10.1 本章我们将开发的步骤概述,以帮助玛丽学习其分子的基态能量图像
在本章中,我们将实现以下步骤以用于我们的哈密顿量模拟算法:
-
与玛丽合作,找出描述她感兴趣的系统中能级和基态(或最低能量态)近似的哈密顿量。
-
准备基态的近似,并使用 Q# 中的
Exp操作来实现哈密顿量每一项的量子系统的演化。 -
使用 Q# 函数
DecomposedIntoTimeStepsCA中实现的 Trotter–Suzuki 分解,通过将演化分解成小步骤,一次性模拟系统在哈密顿量所有项的作用下的演化。 -
在模拟系统在哈密顿量下的演化后,使用相估计来了解我们量子设备相的变化。
-
在估计系统相之后进行最终校正,之后我们就有 H2 的基态能量。
下面的列表展示了将这些步骤转换为代码的过程。
列表 10.1 估计 H2 基态能量的 Q# 代码
operation EstimateH2Energy(idxBondLength : Int) : Double {
let nQubits = 2;
let trotterStepSize = 1.0;
let trotterStep = EvolveUnderHamiltonian(idxBondLength,
trotterStepSize, _);
let estPhase = EstimateEnergy(nQubits,
PrepareInitalState,
trotterStep,
RobustPhaseEstimation(6, _, _));
return estPhase / trotterStepSize + H2IdentityCoeff(idxBondLength);
}
那么,无需多言,让我们深入探讨第一个需要帮助玛丽理解的量子概念:能量。
10.2 多条路径通向量子力学
到目前为止,我们使用计算语言(比特、量子比特、指令、设备、函数和操作)来了解量子力学。然而,玛丽对量子力学的思考方式却非常不同(图 10.2)。对她来说,量子力学是一种物理理论,它告诉她亚原子粒子(如电子)是如何行为的。从物理和化学的角度思考,量子力学是关于我们周围所有物质构成的物质的理论。

图 10.2 以两种非常不同的方式思考量子力学
当涉及到模拟像分子这样的物理系统行为时,两种思维方式相遇了。我们可以使用量子计算机来模拟其他量子系统随时间如何演变和变化。也就是说,量子计算不仅仅是关于物理或化学;它还可以帮助我们理解玛丽遇到的科学问题。
信息是我们思考量子计算的核心,但对于物理和化学的思维方式来说,量子力学在很大程度上依赖于能量的概念。能量告诉我们物理系统(如球和指南针)如何受到周围世界的影响,为我们提供了一个理解这些不同系统的统一方式。在图 10.3 中,我们可以看到如何使用能量的概念以相同的方式描述山顶上球的状况和指南针的状态。

图 10.3 利用能量来理解不同物理系统如何受到其环境的影响。山顶上的球和指向南方的指南针是比山谷中的球或指向北方的指南针具有更高能量的系统的例子。
事实上,能量不仅适用于像球和指南针这样的经典系统。确实,我们可以通过理解不同配置的能量来理解像电子和原子核这样的量子系统的行为。在量子力学中,能量由一种特殊的矩阵来描述,称为哈密顿量。任何其自身伴随矩阵的矩阵都可以用作哈密顿量,而哈密顿量本身不是操作。
从第八章和第九章回顾,矩阵 A 的 伴随矩阵是其共轭转置,记作 A†。这个概念与 Q#中的Adjoint关键字密切相关:如果一个操作 op 可以通过单位矩阵 U 来模拟,那么操作 Adjoint op 可以通过 U† 来模拟。
在本章中,我们将学习所有工具和技术来找出具有哈密顿量的量子系统的能量。通常,获取系统哈密顿量的过程是一个合作过程,但一旦我们有了它们以及一些更多信息,我们就可以估算该系统的能量。这个过程被称为哈密顿量模拟,对于量子计算在许多不同应用中都是至关重要的,包括化学。
小贴士:我们在前面的章节中看到了几个哈密顿量的例子:所有的泡利矩阵(x,y和z)都是哈密顿量的例子,同时也是单位矩阵。但并非所有单位矩阵都可以用作哈密顿量!本章中的大多数例子在我们可以将它们作为量子操作应用之前都需要做一些额外的工作。
玛丽对了解她化学物质中键的能量感兴趣。因此,提出一个描述她的分子的哈密顿量是有意义的;然后我们可以帮助她估算她感兴趣的能量。在化学中,这种能量通常被称为基态能量,相应的状态被称为基态(或最低能量状态)。
一旦我们有了哈密顿量,下一步就是弄清楚如何构建操作来模拟量子系统随时间的变化,正如哈密顿量所描述的那样。在下一节中,我们将学习如何描述在哈密顿量下量子系统的演化。
然后,有了代表哈密顿量的算子在手,下一个挑战是如何在我们的量子设备上模拟哈密顿量。物理设备中可能不会内置一个能够完全满足我们需求的单一操作,因此我们必须找到一种方法,将我们的哈密顿量操作分解为我们的设备可以提供的形式。在第 10.4 节中,我们将介绍如何将任何操作表示为泡利操作,这些操作通常作为硬件指令可用。
一旦我们将我们的哈密顿量表示为泡利矩阵的和,我们如何在我们的系统中模拟所有这些呢?很可能会有一系列项相加来表示哈密顿量的作用,而这些项不一定是对易的。在第 10.6 节中,我们将学习如何使用 Trotter-Suzuki 方法将操作中的每一项稍微应用一次来模拟整个系统的演化。然后我们将以代表玛丽哈密顿量的方式演化我们的量子系统!
最后,为了计算由我们找到的哈密顿量描述的系统的能量,我们可以使用相位估计来帮助玛丽。在第 10.7 节中,我们将使用在第九章中学到的算法来模拟哈密顿量对我们的量子比特施加的相位。让我们开始吧!
10.3 使用哈密顿量描述量子系统随时间演化的过程
图 10.4 显示了一个用于模拟我们量子计算机上的另一个量子系统的步骤跟踪器。为了使用哈密顿量来描述物理或化学系统的能量,我们需要查看其本征态及其本征值。

图 10.4 我们从这里开始学习关于玛丽(Marie)的 H2 分子以及描述其演化的哈密顿量。
记住第九章的内容,如果一个状态|Ψ〉是操作op的本征态,那么将op应用于处于状态|Ψ〉的寄存器最多只对|Ψ〉应用一个全局相位。这个相位被称为对应于该本征态的本征值或本征相位。像所有其他全局相位一样,这个本征相位不能直接观察到,但我们可以使用第九章中我们了解到的Controlled函子将这个相位转换为局部相位。
哈密顿量的每个本征态是一个恒定能量的状态;就像量子操作对本征态不做任何事情一样,处于其哈密顿量本征态的系统将随时间保持在那个能量上。我们在第九章中看到的本征态的另一个性质在这里同样适用:每个本征态的相位随时间演化。
观察到本征态的相位随时间演化是薛定谔方程的内容,这是量子物理中最重要方程之一。薛定谔方程告诉我们,随着量子系统的演化,哈密顿量的每个本征态都会积累与其能量成比例的相位。使用数学,我们可以将薛定谔方程写成如图 10.5 所示的形式。

图 10.5 薛定谔方程,用数学符号表示
在旅途中结识的真正专家是我们的朋友。
怎么可能这是第十章,而我们第一次看到量子物理中最重要的方程?开发量子应用可能与量子物理密切相关,但这不是同一件事,我们不需要成为物理专家就能编写量子应用——如果我们感兴趣,我们可以,但不必。薛定谔方程之所以出现在这里,是因为我们需要它来理解量子计算机如何用于实际影响。
就像我们的朋友玛丽是量子化学专家而不是量子计算专家一样,我们不需要知道所有事情就能做些了不起的事情。这就是朋友的作用!
提示:薛定谔方程将不同状态的相位随时间的演化与这些状态的能量联系起来。由于全局相位是不可观测的,并且由于哈密顿量的本征态只有在演化过程中才会获得全局相位,薛定谔方程告诉我们哈密顿量的本征态不会随时间演化。
在本章中,薛定谔方程对我们来说至关重要,因为它将系统的能量与相位联系起来,这是一个非常有用的联系,因为我们在第九章中学习了如何进行相位估计!薛定谔方程还有其他有用的方式,其中一种是通过另一种方式来看待在量子系统上实施操作。
实现书中所见的旋转的一种方法是为它们设置正确的哈密顿量,然后——等等。薛定谔方程中的时间导数 (∂/∂**t) 告诉我们,我们量子比特的状态旋转方式完全由每个状态的能量描述。例如,薛定谔方程告诉我们,如果我们的哈密顿量 H = ωZ 对于某个数 ω,如果我们想绕 Z-轴旋转一个角度 θ,我们可以让我们的量子比特演化时间 t = θ / ω。
练习 10.1:全方位旋转
尝试将书中早些时候展示的其他旋转(例如,Rx 和 Ry)写成哈密顿量。
练习解答
本书所有练习的解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,然后打开提及练习解答的 Jupyter 笔记本。
列表 10.2 展示了一个简单的 Q# 操作,它模拟在哈密顿量 H = ω**Z 下演化。
注意:在实践中,构建量子计算机的许多挑战在于确保量子比特不会按照量子程序的指令之外的方式演化。如果我们离开我们的量子设备一会儿,而当我们回来时所有量子比特都处于完全不同的状态,那就没什么用了。这也是为什么作为量子开发者,我们倾向于在指令层面思考——即量子操作——而不是直接用哈密顿量来思考。
暂时转换一下思路,从哈密顿量的角度思考,这让我们对解决玛丽请求我们帮助的问题有了些许进展。毕竟,玛丽处理的问题用那种语言描述起来更容易。例如,在第九章中,我们看到了如何了解像 Dagonet 从兰斯洛特那里隐藏的旋转所施加的相位。我们也可以用哈密顿量的方式表达 Dagonet 和兰斯洛特的比赛。假设 Dagonet 隐藏的旋转角度是 2.1π;那么,由于他的旋转是关于 Z-轴的,我们也可以将这个隐藏的旋转描述为隐藏的哈密顿量 H = –2.1π z。
注意:我们需要减号(-)是因为薛定谔方程中的减号。在量子编程中,犯这样的错误就像在其他语言中犯“减一”错误一样常见,所以如果你偶尔忘记一次或两次,或者几乎每次都忘记那个讨厌的减号,请不要担心。你仍然做得很好。
使用这种描述,兰斯洛特的尺度对应于他在达戈内特的隐藏哈密顿量下让量子比特演化的时间长度。虽然从游戏的角度思考使得编写学习达戈内特隐藏旋转的量子程序变得容易,但用哈密顿量的角度思考则更容易映射到玛丽关心的物理概念,例如场强和时间。唯一棘手的部分是我们需要将 Rz 的角度乘以 2.0,因为在 Q# 中,按照惯例 Rz 会将其角度乘以 -1/2。由于薛定谔方程告诉我们角度需要一个负号,所以 2.0 给出了我们需要的角度,以匹配图 10.5。
列表 10.2 在哈密顿量 H = ω**z 下演化
operation EvolveUnderZ( ❶
strength : Double, ❷
time : Double, ❸
target : Qubit
) : Unit is Adj + Ctl {
Rz(2.0 * strength * time, target); ❹
}
❶ 使用 Q# 模拟 H = ω**Z 下演化的操作
❷ ω 表示哈密顿量描述的能量的大小。这扮演了第九章中达戈内特隐藏角度的角色!
❸ 我们想要模拟哈密顿量的时间长度。这与第九章中兰斯洛特的尺度类似。
❹ 实际的模拟只是一行,因为绕 Z-轴的旋转已经内置到 Q# 中。
由于薛定谔方程告诉我们演化的哈密顿量会根据其能量旋转量子系统,如果我们能够 模拟 玛丽给出的哈密顿量,那么我们可以玩与第九章中完全相同的相位估计游戏来学习该哈密顿量的能级。
深入探讨:哈密顿量是我唯一能控制的东西
当我们首次介绍像 H、X 和 Z 这样的量子操作时,你可能想知道我们如何在真实量子设备上实现它们。使用哈密顿量的概念,我们可以重新审视这个问题,并探索内在量子操作在硬件上的工作方式。
当一个磁场被施加到一个具有磁偶极子(例如,电子自旋)的物理系统上时,该系统的哈密顿量包括一个描述系统如何与磁场相互作用的项。通常,我们将其写成 H = γBZ,其中 B 是该磁场的强度,γ 是一个描述该系统对磁场的响应强度的数字。因此,为了在量子硬件上应用 Rz 旋转,我们可以打开一个磁场,然后等待正确的时间长度。类似的效果可以用来实现其他哈密顿量项或控制其他量子设备的哈密顿量。
同样的原理也应用于其他量子技术,例如核磁共振成像(NMR),在那里已经开发出有效的经典算法,通过在正确的频率下脉冲磁场或构建复杂形状的脉冲来应用量子操作。在 NMR 和量子计算更广泛的应用中,脉冲设计算法通常被赋予一些有趣的缩写作为名称,如 GRAPE、CRAB、D-MORPH,甚至 ACRONYM。然而,不管多么有趣,这些算法都让我们能够使用经典计算机来设计量子操作,给定控制哈密顿量如H = γBZ。如果你对了解更多感兴趣,原始的 GRAPE 论文概述了自那时以来一直使用的许多最优控制理论.^a
在实践中,这当然不是完整的故事。设计控制脉冲还有很多内容,而且对于容错量子计算机来说,我们作为量子开发者所使用的内在操作并不直接映射到近期能硬件中的物理操作。相反,这些低级硬件操作被用来构建纠错码,这样单个内在操作可能会分解成许多不同的脉冲,这些脉冲被应用到我们的设备上。
^a Navin Khaneja 等人,“耦合自旋动力学最优控制:通过梯度上升算法设计 NMR 脉冲序列”,《磁共振杂志》172 卷第 2 期(2005 年):296 页,www.sciencedirect.com/science/article/abs/pii/S1090780704003696.
假设 Marie 不是询问我们能否模拟H = ω**Z,而是询问我们能否模拟H = ω**X。幸运的是,Q#也提供了关于X-轴的旋转,因此我们可以通过调用Rx来修改列表 10.2 中的Rz调用。不幸的是,Marie 感兴趣的不是像H = ω**Z或H = ω**X那样简单的哈密顿量,所以让我们看看我们可以使用哪些量子开发技术来模拟稍微复杂一些的哈密顿量。
这些不是我们寻找的哈密顿量
很可能当我们开始与玛丽交谈时,她也会在她的模拟和建模软件中描述她系统的哈密顿量。然而,这些可能是费米子哈密顿量,与我们用来描述量子设备随时间变化的哈密顿量不同。作为与我们与玛丽合作的流程的一部分,我们可能需要使用一些工具,如 NWChem (nwchemgit.github.io),在描述化学随时间变化和量子比特随时间变化的哈密顿量之间进行转换。本书的范围不包括对这些方法的详细探讨,但有一些优秀的软件工具可以帮助完成这项工作。如果您对此感兴趣,请查看量子开发套件文档以获取详细信息:docs.microsoft.com/azure/quantum/user-guide/libraries/chemistry/。目前这并不是什么大问题,只是当您与您的合作者交谈时的一个实用提示!
10.4 使用泡利算子绕任意轴旋转
在复杂性上升的过程中,也许玛丽对需要比单个量子比特哈密顿量更多来描述的东西感兴趣(图 10.6)。如果她给我们一个如 H = ω**X ⊗ X 的哈密顿量,我们能做些什么来模拟它?幸运的是,我们在本书第一部分学到的关于旋转的知识对于这种双量子比特哈密顿量仍然有用,因为我们可以将它视为描述另一种类型的旋转。

图 10.6 在步骤 2 中,我们看看如何将我们的哈密顿量分解成更易于模拟的广义旋转。
注意:在上一节中,我们了解到旋转如 Rx、Ry 和 Rz 分别对应于哈密顿量如 X、Y 和 Z。我们可以将类似 X ⊗ X 的双量子比特哈密顿量视为以类似的方式指定一个轴。实际上,对于双量子比特寄存器,有 15 个可能的正交旋转轴,而单量子比特寄存器只能得到 3 个维度。因此,为了绘制一个图像,我们需要比通常的纸张多 13 个维度,这使得说明变得有些困难!
这种旋转看起来不像我们之前见过的任何内置(即内在)指令,所以我们可能觉得我们陷入了困境。然而,实际上,只要我们在两侧使用一些双量子比特操作,我们仍然可以使用 Rx 这样的单量子比特旋转来模拟这个哈密顿量。在本节中,我们将看到这是如何工作的,以及 Q# 如何使自动化应用多量子比特旋转变得容易。
为了入门,让我们看看我们可以通过围绕其他操作来改变量子操作的方式。我们总是可以使用数学来推理事物,就像我们在第九章中看到的那样,但幸运的是,Q#也提供了一些很好的测试函数和操作,可以帮助我们。例如,在第九章中,我们看到将CNOT操作与H操作包围起来可以给我们一个方向相反的CNOT。让我们看看我们如何使用 Q#来检查这一点!
提示:回想一下列表 10.3 中的within/apply块应用了我们在第九章首次了解的鞋袜原则。本节中的大多数代码示例都使用within/apply块来帮助我们跟踪鞋袜思维。
列表 10.3 更改CNOT的控制和目标
open Microsoft.Quantum.Diagnostics; ❶
operation ApplyCNOT(register : Qubit[])
: Unit is Adj + Ctl { ❷
CNOT(register[0], register[1]);
}
operation ApplyCNOTTheOtherWay(register : Qubit[])
: Unit is Adj + Ctl { ❸
within {
ApplyToEachCA(H, register);
} apply {
CNOT(register[1], register[0]);
}
}
operation CheckThatThisWorks() : Unit {
AssertOperationsEqualReferenced(2,
ApplyCNOT, ApplyCNOTTheOtherWay); ❹
Message("Woohoo!"); ❺
}
❶ Microsoft.Quantum.Diagnostics 命名空间中的操作和函数有助于测试和调试量子程序,并且对于确保程序按预期工作非常有用。
❷ 为了比较两种编写 CNOT 操作的方式,我们需要每种方式都能作为一个操作,该操作接受一个表示量子寄存器的 qubit 数组。
❸ 为了检查我们在第七章首次看到的等价性,我们可以编写第二个操作,该操作反转 CNOT 操作的控制和目标。
❹ 第一个输入指定了每个操作作用在寄存器上的大小,第二个和第三个输入代表正在比较的操作。如果操作有任何不同之处,断言失败,量子程序结束。
❺ 如果我们看到消息“Woohoo!”,我们可以安全地得出结论,这两个操作在观察它们对量子寄存器状态的影响时是无法区分的。
注意:像AssertOperationsEqualReferenced这样的断言只有在模拟器上运行时才有意义,因为运行它们需要违反不可克隆定理。在实际硬件上,这些类型的断言会被移除,就像使用带有-O命令行参数的 Python 禁用assert关键字一样。这意味着 Q#断言为我们提供了一种安全作弊的方法,因为使用断言的量子程序无论我们是否作弊都会做同样的事情。
练习 10.2:验证 CNOT 恒等式
使用 QuTiP 验证两个操作ApplyCNOT和ApplyCNOTTheOtherWay可以由相同的幺正矩阵模拟,因此它们确实做同样的事情。
练习 10.3:三个 CNOT 实现一个 SWAP
就像我们可以使用三个经典的XOR指令来实现就地经典交换一样,我们也可以使用三个CNOT操作来完成单个SWAP操作。以下 Q#代码片段与SWAP(left, right)做同样的事情:
CNOT(left, right);
CNOT(right, left);
CNOT(left, right);
通过使用AssertOperationsEqualReferenced和 QuTiP 来双重检查这确实与SWAP(left, right)相同。
额外加分:SWAP(left, right)与SWAP(right, left)相同,因此前面的代码片段即使以CNOT(right, left)开始也能正常工作。请再次检查!
深入探讨:Choi–Jamiłkowski 同构
列表 10.3 中的AssertOperationsEqualReferenced操作使用了一种称为Choi–Jamiłkowski 同构的巧妙数学方法,它表明任何可以用幺正矩阵模拟的操作与一个特定状态(称为其Choi 状态)完全等价。这意味着模拟器可以通过找到其 Choi 状态来有效地找到任何可逆操作(即任何在其签名中有is Adj的操作)的整个真值表。AssertOperationsEqualReferenced操作使用这个概念为每个作为输入传递的操作准备一个处于 Choi 状态的量子比特寄存器。在模拟器上,很容易作弊并检查两个状态是否相同,尽管克隆定理告诉我们我们实际上不能在真实设备上这样做。
当编写单元测试和其他检查量子程序是否正确时,这可以是一种强大的技术,可以在使用经典模拟器的同时防止在真实硬件上作弊。
当我们在 Jupyter Notebook(如我们在第七章中看到的)或命令行中运行CheckThatThisWorks时,我们应该看到消息"Woohoo!",告诉我们我们的 Q#程序已经通过了AssertOperationsEqualReferenced调用。由于该断言只有在我们给出的两个操作对所有可能的输入都执行完全相同操作时才会通过,因此我们知道我们在第七章中学到的等价性是有效的。
我们可以使用相同的逻辑来检查双量子比特操作(如CNOT)如何转换其他操作。例如,通过调用CNOT转换X调用与多次调用X具有相同的效果,如下一列表所示。
列表 10.4 在寄存器中的每个量子比特上应用X
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Arrays;
operation ApplyXUsingCNOTs(register : Qubit[])
: Unit is Adj + Ctl { ❶
within {
ApplyToEachCA( ❷
CNOT(register[0], _), ❸
Rest(register) ❹
);
} apply {
X(register[0]); ❺
}
}
operation CheckThatThisWorks() : Unit {
AssertOperationsEqualReferenced(2,
ApplyXUsingCNOTs,
ApplyToEachCA(X, _) ❻
);
Message("Woohoo!");
}
❶ 表示单个 X 操作调用并使用 CNOTs,使用内部/应用块
❷ 对于/应用块中的“袜子”部分,我们可以通过使用 ApplyToEachCA 与我们在第七章中学到的部分应用技术来编写所需的 CNOT 调用。
❸ 我们在调用 ApplyToEachCA 的这一部分中,指示将控制于寄存器第一个量子比特的 CNOT 操作应用于量子比特数组中的每个元素。
❹ 使用 Rest 选择寄存器数组中除了第一个(即 0th)元素之外的所有元素
❺ 我们在/应用块中的“鞋子”部分稍微简单一些:只是对用作我们 CNOT 调用序列控制器的相同量子比特执行 X 操作。
❻ 这次,我们不是编写自己的操作进行比较,而是通过部分应用,将寄存器中每个量子比特的 X 操作进行比较。
练习 10.4:幺正等价
使用 QuTiP,检查当在双量子比特寄存器上运行时,列表 10.4 中的两个程序可以通过相同的幺正矩阵进行模拟,因此对它们的输入寄存器执行相同操作。
练习 10.5:程序等价
尝试修改列表 10.4,看看当应用于超过两个量子比特时,这两个程序是否等效。
注意:对于超过几个量子比特,使用 AssertOperationsEqualReferenced 可能相当昂贵。
我们还可以通过使用 within/ apply 概念来构建其他有趣的操作类型。特别是,通过使用与列表 10.4 相同的方式应用 CNOT 操作来转换旋转,我们可以实现 Marie 在本节开头所要求的多量子比特旋转类型。使用我们在第九章中学到的 DumpMachine 和 DumpRegister 功能,我们可以看到,正如 Rx 在 |0〉 和 |1〉 之间应用 X-轴旋转一样,我们也可以实现 |00〉 和 |11〉 之间的 (X ⊗ X)-轴旋转。
列表 10.5 创建一个多量子比特 Rx 操作
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;
operation ApplyRotationAboutXX(
angle : Double, register : Qubit[]
) : Unit is Adj + Ctl {
within {
CNOT(register[0], register[1]); ❶
} apply {
Rx(angle, register[0]); ❷
}
}
operation DumpXXRotation() : Unit {
let angle = PI() / 2.0;
use register = Qubit[2]; ❸
ApplyRotationAboutXX(angle, register); ❹
DumpMachine(); ❺
ResetAll(register); ❻
}
❶ 为了简单起见,我们在本列表中专门针对双量子比特情况,但我们可以使用相同的 ApplyToEachCA 调用来处理超过两个量子比特的寄存器。
❷ 我们不想对控制量子比特应用 X 操作,而是想对控制量子比特应用一个任意角度的 X 旋转。
❸ 为了检查我们新的 ApplyRotationAboutXX 操作的作用,我们首先通过一个“using”语句向我们的目标机器请求一个双量子比特寄存器。
❹ 然后,我们将新的旋转应用于我们的新寄存器,以查看它会产生什么效果。
❺ 当在模拟器上运行时,DumpMachine 会打印出模拟器的完整状态,让我们检查我们的新旋转操作如何转换了我们的寄存器状态。
❻ 与往常一样,在我们将寄存器释放回目标机器之前,我们需要将所有量子比特重置回 |0〉 状态。
练习 10.6:预测 ApplyRotationAboutXX
在调用 ApplyRotationAboutXX 之前,尝试将寄存器准备在 |00〉 状态之外。这个操作是否如您预期的那样工作?
提示:回想一下本书的第一部分,我们可以通过应用一个 X 操作来准备 |1〉 状态的副本,并且我们可以通过应用一个 H 操作来准备 |+〉。
您从列表 10.5 中的输出可能看起来与图 10.6 略有不同,因为 Jupyter Notebook 的 IQ# 内核支持多种不同的方式来标记量子比特状态。

图 10.7 在 Jupyter Notebook 中运行列表 10.5 的输出。我们可以从 DumpMachine 在 DumpXXRotation 中的输出中看到,我们的结果状态是 |00〉 和 |11〉 之间的叠加。
小贴士:默认情况下,IQ# 使用“小端”约定,这对于我们在第十二章中将要看到的算术问题很有用。要使用与本书中迄今为止所见的类似位字符串来标记量子比特状态,请在新的 Jupyter Notebook 单元中运行 %config dump.basisStateLabelingConvention = "Bitstring"。
练习 10.7:Rx 与 X ⊗ X 的旋转
尝试使用 DumpMachine 来探索 Rx 操作如何作用于单个量子比特,并将其与我们实现的关于 (X ⊗ X)-轴的二维量子比特旋转进行比较。这两个旋转操作有哪些相似之处,又有哪些不同之处?比较围绕 (X ⊗ X)-轴的旋转与对二维量子寄存器中每个量子比特应用 Rx 操作。
通常,任何围绕由泡利矩阵的张量积给出的轴(如 X ⊗ X,Y ⊗ Z 或 Z ⊗ Z ⊗ Z)的旋转都可以通过应用单个量子比特旋转和一系列操作(如 CNOT 和 H)来实现。然而,找到正确的转换可能有点麻烦,所以 Q# 提供了一个名为 Exp 的内置操作来帮助。
列表 10.6 使用 Exp 来找出如何转换状态
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;
operation ApplyRotationAboutXX(
angle : Double, register : Qubit[]
) : Unit is Adj + Ctl {
within {
CNOT(register[0], register[1]);
} apply {
Rx(angle, register[0]);
}
}
operation CheckThatThisWorks() : Unit {
let angle = PI() / 3.0;
AssertOperationsEqualReferenced(2,
ApplyRotationAboutXX(angle, _),
Exp([PauliX, PauliX], -angle / 2.0, _)
);
Message("Woohoo!");
}
警告:Exp 和 Rx 用于表示角度的约定相差一个因子 -1/2。当在同一个程序中使用 Exp 操作和单个量子比特旋转操作时,务必仔细检查所有角度!
使用 Exp,模拟哈密顿量 H = ω**X ⊗ X 或由泡利矩阵的张量积组成的任何其他哈密顿量(如图 10.8 所示)变得非常容易。正如以下列表所示,在 Q# 中,我们可以通过 Q# 值 [PauliX, PauliX] 来指定 (X ⊗ X)。

图 10.8 在第 3 步中,我们看看如何使用 Exp 操作来编程表示我们试图模拟的哈密顿量的广义旋转。
列表 10.7 使用 Exp 来模拟在 X ⊗ X 下演化
operation EvolveUnderXX( ❶
strength : Double, ❷
time : Double, ❸
target : Qubit
) : Unit is Adj + Ctl {
Exp([PauliX, PauliX], strength * time,
target); ❹
}
❶ 使用我们迄今为止学到的知识,我们可以编写一个操作来模拟在 (X ⊗ X) 成比例的哈密顿量下演化,就像我们在列表 10.2 中编写的操作模拟在成比例的 Z 哈密顿量下演化一样。
❷ 表示哈密顿量强度的参数:我们的哈密顿量描述的能量有多大
❸ 描述模拟演化时间(类似于第九章中 Lancelot 的尺度参数)
❹ 使用 Microsoft.Quantum.Intrinsic 命名空间提供的 Exp 操作请求围绕 (X ⊗ X)-轴的旋转
Z ⊗ Z 并不仅仅是两个 Z 旋转
可能会让人想当然地认为我们可以通过先围绕 Z 旋转第一个量子比特,然后围绕 Z 旋转第二个量子比特来实现关于 Z ⊗ Z 的二维量子比特旋转。然而,这些操作却非常不同:

可以这样理解,围绕 Z ⊗ Z 的旋转只对每个计算基态的 奇偶性 敏感,所以 |00〉 和 |11〉 都以相同的相位旋转。
现在我们有了 Exp 操作,用它来编写一个模拟玛丽给出的哈密顿量中每个项的操作变得非常容易。
列表 10.8 operations.qs:模拟单个项的演化
operation EvolveUnderHamiltonianTerm(
idxBondLength : Int, ❶
idxTerm : Int, ❷
stepSize : Double, ❸
qubits : Qubit[])
: Unit is Adj + Ctl {
let (pauliString, idxQubits) =
H2Terms(idxTerm); ❹
let coeff =
(H2Coeff(idxBondLength))[idxTerm]; ❺
let op = Exp(pauliString,
stepSize * coeff, _); ❻
(RestrictedToSubregisterCA(op, idxQubits))
(qubits); ❼
}
❶ 查找玛丽给我们提供的哈密顿量索引。每个对应不同的键长。
❷ 我们想要在 Marie 的哈密顿量下模拟演化的项
❸ 模拟演化的时长:也就是说,模拟步骤的时长
❹ 通过使用 idxTerm 与代码库中提供的 H2Terms 函数一起从哈密顿量中获取项
❺ 通过使用 H2Coeff 函数(也提供在本书的样本库中)来获取该项的系数
❻ 使用 Exp 进行旋转,旋转的幅度由模拟步长决定,就像列表 10.7 中的 EvolveUnderXX 操作一样,来模拟该项下的演化
❼ 由于并非所有项都会影响所有量子比特,我们可以使用 Q#提供的RestrictedToSubregisterCA操作来仅将我们的 Exp 调用应用于输入的子集。
在下一节中,我们将看到如何使用这个方法来模拟在 Marie 的整个哈密顿量下的演化。
10.5 在系统中实现我们想要看到的变化
现在我们已经学会了如何使用哈密顿量的概念来描述量子设备随时间的变化,一个非常自然的问题就是,我们如何实现我们想要模拟的特定哈密顿量?大多数量子设备都有一些它们容易执行的操作。例如,我们在上一节中看到,模拟任何由泡利矩阵张量积给出的哈密顿量的演化是直接的。话虽如此,我们(以及 Marie)感兴趣的是哈密顿量可能不是内置操作,而是一种在我们量子计算机上不可直接获得的东西。
提示:通常设备很容易实现一些泡利算符以及可能的一些其他操作。那么游戏就变成了如何将我们想要的操作转换成设备可以轻松执行的操作。
如果没有简单的按钮来模拟我们的哈密顿量下的演化,我们如何实现一个特定的哈密顿量模拟,我们可以将其应用于我们设备中的量子比特?
让我们将其分解。字面意义上来说。我们早在第二章就学到了,我们可以将一个向量描述为基向量或方向的线性组合。结果是我们也可以用矩阵来做同样的事情,而一个做这件事非常方便的基是泡利算符。
泡利矩阵复习
如果你需要复习泡利矩阵是什么,不用担心,我们已经为你准备好了:

正如我们可以用北和西来描述地图上的任何方向一样,我们也可以用泡利矩阵的线性组合来描述任何矩阵。例如,

同样,

对于作用于多个量子比特的矩阵,情况也是一样的:

练习 10.8:验证恒等式
使用 QuTiP 验证前面的方程。
提示:你可以使用qt.qeye(2)来获取 1 的副本,qt.sigmax()来获取 X 的副本,以此类推。要计算如X ⊗ X这样的张量积,你可以使用qt.tensor。
这是一个好消息,因为这样我们就可以将我们想要模拟的哈密顿量写成泡利矩阵的线性组合。在上一节中,我们看到了我们可以使用 Exp 来轻松模拟仅由泡利矩阵的张量积组成的哈密顿量。这使得泡利基非常方便,因为它很可能是玛丽化学工具的工作流程已经以泡利基的形式输出了我们量子设备的哈密顿量。
让我们看看玛丽想要我们模拟的哈密顿量的表示,使用泡利基来展开它。利用她的化学建模技能,玛丽可以方便地告诉我们,我们需要用我们的量子位模拟的哈密顿量由以下方程给出,其中每个a、b[0]、...和b[4]都是实数,这些实数取决于她想要模拟 H[2]的键长:

提示玛丽所使用的所有项和系数都来自论文“可扩展的分子能量量子模拟。”^(1)。确切的系数取决于氢原子之间的键长,但所有这些常数都已经在代码库中方便地为您输入:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。
拥有玛丽哈密顿量的这种表示后,现在是时候弄清楚如何实际使用它了。这个哈密顿量有六个项,那么我们应该先应用哪个项?顺序重要吗?不幸的是,在模拟系统在哈密顿量下的演化时,项的使用顺序通常很重要。在下一节中,我们将学习一种方法,它允许我们将系统的演化分解成小步骤,以同时模拟所有项的演化。
10.6 经历(非常小的)变化
到目前为止,退一步评估我们在帮助玛丽方面所处的位置是有帮助的。我们已经看到了如何将任意哈密顿量分解为泡利矩阵的和,以及如何使用Exp操作来模拟该和中每个项的演化。要模拟任意哈密顿量,剩下的只是将这些模拟组合起来,以模拟整个哈密顿量(图 10.9)。为此,我们可以使用另一个名为* Trotter-Suzuki 分解*的量子计算技巧。

图 10.9 在本节中,我们探讨如何使用* Trotter-Suzuki 分解*通过将其分解为从步骤 3 开始的每个项的更小的演化来模拟总哈密顿量的作用。
在深入探讨 Trotter-Suzuki 分解的细节之前,让我们回到我们在整本书中用来分解线性代数概念的映射类比(附录 C 中讨论)。
假设我们正在探索凤凰城的市中心,并决定体验一下在整个城市中向东北方向行走的感受。如果我们先向北走几个街区,然后向东走几个街区,我们在地图上留下的路线不会看起来像一条对角线。另一方面,如果我们每走一个街区就在北和东之间切换,我们留下的路线将更接近于看起来像是来自附录 C 中出现的明尼阿波利斯地图的路径。也就是说,即使我们被困在凤凰城,我们也可以通过快速切换行走方向来模拟我们在明尼阿波利斯行走的方式;参见图 10.10。

图 10.10 如果我们在凤凰城的市中心,我们仍然可以通过快速交替方向来模拟我们在明尼阿波利斯市中心行走的方式。理想情况下,我们只会以对角线的方式到达目的地,但考虑到街道布局,我们可以通过做出短的之字形来近似对角线。照片由 davecito 拍摄。
在上一节中,我们了解到,就像状态一样,哈密顿量中的不同项可以被视为高维地图上的方向。泡利矩阵的张量积,例如 Z ⊗ 𝟙 和 X ⊗ Z,在地图上的基本方向或轴上扮演着类似的角色。然而,当我们尝试模拟玛丽的哈密顿量时,它并不指向一个单一轴,而是在更高维空间中沿着一种斜线。这就是 Trotter–Suzuki 分解发挥作用的地方。
正如我们快速切换行走方向时路径看起来更像是对角线一样,我们可以在模拟不同的哈密顿量项之间快速切换。如图 10.11 所示,Trotter–Suzuki 分解告诉我们,当我们这样快速切换时,我们近似地经历了我们模拟的不同项的总和。

图 10.11 使用 Trotter–Suzuki 分解同时近似演化两个哈密顿量项。就像之前的地图类比一样,如果我们想尽可能快地应用两个哈密顿量的效果,我们应该交替地在每个项下进行一点演化,直到达到完整的演化。
我们原则上可以用 Q#编写一个for循环。在伪代码中,我们可能会有以下内容。
列表 10.9 使用 Trotter–Suzuki 分解模拟哈密顿量
operation EvolveUnderHamiltonian(time, hamiltonian,
➥ register) { ❶
for idx in 0..nTimeSteps - 1 { ❷
for term in hamiltonian { ❸
evolve under term for time / nTimeSteps
}
}
}
❶ 由于这是伪代码,我们暂时不必担心类型。没有类型,这个操作将无法编译,但现在这没关系。
❷ 对于我们想要将模拟分割成多少步(想想凤凰城的街区或屏幕上的像素),我们需要对每个哈密顿量项做一点工作。
❸ 在每个时间步内,我们可以遍历我们需要模拟的每个项,并对每个项进行一步模拟。
幸运的是,Q#提供了一个标准库函数,可以为我们精确地完成这项工作:DecomposedIntoTimeStepsCA。在列表 10.10 中,我们展示了如何调用DecomposedIntoTimeStepsCA,这使得使用 Trotter-Suzuki 分解来模拟 Marie 的哈密顿量下的演化变得容易。DecomposedIntoTimeStepsCA函数支持比本章中我们探索过的第一阶近似(由trotterOrder为 1 表示)更高阶的 Trotter-Suzuki 分解。在某些情况下,这个特性可能有助于提高我们模拟的精度,但对我们来说,trotterOrder为 1 已经足够好。
列表 10.10 operations.qs:使用DecomposedIntoTimeStepsCA
operation EvolveUnderHamiltonian(
idxBondLength : Int, ❶
trotterStepSize : Double, ❷
qubits : Qubit[])
: Unit is Adj + Ctl {
let trotterOrder = 1; ❸
let op = EvolveUnderHamiltonianTerm(
idxBondLength, _, _, _); ❹
(DecomposedIntoTimeStepsCA ((5, op), trotterOrder))
(trotterStepSize, qubits); ❺
}
❶ EvolveUnderHamiltonian 根据 Marie 要求我们帮助其调整的 H[2]分子所需键长的系数,应用适当的哈密顿量。
❷ 表示我们想要模拟哈密顿量演化多长时间的步长
❸ 在某些情况下,trotterOrder >1 可以有助于提高我们模拟的精度,但对我们来说,trotterOrder 为 1 已经足够好。
❹ 部分应用可以将 EvolveUnderHamiltonianTerm 的 idxBondLength 输入固定下来,而将 idxTerm、stepSize 和 qubits 参数留空。
❺ 此函数输出一个操作,可以用来自动模拟整个哈密顿量下的演化,使用模拟每个项的操作逐一进行,这样我们就可以继续应用它。
10.7 整合一切
现在我们对哈密顿量是什么以及我们如何模拟其下的演化来理解量子系统随时间的变化有了更好的了解,我们准备编写一个程序来帮助 Marie 解决她的问题(图 10.12)。作为提醒,Marie 是一位化学家,研究不同化学物质的基态能量(即最低可能的能量)。她要求我们用我们的量子设备帮助确定 H[2]分子的基态能量。由于组成 H[2]分子的氢原子也是量子系统,使用量子比特模拟 H[2]的行为比使用经典计算机要容易得多。

图 10.12 最后一步是帮助 Marie 模拟她的 H[2]分子,即使用相估计来读取基态能量。
小贴士:量子计算机非常适合模拟其他量子系统的行为,这可能是量子计算首次被提出的应用!
图 10.13 是本章中我们学习到的所有步骤和技术,用于在我们的量子设备中模拟 Marie 的 H[2]分子的演化。

图 10.13 本章开发的步骤概述,以帮助 Marie 学习其分子的基态能量
因此,作为量子开发者,我们可以与玛丽合作模拟 H[2]分子随时间演化的过程,并计算基态能量,这要归功于薛定谔方程。需要记住的关键点是 H[2]分子的可能能级对应于哈密顿量的不同本征态。
假设我们的量子比特处于哈密顿量的本征态。那么,在那种哈密顿量下模拟演化不会改变我们的量子寄存器的状态,除了应用一个与该状态能量成比例的全局相位。这种能量告诉我们需要解决玛丽问题的确切信息,但全局相位是不可观测的。幸运的是,在第九章中,我们从 Lancelot 和 Dagonet 的游戏中学习了如何将全局相位转化为我们可以通过相估计学习的东西——这里是一个很好的应用它的地方!总结来说,与玛丽合作并解决她问题的步骤如下:
-
准备玛丽给我们的初始状态。在这种情况下,她很有帮助地告诉我们准备|10〉。
-
将表示系统的哈密顿量分解成可以顺序模拟的小步骤,以表示整个操作。
-
将代表哈密顿量的每个步骤应用到我们的初始状态上。
-
使用相估计算法来了解我们量子状态上累积的全局相位,这将与能量成正比。
我们在这个章节的前几节中有了技能和代码,可以将所有这些内容整合起来,所以让我们试试看。
从 Q#文件(在这里称为 operations.qs 以匹配我们之前章节中看到的)开始,我们可以打开一些命名空间来利用预制的函数和操作。
列表 10.11 operations.qs:从 QDK 需要的命名空间
namespace HamiltonianSimulation {
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Canon; ❶
open Microsoft.Quantum.Simulation; ❷
open Microsoft.Quantum.Characterization; ❸
❶ 我们之前已经见过 Microsoft.Quantum.Intrinsic 和 Microsoft.Quantum.Canon;它们包含我们需要的基本实用工具/辅助函数和操作。
❷ Microsoft.Quantum.Simulation 是一个 QDK 的命名空间,正如我们可能预期的,它包含用于模拟系统的实用工具。
❸ Microsoft.Quantum.Characterization 包含了我们第九章中开发的相估计算法的易于使用的实现。
接下来,我们需要添加玛丽关于她的分子的数据。所有这些都在 GitHub 仓库中这本书的示例文件中打印出来:github.com/crazy4pi314/learn-qc-with-python-and-qsharp/blob/master/ch10/operations.qs。H2BondLengths、H2Coeff和H2IdentityCoeff是我们需要的函数(它们在这里的文字中复现起来有点长)。
一旦我们在文件中获得了玛丽的所有系数数据,我们就需要使用这些刚刚添加的系数的实际哈密顿量项/结构。以下列表显示了一个函数的概要,该函数返回以 Pauli 算子表示的玛丽的哈密顿量项,以及一个将我们的双量子比特寄存器准备为适合此算法的正确状态的运算。
列表 10.12 operations.qs:从哈密顿量返回项的函数
function H2Terms(idxHamiltonian : Int) ❶
: (Pauli[], Int[]) {
return [
([PauliZ], [0]), ❷
([PauliZ], [1]),
([PauliZ, PauliZ], [0, 1]), ❸
([PauliY, PauliY], [0, 1]),
([PauliX, PauliX], [0, 1])
][idxHamiltonian];
}
operation PrepareInitalState(q : Qubit[]) ❹
: Unit {
X(q[0]);
}
❶ H2Terms 函数使得构建玛丽的哈密顿量项变得容易。
❷ 这个函数实际上只是一个硬编码的元组列表,描述了哈密顿量的项。这个第一个元组表示哈密顿量的第一项是对零比特的 PauliZ 操作。
❸ 将 PauliZ 应用于零比特和第一个量子比特
❹ 我们还需要一种方法来为算法准备我们的量子比特。遵循玛丽的建议,我们将第一个量子比特置于|1〉状态,其余的输入量子比特保持在|0〉状态。
为了处理我们的量子算法的步骤 2 和 3,我们需要之前定义的操作:EvolveUnderHamiltonianTerm和EvolveUnderHamiltonian。
全局相位和 Controlled 函子
正如我们在第九章中看到的,应用EvolveUnderHamiltonian对玛丽哈密顿量的本征态准备好的量子比特没有任何作用——这确实是整个目的!在第九章中,我们能够通过使用Controlled函子将应用在准备为本征态的量子比特上的 Dagonet 操作产生的全局相位转换为可观察的局部相位,然后使用相位回弹将该相位应用到控制量子比特上来解决这个问题。量子开发工具包提供的EstimateEnergy操作使用完全相同的技巧来学习我们EvolveUnderHamiltonian操作中原本可能的全局相位。这意味着我们的操作通过向传递给EstimateEnergy的操作的签名中添加is Ctl来支持Controlled函子,这对于帮助玛丽来说是至关重要的。
最后,我们可以使用量子开发工具包提供的EstimateEnergy操作来自动应用 Trotter-Suzuki 步骤和相位估计步骤。我们还可以使用内置的相位估计操作,该操作实现了一个比我们在第九章中学到的相位估计算法更好的版本。例如,Microsoft.Quantum.Simulation库中有一个名为EstimateEnergy的操作,它使用相位估计来估计本征态的能量。它需要一个量子比特数量的规范(nQubits),一个准备我们所需初始状态的运算(PrepareInitialState),如何应用我们的哈密顿量(trotterStep),以及我们想要用于估计应用哈密顿量产生的相位的算法。让我们看看它的实际应用。
列表 10.13 operations.qs:估计 H[2]基态能量的 Q#操作
operation EstimateH2Energy(idxBondLength : Int)
: Double { ❶
let nQubits = 2; ❷
let trotterStepSize = 1.0; ❸
let trotterStep = EvolveUnderHamiltonian(
idxBondLength, trotterStepSize, _); ❹
let estPhase = EstimateEnergy(nQubits, ❺
PrepareInitalState, trotterStep,
RobustPhaseEstimation(6, _, _));
return estPhase / trotterStepSize
+ H2IdentityCoeff(idxBondLength); ❻
}
❶ 这就是全部!操作 EstimateH2Energy 接收分子键长的索引,并返回其基态或最低能量态的能量。
❷ 定义我们需要两个量子位来模拟这个系统。
❸ 为应用我们哈密顿量项到量子位上的 Trotter–Suzuki 步设置一个比例参数。
❹ 基于 ApplyHamiltonian 构建操作,并为应用我们哈密顿量项的操作提供了一个方便的名称。
❺ 我们在 Microsoft.Quantum.Simulation 库中内置了一个操作,用于估计应用我们的哈密顿量产生的相位,我们知道这代表系统的能量。
❻ 为了确保返回的能量单位正确,我们必须除以 trotter 步长,并加上哈密顿量中恒等项的能量。
现在真正运行算法!由于基态能量是分子键长的函数,我们可以使用 Python 主机来运行 Q# 算法,然后将结果作为键长的函数绘制出来。
列表 10.14 host.py:Python 模拟的设置。
import qsharp ❶
import HamiltonianSimulation as H2Simulation
bond_lengths = H2Simulation.H2BondLengths.simulate() ❷
def estimate_energy(bond_index: float, ❸
n_measurements_per_scale: int = 3
) -> float:
print(f"Estimating energy for bond length of {bond_lengths[bond_index]} Å.")
return min([H2Simulation.EstimateH2Energy.simulate(idxBondLength=bond_index)
for _ in range(n_measurements_per_scale)])
❶ 导入 Q# 的 Python 包,然后从我们的 operations.qs 文件中导入 Q# 命名空间(HamiltonianSimulation)。qsharp Python 包使得 Q# 命名空间可以作为常规导入语句使用。
❷ 为了简化操作,从 Q# 函数 H2BondLengths 中提取我们可以模拟的 H[2] 的键长列表。
❸ estimate_energy 函数是 EstimateH2Energy Q# 操作的 Python 包装器,但运行了几次以确保能量估计最小化。
为什么我们需要多次运行 EstimateH2Energy?
Marie 给我们的状态 |01〉实际上不是任何 H[2] 哈密顿量的本征态,而是她使用一种称为 Hartree–Fock 理论 的量子化学近似方法计算出来的。由于量子化学是她的专业领域,她可以通过提供这样的近似来帮助我们。
实际上,这意味着当我们使用 Microsoft.Quantum.Characterization 命名空间提供的工具运行相位估计时,我们并不是在学习特定本征态的能量;相反,我们是在随机投影到 某个 本征态并学习其能量。由于我们的初始状态是一个相当好的近似,大多数情况下,我们会投影到 Marie 哈密顿量的最低能量态(即基态),但我们可能会不幸地正确学习到错误本征态的能量。由于我们正在寻找最小的能量,多次运行并取最小值使得我们更有可能学习到我们想要的能量。
在 Python 主机中设置好一切之后,剩下的就是编写和运行主函数。
列表 10.15 host.py:我们模拟的主程序。
if __name__ == "__main__":
import matplotlib.pyplot as plt ❶
print(f"Number of bond lengths: {len(bond_lengths)}.\n")
energies = [estimate_energy(i) for i in range(len(bond_lengths))] ❷
plt.figure() ❸
plt.plot(bond_lengths, energies, 'o') ❸
plt.title('Energy levels of H2 as a function of bond length') ❸
plt.xlabel('Bond length (Å)') ❸
plt.ylabel('Ground state energy (Hartree)') ❸
plt.show() ❹
❶ 将 host.py 作为脚本运行会绘制出 Q# 量子算法估计的基态能量。
❷ 直接生成每个 H2 分子键长的估计能量列表。
❸ 设置图表的数据和样式。
❹ 调用 plt.show()应该弹出或返回一个图像的图!
图 10.14 展示了运行python host.py应该输出的示例。这个图显示了我们对 H[2]分子不同键长度的各种哈密顿量的模拟结果。我们可以看到,当键长较短时,最低能级的能量要高得多,并且随着键长的增加而趋于平稳。理论上,最低能量应出现在大约 0.75 Å的键长处。如果我们查阅资料,氢气的稳定(即平衡)键长为 0.74 Å!所以,玛丽分子的确是相当知名的,但我们可以看到,我们可以如何不仅对其他化学物质,而且对模拟其他量子系统进行这个过程。

图 10.14 运行 host.py 应该生成的图的一个示例。具体数据可能会有所不同,但总的来说,我们应该看到最小基态能量发生在水平轴上的大约 0.75 Å处,这将是具有最低能量的键长。
恭喜:我们已经实现了量子计算机的第一个实际应用!当然,我们这里使用的实际化学物质相当简单,但这个过程适用于我们可能想要模拟的大多数其他量子系统。在接下来的两章中,我们将探讨量子计算机的另外两种应用:使用 Grover 算法进行无结构搜索和使用 Shor 算法分解数字。
摘要
-
量子计算最令人兴奋的应用之一是帮助我们理解量子力学系统的性质,例如化学相互作用。
-
我们可以从许多不同的角度来思考量子力学。使用 Python 和 Q#,我们把量子力学看作是一种计算,但化学家和物理学家可能会把量子力学看作是一套规则,描述物理系统如何相互作用和表现。
-
物理学家和化学家使用一种特殊的矩阵,称为哈密顿量,来预测量子力学系统随时间的变化。如果我们能在量子计算机上模拟哈密顿量,那么我们就可以模拟由这些矩阵描述的物理系统。
-
哈密顿量的一个重要特殊情况是泡利矩阵的张量积。这些哈密顿量描述了我们在整本书中看到的所有旋转的一种推广,可以使用 Q#的
Exp操作来模拟。 -
更复杂的哈密顿量可以被分解为更简单哈密顿量的和,这使得我们可以一次模拟哈密顿量的一个部分。
-
如果我们快速地在哈密顿量的不同部分之间交替模拟,我们可以得到模拟完整哈密顿量的更好近似。这类似于当我们放大时,快速在北和西之间交替行走看起来有点像西北对角线行走。
-
使用化学模型(例如我们可以从化学家朋友那里获得),我们可以编写和模拟量子化学的哈密顿量。将这种模拟与相位估计相结合,使我们能够学习不同化学品的能量结构,帮助我们预测它们的行为。
(1.) P. J. J. O’Malley 等人,“可扩展的分子能量量子模拟”(2015),arxiv.org/abs/1512.06860。
11 使用量子计算机进行搜索
本章涵盖
-
使用量子算法搜索非结构化数据
-
使用 QDK 资源估算器理解运行算法的成本
-
反转量子寄存器关于状态
在第十章中,我们通过与我们的同事玛丽合作,帮助计算氢分子的基态能量,从而深入了解了量子计算的第一项应用。为此,我们实现了一个哈密顿量模拟算法,该算法使用了我们在第九章中开发的某些相位估计技术。
在本章中,我们将探讨量子计算的另一项应用:搜索数据。这个应用领域一直是高性能计算领域的热门话题,展示了我们可以如何使用之前学到的技术构建另一个量子程序,在这种情况下是基于相位回弹。我们还研究了内置在量子开发工具包(QDK)中的资源估算器,以了解它如何帮助我们理解量子程序的扩展性,即使它们变得太大而无法在本地运行。
11.1 搜索非结构化数据
假设我们想要搜索一些数据以找到联系人的电话号码。如果联系人列表按名字排序,那么通过使用二分搜索来找到与特定名字关联的电话号码就非常容易:
算法 11.1:二分搜索的伪代码
-
在我们的列表中间选择一个名字/电话号码对。称这个对为我们的枢轴。
-
如果枢轴的名字是我们正在寻找的名字,则返回枢轴的电话号码。
-
如果我们正在寻找的名字在枢轴的名字之前,则在列表的前半部分重复搜索。
-
否则,如果我们正在寻找的名字在枢轴的名字之后,则在列表的后半部分重复搜索。
不只是《星际迷航》中的一个角色
在本章中,我们将大量讨论通过数据搜索的内容。这些数据可以以很多不同的形式出现:
-
电话号码
-
狗的名字
-
气象测量
-
门铃的类型
所有这些的共同之处在于,我们可以将它们在经典计算机上表示为位串,使用各种不同的约定来定义这种表示应该如何工作。
以这种方式搜索可以非常快,这是我们能够搜索充满信息的数据库的关键。问题是,在算法 11.1 中,我们严重依赖于我们的名字和电话号码列表是排序的。如果它没有排序,二分搜索就根本不起作用。
注意:现在我们有了解决量子计算机更难问题的所需工具,本章的场景比我们之前的大部分游戏和场景都要复杂一些。如果一开始事情不太明白,请不要担心:慢慢来,仔细阅读。我们保证这会值得你的付出!
换句话说,为了快速搜索我们的数据,我们需要对数据进行某种结构化:对数据进行排序或做出其他假设,使我们能够避免查看每个单独的项目。如果我们没有任何结构,我们能做的最好的事情就是随机浏览数据,直到找到我们想要的东西。算法 11.2 中列出的步骤显示了如何在没有结构的情况下搜索列表的伪代码。我们可能会走运,但平均而言,随机搜索的速度最多只有查看每个项目速度的两倍:
算法 11.2:搜索无结构列表的伪代码
-
从我们的列表中随机选择一个元素。
-
如果它是正确的元素,则返回它。否则,选择一个新的元素并重复。
搜索无结构列表的困难性是许多密码学基础的基础。在这种情况下,我们尝试破解加密算法的任务不是明确写出列表,而是尝试不同的密钥,直到找到一个有效的密钥。我们可以将解密函数视为隐式地定义了一个列表,其中有一个特殊的“标记项”对应于正确的秘密密钥。
算法 11.3 中的伪代码可以代表这个解密任务。我们随机选择的输入是密钥,我们使用解密“函数”或算法来查看它是否解密了信息:
算法 11.3:搜索无结构函数输入的伪代码
-
随机选择一个输入。
-
使用该输入调用我们的函数。如果它有效,则返回我们的输入。
-
否则,选择一个新的随机输入并重复。
如果我们能够更快地搜索无结构列表,那么这将使我们能够排序数据库,解决数学问题,甚至——是的——破解某些类型的经典加密。
比起意外的是,如果定义我们的列表的函数可以写成量子操作(使用我们在第八章中学到的关于预言机的知识),那么我们可以使用一种名为Grover 算法的量子算法,比算法 11.3 更快地找到一个输入。
小贴士:我们正在接近本书的结尾,这意味着我们有机会将本书中学到的知识整合起来。特别是,在本章中,我们将使用第八章中从 Nimue 和 Merlin 的游戏中学到的关于预言机的知识来表示 Grover 算法的输入。如果你需要复习预言机是什么,不用担心;第八章就是为了帮助你的。
当我们运行 Grover 算法时,我们正在搜索函数在所有可能的函数输入上的一个或多个特定值。如果我们想要搜索无结构的数据列表,我们可以考虑定义一个函数,该函数负责查找列表中的特定条目。然后我们可以搜索这个函数的输入,以找到我们想要的特定函数输出值。
考虑一个场景,我们需要在 1 分钟内解密一条信息。我们需要尝试 250 万个不同的密钥,但只有一个可以解密信息,逐个尝试密钥将花费太长时间。我们可以使用 Grover 算法和一个表示问题的函数,例如“这个加密密钥能否解密特定的信息?”来更快地找到正确的密钥,而无需逐个测试每个密钥!这就像图 11.1 中展示的挂锁示例,我们将不同的可能密钥视为输入。

图 11.1 结构化和非结构化搜索。如果我们正在查阅一个按字母顺序排列的地址簿,数据中存在一些我们可以利用的结构来更快地找到数据。在随机密钥盒的情况下,我们只能不断尝试随机密钥,直到锁打开。
当我们在本章稍后回顾或 acles 时,我们将使 Grover 算法所需的函数更精确地表示这个问题;但记住,当我们使用 Grover 算法进行搜索时,我们是在搜索函数的输入,而不是数据列表。考虑到这一点,以下是 Grover 算法的伪代码:
算法 11.4:执行非结构化搜索(Grover 算法)的伪代码
-
分配一个足够大的量子寄存器来表示我们正在搜索的函数的所有输入。
-
准备寄存器处于均匀叠加状态:即所有可能状态具有相同的振幅。这是因为由于问题的类型,我们没有关于哪个输入是“正确”的任何额外信息,所以这代表了对数据的一个均匀概率分布(或先验)。
-
反射寄存器关于标记状态或我们正在搜索的状态。在这里,反射意味着选择一个特定的状态并翻转其符号;我们将在下一节中看到更多细节,以及如何在 Q#中实现反射。
-
关于初始状态(均匀叠加)对寄存器进行反射。
-
重复步骤 3 和 4,直到测量我们正在寻找的项目的概率足够高。然后测量寄存器。我们可以从数学上计算出需要执行此操作的优化次数,以最大化正确答案。
图 11.2 展示了这些步骤。

图 11.2 Grover 算法的步骤,该算法搜索函数的输入,寻找特定的函数输出。我们首先分配一个足够大的量子寄存器来表示我们想要搜索的所有输入,并将其置于均匀叠加状态。最后,我们可以通过正确地反射寄存器的状态来最大化测量我们正在寻找的答案的概率。
注意:在本章的讨论过程中,我们会看到将 Grover 算法视为一种在表示我们是否找到了正确标记项的状态之间旋转的方法:我们场景中的解密密钥。如果我们多次应用 Grover 算法的步骤 3 和 4,我们将旋转到我们寻找的状态之外,因此选择迭代次数是算法的一个关键部分!
图 11.3 展示了使用 Grover 算法在经典方式下搜索非结构化列表的成本可能如何进行比较。

图 11.3 展示了经典计算机和量子计算机搜索非结构化列表所需时间可能如何缩放。我们可以看到,对于较小的项目数量,量子方法需要更多的时间;但随着搜索中项目数量的增加,量子搜索方法所需的时间减少。
提示:描述图 11.3 中所示内容的一种方法是用一个称为“渐近复杂度”的概念。具体来说,我们说经典非结构化搜索需要O(N)次函数调用以搜索N个输入,而 Grover 算法需要O(√N)次调用。如果你对此不熟悉,不要担心;但如果你对如何以这种方式理解算法感兴趣,可以查看 Aditya Y. Bhargava 所著的《Grokking Algorithms》(Manning, 2016)的第一章。
如前所述,让我们跳进去看看这段代码的样子。列表 11.1 是一个使用 Grover 算法在非结构化列表中搜索标记项的 Q#操作的示例。在这里,我们将范围缩小到 8 个键,标记项或正确键由 0 到 7 范围内的整数表示。是的,这意味着我们可能在经典计算机上也能以同样的速度解决这个问题。然而,在本章的结尾,我们将看到随着我们需要搜索的键的数量增加,使用 Grover 算法找到正确键所需的步骤或计算量要低得多。此外,对于这里的示例代码,代表解密算法的函数实际上并没有进行任何解密;它只是像在玩猜谜游戏一样,如果给出了正确的键,就返回一个布尔值。为本章实现特定的解密算法超出了范围,并且可能需要进行一些研究才能实现。这里的目的是展示如何使用 Grover 算法搜索函数的输入可以加快特定问题的解决。
列表 11.1 operations.qs:运行 Grover 搜索的 Q#代码
operation RunGroverSearch(nItems : Int, idxMarkedItem : Int) : Unit {
let markItem = ApplyOracle(
idxMarkedItem, _, _); ❶
let foundItem = SearchForMarkedItem(
nItems, markItem); ❷
Message(
$"Marked {idxMarkedItem} and found
➥ {foundItem}."); ❸
}
❶ 我们可以使用部分应用来在提供给搜索算法的预言者中包含标记项的索引。
❷ 在三个量子比特的寄存器上运行 Grover 搜索算法,并提供了我们之前定义的预言者 markItem
❸ 发出消息以验证它找到了正确的项
如果我们运行列表 11.1 中的示例,我们应该得到以下输出:
In [1]: %simulate RunGroverSearch nItems=7 idxMarkedItem=6
Out[1]: Marked 6 and found 6
从运行 Grover 算法的示例中,我们可以看到我们寻找的解密密钥是索引或标记为数字 6 的那个,算法也找到了标记为数字 6 的密钥。现在,由于SearchForMarkedItem操作确实是这个示例的核心,让我们看看它的实现。
列表 11.2 operations.qs:将 Grover 算法编写为 Q#操作
operation SearchForMarkedItem( ❶
nItems : Int, ❷
markItem : ((Qubit[], Qubit) => Unit is Adj) ❸
) : Int { ❹
use qubits = Qubit[BitSizeI(nItems)]; ❺
PrepareInitialState(qubits); ❻
for idxIteration in ❼
0..NIterations(BitSizeI(nItems)) - 1 {
ReflectAboutMarkedState(markItem, qubits);
ReflectAboutInitialState(PrepareInitialState, qubits);
}
return MeasureInteger(LittleEndian(qubits)); ❽
}
❶ 如同往常,我们首先定义一个新的操作,使用“operation”关键字。
❶ 我们的操作需要的第一输入是我们列表中的项目数量。
❽ 下一个输入是我们搜索问题的表示。我们可以通过一个标记我们列表中项目是否正确的预言机来隐式定义我们的搜索问题。
❾ 当我们完成搜索后,我们将有一个标记项的位置索引。将我们的输出定义为 Int 允许我们返回该索引。
❺ 为了开始搜索,我们需要分配一个足够大的寄存器来存储列表中的索引。
❻ 由于我们是在一个无结构的列表上开始搜索,所以所有项目都是同样好的查找位置。我们通过在列表的所有索引上准备一个均匀叠加来表示这一点。
❻ Grover 算法的核心在于反复关于我们的起始状态和我们正在寻找的项目索引进行反思。
❽ 完成后,测量我们的量子寄存器会告诉我们 Grover 算法找到的项目索引。
Q#标准库提供了一个有用的操作MeasureInteger,它将测量结果解释为经典整数。要像列表 11.2 中那样使用MeasureInteger,我们可以通过使用Microsoft.Quantum.Arithmetic.LittleEndian用户定义类型来标记我们的寄存器,表示它以小端格式编码一个整数。
用户定义类型 如果你需要提醒一下用户定义类型(UDTs)是什么以及如何使用它们,请查看第九章,我们在那里使用它们来帮助 Lancelot 和 Dagonet 玩他们的角度猜测游戏。
到这本书的这一部分,我们几乎已经拥有了理解列表 11.2 所需的所有量子概念。在本章的其余部分,我们将看到如何使用我们所学到的知识来实现一个示例预言机,它可以定义一个简单的搜索问题,以及如何实现构成 Grover 算法的两个反射来解决该问题。
我的好朋友——我对你的查询有异议!
使用像ReflectAboutMarkedState这样的操作作为预言机来定义 Grover 算法的查询可能显得有些牵强。毕竟,由于标记项目的索引作为输入嵌入到ReflectAboutMarkedState中,这似乎意味着我们在这个例子中严重作弊。话虽如此,SearchForMarkedItem只看到我们的预言机作为一个不透明的盒子,并且看不到它的输入,这样嵌入输入实际上并没有让我们作弊。
使用这样一个简单的预言机有助于我们专注于 Grover 算法的工作原理,而无需理解更复杂的预言机。然而,在实践中,我们希望使用更复杂的预言机来代表更困难的搜索问题。例如,为了搜索表示为项目列表的数据,我们可以使用称为量子随机存取存储器(qRAM)的技术将我们的列表转换为预言机。qRAM 的细节超出了本书的范围,但网上有一些关于 qRAM 及其在特定应用中可能多么昂贵的优秀资源。查看github.com/qsharp-community/qram以了解 qRAM 的出色入门指南和可以开始使用的 Q#库。
在许多领域,Grover 算法都得到了广泛的应用,特别是在对称密钥加密中(与第十二章将要介绍的公钥加密相对)。例如,github.com/microsoft/grover-blocks提供了代表 AES 和 LowMC 密码关键部分的预言机实现,这样就可以使用 Grover 算法来理解这些密码。
11.2 关于状态的反射
在算法 11.4 和列表 11.2 中,我们在for循环中反复使用了两个操作:ReflectAboutInitialState和ReflectAboutMarkedState。让我们深入了解这些操作是如何帮助搜索代表我们解密场景的函数的输入的。
这些操作中的每一个都是关于特定状态的反射的例子。这是一个新型量子操作的例子,但我们可以像以前一样使用单位矩阵来模拟它。术语关于状态的反射意味着当我们有一个量子比特寄存器时,我们选择它可能处于的特定状态,如果它恰好处于该状态,我们就翻转该状态上的符号(改变该状态相)。如果你认为这听起来像我们之前查看的一些受控操作,你是正确的;我们将使用受控操作来实现这些反射。
11.2.1 全 1 态的反射
让我们从查看一个特别有用的例子开始:关于全 1 态的反思,|11⋅⋅⋅1〉。我们可以使用在第九章首次看到的CZ(受控Z)操作来实现这种反射。
提示:记住,Controlled函子不仅仅是一个花哨的if块,而可以用于叠加。要了解Controlled函子是如何工作的,请参阅第九章,在那里我们使用它来帮助 Lancelot 和 Dagonet 玩游戏。
列表 11.3 operations.qs:关于|11⋅⋅⋅1〉状态的反射
operation ReflectAboutAllOnes(register : Qubit[]) : Unit is Adj + Ctl {
Controlled Z(Most(register),
Tail(register)); ❶
}
❶ 受控函子允许我们以受控的方式使用 Z 操作。
与其他Controlled操作一样,Controlled Z需要两个输入:用作控制量子比特的寄存器以及如果控制寄存器中的所有量子比特都处于|1〉状态,则将对该量子比特应用Z操作的量子比特。在列表 11.3 中,我们可以使用Microsoft.Quantum.Arrays中的Most函数来获取除最后一个量子比特之外的所有量子比特,并使用Tail来获取最后一个量子比特。
小贴士:通过使用CZ与Most和Tail一起,我们的实现不依赖于寄存器中有多少量子比特。这将在以后很有用,因为我们可能需要不同数量的量子比特来表示列表中的数据。
回想第九章,CZ操作将对|11⋅⋅⋅1〉状态应用-1 的相移,并对每个其他计算基态不做任何操作。回想第二章,每个计算基态都是一种方向,这意味着单个方向会被CZ翻转,而所有其他输入状态保持不变。这种图像是我们称之为反射的原因,尽管由于涉及到的维度数量,实际的图形表示很复杂。
CZ 的矩阵表示
一种可以看到CZ操作翻转单个输入状态的符号,如前所述,的方法是编写一个模拟CZ的单位矩阵。以下是一个只有一个控制量子比特的例子:

下面是一个有两个控制量子比特的例子:

通过本书中关于单位矩阵所学到的知识,这些矩阵清楚地表明,输入状态|11〉和|111〉分别被-1 翻转,而所有其他输入状态保持不变(获得+1 的相移)。无论我们使用多少控制量子比特与CZ一起使用,这种模式都会持续。
练习 11.1:CZ 的诊断
使用DumpMachine来查看CZ如何作用于均匀叠加态|+⋅⋅⋅〉。
提示:回想一下|+〉 = H|0〉,因此我们可以使用程序ApplyToEachCA(H, register)在以|00⋅⋅⋅0〉状态开始的寄存器上准备|+⋅⋅⋅+〉。
练习题解决方案
本书中的所有练习题的解决方案都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入我们所在的章节文件夹,并打开提及练习题解决方案的 Jupyter 笔记本。
11.2.2 对任意状态的反射
一旦我们对|11⋅⋅⋅1〉有了反思,我们就可以用它来对其他状态进行反思。这很重要,因为我们可能无法设置代表解密算法的预言函数,以便我们想要的输入或密钥由全 1 输入表示。也请从我们的示例代码中回忆,预言函数仅实现了一种猜谜游戏式的解密,而不是真正的解密算法。
深入探讨:反射是旋转吗?
根据我们对反射的几何理解,我们自然会认为它们是一种 180°的旋转。这之所以成立,仅仅是因为量子状态是复数的向量;如果我们只能访问实数,我们就无法通过旋转来获得反射!我们可以通过回想我们在第二章和第三章中如何描述状态和旋转来看到这一点:作为二维圆的旋转。
如果我们拿起一个二维物体,将其翻转,然后放回原处,没有拿起它就无法将其转换回原来的样子。另一方面,三维空间为我们提供了足够的空间来组合不同的旋转以进行反射。由于复数在描述量子位的状态时提供了第三个轴(即Y轴),这也使我们能够在 Grover 算法中进行所需的反射。
关于除了全一状态以外的状态的反射技巧是将我们想要反射的任何状态转换为全一状态,调用ReflectAboutAllOnes,然后撤销我们用来将反射映射到全一状态的运算。我们可以通过从全零状态开始来描述任何状态,因此我们需要一种方法从全零状态转换到我们可以使用我们刚刚学到的反射的全一状态。下面的列表显示了从全零状态准备寄存器到全一状态的示例。
列表 11.4 operations.qs:准备全一状态
operation PrepareAllOnes(register : Qubit[]) : Unit is Adj + Ctl {
ApplyToEachCA(X, register); ❶
}
❶ ApplyToEachCA 操作允许我们将第一个输入(一个操作)应用于寄存器中的每个量子位(第二个输入)。
在 Q#中,所有新分配的寄存器都从|00⋅⋅⋅0〉状态开始。因此,在列表 11.4 中,当我们对每个新分配的量子位应用X时,我们就将我们的新寄存器准备成|11⋅⋅⋅1〉状态。
对于下一步,我们需要考虑准备我们想要反射的状态的操作。如果我们有一个可伴随的操作(is Adj),它可以准备我们想要反射的特定状态,那么我们只需要解除准备该状态,准备全一状态,关于全一状态(|11⋅⋅⋅1〉)进行反射,解除全一状态的准备,然后重新准备我们试图反射的状态。
我们为什么喜欢 Dirac 符号
在理解算法 11.5 中的步骤时,思考每个操作在该步骤序列中对输入状态做了什么是有帮助的。幸运的是,Dirac 符号(首次在第二章中遇到)可以帮助我们写出单位矩阵如何转换不同的状态,这样我们就可以理解和预测相应的 Q#操作将对我们的量子位做什么。
例如,考虑 Hadamard 操作H。正如我们在整本书中看到的那样,H可以通过以下单位矩阵来模拟:

这个幺正矩阵充当一种真值表,告诉我们H将|0〉状态转换成 1 / √2 (|0〉 + |1〉)状态。使用狄拉克符号,我们可以通过写出H = |+〉〈0| + |−〉〈1|来使这一点更清晰。通过将基(|⋅〉)视为表示输入,将共轭基(〈⋅|)视为表示输出,我们可以将其读作“H操作将|0〉转换成|+〉和|1〉”。
此图显示了狄拉克符号如何作为一种视觉语言,告诉我们不同幺正矩阵的输入和输出,这使得理解 Q#操作序列如何协同工作变得更容易。

使用狄拉克(又称括号)符号分解关于状态的反思。ReflectAboutMarkedState的每一步都被拆分,我们展示了该操作的狄拉克符号。当所有单独的步骤连接起来时,得到的结果状态是-2|Ψ〉〈Ψ〉。
算法 11.5:如何反思任意状态
-
使用
Adjoint函子,“取消准备”我们的任意状态,将其映射到全零的|00⋅⋅⋅0〉状态。 -
从全零状态准备全一状态
|11⋅⋅⋅1〉。 -
使用
CZ来反思|11⋅⋅⋅1〉。 -
取消准备全一状态,将其映射回全零状态。
-
再次准备我们的状态,将全零状态映射到我们的任意状态。
提示:在算法 11.5 中,步骤 1 和 5 相互抵消伴随,同样步骤 2 和 4 也是如此。利用我们关于“鞋袜”思维的所学,这使得算法 11.5 的流程非常适合实现 Q#!的within/apply特性。为了了解此功能的工作方式,请查看第八章,其中我们使用了within/apply块来实现 Nimue 和 Merlin 的 Deutsch–Jozsa 算法。
由于我们在运行 Grover 算法时对或 acle 的正确输入没有任何先验概念,我们希望从均匀叠加|+⋅⋅⋅〉开始搜索,以表示任何输入都可能是正确的。这给了我们一个机会来使用我们从算法 11.5 中学到的知识来练习在 Q#中实现反思。遵循反思初始状态的步骤,我们可以实现列表 11.2 中使用的ReflectAboutInitialState操作。以下列表显示了如何在 Q#操作中遵循算法 11.5。
列表 11.5 操作.qs:反思任意状态
operation PrepareInitialState(register : Qubit[]) : Unit is Adj + Ctl {
ApplyToEachCA(H, register); ❶
}
operation ReflectAboutInitialState(
prepareInitialState : (Qubit[] => Unit is Adj), ❷
register : Qubit[]) ❸
: Unit {
within { ❹
Adjoint prepareInitialState(register); ❺
PrepareAllOnes(register);
} apply {
ReflectAboutAllOnes(register);
}
}
❶均匀叠加状态表示我们在搜索中没有任何先验信息(毕竟,它是一个无结构搜索问题)。
❷遵循算法 11.5,为了反思初始状态,我们需要提供一个准备它的操作。
❸当然,我们还需要一个量子比特寄存器来应用我们的反思!
❹执行算法 11.5 中的步骤 1 和 2
❺伴随函子表明我们想要执行“反向”或相反的操作,以准备初始状态。换句话说,如果我们从初始状态开始,应用伴随的prepareInitialState操作将带我们回到|00⋅⋅⋅0〉状态。
我们现在有了关于那个初始状态的反射代码,那么我们如何检查它是否如我们所期望的那样工作?当运行模拟目标机器时,我们可以使用DumpRegister等命令来显示它用于模拟我们的量子比特寄存器的所有信息。图 11.4 显示了DumpRegister在准备均匀叠加后的输出。

图 11.4 使用DumpRegister查看PrepareInitialState操作准备的初始状态。每个可能的本征态具有相同的振幅(因此相同的测量概率),这被称为均匀叠加。
对于关于标记态的反射,这是 Grover 算法所需的另一个反射,我们必须采用稍微不同的方法。毕竟,我们不知道如何准备标记态——这正是我们使用 Grover 算法要解决的问题!幸运的是,回想第八章,我们可以利用我们从 Nimue 和 Merlin 的游戏中学到的知识来实现关于一个状态的反射,即使我们不知道那个状态是什么。
注意:这是 Grover 算法的核心:我们可以通过用正确的输入叠加调用预言机一次来使用我们的预言机来反射关于标记态。正如我们在下一节中看到的,这些反射中的每一个都给我们关于标记项的一些信息。相比之下,每个经典函数调用最多可以消除一个可能的输入。
为了了解这种情况下的工作原理,让我们首先退一步,看看我们的标记态是什么。由于我们的列表是由预言机定义的,我们可以写一个单位算子O,它允许我们模拟那个预言机。以下是狄拉克符号:

在第八章中,我们了解到对一个处于|−〉态的量子比特应用X操作会引入一个-1 的相因子,因为|−〉是X操作的本征态。使用同样的技巧,我们可以写出当标志量子比特(即|y〉寄存器)处于|−〉态时,我们的预言机所执行的操作:

这正是我们需要实现反射的操作!因此,根据我们在第八章中学到的知识,我们可以以相同的方式实现它:只需将我们的预言机应用于一个从|−〉态开始的量子比特。这个预言机代表了该场景的解密算法,这里简化为一个接受可能的密钥作为输入并仅返回一个 bool 值以指示它是否是正确密钥的函数。接下来的列表显示了一个使用此方法的 Q#操作示例;请记住,在 Q#中,我们可以将一个操作作为输入传递给另一个函数或操作。
列表 11.6 operations.qs:关于标记态的反射
operation ReflectAboutMarkedState(
markedItemOracle : ❶
((Qubit[], Qubit) => Unit is Adj),
inputQubits : Qubit[]) ❷
: Unit is Adj {
use flag = Qubit(); ❸
within {
X(flag); ❹
H(flag);
} apply{
markedItemOracle(inputQubits,
flag); ❺
}
}
❶ 我们的项目标记预言机的操作类型为((Qubit[], Qubit) => Unit is Adj),表示它接受一个量子比特寄存器加上一个额外的量子比特,并且是可伴随的。
❷ 反射关于标记态的第二个输入是一个我们想要应用反射的寄存器。
❸ 我们需要分配一个额外的量子比特(称为标志)来应用我们的预言者,这对应于早期方程中的y寄存器。
❹ 就像我们在第八章中使用 H 和 X 操作来准备 Nimue 的量子比特在|−〉状态一样,我们在这里使用|−〉 = HX|0〉来准备我们的标志量子比特。
❺ 我们应用我们的预言者来使用 Deutsch–Jozsa 技巧,并将-1 的相位应用于我们的预言者标记的状态。
在列表 11.6 中,由于这个准备是在within/apply块中,Q#会自动通过撤销X和H操作来将我们的量子比特放回|0〉状态。毕竟,正如我们在第八章中看到的,应用我们的预言者会将其目标留在|−〉状态。
练习 11.2:标志准备
在列表 11.6 中,我们也可以写H(flag); Z(flag);。使用 QuTiP 和AssertOperationsEqualReferenced中的任意一种或两种方法都可以证明这两种准备我们的标志量子比特的方式给我们带来了相同的反射。
令人惊讶的是,我们使用了 Deutsch–Jozsa 技巧来反思一个由我们的预言者隐式地定义的状态!我们不必明确知道标记状态是什么,就可以应用反思:非常适合用于无结构的搜索。
在下一节中,我们将看到如何结合初始状态和标记状态反射,将所有内容整合在一起,完全实现 Grover 算法,并找到我们的密钥。
11.3 实现 Grover 搜索算法
现在我们已经了解了关于状态旋转的知识,并回顾了预言者,是时候将所有这些整合起来进行一些无结构的搜索了!让我们首先回顾实现 Grover 算法的所有步骤(图 11.5):
-
分配一个足够大的量子比特寄存器来索引我们正在搜索的数据集。
-
准备寄存器在均匀叠加状态:即所有可能的状态具有相同的振幅。这是因为由于问题的类型,我们没有关于数据集的任何额外信息,所以这代表了对数据的一个均匀概率分布(或先验)。
-
反射寄存器关于标记状态或我们正在搜索的状态。
-
反射寄存器关于初始状态(均匀叠加)。
-
重复步骤 3 和 4,直到测量我们正在搜索的项目的概率足够高。然后测量寄存器。我们可以从数学上计算出我们需要这样做多少次才能最大化得到标记项目的概率。

图 11.5 回顾 Grover 算法的步骤,该算法搜索函数的输入,寻找特定的函数输出。我们首先分配一个足够大的量子比特寄存器来表示我们想要搜索的所有输入,并将其置于均匀叠加状态。最后,我们可以通过正确地反射寄存器的状态来最大化测量我们正在寻找的答案的概率。
尽可能接近但不超出
如果我们应用太多的 Grover 算法迭代,我们想要测量的状态的振幅会减小。这是因为每次迭代实际上是一个旋转;关键是停止在正确的点上。为了确定停止标准的三角学,我们将 Grover 算法中使用的寄存器状态写成未标记和标记状态的叠加。我们不会在这里详细说明这个推导过程,但如果你对幕后数学感兴趣,可以查看docs.microsoft.com/quantum/libraries/standard/algorithms或 Michael A. Nielsen 和 Isaac L. Chuang 所著的《量子计算与量子信息》(Cambridge University Press,2010)的第 6.1.3 节。这个公式在本书的示例仓库中已经实现,但如果你想在 Q#中自己编程实现,公式如下:

在上一节中,我们开发了一些完整实现所需的操作。例如,我们用 PrepareInitialState 操作实现了步骤 2,用 ReflectAboutMarkedState 和 ReflectAboutInitialState 操作分别实现了步骤 3 和 4 中的反射。我们仍然需要一个函数来确定步骤 3 和 4 需要循环多少次,以及一个识别我们要找的项的预言者的实现。让我们从帮助定义 Grover 算法停止标准的函数开始。
列表 11.7 operations.qs:Grover 算法的停止标准
function NIterations(nQubits : Int) : Int {
let nItems = 1 <<< nQubits; ❶
let angle = Arcsin(1\. /
Sqrt(IntAsDouble(nItems))); ❷
let nIterations =
Round(0.25 * PI() / angle - 0.5); ❸
return nIterations;
}
❶ <<< 是左移位运算符,用于计算 2^(nQubits),它代表 nQubits 量子寄存器可以索引的最大项目数。
❷ 确定 Grover 算法每次迭代的实际旋转角度
❸ 使用有效旋转角度和一些三角学,我们可以计算出多少次迭代将最大化我们测量标记项的概率。
现在我们可以在 Grover 算法的实现中计算何时停止循环,我们最后需要的是一个可以——给定我们要找的项和数据集中的潜在项——如果潜在项是我们正在寻找的项,则翻转寄存器部分相位的预言者。为了举例说明,让我们将预言者视为代表一种猜测游戏。如果有人正在想数字 4 并让我们猜他们的数字,那就是一个经典函数的例子:

在经典情况下,我们最好的策略就是尝试不同的输入到 f,直到我们尝试 x = 4。如果我们想尝试 Grover 算法,利用我们在第八章学到的知识,我们知道我们需要一个表示 f 的操作:

使用 Q#函数ControlledOnInt(作为 Q#标准库的一部分提供)实现一个可以被模拟的Uf操作相当简单。与Controlled函子类似,ControlledOnInt函数允许我们控制另一个寄存器状态的运算。区别在于,Controlled总是控制于全 1 状态|11⋅⋅⋅1〉,而ControlledOnInt函数允许我们控制于一个不同的状态,该状态由一个整数指定。例如,如果Length(register)是 3,那么(ControlledOnInt(4, X))(register, flag)会在register处于状态|100〉时翻转flag的状态,因为 4 在小端表示法中写作100。
练习 11.3:ControlledOnInt 的作用
尝试使用狄拉克符号(如果需要复习,请查看第二章和第四章)或编写一个可以用来模拟(ControlledOnInt(4, X))在三个量子比特寄存器和标志量子比特上作用的单位矩阵,来描述(ControlledOnInt(4, X))(register, flag)对register + [flag]状态的影响。
提示:由于在这个例子中(ControlledOnInt(4, X))作用于四个量子比特(三个控制量子比特和一个目标量子比特),单位矩阵应该是一个 16 × 16 的矩阵。
尝试做同样的事情,但对于(ControlledOnInt(4, Z))。
使用ControlledOnInt函数,我们可以快速编写一个预言机,该预言机根据对预言机的输入翻转标志量子比特的状态,如下一列表所示。在这里,我们的预言机应该在预言机的输入处于标记状态时翻转其标志量子比特。
列表 11.8 operations.qs:标记我们想要的状态的预言机
operation ApplyOracle(
idxMarkedItem : Int, ❶
register : Qubit[],
flag : Qubit
) : Unit is Adj + Ctl {
(ControlledOnInt(idxMarkedItem, X))
(register, flag); ❷
}
❶ 将我们正在寻找的项目索引作为一个整数表示(这里的示例使用三个量子比特,因此我们可以输入任何 0 到 2³ − 1 = 7 的整数)
❷ 我们之前学到的ControlledOnInt函数可以在输入寄存器处于正确标记的项目时对标志应用 X 操作。
添加这两个代码片段后,我们可以回到之前提供的示例代码。
列表 11.9 operations.qs:Grover 算法作为 Q#操作
operation SearchForMarkedItem( ❶
nItems : Int, ❷
markItem : ((Qubit[], Qubit) => Unit is Adj) ❸
) : Int { ❹
use qubits = Qubit[BitSizeI(nItems)]; ❺
PrepareInitialState(qubits); ❻
for idxIteration in ❼
0..NIterations(BitSizeI(nItems)) - 1 {
ReflectAboutMarkedState(markItem, qubits);
ReflectAboutInitialState(PrepareInitialState, qubits);
}
return MeasureInteger(LittleEndian(qubits)); ❽
}
❶ 如同往常,我们首先通过使用“operation”关键字定义一个新的操作。
❷ 第一个输入是我们列表中的项目数量。
❸ 正如加密示例一样,我们可以通过一个标记我们列表中项目是否正确的预言机隐式定义我们的列表。
❹ 当我们完成搜索后,我们有一个标记项目的索引。将我们的输出定义为 Int 允许我们返回该索引。
❺ 为了开始搜索,我们需要分配一个足够大的寄存器来存储列表中的索引。
❻ 由于我们正在搜索一个无结构的列表,当我们开始搜索时,所有项目都是同样好的查找位置。
❼ Grover 算法的核心在于反复关于我们的起始状态和我们正在寻找的项目索引进行反思。
❽ 完成后,测量我们的量子比特寄存器会告诉我们 Grover 算法找到的项目索引。
Q#标准库提供了一个有用的操作MeasureInteger,它将测量结果解释为经典整数。要使用MeasureInteger,如列表 11.10 所示,我们可以使用Microsoft.Quantum.Arithmetic.LittleEndian UDT 将我们的寄存器标记为编码在 little-endian 寄存器中的整数。
我们已经有了所有需要的代码,所以让我们运行一个示例。
列表 11.10 operations.qs:Grover 算法的一个具体示例
operation RunGroverSearch(nItems : Int, idxMarkedItem : Int) : Unit {
let markItem = ApplyOracle(
idxMarkedItem, _, _); ❶
let foundItem = SearchForMarkedItem(
nItems, markItem); ❷
Message(
$"Marked {idxMarkedItem} and found
➥ {foundItem}."); ❸
}
❶ 我们可以使用部分应用来将标记项的索引包含在我们提供给搜索算法的或门中。
❷ 在三个量子比特的寄存器上运行 Grover 算法,并提供了我们之前定义的 markItem 或门
❸ 发送消息以验证它找到了正确的项
如果我们运行这个示例,我们应该得到以下输出:
In [1]: %simulate RunGroverSearch nItems=7 idxMarkedItem=2
Out[1]: Marked 2 and found 2.
练习 11.4:更改或门
尝试更改或门的定义以控制不同的整数。当你运行 Grover 算法时,这会改变输出吗?
恭喜:我们现在可以使用量子程序进行无结构搜索了!但实际上发生了什么?使 Grover 算法工作的几何关键洞察是,当我们关于两个不同的轴进行反射时,我们得到一个旋转。图 11.6 展示了映射如何工作的一个例子。

图 11.6 展示了成对的反射如何进行旋转。如果我们把映射反射到从水平线向上 25°的线上,然后再反射到水平线上,这相当于将映射绕水平线旋转 50°向下。
同样的想法也适用于量子状态。在 Grover 算法中,初始和标记状态反射组合成一个从未标记状态到标记状态的单一旋转。为了理解这是如何工作的,我们可以使用我们在整本书中学到的技术来查看寄存器每个状态振幅在算法步骤中发生的变化。
从图 11.7 中我们可以看到,每一轮反射似乎都会放大对应于我们正在寻找的索引的状态振幅。通过在未标记和标记状态之间旋转,我们可以使我们的量子比特状态与我们要找到的标记状态对齐。

图 11.7 展示了随着我们逐步通过 Grover 算法,我们量子寄存器状态的振幅如何变化。当我们继续进行反射时,一些振幅被放大,而其余的振幅被减小。
实际上,我们还可以在其他应用中使用相同类型的想法。Grover 算法是更广泛类别的量子算法的一个例子,这些算法执行所谓的振幅放大。这意味着当我们测量我们的量子比特寄存器时,测量结果的经典比特字符串有很大可能是我们正在寻找的项。
确定性量子算法与概率性量子算法
Grover 算法通过在每次迭代中增加我们得到正确答案的概率来工作。然而,一般来说,Grover 算法可能无法将这个概率增加到 100%。因此,Grover 算法是一个概率量子算法的例子,这意味着我们并不能保证每次运行都能得到我们想要的答案。在实践中,这并不是一个问题,因为我们总是可以运行它几次,以获得更高的成功概率。
可能会诱使我们得出结论,所有量子算法都是通过这种方式概率性的——但事实并非如此。正如我们在第八章中看到的,Deutsch-Jozsa 算法是一个确定性量子算法的例子,每次运行都会给出相同的结果。
练习 11.5:使用 DumpMachine 探索 Grover 算法
在这本书中,我们已经学到了很多关于旋转的知识,这有助于理解 Grover 算法每次迭代的旋转应用。尝试修改 Grover 算法的实现,使其迭代次数加倍,并使用DumpMachine来查看结果状态。这看起来像是应用旋转两次后你所期望的吗?
更一般的振幅放大示例
除了相位估计之外,振幅放大是量子算法中使用的最基本技术之一。自从 Grover 算法首次引入振幅放大的概念以来,已经开发出大量变体来覆盖广泛的问题,例如当存在多个标记项时,当我们想要优化一个函数而不是寻找标记项时,或者当我们只能有时正确准备初始状态时。许多这些技术都可在 Q#标准库中的Microsoft.Quantum.Amplitude.Amplification命名空间下找到。去看看吧!
在我们结束这一章之前,简要讨论一下,与使用经典硬件相比,在量子硬件上执行我们刚刚实现的搜索操作如何进行扩展,这将是有益的。
11.4 资源估计
我们之前提到,当我们从 250 万个密钥简化到 8 个密钥时,使用 Grover 算法将会有优势。那么,在实践中运行 Grover 算法需要多长时间?这实际上是一个相当复杂的问题——我们可以为此写几本书。部分原因是,估计资源需求必然取决于我们量子计算堆栈的许多不同部分。
例如,量子设备中常见的错误,我们需要在运行过程中使用纠错来保护我们的计算。我们用来保护计算的错误纠正方法对我们的程序运行所需条件有巨大影响。整个会议都是因为这个原因,致力于寻找更好的纠错码。
幸运的是,Q#和量子开发工具包提供了一些我们需要开始了解运行不同量子程序所需工具的工具。我们不必在模拟器上运行我们的程序,该模拟器模拟真实量子计算机的工作方式,我们可以在资源估算器上运行它,它会告诉我们需要调用每种类型的基本操作的次数,程序需要多少量子位,以及程序中可以并行调用的量子操作的次数。让我们看看一个小例子,使用我们在第八章中学到的 Deutsch–Jozsa 算法。
列表 11.11 再次定义 Deutsch–Jozsa 算法
In [1]: operation ApplyNotOracle(control : Qubit, target : Qubit)
: Unit { ❶
within {
X(control);
} apply {
CNOT(control, target);
}
}
Out[1]: - ApplyNotOracle
In [2]: open Microsoft.Quantum.Measurement; ❷
operation CheckIfOracleIsBalanced( ❸
oracle : ((Qubit, Qubit) => Unit)
) : Bool {
use control = Qubit();
use target = Qubit();
H(control);
within {
X(target);
H(target);
} apply {
oracle(control, target);
}
return MResetX(control) == One;
}
Out[2]: - CheckIfOracleIsBalanced
In [3]: operation RunDeutschJozsaAlgorithm()
: Bool { ❹
return CheckIfOracleIsBalanced(ApplyNotOracle);
}
Out[3]: - RunDeutschJozsaAlgorithm
❶ 与我们之前看到的相同的 ApplyNotOracle,但现在它使用的是 within/apply 流程
❷ 记住,当使用 Q# Jupyter 笔记本时,我们必须在每个我们想要使用它们的单元中打开命名空间。
❸ 检查 IfOracleIsBalanced 操作与之前相同,除了再次使用 within/apply 块来替换重复的 H 和 X 操作。
❹ 在 Q#笔记本中,我们需要一个无参数的操作来与%simulate 和%estimate 命令一起使用。
当我们在 IQ#笔记本中运行%estimate魔法命令时,我们会得到一个类似于图 11.8 所示的表格。这个表格报告了量子开发工具包估计我们的程序运行所需资源类型。

图 11.8 在列表 11.11 的程序上运行%estimate RunDeutschJozsaAlgorithm的输出。当我们使用%estimate时,我们得到一个计数,表示我们的量子设备(或模拟器)需要提供各种类型的资源,以便运行程序。查看表 11.1 以了解更多关于这些计数的含义。
表 11.1 %estimate魔法命令跟踪的资源类型
| 资源类型 | 描述 |
|---|---|
CNOT |
CNOT操作被调用的次数 |
QubitClifford |
X、Y、Z、H和S操作被调用的次数 |
R |
单量子位旋转操作被调用的次数 |
Measure |
调用测量操作的次数 |
T |
T操作被调用的次数 |
Depth |
在单个量子位上需要连续调用多少次T操作 |
Width |
我们程序需要多少量子位 |
BorrowedWidth |
我们程序需要多少量子位才能借用(一种比本书中介绍的高级技术) |
提示:我们也可以从 Python 中估算资源!只需使用estimate_resources方法代替我们在前几章中学到的simulate方法。
如我们从笔记本中的%estimate运行和表 11.1 中看到的那样,一些类别可能是有意义的,比如宽度、测量次数和 R 表示单量子位旋转使用的次数。其他的是新的,比如计数T操作和深度。我们之前没有见过T操作,但它们只是另一种单量子位操作。
见先生T
与本书中迄今为止看到的大多数其他操作一样,T操作可以通过一个单位矩阵来模拟:

即,T是在Z轴上的 45°(π / 4)旋转。
另一种思考T操作的方式是将其视为我们之前看到很多次的Z操作的四次方根。由于 45° × 4 = 180°,如果我们连续四次应用T,那是一种昂贵的方式来应用一次Z操作。
在 Q#中,T操作作为Microsoft.Quantum.Intrinsic.T提供,类型为Qubit => Unit is Adj + Ctl。
练习 11.6:四个 T 构成一个 Z
使用AssertOperationsEqualReferenced来证明应用四次T操作与应用一次Z操作相同。还有一个操作S,可以将其视为Z的平方根(Z轴上的 90°旋转);检查应用两次T与应用一次S相同。
使T操作在估计资源时显得特别并因此值得高度重视的原因是,当在更大的量子设备上运行时,它们与所需的错误纠正方法一起使用时成本较高。我们迄今为止使用的多数操作都属于克利福德群:易于与错误纠正一起使用的操作。正如之前所述,详细讨论错误纠正超出了本书的范围;但简而言之,我们拥有的非克利福德操作越多,在错误纠正硬件上实现我们的程序就越困难。因此,统计“昂贵”操作(如T)的数量对于运行我们目前可用的硬件来说非常重要。
小贴士:从高层次来看,必须按顺序应用(即不能并行运行)的T操作的数量是量子程序在错误纠正量子计算机上运行时间的良好近似。资源估计器将其报告为深度指标。
那么,在 Q#的资源估计器中,我们可以计数资源的典型或异常值是什么?对于一个简单的程序如RunDeutschJozsaAlgorithm,所需的资源非常有限。然而,查看表 11.1 时,对T操作的关注很多,因此让我们深入探讨一下,看看这个操作是什么以及为什么它对资源估计至关重要。图 11.9 显示了估计调用我们在第九章中学习的CCNOT操作所需资源的结果。

图 11.9 为估计调用CCNOT所需资源的结果。从代码中看起来似乎只有一个操作;但事实上,CCNOT根据目标机器被分解成更容易实现的操作。
练习 11.7:重置寄存器
为什么我们不需要重置如图 11.9 所示的EstimateCcnotResources中分配的量子比特寄存器?
这个输出有点令人惊讶,因为我们的小程序需要 10 次CNOT操作,5 次单量子位操作和 7 次T操作,尽管我们没有直接调用它们。实际上,在纠错量子程序中直接应用CCNOT之类的操作非常困难。因此,Q#资源估算器首先将我们的程序转换为更接近实际在硬件上运行的程序,使用对更基本操作(如CNOT和T)的调用。
练习 11.8:T 缩放
随着控制量子位数量的增加,T操作调用次数如何变化?一个粗略的趋势就足够了。
提示:正如我们之前所看到的,一个具有任意数量量子位的受控-NOT 操作可以写成Controlled X(Most(qs), Tail(qs));,这是使用Microsoft.Quantum.Arrays命名空间提供的函数实现的。
当我们想要估算运行一个太大而无法在经典计算机上模拟的程序所需的资源时,这非常有用。图 11.10 显示了在 20 位量子列表(约 100 万项)上运行 Grover 算法的输出。

图 11.10 运行资源估算器在 Grover 算法上的结果。这些资源计数清楚地说明了为什么我们无法直接模拟这么大的 Grover 实例,因为我们需要 39 个量子位。然而,我们可以使用来自多个搜索大小的数据来了解我们的 Grover 实现将如何扩展。
如果我们为各种列表大小运行这个程序,我们会得到图 11.11 中所示的那种曲线。对于我们的 2.5 百万个密钥的场景,量子步骤数远低于经典步骤成本。当然,这并不是全部故事,因为量子计算机上的每一步可能比经典计算机上的对应步骤慢得多;但这确实是理解在实践中运行不同的量子程序所需条件的一个很好的起点。

图 11.11 估计运行不同大小列表的 Grover 算法所需的资源输出
我们现在已经学会了如何将我们在第七章中学到的预言机与一种新的量子操作(反射)结合起来,以搜索函数的输入。这在有严格时间约束的场景中非常有用,有助于更快地找到解密密钥。
在下一章中,我们将使用本章中学到的技能来回答量子计算提出的最重要问题之一:量子计算机需要多长时间才能破解现代加密?
概述
-
量子计算机的另一个应用是搜索一个不透明函数的输入,以找到一个产生所需输出的输入(即,一个标记输入)。我们可以使用 Grover 算法通过比经典计算更少的调用我们的预言机来搜索。
-
Grover 算法使用反思,这是一种量子操作,其中一个输入状态的相位被翻转,而所有其他输入状态保持不变。我们可以通过旋转以及我们在第八章中看到的鞋袜模式来构建许多不同类型的反思。
-
使用 Q#提供的各种旋转操作,结合
within和apply,我们可以定义一个预言机来标记特定项目,然后通过单个预言机调用对该标记项目进行反思。综合这些技术,我们可以实现 Grover 算法。 -
为了验证 Grover 算法在足够大的问题上的性能优于经典方法,我们可以在资源估算器上运行我们的 Q#程序。与量子开发工具包中提供的模拟器不同,资源估算器并不模拟量子程序,而是计算它们需要多少个量子比特以及它们在量子设备上需要调用多少操作。
12 使用量子计算机进行算术
本章涵盖
-
使用 Q#数值库进行编程
-
实现 Shor 算法以分解整数
-
认识量子计算对安全基础设施的影响
在第十一章中,我们使用了 Grover 算法中的一种量子编程技术——振幅放大,来加速搜索非结构化数据集。虽然对于较小的数据集来说,Grover 的搜索方法并不是最有效的方法,但当我们着眼于解决更大和更大的问题时,我们的量子方法提供了明显的优势。在本章的最后,我们将利用本书中开发的所有技能来处理最著名的量子算法之一:Shor 算法。我们将实现 Shor 算法,并展示它如何在我们尝试分解大整数时给我们带来优势。虽然这可能不是最有意思的任务,但分解整数的难度实际上支撑着我们当前的大部分加密基础设施。
12.1 将量子计算分解为安全性
在本书的第一部分,我们看到了量子概念如何应用于使用量子密钥分发等技术安全地发送数据。尽管如此,没有 QKD,关键数据也一直在互联网上秘密共享。互联网被用来共享支付数据、个人健康数据和约会偏好,甚至用来组织政治运动。在本章中,我们将探讨经典计算机如何保护我们的隐私,以及量子计算如何改变我们决定使用哪些工具来保护我们的数据的方式。
注意:现在我们有了用量子计算机解决更难问题的工具,本章的场景比我们之前的大部分游戏和场景都要复杂一些。如果一开始事情没有弄明白,请不要担心:慢慢来,仔细阅读。我们保证这会物有所值!
首先,让我们看看使用经典计算机保护数据的最先进技术。实际上,在经典数学中存在许多不同的问题,其中一些非常容易解决(例如,“2 + 2 等于多少?”),而另一些则非常困难(例如,“P 是否等于 NP?”)。在这两个极端之间,我们得到一些问题,除非有人给我们提示,否则它们很难解决。在这种情况下,它们变得容易解决。这些问题往往看起来更像谜题,可以用于隐藏数据:我们必须知道秘密提示或使用大量的计算时间来解决它们。
小贴士:在第三章中,我们看到了量子密钥分发,这是一种依靠量子力学而不是谜题来安全分享信息的好方法。尽管如此,我们可能并不总是能将量子比特发送给我们的朋友,因此理解如何使用谜题来安全且私密地交流仍然很重要。
正如我们将在本章后面看到的那样,分解数字可以是这些密码学算法可以依赖的谜题之一,以实现安全性。目前正在使用许多非常重要的算法和密码学协议,它们依赖于分解大数难题的难度。如果你猜到量子计算机可以帮助我们分解大数,你就在正确的道路上。
进入 Shor 算法。使用经典计算机,我们可以将寻找整数因子的难题或谜题简化为解决一种关于在模运算(也称为 时钟运算,我们将在本章后面看到更多)中使用函数重复速度的谜题。如果我们使用 Shor 算法,估计函数重复速度正是我们可以在量子计算机上轻松解决的谜题。让我们深入研究 Shor 算法的步骤,然后看看如何使用它的一个例子:
场景:分解整数 N
假设我们正在尝试分解整数 N,并且我们事先知道 N 恰好有两个质因数。使用 Q#实现 Shor 算法来分解 N。
同余数和半同余数
作为一项有用的术语,我们说两个除了 1 以外没有其他公因数的数是 互质数。例如,15 和 16 都不是质数,但就它们彼此而言,15 和 16 是互质的。
同样,我们说一个恰好有两个质因数的数是 半同余数。例如,15 是半同余数,因为 15 = 3 × 5,并且因为 3 和 5 都是质数。另一方面,28 不是半同余数,因为 28 = 4 × 7 = 2 × 2 × 7。在考虑密码学时,半同余数经常出现,因此在我们的场景中通常假设这一点是有用的。
我们可以执行算法 12.1 中的步骤(如图 12.1 所示的流程图),结合我们在第九章和第十章中学到的相位估计知识以及一些经典数学,来找到 N 的因子。
模运算小组
在算法 12.1 中,我们需要一个额外的经典数学运算符:模运算符。如果你之前没有见过这个运算符,不要担心;我们将在本章后面更详细地介绍它。
算法 12.1:使用 Shor 算法分解整数的伪代码
-
选择一个随机整数 g,我们称之为 生成器。
-
通过确定生成器 g 和 N 是否互质来检查生成器是否意外地是一个因子。如果它们共享一个公因数,那么我们就有了一个新的 N 因子;否则继续执行算法的其余部分。
-
使用迭代相位估计来找到经典函数 f(x) = g^x mod N 的 频率。频率告诉我们当 x 增加时,f 多快会回到相同的值。
-
使用一个称为 连分数展开 的经典算法,将上一步中的频率转换为周期 (r)。周期 r 应该具有这样的性质,即对于所有输入 x,f(x) = f(x + r)。
-
如果我们找到的周期r是奇数,则回到步骤 1 并做出新的猜测。如果r是偶数,则进入下一步。
-
要么是g^(r/2) − 1,要么是g^(r/2) + 1 与N共享一个因子。
注意:在算法 12.1 中,需要注意的是,只有第 3 步涉及任何量子计算。Shor 算法的大部分步骤最适合经典硬件,并展示了量子硬件可能会如何被使用。也就是说,量子硬件和算法作为量子经典算法的子程序工作得很好。

图 12.1 将 Shor 算法表示为流程图。为了分解整数N,Shor 算法使用相估计和量子计算机来找到另一个整数 a 的幂在模N下的函数的周期。经过一些经典的后处理,这个周期可以用来找到N的因子。
现在我们已经看到了 Shor 算法的步骤,列表 12.1 展示了最终实现可能的样子。操作FactorSemiprimeInteger是算法的入口点:它接受我们想要分解的整数作为输入,并返回它的两个因子。
列表 12.1 分解半素整数 Q#代码
operation FactorSemiprimeInteger(number : Int) : (Int, Int) {
if (number % 2 == 0) { ❶
Message("An even number has been given; 2 is a factor.");
return (number / 2, 2);
}
mutable factors = (1, 1);
mutable foundFactors = false;
repeat {
let generator = DrawRandomInt(3,number - 2); ❷
if (IsCoprimeI(generator, number)) {
Message($"Estimating period of {generator}...");
let period = EstimatePeriod(generator, number); ❸
set (foundFactors, factors) = MaybeFactorsFromPeriod( ❹
generator, period, number
);
} else {
let gcd = GreatestCommonDivisorI(number, generator);
Message(
$"We have guessed a divisor of {number} to be " +
$"{gcd} by accident. Nothing left to do."
);
set foundFactors = true;
set factors = (gcd, number / gcd);
}
}
until (foundFactors) ❺
fixup {
Message(
"The estimated period did not yield a valid factor, " +
"trying again."
);
}
return factors; ❻
}
❶ 首先检查我们是否被要求分解一个偶数,因为在这种情况下 2 必须是因子
❷ 按照算法 12.1 的第 1 步,我们选择一个随机数来定义我们用来分解“数字”的周期函数。
❸ 在本章中,我们学习如何编写一个 EstimatePeriod 操作来处理算法 12.1 的第 3 步和第 4 步,使用我们关于相估计所学的知识。
❹ 一旦我们得到了周期,我们就可以使用算法 12.1 的第 5 步和第 6 步来猜测“数字”的因子;我们将在本章后面写 MaybeFactorsFromPeriod。
❺ 如果出现问题(例如,我们的发生器有一个奇数周期),我们使用重复/直到循环再次尝试。
❻ 返回我们使用量子程序找到的“数字”的两个因子
幸运不是一切,但它可以有所帮助!
在列表 12.1 中,我们在继续 Shor 算法的其余步骤之前,使用IsCoprimeI来检查generator是否是number的因子。如果我们真的很幸运,那么generator已经是因子,在这种情况下我们不需要量子计算机来帮助分解number。
虽然我们在笔记本电脑或台式机上可以模拟的小例子中可能经常幸运,但随着“数字”变大,偶然猜对正确因子的难度越来越大,因此 Shor 算法几乎总是非常有帮助。
由于这是本书的最后一章,我们拥有了理解列表 12.1 中发生的事情所需的全部量子概念;唯一缺少的是将我们所学内容与分解半素数问题相联系的经典部分,以及一些有用的 Q# 库的部分,这些部分可以帮助我们。如前所述,这里只使用了一步量子技术,它是通过创建一个实现我们想要学习的经典函数的或然函数来实现的。通过使用叠加态,应用或然函数,并进行相位估计,我们可以了解经典函数的性质:在这里,是周期。在本章的其余部分,我们将详细讲解算法 12.1,并涵盖运行 Shor 算法所需的最后几块拼图。我们需要理解算法 12.1 的第一块拼图是经典数学中称为 模数算术 的一部分,所以让我们开始吧!
12.2 将模数数学与分解相联系
一种寻找可用于安全环境中的谜题的方法是观察模数算术是如何工作的。与普通算术不同,在模数算术中,一切就像时钟上的小时一样循环回来。例如,如果有人问我们在 11 点之后两小时是什么时间,如果我们回答说“13 点”,我们可能会得到一个非常奇怪的表情。更有可能的是,那个人希望得到一个像“1 点”这样的答案——也就是说,如果他们不使用 24 小时制的话。
使用模运算,我们可以通过以下方式表达这个想法:11 + 2 = 1 mod。在这个等式中,mod 12 表示我们希望任何超过 12 的数都循环回到起点,如图 12.2 所示。

图 12.2 使用时钟来理解模数运算。当进行“mod N”的加法和乘法运算时,我们可以将正常的数线想象成被 N 个小时环绕的时钟面。就像 11 点过两小时是 1 点一样,当在 mod 12 下工作时,11 + 2 = 1。
当算术可以像这样循环时,确定不同的计算起点可能会变得困难。如果我们处理的是普通实数,例如,如果我们已知 a 和 a^b,那么计算 b 是很容易的;我们可以通过对 a^b 取对数来找到 b。如果我们尝试在模数算术中解决相同的问题,它可能会很快变得复杂。例如,当计算 mod 21 时,5 的幂是 1, 5, 4, 20, 16, 17, 1,……。表面上,5, 4 和 16 不像是同一个数的幂,更不用说按递增顺序排列了,当我们从取模 21 的指数反向工作时,我们需要检查的起点可能更多。
练习 12.1:11 的幂
当计算 mod 21 时,11 的幂是多少?需要多长时间才能循环回到 11⁰ = 1?
在最后取模 21 是否重要,或者是否在每一步都计算模数?
提示:Python 或 Q# 都非常适合这个任务,因为两者都定义了模运算符 %。
练习解答
本书中的所有练习题解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入我们当前章节的文件夹,然后打开名为练习题解答的 Jupyter notebook。
观察到给定 a^b mod N 求解 b 是困难的这一事实,已经给我们提供了一个可以用来隐藏一些数据的谜题!这个谜题通常被称为离散对数问题。如果爱丽丝想与我们分享一个秘密,我们可以先公开地同意一个小的数字,比如 g = 13,和一个大的数字,比如 N = 71。然后我们各自随机选择一个秘密数字:假设爱丽丝选择了 a = 4,而我们选择了 b = 5。爱丽丝随后发送给我们 g^a mod N = 19,而我们发送回 g^b mod N = 34。如果我们计算 (ga*)(b) mod N = 19⁵ mod 71 = 45 和爱丽丝计算 (gb*)(a) mod 71 = 34⁴ mod 71 = 45,我们都会得到相同的数字,但窃听者必须解决我们之前看到的时钟跳跃谜题才能计算出结果(见图 12.3)。由于 g^(ab) = 45 是我们和爱丽丝都知道但其他人不知道的数字,我们可以使用 g^(ab) 作为密钥,利用我们在第三章中学到的知识来隐藏我们的信息。
警告:不要在家中尝试
这个协议的许多技术条件超出了本书的范围。选择 g 和 N 不当可能会破坏这种技术提供的任何安全性,使经验丰富的攻击者轻易破解。自己这样做也很容易引入错误,所以请仅将其视为一个概念示例!
如果你对使用这类谜题来保护数据安全的实际方面感兴趣,Niels Ferguson、Bruce Schneier 和 Tadayoshi Kohno 的《密码学工程》(Wiley,2010)是一本很好的书,你可以从中继续学习。

图 12.3 使用离散对数问题作为谜题来隐藏秘密信息。在这里,我们与爱丽丝分享的信息受到保护,如果某人难以计算撤销模运算操作,如指数函数,那么这些信息就是安全的。
与 QKD 不同,这种共享秘密数据的方式(称为迪菲-赫尔曼协议)依赖于我们和爱丽丝使用的谜题在没有窃听者可以访问的提示下难以解决的假设。如果有人可以有效地解决像为 a 给定 g 和 N 求解 g^a mod N 这样的谜题,那么我们的数据几乎就是公开的。
另一个常用于保护数据的谜题是 RSA 算法,它使用更高级的经典数学将分解大整数变成一个谜题。正如我们可以通过解 g^a mod N 来破解 Diffie–Hellman,我们也可以通过解 N = pq 来破解 RSA,给定只有 N。在 RSA 谜题中,我们称 N 为 公钥,因子 p 和 q 为 私钥;如果我们能够轻松地分解 N,我们就可以仅通过公钥获得私钥。考虑到这一点,我们可以使之前的情况更加精确:
场景:破解 RSA
假设我们知道一个公钥 N。使用 Q# 实现 Shor 算法来分解 N 以恢复私钥 p 和 q。
破解它,直到我们成功
与前几章的场景相比,这个场景可能显得有点 ... 恶劣。然而,在实践中,理解对我们保持数据安全所使用的工具和协议的攻击是至关重要的,这样我们才能相应地调整我们的方法。如果我们使用像 RSA 这样的加密算法来保护数据,那么在该算法上实施量子攻击可以帮助我们了解攻击者需要多大的量子设备来破坏我们的数据。毕竟,有从“RSA 完全没问题”到“我们应该感到恐慌”的各种说法;区分这些极端需要了解攻击者所需的资源。
换句话说,通过探索一个让我们暂时扮演攻击者角色的场景,我们可以了解成功攻击 RSA 需要多少量子计算能力。我们将在本章末尾回到这个话题,但就目前而言,像攻击者一样思考是有帮助的。了解量子计算机如何攻击经典密码学是一个很好的场景,可以练习应用我们的量子计算技能!
尽管如此,使用这个例子时仍需谨慎。量子计算对信息安全的影响取决于我们对经典算法的假设、量子算法的改进、量子硬件发展的进展、我们需要前向保密持续多长时间,以及许多其他此类担忧。不幸的是,要充分涵盖所有这些内容以做出关于如何最佳部署密码学的负责任决策,需要的空间比这本书剩下的空间要多,因此我们建议记住,本章中的 RSA 场景只是一个例子,而不是对该主题的完整分析。
结果表明,尽管 Diffie–Hellman 和 RSA 看起来非常不同,我们仍然可以使用一些经典数学将 RSA 分解难题转化为另一个例子,即当我们执行模运算时如何快速在时钟面上移动(Diffie–Hellman 难题)。然后,我们可以使用第十章中学到的知识在量子计算机上轻松解决这个问题。让我们快速通过一个使用 Shor 算法分解小整数的例子,以便我们可以看到所有这些部分是如何工作的。
12.2.1 使用 Shor 算法分解的例子
Shor 算法列出的步骤可能看起来非常抽象,所以在我们深入探讨它们是如何工作之前,让我们尝试一个例子,使用我们之前学到的关于模运算的知识。比如说,我们想要分解的数是 21;真实的 RSA 公钥将会大得多,但为了手动计算方便,我们还是用 21。请相信我们——这样可以使数学问题简单得多。
这里是算法 12.1 分解 21 的步骤:
-
选择一个随机整数作为生成器;让我们假设我们使用 11。
-
我们可以验证,由于 11 与 21 没有公共因子,我们可以将其用作下一步的生成器。
-
很遗憾,我们无法在脑海中完成量子步骤,因此我们使用 Q#迭代相位估计操作来估计通过应用实现经典函数 f(x) = 11^(x) mod 21 的或然函数产生的相位。它返回一个相位 ϕ,我们可以通过以下方程将其转换为频率 427:(ϕ * 2⁹)) / 2π。
-
我们在 427 上使用连分数算法来猜测周期可能是什么。手动计算,我们得到周期的估计值为 6。
-
我们找到的周期是偶数,因此我们可以继续到下一步。
-
使用周期 6,我们得到 11^(6/2) − 1 mod 21 = 7 或者 11^(6/2) + 1 mod 21 = 9 与 21 有公共因子。我们可以检查并验证每个可能性,以确认 7 确实是 21 的因子。
练习 12.2:寻找公共因子
尝试从上一个过程中的第 6 步开始,但使用 35 作为要分解的数,17 作为生成器,12 作为周期。检查从第 6 步得到的答案中,是否有任何一个或两个与 35 有公共因子。
使用 Python 或 Q#,尝试用 N = 143,g = 19,和周期 r = 60 进行相同的操作。
注意:在下一节中,我们将看到如何使用经典计算机轻松分解一个数,当给定另一个与其共享一些因子的数时。
虽然分解像 21、35 或 143 这样小的数需要做很多工作,但完全相同的过程也适用于更大的整数,例如我们在尝试解决 RSA 算法用于保护数据的难题时可能会遇到的整数。
本章的其余部分将详细说明每个步骤,并展示它们如何协同工作以分解整数。为了启动这个过程,让我们看看周期查找如何帮助我们分解整数,以及我们如何可以使用 Q#来实现这种经典数学。
12.3 经典代数和分解
在心中考虑使用 Shor 算法的具体例子,我们可以看到经典算术和代数如何帮助利用量子计算。在深入探讨算法的核心量子部分之前,了解经典部分为什么有助于分解整数是有帮助的。
我们可能从代数中记得,对于任何数 x,x² − 1 = (x + 1)(x − 1)。实际上,这在模(时钟)算术中同样适用。如果我们发现生成器 g 的周期 r 是偶数,
-
那就意味着存在一个整数 k ...
-
使得 g^r = g^(2k) mod N = 1。
从每一侧减去 1,我们得到 (g^(2k) − 1) mod N = 0,
-
因此,使用 x² − 1 = (x + 1)(x − 1) 可以给我们带来...
-
(g^k + 1)(g^k − 1) mod N = 0。
这有什么关系呢?如果我们有 x mod N = 0,那么这告诉我们 x 是 N 的倍数。回想一下时钟的类比,0,12,24,36 等等都等于零模 12。换句话说,如果 x mod N = 0,那么存在某个整数 y 使得 x = yN。利用这一点和周期,我们知道存在某个整数 y 使得 (g^k + 1)(g^k − 1) = yN。如果 g^k − 1 或 g^k + 1 是 N 的倍数,我们并没有学到多少;但在任何其他情况下,它告诉我们 g^k − 1 或 g^k + 1 必须与 N 共享一个因子。
为了确定 g^k − 1 或 g^k + 1 是否与 N 共享一个因子,我们可以计算每个猜测与 N 的最大公约数(GCD)。使用称为欧几里得算法的技术,在经典计算机上这样做是直截了当的。
注意:由于最大公约数(GCD)在经典计算中非常容易计算,为什么我们需要量子计算机来帮助分解?在 Shor 算法的这个阶段,我们已经将潜在的因子缩小到两个非常好的猜测,并且只使用 GCD 在这些猜测上进行。如果我们没有将事情缩小到如此好的程度,我们就需要在许多许多更多的猜测上使用 GCD,才有可能找到 N 的因子。即使最大公约数(GCD)的确定如此简单,我们仍然需要一种好方法来首先将其缩小到一组好的猜测。
在 Q# 中,我们可以使用 GreatestCommonDivisorI 函数来计算最大公约数(GCD),如列表 12.2 所示,其中代码在 Q# Jupyter Notebook 中运行。我们可以通过以下方式检查是否从 GreatestCommonDivisorI 获得了正确的输出:从两个以质因数乘积表示的整数开始:例如,a = 2 × 3 × 113 和 b = 2 × 3 × 5 × 13。由于这两个整数只共享 2 和 3 作为因子,它们的最大公约数应该是 2 × 3 = 6。
Q# 标准库文档
如同往常,列表 12.2 以 open 语句开始,允许我们使用 Q# 标准库中提供的函数和操作。在这种情况下,计算两个整数 GCD 的 Q# 函数位于 Microsoft.Quantum.Math 命名空间中,因此我们首先打开该命名空间以使该功能可用。同样,我们需要打开 Microsoft.Quantum.Diagnostics 命名空间来使用测试我们新 GcdExample 函数所需的事实和断言。
要查看 Q# 标准库中可用的完整列表,请查看 docs.microsoft.com/en-us/qsharp/api/ 以获取完整参考。
列表 12.2 查找两个整数的最大公约数
In [1]: open Microsoft.Quantum.Math;
open Microsoft.Quantum.Diagnostics;
function GcdExample() : Unit { ❶
let a = 2 * 3 * 113;
let b = 2 * 3 * 5 * 13;
let gcd = GreatestCommonDivisorI(a, b); ❷
Message($"The GCD of {a} and {b} is {gcd}.");
EqualityFactI(gcd, 6, "Got the wrong GCD."); ❸
}
Out[1]: - GcdExample
In [2]: %simulate GcdExample ❹
The GCD of 678 and 390 is 6.
Out[2]: ()
❶ 这个函数是一个简单的测试案例,用来查看 GCD 的工作原理。
❷ 要计算 GCD,我们调用之前打开的 Microsoft.Quantum.Math 命名空间中的 GreatestCommonDivisorI。
❸ 使用 EqualityFactI 函数来确认我们得到的答案符合预期(2 × 3 = 6)
❹ 如同往常,我们可以使用 %simulate 在模拟器上运行函数或操作。在这里,我们得到空输出 (),因为 GcdExample 返回的是类型为 Unit 的输出。
Q# 中的输入类型和命名约定
注意 GreatestCommonDivisorI 名称末尾的 I。这告诉我们 GreatestCommonDivisorI 在 Int 类型的输入上工作。当实际使用 Shor 算法时,N 将比我们能够放入普通 Int 值的数值大得多,因此 Q# 还提供了一个名为 BigInt 的其他类型来帮助。
要处理 BigInt 输入,Q# 还提供了 GreatestCommonDivisorL 函数。为什么是 L 而不是 B?在这种情况下,L 代表“长”,有助于区分以B开头的其他类型,例如 Bool。
这个约定也适用于 Q# 标准库的其余部分。例如,我们之前使用的等式事实比较了两个整数,因此被称为 EqualityFactI。比较两个大整数的对应事实被称为 EqualityFactL,而比较两个 Result 值的事实被称为 EqualityFactR。
练习 12.3:最大公约数
35 和 30 的最大公约数是多少?这能帮助你找到 35 的因子吗?
提示:将此视为上一个练习的第 2 步。
将所有内容综合起来,如果我们有了生成器的周期,以下列表展示了我们如何在 Q# 中使用它来编写 MaybeFactorsFromPeriod。函数名以“maybe”开头,因为找到的周期可能不会满足了解数的信息所必需的条件。
列表 12.3 operations.qs:从周期计算可能的因子
function MaybeFactorsFromPeriod(
generator : Int, period : Int, number : Int) ❶
: (Bool, (Int, Int)) { ❷
if period % 2 == 0 { ❸
let halfPower = ExpModI(generator,
period / 2, number); ❹
if (halfPower != number - 1) { ❺
let factor = MaxI( ❻
GreatestCommonDivisorI(halfPower - 1, number),
GreatestCommonDivisorI(halfPower + 1, number)
);
return (true, (factor, number / factor));
} else {
return (false, (1, 1));
}
} else {
return (false, (1, 1));
}
}
❶ 要从周期计算可能的因子,我们需要为要分解的数 N、周期 r 和生成器提供输入。
❷ 如果 g^(r/2) + 1 或 g^(r/2) − 1 是 N 的倍数,我们就无法找到任何因子,需要再次尝试。布尔输出让调用者知道需要重试。
❸ 如果周期是奇数,我们不能使用 x² − 1 = (x + 1)(x − 1) 的技巧,所以我们首先检查周期是否为偶数。
❹ Microsoft.Quantum.Math.ExpModI 函数返回形式为 g^x mod N 的模算术指数。我们可以利用这个函数来找到给定 g、r 和 N 的情况下 g^(r/2) mod N。
❺ 检查 g^(r/2) + 1 是否是 N 的倍数,因此我们知道可以安全地继续
❻ 最大公约数告诉我们我们的猜测中是否有一个与 N 有公共因子。如果我们的猜测没有公共因子,最大公约数返回 1。这检查了两个猜测,并取了除了 1 以外的任何结果。
现在我们知道了如何将周期转换为潜在的因子,让我们看看 Shor 算法的核心:使用相位估计来估计生成器的周期。为此,我们将使用本书其余部分所学的内容以及一些新的 Q# 操作,在量子计算机上进行算术运算。让我们开始吧!
深入探讨:这里看看欧几里得
之前,我们使用了 Q# 标准库中提供的 GreatestCommonDivisorI 函数来计算两个整数的最大公约数。这个函数通过使用欧几里得算法,递归地尝试将一个整数除以另一个整数,直到没有余数为止。
假设我们想要找到两个整数 a 和 b 的最大公约数。我们通过找到两个额外的整数 q 和 r(简称“商”和“余数”)来开始欧几里得算法,使得 a = qb + r。使用整数除法指令找到 q 和 r 是直截了当的,所以在经典计算机上这一步并不太难。在那个点上,如果 r = 0,我们就完成了:b 是 a 和它自己的除数,所以不可能有一个更大的公约数。如果不是,我们知道 a 和 b 的最大公约数也必须是 r 的除数,因此我们可以通过找到 b 和 r 的最大公约数来递归。最终,这个过程必须结束,因为我们正在寻找的整数的最大公约数会随着我们的进行而越来越小,但永远不会变成负数。
为了使事情更加具体,我们可以通过以下表格来处理列表 12.2 中的示例。
使用欧几里得算法求 678 和 390 的最大公约数
| a | b | q | r |
|---|---|---|---|
| 678 | 390 | 1 | 288 |
| 390 | 288 | 1 | 102 |
| 288 | 102 | 2 | 84 |
| 102 | 84 | 1 | 18 |
| 84 | 18 | 4 | 12 |
| 18 | 12 | 1 | 6 |
| 12 | 6 (答案) | 2 | 0 (完成) |
12.4 量子算术
到目前为止,我们已经看到了 Q#标准库的许多不同部分,而本章的重点是算术,因此介绍一些由 Q#的数值库提供的Microsoft.Quantum.Arithmetic命名空间中的函数和操作是有意义的。正如你可能猜到的,这个命名空间提供了许多有用的函数、操作和类型,这些都可以简化在量子系统中进行算术运算。特别是,我们可以使用支持像BigEndian(最低有效位在左侧)和LittleEndian(最低有效位在右侧)这样的多量子位寄存器编码的添加和乘法等操作的实现。让我们看看使用 Q#数值库添加两个整数的示例代码。
注意:在 samples 仓库中的 Q#笔记本(github.com/crazy4pi314/learn-qc-with-python-and-qsharp)中,所有这些片段都已为你编写出来!
首先,由于数值包默认没有加载,我们需要使用魔法命令%package请求 Q#内核加载它。%package魔法命令将一个新的包添加到我们的 IQ#会话中,使得该包实现的函数、操作和用户定义的类型在我们的会话中可用。
为了简化操作,我们还可以关闭显示来自诊断输出(如DumpMachine)的小振幅,如下一个列表所示。
列表 12.4 在 IQ#中加载包和设置首选项
In [1]: %package Microsoft.Quantum.Numerics ❶
Adding package Microsoft.Quantum.Numerics: done!
Out[1]: - Microsoft.Quantum.Standard::0.15.2101125897 ❷
- Microsoft.Quantum.Standard.Visualization::0.15.2101125897
- Microsoft.Quantum.Numerics::0.15.2101125897
In [2]: %config dump.truncateSmallAmplitudes = "true" ❸
Out[2]: "true"
❶ 使用%package加载 Microsoft.Quantum.Numerics 包,该包提供了用于处理由量子位寄存器表示的数字的额外操作和函数。
❷ 运行%package后,IQ#会报告我们当前 IQ#会话中可用的包。你的版本号可能会有所不同。
❸ %config魔法命令设置我们当前 IQ#会话的各种首选项。例如,我们可以使用%config来告诉DumpRegister和DumpMachine可调用对象省略每个状态向量中具有非常小振幅的部分。这使得在多个量子位上可视化状态变得容易得多,因为打印出每个计算基态可能会很快变得难以控制。
小贴士:当我们从 Jupyter Notebooks 内部使用 Q#时,IQ#内核提供了几个其他魔法命令来帮助我们编写量子程序,除了我们之前看到的%simulate、%package和%config等命令之外。要获取完整列表,请查看docs.microsoft.com/qsharp/api/iqsharp-magic上的文档。
12.4.1 使用量子位进行加法
现在,让我们编写一个示例,演示在量子比特寄存器编码整数时如何将两个整数相加。列表 12.5 使用AddI操作将两个量子寄存器的内容相加。此列表使用 Q#标准库提供的LittleEndian UDT 来标记每个量子比特寄存器都应解释为使用小端编码(也称为最低有效顺序)的整数。也就是说,当将LittleEndian寄存器解释为整数时,将最低量子比特索引视为最低有效位。例如,为了表示整数 6 作为小端表示法中的三量子比特量子态,我们写|011〉,因为 6 = 0 × 2⁰ + 1 × 2¹ + 1 × 2² = 2 + 4。
列表 12.5 使用数值库将量子比特编码的整数相加
In [3]: open Microsoft.Quantum.Arithmetic;
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;
operation AddCustom(num1 : Int, num2 : Int) : Int {
let bitSize = BitSizeI(MaxI((num1, num2))) + 1; ❶
use reg1 = Qubit[bitSize];
use reg2 = Qubit[bitSize];
let qubits1 = LittleEndian(reg1); ❷
let qubits2 = LittleEndian(reg2); ❷
ApplyXorInPlace(num1, qubits1); ❸
ApplyXorInPlace(num2, qubits2);
Message("Before addition:");
DumpRegister((), reg2);
AddI(qubits1, qubits2); ❹
Message("After addition:");
DumpRegister((), reg2);
ResetAll(reg1); ❺
return MeasureInteger(qubits2);
}
Out[3]: - AddCustom
❶ 我们的数据寄存器必须足够大,以便能够存储两个整数可能的最大和。最多,我们需要的位数比表示最大输入所需的位数多一位。
❷ 表示我们希望将 reg1 和 reg2 解释为整数,使用小端编码表示
❸ 将整数的 LittleEndian 表示准备到量子比特寄存器中,因为x ⊕ 0 = x,无论x是 0 还是 1
❹ 使用从数值包加载的操作 AddI,我们可以将两个输入寄存器 qubits1 和 qubits2 表示的整数相加
❺ 重置第一个寄存器,以便可以释放它,然后测量寄存器以获得结果
我们可以在图 12.4 中看到运行此代码片段的输出。

图 12.4 使用数值库在量子比特寄存器中编码的整数相加的输出
12.4.2 使用叠加态的量子比特进行乘法
我们已经看到了如何在 Q#中进行一些基本的模运算,但像我们的大多数量子算法一样,除非我们使用独特的量子属性/操作,否则我们只是进行了一次非常、非常昂贵的计算。在本节中,我们将利用我们可以有量子比特在数字的叠加态中的事实来帮助我们获得使 Shor 算法工作所需的优势。幸运的是,AddI和许多其他类似的算术操作可以在叠加态中工作:这是我们将在本章下一部分使用相位估计进行这些算术操作时需要使用的属性。在跳到那之前,玩一玩在叠加态中加法或乘法整数的含义可能会有所帮助。
在这里,我们看到如何通过使用MultiplyByModularInteger作为示例来应用我们关于叠加在算术中的知识。很快,我们将使用相同的操作来构建稍后用于 Shor 分解算法所需的预言机,所以这是一个相当实用的应用。
首先,让我们看看我们可以用来准备一个寄存器以两个整数的叠加态的操作。列表 12.6 展示了如何使用我们在上一节中学到的ApplyXorInPlace操作和第九章中的Controlled函子来完成这一操作。
正如我们通过其他 Controlled 的使用所看到的,当控制寄存器处于全 1 状态(|11⋅⋅⋅1〉)时,受控操作会执行某些操作。列表 12.6 通过使用 X 操作将 |0〉 状态映射到 |1〉 状态,而不是使用 X 操作来控制零状态。通过将 X 的调用放在 within/apply 块中,我们确保 Q# 在我们应用受控操作后撤销我们的 X 调用。
提示 使用 within/apply 的这种方式实现了与我们在第十一章中使用的 ControlledOnInt 函数非常相似的功能,这也是该函数在 Q# 标准库中实现的方式。
列表 12.6 准备一个整数叠加的寄存器
open Microsoft.Quantum.Arithmetic;
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;
operation PrepareSuperpositionOfTwoInts( ❶
intPair : (Int, Int),
register : LittleEndian,
) : Unit is Adj + Ctl {
use ctrl = Qubit();
H(ctrl); ❷
within {
X(ctrl); ❸
} apply {
Controlled ApplyXorInPlace( ❹
[ctrl],
(Fst(intPair), register)
);
}
Controlled ApplyXorInPlace( ❺
[ctrl],
(Snd(intPair), register)
);
(ControlledOnInt(Snd(intPair), Y))(register!, ctrl); ❻
}
❶ 接收一个寄存器和一对整数,并以 LittleEndian 编码准备该寄存器,使其处于这些整数的叠加态
❷ 准备我们的控制量子比特处于 |+〉 = (|0〉 + |1〉) / √2 状态,以便当我们随后对该量子比特进行控制操作时,它们也处于叠加态
❸ 如前所述,在 within/apply 块中使用 X 允许我们控制 |0〉 状态而不是 |1〉 状态。
❹ 为控制寄存器添加一个新的输入(参见第九章)。另一个输入是一个元组,包含原始参数:我们想要准备为状态的整数,以及我们想要在该寄存器上准备该状态的寄存器。
❺ 对 intPair 中的第二个整数执行相同操作,并将其编码在寄存器中,作为受控于 ctrl 量子比特的状态
❻ 通过使用 Y 操作旋转,并在我们的控制量子比特处于 |1〉 状态时进行控制,向我们的叠加态的两个分支之一添加一些相位
提示 在这种情况下不需要 Y 操作,但它有助于我们了解受控 Y 操作施加的相位如何传播到后续步骤。
一旦我们有一个表示两个整数叠加的量子寄存器,我们就可以在该叠加上应用其他算术操作。在下面的列表中,例如,我们使用 DumpMachine 可调用函数来查看当使用 Q# 标准库提供的 MultiplyByModularInteger 操作时,我们的寄存器状态如何变化。
列表 12.7 使用数值库在叠加中进行乘法
operation MultiplyInSuperpositionMod(
superpositionInts : (Int, Int),
multiplier : Int,
modulus : Int
) : Int {
use target = Qubit[BitSizeI(modulus - 1)]; ❶
let register = LittleEndian(target);
PrepareSuperpositionOfTwoInts(superpositionInts, register); ❷
Message("Before multiplication:");
DumpMachine();
MultiplyByModularInteger( ❸
multiplier, modulus, register
);
Message("After multiplication:");
DumpMachine();
return MeasureInteger(register); ❹
}
❶ 我们需要做的第一件事是分配一个足够大的寄存器。在这里,由于我们正在进行模乘法,寄存器必须能够持有的最大可能值是模数 - 1。
❷ 使用列表 12.6 中定义的操作来准备我们的目标寄存器,使其处于整数的叠加态
❸ 数值包提供了 MultiplyByModularInteger 函数,它接受一个 LittleEndian 寄存器,并将其乘以经典乘数,模一个给定的模数。
❹ 测量一个寄存器,并返回由该寄存器表示的经典 Int
如果我们在示例笔记本中运行列表 12.6 和 12.7 中的代码,我们会看到图 12.5 所示的输出:在乘法之前,寄存器正确地显示了 2 和 3 的叠加,然后显示乘法之后的 1 和 6 的叠加。我们期望正确的输出是什么?如果我们像平常一样乘法,应该是 6 和 9;但由于我们在模 8 的算术中进行运算,9 是 1。当我们测量该寄存器时,我们有一半的时间得到 6,另一半时间得到 1,因为它们处于相等的叠加状态,正如我们可以从图 12.5 中描述的状态振幅条中看到的那样。

图 12.5 乘以 2 和 3 的叠加模 8 的输出
练习 12.4:模乘法
假设你已将寄存器准备在 1 / √2 (|2〉 + |7〉) 状态,每个基态表示一个以小端编码的整数。在用 5 模 9 乘法之后,你的寄存器将处于什么状态?写一个 Q# 程序,使用 DumpMachine 来确认你的答案。
练习 12.5:附加题
如果你运行与上一练习相同的程序,但尝试用 3 乘以模 9,你会得到一个错误。为什么?
提示:考虑你在第八章中学到的关于经典函数必须满足的条件,以便它能够被量子或然子表示。如果你卡住了,答案将在下面提供,使用 rot13.com 轻微隐藏。
答案:Zhygvcylvat ol guerr zbq avar vf abg erirefvoyr. Sbe vafgnapr, obgu bar gvzrf guerr naq sbhe gvzrf guerr zbq avar tvir mreb, rira gubhtu bar naq sbhe nera’g rdhny zbq avar. Fvapr pynffvpny shapgvbaf unir gb or erirefvoyr va beqre gb or ercerfragrq ol dhnaghz bcrengvbaf, gur ZhygvcylOlZbqhyneVagrtre envfrf na reebe va guvf pnfr.
注意:注意我们在这里做了什么:我们使用量子程序乘以由量子寄存器表示的整数和经典整数。计算完全在量子设备上完成,不使用任何测量;这意味着当我们的寄存器以叠加状态开始时,乘法也在叠加中进行。
12.4.3 Shor 算法中的模乘法
现在我们已经看到了一些数值库,让我们回到分解场景。算法 12.1 中我们需要进行一些模运算并利用数值库的主要地方是步骤 3 和 4(下面重复)。我们可以实现三个操作来简化 Shor 算法中这些步骤的实现,并帮助我们分解整数。
我们可以从本章示例代码中查看的第一个操作实现了算法 12.1 的步骤 3,即 EstimateFrequency 操作(列表 12.8)。
算法 12.1,步骤 3 和 4(Shor 算法分解整数的伪代码)
-
使用迭代相位估计来找到经典函数 f(x) = a^x mod N 的 频率。频率告诉我们当 x 增加时,f 多快会回到相同的值。
-
使用一个名为连分数展开的经典算法将前一步骤中的频率转换为周期(r)。周期r应该具有这样的性质:对于所有输入x,f(x) = f(x + r)。
提示:此操作使用 Q#标准库中提供的相位估计操作,我们在第十章中已经看到过。如果您需要复习,请回到第九章了解相位估计的概述,或回到第十章了解如何使用标准库运行相位估计。
列表 12.8 operations.qs:使用相位估计学习生成器的频率
operation EstimateFrequency(
inputOracle : ((Int, Qubit[]) => Unit is Adj+Ctl),
nBitsPrecision : Int,
bitSize : Int)
: Int {
use register = Qubit[bitSize]; ❶
let registerLE = LittleEndian(register); ❷
ApplyXorInPlace(1, registerLE); ❸
let phase = RobustPhaseEstimation( ❹
nBitsPrecision,
DiscreteOracle(inputOracle), ❺
registerLE!
);
ResetAll(register); ❻
return Round( ❼
(phase * IntAsDouble(2 ^ nBitsPrecision)) / (2.0 * PI())
);
}
❶ 由于这是使用量子比特的主要步骤,我们需要分配一个足够大的寄存器来表示模数。
❷ 新分配的寄存器必须指定如何编码它将要表示的整数,因此我们可以使用 LittleEndian UDT 来包装我们新分配的寄存器。
❸ 接收一个整数,并将其与第二个参数提供的寄存器中存储的整数进行异或运算。由于 registerLE 从 0 开始,这会将寄存器准备为 1。
❹ 使用 RobustPhaseEstimation(见第十章)学习 inputOracle 的相位,并传递一个量子寄存器和我们想要估计的相位精度的位数。
❺ 将 inputOracle 包装在 DiscreteOracle UDT 中,使 RobustPhaseEstimation 清楚地知道我们希望 inputOracle 被解释为或门。
❻ 一旦完成相位估计,就重置寄存器中的所有量子比特。
❼ 我们估计的相位就是相位本身。此方程将其转换为频率:(相位 * 2^(nBitsPrecision) − 1) / π。
现在我们已经掌握了EstimateFrequency的框架,我们可以看看实现此算法所需的或门的操作。ApplyPeriodFindingOracle操作正是这样:一个结构类似于函数f(power) = generator^(power) mod modulus 的或门的操作。下一个列表显示了ApplyPeriodFindingOracle的实现。
列表 12.9 operations.qs:实现函数f的或门
operation ApplyPeriodFindingOracle(
generator : Int, modulus : Int, power : Int, target : Qubit[])
: Unit is Adj + Ctl {
Fact( ❶
IsCoprimeI(generator, modulus),
"The generator and modulus must be co-prime."
);
MultiplyByModularInteger( ❷
ExpModI(generator, power, modulus), ❸
modulus,
LittleEndian(target) ❹
);
}
❶ 对提供的生成器和模数进行一些输入检查,确保它们互质。
❷ 与列表 12.7 中的相同。这里,它帮助这个或门将目标寄存器中表示的整数乘以f(power) = generator^(power) mod modulus。
❸ Microsoft.Quantum.Math 还提供了 ExpModI 函数,它允许我们轻松计算f(power) = generator^(power) mod modulus。
❹ LittleEndian 告诉我们,ApplyPeriodFindingOracle 函数所接受的量子比特寄存器是以小端编码方式解释为整数的。
前两个操作构成了算法 12.1 的第 3 步的基础。现在我们需要一个操作来处理第 4 步,即将生成器的估计频率转换为周期。以下列表中的 EstimatePeriod 操作正是这样做的:给定一个 generator 和一个 modulus,它重复使用 EstimateFrequency 估计频率,并使用连分数算法确保估计的频率产生一个有效的周期。
列表 12.10 operations.qs:从频率估计周期
operation EstimatePeriod(generator : Int, modulus : Int) : Int {
Fact( ❶
IsCoprimeI(generator, modulus), ❷
"`generator` and `modulus` must be co-prime"
);
let bitSize = BitSizeI(modulus); ❸
let nBitsPrecision = 2 * bitSize + 1; ❹
mutable result = 1; ❺
mutable frequencyEstimate = 0;
repeat { ❻
set frequencyEstimate =
EstimateFrequency( ❼
ApplyPeriodFindingOracle( ❽
generator, modulus, _, _
),
nBitsPrecision, bitSize
);
if frequencyEstimate != 0 { ❾
set result =
PeriodFromFrequency( ❿
frequencyEstimate, nBitsPrecision,
modulus, result
);
} else {
Message("The estimated frequency was 0, trying again.");
}
}
until ExpModI(generator, result, modulus) == 1 ⓫
fixup {
Message( ⓬
"The estimated period from continued fractions failed, " +
"trying again."
);
}
return result;
}
❶ 对提供的生成器和模数是否互质进行一些输入检查。
❷ Microsoft.Quantum.Math 命名空间中的 IsCoprimeI 函数简化了检查生成器和模数是否互质的操作。
❸ 一个量子比特寄存器需要存储的最大整数是模数,因此我们使用 BitSizeI 来帮助计算位数,使得模数 ≤ 2^({# of bits})。
❹ 要使用浮点数来表示 k/r(其中 r 是周期,k 是另一个整数),我们需要足够的精度位来近似 k/r:即比表示 k 和 r 所需的位数多一位。
❺ 结果可变变量跟踪我们在重复“repeat”块时的当前最佳周期猜测。
❻ 重复频率估计步骤,直到我们有足够的周期估计值可以继续前进。
⓬ 调用我们之前查看的 EstimateFrequency 操作,并传递适当的参数。
❽ 部分应用 ApplyPeriodFindingOracle 以确保 EstimateFrequency 可以将其应用于正确的幂和寄存器值
❾ 如果频率估计值为 0,则需要再次尝试,因为那不是一个合理的周期(1/0)。如果我们得到 0,那么“repeat”块将再次运行,因为在这种情况下“until”条件不满足。
❿ 捕获算法 12.1 的第 4 步,该步骤使用 Q# 标准库中的连分数算法从频率计算周期。
⓫ 重复频率估计和周期计算,直到我们得到一个周期,使得生成器结果模模数等于 1。
⓬ 如果“until”条件未满足,则运行修复块,它只是发出一条消息,表示它将再次尝试。
通过这个最后的操作,我们拥有了完全实现 Shor 算法所需的所有代码!在下一节中,我们将把它们全部放在一起,并探讨这个整数分解算法的含义。
12.5 将一切整合
我们现在已经学习和实践了编写和运行 Shor 算法所需的所有技能。Shor 算法的量子部分相当熟悉,这得益于我们在第九章和第十章中关于相位估计的学习;并且我们解决了将分解数字和寻找生成器周期联系起来的经典代数问题。这并非易事——你应该为自己的量子之旅走到这一步而感到自豪!
连分数收敛
你可能已经注意到了 Shor 算法中发生的一个其他经典数学问题,我们还没有涉及。特别是,在继续之前,我们在相位估计的输出上调用PeriodFromFrequency函数:
function PeriodFromFrequency(
frequencyEstimate : Int, nBitsPrecision : Int,
modulus : Int, result : Int)
: Int {
let continuedFraction = ContinuedFractionConvergentI(
Fraction(frequencyEstimate, 2^nBitsPrecision), modulus
);
let denominator = AbsI(Snd(continuedFraction!));
return (denominator * result) / GreatestCommonDivisorI(
result, denominator
);
}
这是因为相位估计并没有告诉我们我们确切需要什么。它不是告诉我们我们的函数围绕时钟旋转需要多长时间(函数的周期),而是告诉我们我们的函数围绕这个时钟旋转有多快:更像是频率。不幸的是,我们不能像取频率的倒数来回到我们的周期一样做,因为我们正在寻找一个整数的周期。因此,如果我们得到频率估计为f,我们需要在f/2n附近搜索最接近的分数形式N/r来找到我们的周期r。
这是一个完全经典的算术问题,幸运的是,它已经通过一种称为连分数收敛的技术得到了很好的解决。这个解决方案通过ContinuedFractionConvergentI函数在 Q#标准库中提供,使得从相位估计得到的估计值到关于函数周期的某些信息变得容易转换。
让我们花点时间回顾一下我们在本章开头看到的FactorSemiprimeInteger操作,考虑到我们现在所学的知识。如果你需要复习,请查看图 12.6。

图 12.6 将 Shor 算法表示为流程图。为了分解整数N,Shor 算法使用相位估计和量子计算机来找到函数的周期,该函数使用模运算 modN对另一个整数a的幂进行运算。经过一些经典的后处理,该周期可以用来找到N的因子。
列表 12.11 operations.qs:使用 Shor 算法分解半素数整数
operation FactorSemiprimeInteger(number : Int) : (Int, Int) {
if number % 2 == 0 { ❶
Message("An even number has been given; 2 is a factor.");
return (number / 2, 2);
}
mutable factors = (1, 1); ❷
mutable foundFactors = false; ❸
repeat {
let generator = DrawRandomInt(
3, number - 2); ❹
if IsCoprimeI(generator, number) { ❺
Message($"Estimating period of {generator}...");
let period = EstimatePeriod(
generator, number); ❻
set (foundFactors, factors) =
MaybeFactorsFromPeriod( ❼
generator, period, number
);
} else {
let gcd = GreatestCommonDivisorI(
number, generator); ❽
Message(
$"We have guessed a divisor of {number} to be " +
$"{gcd} by accident. Nothing left to do."
);
set foundFactors = true;
set factors = (gcd, number / gcd);
}
}
until foundFactors ❾
fixup { ❿
Message(
"The estimated period did not yield a valid factor, " +
"trying again."
);
}
return factors; ⓫
}
❶ 检查要分解的整数是否为偶数。如果是,那么 2 是一个因子,我们可以提前停止。
❷ 使用可变变量factors来跟踪我们在算法步骤中找到的“number”的因子。
❸ 使用可变标志foundFactors来跟踪我们在算法步骤中是否找到“number”的因子。
❹ 实现算法 12.1 中的步骤 1,使用DrawRandomInt在 1 到“number”-1 的范围内选择一个随机整数作为我们的生成器。
❺ 算法 12.1 中的步骤 2,其中我们验证生成器是否与我们要分解的整数互质;如果不是,则“else”子句处理返回两个之间的公共因子。
❻ 覆盖算法 12.1 中的步骤 3 和 4。它返回一个通过从 EstimatePeriod 内部的频率估计中计算出的连分数得到的周期。
❼ 使用代数将估计的周期转换为可能是因子的整数。有时它可能失败,因此它还返回一个 Bool 值,指示是否成功。
❽ 处理这种情况:我们在一开始猜测的生成器与我们要分解的数有一个公共因子。
❾ 添加重复前面块的持续时间条件。这里我们希望继续寻找,直到找到我们想要的因子。
❿ 在重复主循环之前告诉程序要做什么。这里它让我们知道我们的 Q#程序将再次尝试。
⓫ 返回输入整数的因子元组
如果我们在 IQ#笔记本中运行此操作以分解 21,它看起来是这样的。
列表 12.12 运行FactorSemiprimeInteger的输出
In [1]: open IntegerFactorization; ❶
operation Factor21() : (Int, Int) {
return FactorSemiprimeInteger(21);
}
Out[1]: - Factor21
In [2]: %simulate Factor21
We have guessed a divisor of 21 to be 3 by accident. Nothing left to do.
Out[2]: (3, 7)
In [3]: %simulate Factor21
Estimating period of 19...
The estimated period from continued fractions failed, trying again.
Out[3]: (7, 3)
In [4]: %simulate Factor21
Estimating period of 17...
The estimated period did not yield a valid factor, trying again.
We have guessed a divisor of 21 to be 3 by accident. Nothing left to do.
Out[4]: (3, 7)
❶ 要在笔记本中调用FactorSemiprimeInteger操作,编写一个新的操作来提供输入 21 是有帮助的。
每次我们的代码都正确地返回了 21 的质因子:3 和 7。操作运行了三次,以展示我们可能得到的一些不同结果。在In [2]中,当我们尝试猜测一个生成器时,我们的DrawRandomInt调用最终返回了一个与 21 不互质的整数。因此,我们能够使用GreatestCommonDivisorI找到分解。在In [3]中,我们的DrawRandomInt选择了一个生成器 19,然后必须运行两次频率估计以确保连续分数算法成功。在最后的运行中,In [4],周期查找任务完成了一轮,但没有产生正确的因子;当它尝试选择一个新的生成器时,意外地猜测了一个因子。
注意:鉴于在模拟器或小型硬件设备上运行的局限性,我们将在选择生成器时经常猜测正确的因子。我们也会在小整数上更频繁地运气不佳,猜测像 1 这样的平凡因子。随着我们试图分解的数字的增加,这些边缘情况发生的频率会降低。
使用笔记本电脑、台式机或云中的模拟器,我们可能无法使用 Shor 算法分解任何特别大的数字。例如,在经典计算机上模拟 Shor 算法来分解 30 位数字将非常具有挑战性,但在 1992 年,40 位数字已经被认为在抵御经典分解算法方面严重不足。这可能会让人觉得 Shor 算法毫无用处,但它真正告诉我们的是,使用经典计算机模拟大型量子程序很难;我们在第四章和第五章中看到了为什么是这样的情况。
事实上,由于相同的算法适用于更大的数字(例如,使用 4096 位密钥来保护个人数据并不过度,例如),这些数字通常用于在线保护数据,理解 Shor 算法和其他类似量子算法的工作原理可以帮助我们欣赏现代加密应用中涉及的假设以及我们未来还需要什么。
隐私的下一步是什么?
考虑到我们对 Shor 算法的了解,可能会觉得保护我们健康记录到聊天记录的密码学似乎已经注定要失败。幸运的是,既有量子技术(如我们在第三章中了解到的量子密钥分发)和旨在抵抗如 Shor 算法等算法的新经典技术。后者被称为后量子密码学,是当前许多研究探索的主题。
事实上,Q#可以通过使理解需要多少量子计算机来攻击特定的密码系统变得更加容易,在密码学研究领域发挥重要作用。例如,谷歌的研究人员最近使用 Q#和 Python 改进了 Shor 算法中模乘步骤的成本,这帮助他们估计,使用当前的量子算法攻击合理的 RSA 实例需要 2000 万个量子位。同样,github.com/Microsoft/grover-blocks是使用 Q#理解 Grover 算法(第十一章)如何影响对称密钥算法(如 AES)的一个很好的例子。
在这两种情况下,Q#是理解攻击者需要多少量子计算能力来破坏当前密码系统的宝贵工具。结合对量子算法和硬件将继续以多快速度改进的假设,对攻击者能够购买的量子计算能力的假设,以及对 RSA 等算法需要保持安全多长时间以保证我们隐私的要求,使用 Q#开发的理解可以帮助我们认识到当前密码系统需要多快被替换。像信息安全中的任何事物一样,保证隐私不受量子攻击者侵犯是一个非常复杂的话题,更不用说没有简单答案了。幸运的是,像 Q#和量子开发工具包这样的工具有助于使问题变得更容易处理。
^a Craig Gidney, “Asymptotically Efficient Quantum Karatsuba Multiplication” (2018), arxiv.org/abs/1904.07356.
^b Craig Gidney 和 Martin Ekerå, “How to Factor 2048-bit RSA Integers in 8 Hours Using 20 Million Noisy Qubits (2019), arxiv.org/abs/1905.09749.
摘要
-
现代密码学通过隐藏数学难题来保护秘密,这些难题对经典计算机来说很难解决,例如分解数字。大型量子计算机可以用来分解数字,这改变了我们对密码学的看法。
-
模算术推广了时钟指针的运动方式:例如,在 27 小时制的时钟面上,25 + 5 等于 3。
-
恰好有两个质因数的整数被称为半素数,可以通过使用量子计算机解决模算术问题和相位估计来分解。
-
Q#数值库为在量子计算机上处理模整数提供了有用的函数和操作。
-
Shor 算法结合了在量子计算机上进行的经典预处理和后处理,以及相位估计,以快速使用模运算分解整数。
总结
在我们告别之前,回顾一下我们在本书中学到的各种技能是如何在这一章中汇聚起来,帮助我们理解量子计算机在现实世界中的应用是非常有帮助的。在第一部分,我们学习了如何描述和模拟量子计算机的基础知识,以及使量子计算独特的量子效应。在第三章,我们学习了如何使用单个量子比特和叠加来通过量子密钥分发安全地共享加密密钥。在第 4 至 6 章,我们通过纠缠多个量子比特来玩游戏并在量子设备周围移动数据。我们甚至使用 Python 构建了自己的量子模拟器来实施这些游戏并了解帮助我们描述量子效应的数学。
在掌握了所有这些基础知识之后,在第二部分,我们开始编写量子算法来帮助卡美洛的船员玩游戏。在第七章,我们学习了 Q#,这是一种专门设计用来轻松编写量子计算机程序的编程语言。在第八章,我们实现了 Deutsch–Jozsa 算法来选择新的国王,但在这个过程中,我们也学习了或然以及它们如何帮助我们评估量子程序中的经典函数。在第九章,我们还开发了我们的相位估计程序,在那里我们学习了如何操纵相位并使用相位回弹来检查我们的量子程序中的操作。
拥有新的量子开发技术工具箱,我们解决了量子计算中最激动人心的应用之一。在第十章,我们学习了哈密顿量模拟以及我们如何可以使用量子计算机中的量子系统来模拟各种化学物质中的能级。在第十一章,我们实现了 Grover 算法,通过振幅放大在非结构化数据中搜索信息。在这一章中,我们使用了从 Q#诊断函数和操作到相位估计的一切,以及从within/ apply块到经典函数的或然表示,在量子计算机上分解数字。利用本书中其他部分学到的知识,Shor 算法的难点主要在于连接分解数字问题到周期查找问题的经典部分。
尽管这本书并没有涵盖量子计算的所有内容——毕竟,自 1985 年以来已经发生了许多事情!——但你所学到的知识为你提供了继续学习、探索和推动量子计算前进所需的一切。使用 Python 和 Q#共同,你拥有了参与计算领域最激动人心的进步的工具,帮助你同行和同事与你一起学习,并建立一个能够将量子计算用于良好目的的社区。去享受乐趣吧!
接下来是什么?
虽然关于量子计算总有更多东西要学习,但你现在已经有了一切,可以开始使用 Python 和 Q#共同开发量子应用。如果你对学习和在量子计算方面做更多的事情感兴趣,以下是一些资源,可以帮助你迈出下一步:
-
Q# 社区(qsharp.community)—围绕 Q#量子编程的开源社区,包括博客、代码仓库和在线聚会
-
微软量子文档(
docs.microsoft.com/azure/quantum/)—与量子开发套件相关的所有内容的完整参考文档 -
arXiv(arxiv.org)—科学论文和手稿的在线存储库,包括大量关于量子计算的研究
-
《单元基金》(unitary.fund)—一个非营利组织,为开源量子软件提供补助金和财务支持,以及我们可承担的开源项目的精美建议
-
量子开源基金会(www.qosf.org)—开发开源量子软件的基金会,包括当前项目列表和进一步学习的资源
-
量子计算伦理(qcethics.org)—量子计算伦理的资源
-
Q-Turn(q-turn.org)—包容性的量子计算会议系列
-
《量子算法动物园》(quantumalgorithmzoo.org)—已知量子算法列表,包括每个算法的论文链接
-
《量子计算:轻松入门》(Jack D. Hidary, Springer, 2019)—关于我们在本书中学到的量子算法背后的数学的更多细节
许多大学和学院也提供了一些课程或研究项目,这些项目可能在你继续探索量子计算时对你感兴趣。无论你决定如何继续,我们都希望你能享受乐趣,并努力使量子计算社区变得更加美好!
附录 A 安装所需软件
几乎任何项目的开始都涉及在您的计算机上找到或设置开发环境。本附录帮助您准备在线使用本书的示例(使用 Binder 或 GitHub Codespaces),或者安装您可以在本地使用的 Python 环境和 Microsoft 量子开发工具包。如果您遇到问题,请查看使用量子开发工具包的最新文档(docs.microsoft.com/azure/quantum/install-overview-qdk),并在本书的存储库上提交问题(github.com/crazy4pi314/learn-qc-with-python-and-qsharp)。
A.1 在线运行示例
如果您想尝试本书中的示例而不安装任何东西,有两个很好的选择:
-
Binder (mybinder.org),一个用于探索托管 Jupyter Notebooks 的免费服务
-
GitHub Codespaces,一个云托管开发环境
A.1.1 使用 Binder
要使用 Binder,只需访问mybinder.org/v2/gh/crazy4pi314/learn-qc-with-python-and-qsharp/master。这可能需要几分钟,但 Binder 将启动一个新的 Jupyter Notebook 安装,其中包括您需要的 Python 包和 Q#支持。
警告:Binder 服务仅用于探索,在约 20 分钟的无操作后将会删除您的更改。虽然 Binder 是一个很好的入门方式,但如果您想继续开发量子程序,使用 GitHub Codespaces 或在您的机器上本地安装 Python 和量子开发工具包将很有帮助。
A.1.2 使用 GitHub Codespaces
在撰写本文时,GitHub Codespaces 处于早期预览阶段,请访问github.com/features/codespaces。有关如何使用 Codespaces 中的代码示例的说明,请查看该书的示例存储库github.com/crazy4pi314/learn-qc-with-python-and-qsharp。
A.2 使用 Anaconda 本地安装
在本书的第一部分,我们大量使用 Python 作为探索量子编程的工具,而在第二部分和第三部分,我们则同时使用 Python 和 Q#。这样做,我们依赖于量子开发工具包和几个 Python 库,这些库使得编写科学程序更加容易。因此,在本地安装时,使用科学软件发行版,如Anaconda 发行版(anaconda.com)来帮助管理 Python 和其他科学编程工具会更容易。
A.2.1 安装 Anaconda
要安装 Anaconda,请按照docs.anaconda.com/anaconda/install中的说明操作。
警告 在撰写本文时,Anaconda 发行版附带 Python 2.7 或 3.8。Python 2.7 于 2020 年 1 月正式停止支持,仅提供兼容性原因。本书假设使用 Python 3.7 或更高版本,因此请确保您安装的 Anaconda 版本提供 Python 3。
A.2.2 使用 Anaconda 安装软件包
软件包是我们在尝试学习或开发新代码时协作和节省时间的好方法。它们是收集相关代码并将其打包以便于在其他机器上共享的方式。我们可以使用所谓的 包管理器 在我们的机器上安装软件包,Python 有几种常见的包管理器选项。我们可能会选择一个管理器而不是另一个,因为每个管理器都有自己的软件包列表,而我们想要安装的软件包可能只被某个特定的管理器所知(取决于作者如何部署它)。让我们首先查看作为 Anaconda 部分安装的已安装包管理器。
默认情况下,Anaconda 附带两个包管理器:pip 和 conda。鉴于我们已经安装了 Anaconda,conda 包管理器具有一些额外的 pip 功能,使其成为包管理的良好默认选择。conda 支持在安装软件包时自动安装依赖项;它还有 环境 的概念,这对于为每个正在工作的项目创建专门的 Python 沙盒非常有帮助。一个好的通用策略是在软件包可用时使用 conda 安装软件包,在其他情况下使用 pip 安装软件包。
注意 conda 包管理器可以与大多数常见的命令行环境一起使用。但要在 PowerShell 中使用 conda,您需要版本 4.6.0 或更高版本。要检查您的版本,请运行 conda --version。如果您需要更新,请运行 conda update conda。
Conda 环境
我们可能会遇到这样的情况,即我们为两个不同的项目需要的软件包相互冲突。为了帮助隔离项目,Anaconda 发行版提供了 conda env 作为管理多个 环境 的工具。每个环境都是 Python 的一个完全独立的副本,只包含特定项目或应用所需的软件包。环境甚至可以使用彼此不同的 Python 版本,一个环境使用 2.7,另一个使用 3.8。环境对于与他人协作也非常有用,因为我们可以向队友发送一个单独的小文本文件,即 environment.yml,告诉他们的 conda env 如何创建一个与我们完全相同的环境。
更多信息,请参阅 docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html。
按照那个策略,让我们首先创建一个新的环境,包含你需要的包。本书的代码示例,可以在 github.com/crazy4pi314/learn-qc-with-python-and-qsharp 找到,包含一个名为 environment.yml 的文件,它告诉 conda 你在新环境中需要哪些包。从本书的代码库中克隆或下载代码,然后在你的首选命令行中运行以下命令:
conda env create -f environment.yml
此命令使用 conda-forge 通道的包创建了一个名为 qsharp-book 的新环境,并将 Jupyter Notebook、Python(版本 3.6 或更高)、Jupyter 的 IQ# 内核、IPython 解释器、NumPy、Matplotlib 绘图引擎和 QuTiP 安装到新环境中。conda 包管理器会提示你确认将要安装的包列表。要继续,请按“y”然后按 Enter,然后拿一杯咖啡。
提示:在 github.com/crazy4pi314/learn-qc-with-python-and-qsharp 提供的 environment.yml 文件中安装的 qsharp 包,提供了 Python 和量子开发工具包(你将在下一步安装)之间的集成。
一旦 conda 完成了新环境的创建,让我们来试一下。要测试你的新环境,首先激活它:
conda activate qsharp-book
一旦激活了 qsharp-book 环境,python 命令应该调用该环境中安装的版本。要检查这一点,你可以打印出你环境中 python 命令的路径。运行 python,然后在 Python 提示符下运行以下命令:
>>> import sys; print(sys.executable)
C:\Users\Chris\Anaconda3\envs\qsharp-book\python.exe ❶
❶ 你可能看到不同的路径,这取决于你的系统
如果你成功创建了环境,你可以使用它,无论是通过命令行使用 IPython,还是在浏览器中使用 Jupyter Notebook。要开始使用 IPython,从你的命令行运行 ipython(确保首先激活 qsharp-book):
$ ipython
In [1]: import qutip as qt
In [2]: qt.basis(2, 0)
Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket
Qobj data =
[[1.]
[0.]]
如果你想要了解更多关于使用 NumPy 的信息,请继续阅读第二章。如果你想要了解更多关于使用 QuTiP 的信息,请继续阅读第五章。如果你想要了解量子开发工具包,请继续阅读此处。
更多信息
-
Anaconda 文档:
docs.anaconda.com/anaconda -
NumPy 文档:
numpy.org -
Jupyter Notebook 文档:
jupyter-notebook.readthedocs.io/en/stable/notebook.html
A.3 安装量子开发工具包
提示:量子开发工具包的安装说明的最新版本可以在docs.microsoft.com/azure/quantum/install-overview-qdk找到。为了使用本书提供的代码示例,请确保在遵循量子开发工具包安装指南时安装 Python 和 Jupyter Notebook 支持。
微软的量子开发工具包是一套用于使用和编程 Q#(一种新的量子编程语言)的工具。如果你使用 Anaconda 安装了 Python 环境,那么你可以通过 conda 开始使用量子开发工具包:
conda install -c quantum-engineering qsharp
如果你更愿意使用 Q#与独立程序或不在 conda 环境中使用,那么你可以按照本节中的说明在你的计算机上本地安装量子开发工具包。
注意:本书侧重于使用 Visual Studio Code,但量子开发工具包也可以通过遵循正文中的命令行说明与任何其他文本编辑器一起使用。量子开发工具包还可以通过使用marketplace.visualstudio.com/items?itemName=quantum.DevKit扩展与 Visual Studio 2019 或更高版本一起使用。
使用从设置 Python 环境开始的 Visual Studio Code 安装,你需要做一些事情才能使用 C#、Python 和 Jupyter Notebook 与量子开发工具包一起使用:
-
安装.NET Core SDK。
-
安装 Q#的项目模板。
-
安装 Visual Studio Code 的量子开发工具包扩展。
-
安装 Jupyter Notebook 的 Q#支持。
-
安装 Python 的
qsharp包。
完成这些操作后,你将拥有编写和运行 Q#量子程序所需的一切。
认识.NET
回答.NET 是什么的问题比以前复杂了一些。历史上,.NET是.NET Framework的合理简称,它是任何.NET 语言(最著名的是 C#、F#和 Visual Basic .NET)的虚拟机和编译器基础设施。.NET Framework 仅适用于 Windows,但第三方重实现(如 Mono)存在于其他平台,包括 macOS 和 Linux。
然而,几年前,微软和.NET 基金会开源了一种名为.NET Core 的新.NET 版本。与.NET Framework 不同,.NET Core 是开箱即用的跨平台。.NET Core 也小得多,大部分功能被分离到可选包中。这使得在同一台机器上拥有多个.NET Core 版本以及引入新的.NET Core 功能而不会出现兼容性问题变得容易得多。
然而,.NET 分为 Framework 和 Core 的分支也带来了一些问题。为了使 .NET Core 作为跨平台编程环境工作得更好,.NET 标准库中的一些内容在方式上与 .NET Framework 不完全兼容。为了解决这个问题,.NET 基金会引入了 .NET Standard 的概念,这是一组由 .NET Framework 和 .NET Core 提供的 API。然后可以使用 .NET Core SDK 为 .NET Core 或 .NET Standard 创建库,并为 .NET Core 构建应用程序。Quantum 开发工具包中提供的许多库都针对 .NET Standard,这样 Q# 程序就可以从传统的 .NET Framework 应用程序或使用 .NET Core SDK 构建的新应用程序中使用。
从现在开始,最新的 .NET 版本被称为 “.NET 5”,尽管它是 .NET Core 3.1 之后的下一个版本。在 .NET 5 中,只有一个 .NET 平台,而不是 .NET Framework、.NET Core 和 .NET Standard,这减少了大量的混淆。更多详情请查看 devblogs.microsoft.com/dotnet/introducing-net-5/。
然而,目前来说,.NET Core 3.1 仍然是 .NET 平台最新的 长期支持版本,这使得它成为在下一个长期支持版本 .NET 6 发布之前编写稳定生产软件的最佳选择。因此,Quantum 开发工具包是编写为使用 .NET Core SDK 3.1。
A.3.1 安装 .NET Core SDK
要安装 .NET Core SDK,请访问 dotnet.microsoft.com/download/dotnet-core/3.1 并从页面顶部附近的选择中选取您的操作系统。在标记为“构建应用程序—SDK”的部分下,下载适用于您操作系统的安装程序,然后您就可以开始了!
A.3.2 安装项目模板
可能与您习惯的不同的一点是,.NET 开发围绕着一个名为 项目 的概念,该概念指定了如何调用编译器来生成新的二进制文件。例如,一个 C# 项目 (*.csproj) 文件告诉 C# 编译器应该构建哪些源文件,需要哪些库,开启或关闭哪些警告等。以这种方式,项目文件的工作方式类似于 makefile 或其他构建管理系统。最大的区别在于 .NET Core 上的项目文件如何引用库。
Q# 重新使用此基础设施,以便轻松获取量子程序的新库,例如在 docs.microsoft.com/azure/quantum/user-guide/libraries/additional-libraries 列出的库,或者社区开发者提供的 qsharp.community/projects/ 中的库。使用此基础设施,一个 Q# 项目文件可以指定一个或多个对 NuGet.org 上的 packages 的引用,这是一个软件库的包仓库。每个包可以提供多个不同的库。当构建依赖于 NuGet 包的项目时,.NET Core SDK 会自动下载正确的包,然后使用该包中的库来构建项目。
从量子编程的角度来看,这允许将量子开发工具包作为少数几个 NuGet 包进行分发,这些包可以安装到机器上,也可以安装到每个项目中。这使得在不同的项目上使用量子开发工具包的不同版本或仅包含特定项目所需的组件变得容易。为了帮助您开始使用一组合理的 NuGet 包,量子开发工具包提供了创建新项目的模板,这些模板引用了您所需的一切。
提示:如果您更喜欢在 IDE 中工作,量子开发工具包的 Visual Studio Code 扩展(见下文)也可以用来创建新项目。
要安装项目模板,请在您喜欢的终端中运行以下命令:
dotnet new -i "Microsoft.Quantum.ProjectTemplates"
安装项目模板后,您可以通过再次运行 dotnet new 来使用它们:
dotnet new console -lang Q# -o ProjectName ❶
❶ 请确保将“ProjectName”替换为您想要创建的项目名称。
A.3.3 安装 Visual Studio Code 扩展
一旦您从 code.visualstudio.com/ 安装了 Visual Studio Code,您就需要量子开发工具包扩展来获取对 Q# 的编辑器支持,包括自动完成、内联语法错误突出显示等。
要安装扩展,请打开一个新的 Visual Studio Code 窗口,并按 Ctrl-Shift-X(Windows 和 Linux)或 ⌘-Shift-X 打开扩展侧边栏。在搜索栏中输入 Microsoft Quantum Development Kit 并点击安装按钮。Visual Studio Code 安装扩展后,安装按钮会变为重新加载按钮。点击它以关闭 Visual Studio Code 并重新打开您的窗口,此时已安装量子开发工具包扩展。或者,按 Ctrl-P 或 ⌘-P 打开“转到”调色板。在调色板中输入 ext install quantum.quantum-devkit-vscode 并按 Enter。
在任何情况下,一旦安装了扩展,要使用它,请打开一个包含您想要工作的 Q# 项目的文件夹(Ctrl-Shift-O 或 ⌘-Shift-O)。此时,您应该拥有开始使用量子开发工具包进行编程所需的一切!
更多信息
-
量子开发工具包文档:
docs.microsoft.com/azure/quantum/ -
使用
dotnet命令:docs.microsoft.com/dotnet/core/tools/dotnet -
使用 Visual Studio Code 入门:
code.visualstudio.com/docs/introvideos/basics
A.3.4 为 Jupyter Notebook 安装 IQ#
从你喜欢的命令行运行以下命令:
dotnet tool install -g Microsoft.Quantum.IQSharp
dotnet iqsharp install
小贴士 在某些 Linux 安装中,你可能需要运行 dotnet iqsharp install --user。
这使得 Q# 可作为 Jupyter Notebook 的语言使用,例如第七章中使用的那些。
附录 B 术语表和快速参考
本附录提供了对本书中涵盖的许多量子概念以及 Q#语言(版本 0.15)的快速参考。本附录的大部分内容已在正文中有覆盖,但在此收集以方便查阅。
B.1 术语表
伴随操作 完美反转或取消另一个量子操作作用的量子操作。例如,X和H这样的操作是自伴随的。如果一个操作可以被单位矩阵 U 模拟,那么它的伴随操作可以通过 U 的复共轭模拟,也称为 U 的伴随,记作 U†。在 Q#中,具有伴随操作的运算用is Adj表示。
算法 解决问题的程序,通常指定为一系列步骤。
BB84 “Bennett and Brassard 1984”的缩写。一种通过每次发送一个量子比特来执行量子密钥分发(QKD)的协议。
波恩规则 一个数学表达式,可以用来预测量子测量的概率,给定对该测量的描述以及被测量的寄存器状态。
经典比特 经典计算机中最小的存储和处理功能单元。经典比特可以是“0”或“1”状态之一。
经典计算机 使用经典物理定律进行计算的传统计算机。
控制操作 基于控制寄存器的状态应用的一种量子操作,不进行测量,从而正确地保留叠加态。例如,CNOT 操作是一个控制非或控制 X 操作。同样,Fredkin 操作是一个控制交换操作。在 Q#中,可以控制的操作用is Ctl表示。
复数 形如 z = a + bi 的数,其中 i² = –1。
计算基态 由经典比特字符串标记的状态。例如,|01101〉是一个五比特寄存器上的计算基态。
计算机 一种接收数据作为输入并在该数据上执行某种操作的设备。
互质 没有公因数的两个正整数。例如,21 = 3 × 7 和 10 = 2 × 5 是互质的,而 21 = 3 × 7 和 15 = 3 × 5 共享 3 作为公因数,因此不是互质的。
纠缠 当两个或更多量子比特的状态不能独立写出时。例如,如果两个量子比特处于 (|00〉 + |11〉) / √2 状态,那么不存在两个单量子比特状态 |Ψ〉 和 |ϕ〉 使得 (|00〉 + |11〉) / √2 = |Ψ〉 ⊗ |ϕ〉,并且这两个量子比特是纠缠的。
本征相 量子操作赋予本征态的全局相。例如,X操作的 |−〉 = (|0〉 − |1〉) / √2 本征态具有本征相 –1,因为 X|–〉 = (|1〉 − |0〉) / √2 = –|–〉。
本征态 一个在应用量子操作后未被修改的状态,直到可能的全局相位。例如,|+〉 状态是 X 操作的本征态,因为 X|+〉 = |+〉。
特征值 给定一个矩阵 A,一个数 λ 是 A 的特征值,如果对于某个向量
,有 A
= λ
。
本征向量 给定一个矩阵 A,一个向量
是 A 的本征向量,如果对于某个数 λ,有 A
= λ
。
全局相位 任何两个量子态,只要乘以模为 1 的复数,它们就是等价的。在这种情况下,这两个态是完全等效的。例如,(|0〉 − |1〉) / √2 和 (|1〉 − |0〉) / √2 代表相同的态,因为它们相差一个相位 –1 = e^(iπ)。
测量 一个返回关于量子寄存器状态的经典数据的量子操作。
无克隆定理 一个数学定理,证明不存在一个可以完美复制量子信息的量子操作。例如,不可能制作一个将状态 |Ψ〉 ⊗ |0〉 转换为 |Ψ〉 ⊗ |Ψ〉 的操作,对于任意的量子状态 |Ψ〉。
算子 实现应用于量子寄存器的经典函数的量子操作。
泡利矩阵 单量子比特单位矩阵 𝟙, X, Y, 和 Z。
相位 一个模为 1 的复数(即 a + bi,其中 |a|² + |b|² = 1)。相位可以写成 e^(iθ),其中 θ 是一个实数。注意,作为一个缩写,当上下文清楚时,有时 θ 本身也被称为相位。
相位估计 任何学习与量子操作给定本征态相关的本征相的量子算法。
相位回弹 一种量子编程技术,用于将受控量子操作应用的相位与控制寄存器的状态相关联,而不是目标寄存器的状态。这项技术可以将本应通过单位操作应用的全球相位转换为可观察的物理相位。
程序 一系列可以被经典计算机解释以执行所需任务的指令。
量子计算机 一种量子设备,设计和使用来解决经典计算机难以解决的问题。
量子设备 一个构建来达到某些目的或执行某些任务的量子系统。
量子密钥分发 (QKD) 一种通信协议,用于在两个当事人之间共享随机数,当由正确操作的设备执行时,该协议的安全性由量子力学(特别是无克隆定理)保证。
量子操作 量子程序中的一个子例程,代表发送到量子设备和经典控制流的一系列指令。一些量子操作,如 X 和 H,是内置到量子设备中的,被称为 内在的。
量子程序 通过向该设备发送指令并处理设备返回的测量数据来控制量子设备的经典程序。通常,量子程序是用 Q#等量子编程语言编写的。
量子寄存器 量子比特的集合。寄存器可以处于任何计算基态,由经典比特的字符串标记,或任何叠加态。
量子态 量子寄存器(即量子比特寄存器)的状态,通常表示为 2^(n)个复数向量的形式,其中n是寄存器中量子比特的数量。
量子系统 需要量子力学来描述和模拟的物理系统。
量子比特 量子计算机中最小的功能单元。单个量子比特可以处于 |0〉 状态、|1〉 状态或任何叠加态。
可逆 可以完美反转的经典函数。例如,f(x) = ¬x 可以反转,因为 f(f(x)) = x。同样,g(x,y) = (x,x ⊕ y) 是可逆的,因为 g(g(x,y)) = (x,y)。另一方面,h(x,y) = (x,x AND y) 不可逆,因为 h(0,0) = h(0,1) = (0,0),因此我们无法根据输出(0,0)确定 H 的输入。
半素数 恰有两个素因子的正整数。例如,21 = 3 × 7 是半素数,而 105 = 3 × 5 × 7 有三个素因子,因此不是半素数。
状态 对物理系统或设备的描述,足以允许模拟该设备。
叠加态 可以表示为其他状态的线性组合的量子态处于这些状态的叠加。例如,|+〉 = (|0〉 +|1〉) / √2 是 |0〉 和 |1〉 状态的叠加,而 |0〉 是 |+〉 和 |−〉 = (|0〉 − |1〉) / √2 的叠加。
幺正矩阵 满足 UU† = 𝟙 的矩阵 U,其中 U† 是共轭变换,或 U 的伴随。类似于经典真值表,幺正矩阵是描述量子操作如何变换其输入状态以便可以用于模拟任意输入的矩阵。
幺正操作 可以用幺正矩阵表示的量子操作。在 Q#中,幺正操作是可逆的和可控的(is Adj + Ctl)。
B.2 狄拉克符号
在量子计算中,我们通常使用一种称为狄拉克符号的压缩表示法来表示向量和矩阵。本书中对此有更详细的介绍,但本表总结了狄拉克符号的一些关键点。

B.3 量子操作
本节总结了书中常见的一些量子操作。特别是,我们展示了如何通过传递量子比特作为输入从 Q#调用每个操作;如何使用作用于状态的矩阵、我们用于模拟每个操作的幺正矩阵在 QuTiP 中模拟该操作;以及该操作在数学上的行为的一些示例。
注意:要查看所有内置 Q# 操作的完整列表,请参阅 Q# API 参考docs.microsoft.com/qsharp/api。
对于所有 Q# 示例,我们假设以下 open 语句:
open Microsoft.Quantum.Intrinsic;
对于所有 Python/QuTiP 示例,我们假设以下 import 语句:
import qutip as qt
import qutip.qip.operations as qtops
第二种约定在书中未使用,但在此处为了简洁而使用。
Q# 操作作用于 量子比特,而 QuTiP 通过乘以 状态 的单位算子来表示操作。因此,与 Q# 操作相比,QuTiP 对象不明确列出它们的输入。

B.4 Q# 语言
B.4.1 类型
在下表中,我们使用 *italic monospace font* 来表示占位符。例如,在 *BaseType*``[] 中的 *BaseType* 占位符可以表示 Int、Double、Qubit、(Qubit, Qubit[]) 或任何其他 Q# 类型。
为了强调,我们在一些示例中在每个值之后添加了每个示例的类型作为注释。例如,Sin : Double -> Double 表示 Sin 是一个类型为 Double -> Double 的值。
| 描述 | Q# 类型 | 示例 |
|---|---|---|
| 整数 | Int |
3``-42``108 |
| 浮点数 | Double |
-3.14152.17 |
| 布尔值 | Bool |
true``false |
| 整数范围 | Range |
0..3``0..Length(arr)``12..-1..0 |
| 空元组 | Unit |
() |
| 测量结果 | Result |
Zero``One |
| 泊松算子 | Pauli |
PauliI``PauliX``PauliY``PauliZ |
| 字符串 | String |
"Hello, world!" |
| 量子比特 | Qubit |
(参见 use 语句) |
| 数组 | BaseType[] |
new Qubit[0]``[42, -101] |
| 元组 | (``*T1*``), (``*T1*``, *T2*``), (``*T1*``, *T2*``, *T3*``), 以及更多 |
(PauliX, "X")``(1, true, Zero) |
| 函数 | *InputType* -> *OutputType* |
Sin : Double -> Double``Message : String -> Unit |
| 操作 | InputType => *OutuptTypeInputType* => Unit is Adj (如果可伴随)InputType => Unit is Ctl (如果可控)*InputType* => Unit is Adj + Ctl` (如果既可伴随又可控) |
H : Qubit => Unit is Adj + Ctl``CNOT : (Qubit, Qubit) => Unit ``is Adj + Ctl``M : Qubit => Result``Measure : (Pauli[], Qubit[]) => Result |
B.4.2 Q# 声明和语句
在下表中,我们使用 斜体等宽字体 *font* 来表示占位符。例如,在 function FunctionName``(``input1 : InputType1``) : OutputType 中的 FunctionName 占位符表示正在定义的函数名称,而 InputType1 和 OutputType 占位符表示来自上表的类型。
Q# 关键字以 粗体 表示。
| 描述 | Q# 语法 |
|---|---|
| 行注释至行尾 | // *comment text* |
| 操作或函数前的文档注释 | /// # Summary``/// *summary body*``///``/// # Description``/// *description body*``///``/// # Input``/// ## *input1*``/// *description of input* |
| 命名空间声明 | **namespace** *NamespaceName* { `` // ...``} |
| 函数声明 | **function** *FunctionName*``(``* input1* : *InputType1*``, * input2* : *InputType2*``, `` ...``) : *OutputType* { `` // 函数体``} |
| 操作声明 | **operation** *OperationName*``( * input1* : *InputType1*``, * input2* : *InputType2*``, `` ...``) : *OutputType* { `` // 操作体``} |
| 可逆和可控的操作声明 | **operation** *OperationName*``( * input1* : *InputType1*``, * input2* : *InputType2*``, `` ...``) : Unit **is** Adj + Ctl { `` // 操作体``} |
| 用户定义类型声明 | **newtype** *TypeName* = ( * ItemType1*``, * ItemType2*``, `` ... ``); |
| 带有命名项的用户定义类型声明 | **newtype** *TypeName* = ( * ItemName1*``: *ItemType1*``, * ItemName2*``: *ItemType2*``, `` ... ``); |
| 打开命名空间(使命名空间中的项在 Q#文件或笔记本单元中可用) | **open** *NamespaceName*``**;** |
| 使用别名打开命名空间 | **open** *NamespaceName* **as** *AliasName*``**;**示例:**open** Microsoft.Quantum.Diagnostics as Diag; |
| 局部变量声明 | **let** *name* = *value*``;示例:**let** foo = "Bar"; |
| 可变变量声明 | **mutable** *name* = *value*``; |
| 重新分配(更新)可变变量 | **set** *name* = *newValue*``; |
| 应用并重新分配可变变量 | **set** *name* *operator*``= *expression*``;示例:**set** count += 1;``**set** array w/= 2 <- PauliX;(查看w/运算符) |
| 经典条件语句 | **if** *condition* |
**if** *condition* { // ...} **else** |
|
**if** *condition* { // ...} **elseif** *condition* { // ...} **else** |
|
| 遍历数组 | **for** *element* **in** *array* { `` // 循环体``}注意:*array*必须是数组类型的值。 |
| 遍历范围 | **for** *index* **in** *range* { `` // 循环体``}注意:*array*必须是Range类型的值。 |
| 重复直到成功循环 | **repeat** { `` // 循环体``}``**until** *condition*``; |
| 带有修复块的重复直到成功循环 | **repeat** { `` // 循环体``}``**until** *condition*``**fixup** { `` // 修复体``} |
| 当循环(仅限于函数中) | **while** *condition* { `` // 循环体``} |
| 报错终止 | **fail** "message"; |
| 从函数或操作返回值 | **return** *value*``; |
| 应用鞋袜模式(详见第七章) | **within** { `` // 外部体``} **apply** { `` // 内部体``} |
| 分配单个新量子比特(仅限于操作中) | **use** *name* = Qubit(); |
| 分配量子比特数组(仅限于操作中) | **use** *name* = Qubit[``*size*``]; |
| 在操作中分配量子比特和寄存器的元组(仅限操作) | **use** (``*名称 1*``, *名称 2*``, ...) = (Q``*比特或数组 1*``, *QubitOrArray2*``, ...); |
| 在显式作用域中分配单个量子比特(仅限操作) | **use** *名称* = Qubit() { `` // ... // 名称` 在此处释放。} |
B.4.3 Q# 表达式和运算符
在以下表中,我们使用 *斜体等宽字体* 来表示占位符。例如,new *类型*``[``*长度*``] 中的 *类型* 占位符表示新数组的基类型,而 *长度* 表示其长度。
Q# 关键字以 **粗体** 表示。
| 描述 | Q# 语法 |
|---|---|
| 算术 | +、-、*、... |
将格式值作为字符串表示为 $"... {``*表达式*``} ..." 示例: $"测量结果为 {result}" |
|
| 连接两个数组 | *数组 1* + *数组 2* |
| 分配数组 | **new** *类型*``[``*长度*``] |
| 获取数组中的元素 | *数组*``[``*索引*``] |
| 切片数组 | *数组*``[``*起始*``...]``*数组*``[...``*结束*``]``*数组*``[``*起始*``..``*结束*``]``*数组*``[``*起始*``..``*步长*``..``*结束*``] |
| 复制并更新数组中的项 | *数组* w/ *索引* <- *新值* 示例: [10, 100, 1000] w/ 1 <- 200// [10, 200, 1000] |
| 访问用户定义类型的命名项 | *值*``::``*项名* 示例: let imagUnit = Complex(0.0, 1.0);``Message($"{imagUnit::Real}");`` // 打印 1.0 |
| 解包用户定义类型 | *值*``! 示例: **let** imagUnit = Complex(0.0, 1.0);``Message($"{imagUnit!}");`` // 打印 (0.0, 1.0) |
| 复制并更新用户定义类型的命名项 | *值* w/ *项名* <- *新值* 示例: **let** imagUnit = Complex(0.0, 1.0);``**let** onePlusI = imagUnit w/ Real <- 1.0;``Message($"{onePlusI!}");`` // 打印 (1.0, 1.0) |
B.4.4 Q# 标准库
我们假设以下开放声明:
open Microsoft.Quantum.Intrinsic;
open Microsoft.Quantum.Canon;
open Microsoft.Quantum.Arrays as Arrays;
open Microsoft.Quantum.Diagnostics as Diag;
要获取 Q# 函数、操作和用户定义类型的完整列表,请参阅 docs.microsoft.com/qsharp/api/.
后缀 A、C 和 CA 表示支持可对偶、可控制或两者都支持的运算(分别对应)。
| 描述 | 函数或操作 | 示例 |
|---|---|---|
| 将操作应用于数组的每个元素 | ApplyToEachCA |
ApplyToEachCA(H, register); |
| 调用一个操作多次 | Repeat |
Repeat(PrintRandomNumber, 10, ()); |
| 获取一对中的第一个或第二个元素 | Fst 或 Snd |
-
Fst((1.0, false)) // 1.0 -
Snd((1.0, false)) // false
|
| 将操作应用于数组的每个元素,并收集结果 | Arrays.ForEach |
let results = ForEach(M, register); |
|---|---|---|
| 使用数组的每个元素调用函数,并收集结果 | Arrays.Mapped |
let sines = Mapped(Sin, angles); |
| 如果条件为假则失败 | Diag.Fact |
Fact(2 == 2, "期望两个相等。"); |
| 如果两个测量结果不相等则失败 | Diag.EqualityFactR |
EqualityFactR(M(qubit), Zero, "Expected qubit to be in |0〉 state."); |
| 如果假设的测量没有预期的结果则失败 Notes: |
-
仅在模拟器上物理上可能
-
实际上不执行测量,不会影响量子比特
Diag.AssertMeasurement |
AssertMeasement([PauliZ],``[target], Zero,``"Expected qubit to be in |0〉 state.”); |
|---|---|
| 如果两个操作不同则失败 | Diag.AssertOperationsEqualReferenced |
| 请求模拟器显示所有分配量子比特的诊断信息 | Diag.DumpMachine |
| 请求模拟器显示特定寄存器的诊断信息 | Diag.DumpRegister |
B.4.5 IQ# 魔法命令
要查看 IQ# 魔法命令的完整列表,请参阅 docs.microsoft.com/qsharp/api/iqsharp-magic。
| 描述 | 魔法命令 | 示例 |
|---|---|---|
| 模拟函数或操作 | %simulate |
%simulate PlayMorganasGame winProbability=0.999 |
| 将新包添加到 IQ# 会话 | %package |
%package Microsoft.Quantum.Numerics |
| 使用当前包重新加载 Q# 文件 | %workspace reload |
|
| 列出所有可用的魔法命令 | %lsmagic |
|
| 列出当前打开的命名空间 | %lsopen |
|
| 对操作运行资源估算器 | %estimate |
%estimate FindMarkedItem |
| 设置 IQ# 配置选项 | %config |
%config dump.truncateSmallAmplitudes = true |
| 列出所有当前定义的函数和操作 | %who |
附录 C 线性代数复习
在这个附录中,我们的目标是快速覆盖一些对这本书有用的线性代数技能。我们将讨论什么是向量矩阵,如何使用向量和矩阵来表示线性函数,以及如何使用 Python 和 NumPy 来处理向量和矩阵。
C.1 接近向量
在我们能够了解量子位之前,我们需要理解向量的概念。
假设我们的一个朋友要邀请人来庆祝他们修好了门铃,而我们非常想找到他们的房子,和他们一起庆祝这个时刻。我们的朋友如何帮助我们找到他们的家?
你可以在图 C.1 中看到我们的方向困境的草图。向量是一种数学工具,可以用来表示各种不同的概念——基本上,任何我们可以通过按顺序列出数字来记录的东西:
-
地图上的点
-
显示器中像素的颜色
-
计算机游戏中的伤害元素
-
飞机的速度
-
陀螺仪的方向
例如,如果我们在一个不熟悉的城市迷路,有人可以通过给我们一个指示我们向东走a个街区然后向北走b个街区的向量来告诉我们去哪里(我们将忽略绕建筑物行进的问题)。我们用向量 [[a], [b]](图 C.2)来写这些指示。

图 C.1 寻找我们的朋友的家聚会

图 C.2 向量作为坐标
就像普通数字一样,我们可以将不同的向量相加。
注意 一个普通数字通常被称为标量,以区别于向量。
使用这种关于向量的思考方式,我们可以将向量之间的加法定义为逐元素定义。也就是说,我们将 [[a], [b]] + [[c], [d]] 解释为向东走a个街区,向北走b个街区,然后向东走c个街区,最后向北走d个街区。由于我们走步的顺序无关紧要,这相当于先向东走a + c个街区,然后向北走b + d个街区,所以我们写 [[*a*], [*b*]] + [[c], [d]] 为 [[a + c], [b + d]](图 C.3)。

图 C.3 通过加法向量找到聚会
在d维度的向量可以表示为d个数字的列表。例如,
= [[2], [3]] 是一个二维向量(图 C.4)。

图 C.4 绘制的向量与一系列方向或数字列具有相同的信息。
同样,我们可以通过乘以普通数字来乘以向量以转换向量。我们可能会迷路,比如在一个使用米而不是我们习惯的英尺的城市里。要将以米为单位的向量转换为以英尺为单位的向量,我们需要将我们的向量的每个元素乘以大约 3.28。让我们使用一个名为NumPy的 Python 库来帮助我们管理如何在计算机中表示向量。
提示 完整的安装说明请参阅附录 A。
列表 C.1 使用 NumPy 在 Python 中表示向量
>>> import numpy as np
>>> directions_in_meters = np.array( ❶
... [[30], [50]]) ❷
>>> directions_in_feet = 3.28 * directions_in_meters ❸
>>> directions_in_feet
array([[ 98.4], ❹
[164\. ]])
❶ 向量是 NumPy 数组的一种特殊情况。我们使用“array”函数创建数组,传递一个包含我们向量行的列表。然后,每一行都是一个列的列表——对于向量,我们每行只有一个列,但书中会有一些例子说明这不是真的。
❷ 从一个向东走 30 米然后向北走 50 米的例子开始
❸ NumPy 使用 Python 乘法运算符*表示标量和向量之间的乘法。
❹ 打印乘法的结果。我们需要向东走 98.4 英尺,然后向北走 164 英尺。
练习 C.1:单位转换
25 米向西和 110 米向北换算成英尺是多少?
练习解答
本书所有练习的解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入你所在章节的文件夹,打开名为练习解答的 Jupyter 笔记本。
这种结构使得传达方向变得更容易。如果我们不使用向量,那么每个标量都需要自己的方向,并且保持方向和标量在一起是至关重要的。
C.2 我们自己看矩阵
如我们很快将看到的,我们可以用线性代数中的一个概念——矩阵——来描述我们在量子比特上应用指令时量子比特的转换,就像我们描述向量的转换一样。当我们考虑比加法或缩放更复杂的向量转换时,这一点尤为重要。
要了解如何使用矩阵,让我们回到寻找派对的问题——毕竟,门铃不会自己响!到目前为止,我们只是假设每个向量的第一个分量代表东,第二个分量代表北,但有人可能会选择另一种惯例。如果没有一种方法来协调这两种惯例之间的转换,我们就永远找不到派对。幸运的是,不仅矩阵将帮助我们在这个附录的后面部分模拟量子比特,而且它们还可以帮助我们找到通往朋友的路。
幸运的是,从先列出北到先列出东的转换简单易行:我们将坐标[[a], [b]]交换为[[b], [a]]。假设这个交换是通过一个名为swap的函数实现的。那么swap与之前看到的向量加法很好地配合,即swap(v + w)始终与swap(v) + swap(w)相同。同样,如果我们拉伸一个向量然后交换(即标量乘法),这就像我们先交换然后拉伸一样:swap(a * v) = a * swap(v)。任何具有这两个特性的函数都是线性函数。
一个线性函数是一个函数f,使得对于所有标量a和b以及所有向量x和y,有f(ax + by) = af(x) + bf(y)。
练习 C.2:线性检查
以下哪个函数是线性的?
-
f(x) = 2^x
-
f(x) = x²
-
f(x) = 2x
线性函数在计算机图形学和机器学习中很常见,因为它们包括将数字向量进行各种变换的不同方式。
这些是线性函数的一些例子:
-
旋转
-
缩放和拉伸
-
反射
所有这些线性函数都有一个共同点,那就是我们可以将它们拆分并逐个理解。再次想到地图,如果我们试图找到派对的路线(希望还有一些果汁剩下),而我们得到的地图在南北方向上拉伸了 10%,在东西方向上翻转了,这并不难理解。由于拉伸和翻转都是线性函数,有人可以通过分别告诉我们南北方向和东西方向发生了什么来引导我们走上正确的道路。实际上,我们在本段的开头就做了这样的事情!
小贴士:如果你从这本书中学到了一件事,那么我们最重要的收获就是你可以通过将它们分解成组件来理解线性函数以及量子操作。在本书的其余部分,我们将看到,由于量子计算中的操作由线性函数描述,我们可以通过将它们拆分的方式来理解量子算法。不要担心这目前听起来不太明白——这是一种需要一段时间适应的思维方式。
这是因为一旦我们了解了北向量(让我们像之前一样称之为[[1], [0]])和西向量(让我们称之为[[0], [1]])发生了什么,我们就可以通过使用线性性质来找出所有向量发生了什么。例如,如果我们被告知在我们北边三块地方和西边四块地方有一个非常漂亮的景色,而我们想找出这在地图上的位置,我们可以逐个部分地这样做:
-
我们需要将北向量拉伸 10%,然后乘以 3,得到
[[3.3], [0]]。 -
我们需要翻转西向量并将其乘以 4,得到
[[0], [-4]]。 -
我们通过添加每个方向发生的事情来完成,得到
[[3.3], [-4]]。
线性函数非常特别!💖
在上一个例子中,我们能够使用线性函数拉伸我们的向量。这是因为线性函数对尺度不敏感。交换南北和东西对向量所做的同样的事情,无论它们是以步、区块、英里、英寻还是秒差距来表示。然而,大多数函数并不是这样。考虑一个平方其输入的函数,f(x) = x²。x越大,拉伸得越多。
线性函数无论其输入大小如何都按相同的方式工作,这正是我们能够逐个部分地分解它们的原因:一旦我们知道线性函数在任何尺度上的工作方式,我们就知道它在所有尺度上的工作方式。
因此,我们需要在地图上向北看 3.3 个区块,向东看 4 个区块。
之后,我们将看到“0”和“1”位可以被视为方向或向量,与北或东并不太不同。正如北和东不是帮助理解明尼阿波利斯的最佳向量一样,我们将发现“0”和“1”并不总是帮助理解量子计算的最佳向量(见图 C.5)。

图 C.5 如果我们想要了解去哪里,北和西并不总是最佳的方向。在这张明尼阿波利斯市中心的地图上,市中心网格的一个大区域被旋转以匹配密西西比河的弯曲。照片由 davecito 拍摄。
通过逐部分分解线性函数来理解线性函数的方法也适用于旋转。如果我们的映射将指南针顺时针旋转 45°(哇,我们需要上一堂严肃的制图课),使得北方变为东北方,西方变为西北方,我们仍然可以逐部分地找出事物的位置。使用相同的例子,北向量现在在地图上被映射到大约[[0.707], [0.707]],而西向量被映射到[[-0.707], [0.707]]。
当我们将例子中的所有情况加总起来,我们得到3 * [[0.707], [0.707]] + 4 * [[-0.707], [0.707]],这等于(3 - 4) * [[0.707], [0]] + (3 + 4) [[0], [0.707]],结果是[[-0.707], [4.95]]。这似乎与线性关系较少,而与北和西在某些方面是特殊的有关。然而,我们可以通过完全相同的论证,将西南写作[[1], [0]],西北写作[[0], [1]]。这是因为西南和西北相互垂直,允许我们将任何其他方向分解为西北和西南的组合。除了我们买的指南针易于阅读之外,没有其他东西使北或西变得特殊。如果你曾经试图在明尼阿波利斯市中心(见图 C.5)开车,很快就会变得明显,北和西并不总是理解方向的最佳方式!
形式上,任何一组通过这种方式逐部分分解来理解方向的向量集合被称为基。
技术上,我们这里关注的是数学家所说的正交归一基,因为在量子计算中这通常最有用。这意味着基中的向量与所有其他基向量垂直,并且长度为 1。
让我们尝试一个用基表示向量的例子。向量
= [[2], [3]]可以用基
[0] = [[1], [0]]和
[1] = [[0], [1]]表示为 2
[0] + 3
[1]。
基础 如果任何 d 维向量
可以表示为
[0]、
[1]、....、
[d–1] 的倍数之和,我们称
[0]、
[1]、...,
[d–1] 为一个 基。在二维空间中,一个常见的基是水平和垂直方向。
更一般地,如果我们知道函数 f 对基中每个向量的输出,我们可以计算任何输入的 f。这与我们如何使用真值表通过列出每个可能输入的操作输出来描述经典操作类似。
使用线性方法解决问题
假设 f 是一个线性函数,它表示我们的映射是如何拉伸和扭曲的。我们如何找到我们需要去的地方?我们想要计算值 f(np .array([[2], [3]]))(一个有些任意的值),给定我们的基 f(np.array ([[1], [0]]))(水平)和 f(np.array([[0], [1]]))(垂直)。我们还从查看地图图例的部分知道,地图将水平方向扭曲为 np.array([[1], [1]]),将垂直方向扭曲为 np.array([[1], [–1]])。
计算函数 f(np.array([[2], [3]])) 的步骤如下:
-
我们使用我们的基,
np.array([[1], [0]])和np.array([[0], [1]]),来表示np.array([[2], [3]])等于2 * np.array([[1], [0]]) + 3 * np.array([[0], [1]])。 -
使用这种新的方法来表示我们的函数输入,我们想要计算
f(2 * np.array([[1], [0]]) + 3 * np.array([[0], [1]]))。 -
我们利用
f是线性的性质,将f(2 * np.array([[1], [0]]) + 3 * np.array([[0], [1]]))写作2 * f(np.array([[1], [0]])) + 3 * f(np.array([[0], [1]])):>>> import numpy as np >>> horizontal = np.array([[1], [0]]) ❶ >>> vertical = np.array([[0], [1]]) >>> vec = 2 * horizontal + 3 * vertical ❷ >>> vec array([[2], [3]]) >>> f_horizontal = np.array([[1], [1]]) ❸ >>> f_vertical = np.array([[1], [-1]]) >>> 2 * f_horizontal + 3 * f_vertical ❹ array([[ 5], [-1]])❶ 定义变量 horizontal 和 vertical 来表示我们将用来表示 [[2], [3]] 的基
❷ 我们可以通过添加 horizontal 和 vertical 的倍数来写出 [[2], [3]]。
❸ 通过引入新变量 f_horizontal 和 f_vertical 来表示 f(horizontal) 和 f(vertical),分别定义 f 对 horizontal 和 vertical 的作用
❹ 因为 f 是线性的,我们可以通过将水平和垂直替换为 f_horizontal 和 f_vertical 来定义它对 [[2], [3]] 的作用。
练习 C.3:计算线性函数
假设你有一个线性函数 ɡ,使得 ɡ([[1], [0]]) = [[2.3], [–3.1]] 和 ɡ([[0], [1]]) = [[–5.2], [0.7]]。计算 ɡ([[2], [–2]])。
利用这个洞察力,我们可以制作一个表格,展示线性函数如何转换其每个输入。这些表格被称为矩阵,是线性函数的完整描述。如果我们告诉你一个线性函数的矩阵,那么你可以为任何向量计算该函数。例如,从北/东惯例到东/北惯例的地图方向转换,将指令“向北移动一个单位”从写成[[1], [0]]转换为写成[[0], [1]]]。同样,指令“向东移动一个单位”从写成[[0], [1]]转换为写成[[1], [0]]。如果我们堆叠这两组指令的输出,我们得到以下矩阵:
>>> swap_north_east = np.array([[0, 1], [1, 0]])
>>> swap_north_east
array([[0, 1],
[1, 0]])
提示:这在量子计算中也是一个非常重要的矩阵!在整个书中,我们会看到这个矩阵的很多应用。
要将矩阵表示的线性函数应用于特定的向量,我们需要将矩阵和向量相乘,如图 C.6 所示。

图 C.6 如何将矩阵乘以向量。在这个例子中,f的矩阵告诉我们f([[1], [0], [0]])是[[1], [2], [9]]。
警告:虽然我们添加向量的顺序不重要,但我们乘以矩阵的顺序非常重要。如果我们将地图旋转 90°然后通过镜子看它,我们会得到一个非常不同的图像,如果我们先旋转镜子中的图像 90°,结果会完全不同。旋转和翻转都是线性函数,所以我们可以为每个函数写一个矩阵;让我们分别称它们为r和f。如果我们翻转向量
,我们得到f
。旋转输出给我们RF
,这是一个与先旋转再翻转的FR
非常不同的向量。
矩阵乘法形式化了我们在给定特定输入的输出时计算f的方式,通过“堆叠”f对于向量如[[1], [0], [0]]和[[0], [1], [0]]的输出,如图 C.6 所示。虽然矩阵和向量的实际大小可能会改变,但矩阵可以描述线性变换这一想法保持不变。在本附录的其余部分,我们将查看长度为 2 的向量的线性变换。我们可以将矩阵的每一行(NumPy 的外层索引)视为函数对特定输入的作用。
深入探讨:为什么我们要乘以函数?
当我们将一个矩阵乘以一个向量(甚至是一个矩阵乘以另一个矩阵)时,一开始这似乎有点奇怪。毕竟,矩阵是表示线性函数的另一种方式,那么乘以函数的输入,更不用说乘以另一个函数,意味着什么呢?
要回答这个问题,暂时回到普通的代数是有帮助的,在那里对于任何变量 a、b 和 c,有 a(b + c) = ab + ac。这个性质被称为 分配律,是乘法和加法相互作用的基础。事实上,它如此基础,以至于分配律是我们定义乘法的关键方式之一——在数论和其他更抽象的数学部分,研究人员经常使用称为 环 的对象,我们真正了解的乘法只是它对加法进行分配。尽管这是一个抽象的概念,但环和其他类似代数对象的研究有广泛的应用,尤其是在密码学和错误纠正中。
分配律看起来与线性性质非常相似,即 f(x + y) = f(x) + f(y)。如果我们把 f 视为一个环的一部分,那么分配律与线性性质是相同的。
换句话说,就像程序员喜欢重用代码一样,数学家喜欢重用 概念。考虑矩阵相乘的想法让我们能够以我们习惯的代数方式处理线性函数。
因此,如果我们想知道一个经过矩阵 M 旋转的向量
的 i 个元素,我们可以找到 M 对
中每个元素的输出,将这些结果向量相加,并取第 i 个元素。在 NumPy 中,矩阵乘法由 @ 运算符表示。
注意:以下代码示例仅在 Python 3.5 或更高版本中有效。
列表 C.2 使用 @ 运算符进行矩阵乘法
>>> M = np.array([
... [1, 1],
... [1, -1]
... ], dtype=complex)
>>> M @ np.array([[2], [3]], dtype=complex)
array([[ 5.+0.j],
[-1.+0.j]])
练习 C.4:矩阵乘法
设 X 为矩阵 [[0, 1], [1, 0]],设
为向量 [[2], [3]]。使用 NumPy 计算 X
和 XX。
为什么选择 NumPy?
我们可以手动写出所有的矩阵乘法,但有几个原因使得使用 NumPy 非常方便。NumPy 的核心大部分使用常数时间索引,并且是用原生代码实现的,因此可以利用内置处理器的指令进行快速线性代数。因此,NumPy 通常比手动操作列表快得多。在列表 C.3 中,我们展示了 NumPy 可以将非常小的矩阵的乘法速度提高 10 倍。当我们查看第四章及以后的较大矩阵时,使用 NumPy 而不是手动操作给我们带来了更大的优势。
列表 C.3 NumPy 矩阵乘法评估计时
$ ipython ❶
In [1]: def matmul(A, B):
...: n_rows_A = len(A) ❷
...: n_cols_A = len(A[0])
...: n_rows_B = len(B)
...: n_cols_B = len(B[0])
...: assert n_cols_A == n_rows_B ❸
...: return [ ❹
...: [
...: sum( ❺
...: A[idx_row][idx_inner] * B[idx_inner][idx_col]
...: for idx_inner in range(n_cols_A)
...: )
...: for idx_col in range(n_cols_B)
...: ]
...: for idx_row in range(n_rows_A)
...: ]
...:
In [2]: import numpy as np ❻
In [3]: X = np.array([[0+0j, 1+0j], [1+0j, 0+0j]]) ❼
In [4]: Z = np.array([[1+0j, 0+0j], [0+0j, -1+0j]])
In [5]: matmul(X, Z)
Out[5]: [[0j, (-1+0j)], [(1+0j), 0j]]
In [6]: X @ Z ❽
Out[6]:
array([[ 0.+0.j, -1.+0.j],
[ 1.+0.j, 0.+0.j]])
In [7]: %timeit matmul(X, Z) ❾
10.3 *θ*s ± 176 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [8]: %timeit X @ Z
926 ns ± 4.42 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
❶ 这次,我们使用 Python 的 IPython 解释器,因为它提供了几个在这个例子中有用的额外工具。有关如何安装 IPython 的说明,请参阅附录 A。
❷ 找出我们需要相乘的每个矩阵的大小。如果我们用列表的列表来表示矩阵,那么外层列表的每个元素代表一行。也就是说,一个 n × m 矩阵在这样写出来时有 n 行和 m 列。
❸ 为了使矩阵乘法有意义,两个矩阵的内维必须一致。将每个矩阵视为表示一个线性函数,第一个索引(行数)告诉我们每个输出有多大,而第二个索引(列数)告诉我们每个输入有多大。因此,我们需要第一个函数的输出(右侧的那个)与第二个函数的输入大小相同。这一行检查了这个条件。
❹ 要实际计算矩阵 A 和 B 的乘积,我们需要计算乘积中的每个元素并将它们打包成一个列表的列表。
❺ 我们可以通过对 B 的输出传递给 A 作为输入的地方进行求和来找到每个元素,这与我们在图 C.6 中表示矩阵与向量的乘积的方式类似。
❻ 为了进行比较,我们可以导入 NumPy,它为我们提供了一个使用现代处理器指令加速计算的矩阵乘法实现。
❼ 初始化两个 NumPy 数组作为测试用例。我们将在整本书中看到这两个特定矩阵的更多内容。
❽ 在 Python 3.5 及以后的版本中,NumPy 中的矩阵乘法由@运算符表示。
❾ “%timeit”魔法命令告诉 IPython 运行一小段 Python 代码多次,并报告平均所需时间。
C.2.1 具有内积的派对
在寻找派对时,我们还需要考虑最后一件事。之前,我们说过我们忽略了是否存在一条道路可以让我们朝所需的方向前进的问题,但在一个不熟悉的城市中漫步时,这真是一个糟糕的想法。为了找到我们的路,我们需要一种方法来评估我们应该沿着给定道路走多远才能到达目的地。幸运的是,线性代数为我们提供了这样一个工具:内积(图 C.7)。内积是一种将一个向量
投影到另一个向量
上的方法,告诉我们向量
在
上投下的“影子”有多大。

图 C.7 如何通过内积找到派对
我们可以通过将两个向量的相应元素相乘并求和来计算两个向量的内积。请注意,这个乘法和求和的配方与我们在矩阵乘法中做的是一样的!将一个只有一行的矩阵与一个只有一列的矩阵相乘正好是我们需要的。因此,为了找到向量
在
上的投影,我们需要通过取其转置将其转换为行向量,表示为
^(T)。
示例
的转置。
注意:在第三章中,我们将看到我们还需要取每个元素的复共轭,但现在我们先不考虑这一点。
尤其是矩阵乘积
^(T)(
的转置)与
相乘,给我们一个包含我们想要的内积的 1 × 1 矩阵。假设我们需要向南走两个街区,向东走三个街区,但我们只能沿着指向更南东南的道路走。由于我们仍然需要向南走,这条道路帮助我们到达目的地。但我们在这条道路停止帮助我们之前应该走多远?
列表 C.4 使用 NumPy 计算向量点积
>>> import numpy as np
>>> v = np.array([[-2], [-3]]) ❶
>>> south_east = np.array([[1], [-1]]) ❷
>>> np.linalg.norm(south_east) ❸
1.4142135623730951 ❹
>>> w = np.array([[1], [-1]]) / np.sqrt(2) ❺
>>> np.linalg.norm(w) ❻
0.9999999999999999
>>> v.transpose() ❼
array([[-2, -3]])
>>> v.transpose() @ w ❽
array([[ 0.70710678]]) ❾
❶ 在这种情况下,
是描述我们需要去的地方的向量:向北走两个街区,向东走三个街区。
❷ 如果可用的道路指向东南,那么它每向东走一个街区就向南走一个街区。
❸ 我们可以使用毕达哥拉斯定理通过取每个元素的绝对值之和然后开平方来找到向量的长度。在 NumPy 中,这是通过 np.linalg.norm 函数完成的,因为向量的长度有时也被称为其范数。
❹ 当我们将
定义为东南方向时,我们需要除以√2。
❺ 因此,[[1], [–1]]的长度是
= √2 ≈ 1.4142。
❻ 检查后,我们发现
的长度现在大约为 1。
❼ 转置将
= [[–2], [–3]]转换为“行”[[–2, –3]]。
❽ 我们可以像之前用矩阵与向量相乘一样,用
的转置乘以
。
❾ 在这条道路停止帮助我们到达派对之前,我们需要沿着这条道路走 1 / √2 ≈ 0.707 个街区。
练习 C.5:向量归一化
给定一个向量 [[2], [3]],找到一个指向相同方向但长度为 1 的向量。
提示:你可以通过使用内积或np.linalg.norm函数来完成这个操作。
最后,我们终于到达了派对(只是稍微晚了点)并准备好尝试那个新门铃!
平方根和长度
一个数 x 的平方根是一个数 y = √2,当我们平方 y 时,我们得到 x,即 y² = x。我们在整本书中大量使用平方根,因为它们对于找到向量的长度是必不可少的。例如,在计算机图形学中,快速找到向量的长度对于使游戏工作至关重要(有关平方根在游戏中的应用的一些有趣历史,请参阅en.wikipedia.org/wiki/Fast_inverse_square_root)。
无论这些向量描述的是我们去派对的路线还是这些向量描述的量子比特代表的信息,我们都用平方根来推理它们的长度。
附录 D 通过示例探索 Deutsch–Jozsa 算法
在这个附录中,我们对 Deutsch–Jozsa 算法进行深入研究,以展示它是如何工作的,以及我们如何使用在第八章中开发的技能和工具来检验我们的理解。我们在第七章中实现了 Deutsch–Jozsa 算法,并在某些步骤中大量使用 QuTiP 来检查我们的数学运算。
D.1 使用我们的技能尝试新事物
在第二章和第五章中,我们学习使用 NumPy 和 QuTiP 来模拟当我们向量子计算机发送指令时,量子比特的状态是如何转换的。我们有效地使用这些包来为我们做数学运算,以找出我们的量子状态发生了什么。这就像图 D.1 中的“让计算机做数学”方法。

图 D.1 三种学习量子程序或算法工作原理的不同方法
当我们在 Q# 中编写更大的算法时,我们可以同时使用“让计算机做数学”和一点“按所有按钮”的方法来帮助我们预测特定操作将做什么。图 D.1 中显示的三种方法结合使用,是学习量子编程时的强大问题解决工具。如果我们使用一种方法遇到困难,我们总是可以尝试另一种方法,看看是否有所帮助。
让我们尝试将这种组合方法应用于第八章中的 Deutsch–Jozsa 算法。以下列表显示了算法的四个步骤。
列表 D.1 Deutsch–Jozsa 算法的四个步骤
H(control); ❶
X(target); ❶
H(target); ❶
oracle(control, target); ❷
H(target); ❸
X(target); ❸
set result = MResetX(control); ❹
❶ 准备输入状态 |+−〉。
❷ 应用或 acles。
❸ 取消目标量子比特上的准备。输入状态 |+−〉。
❹ 最后,在 X-基下进行测量。
理解 Deutsch–Jozsa 算法工作的关键是理解我们调用或 acles,oracle(control, target) 的步骤。不过,在我们能够到达那里之前,我们需要理解步骤 1,即我们为 oracle 准备输入。
D.2 步骤 1:为 Deutsch–Jozsa 准备输入状态
让我们用 Python 尝试理解当我们准备我们的 |+−〉 状态时发生了什么。我们在 Q# 中准备输入状态的运算如下:
H(control);
X(target);
H(target);
在这里应用的每个操作都是一个单量子比特门,因此我们可以独立考虑每个量子比特会发生什么。让我们看看哈达玛德操作后控制量子比特会发生什么。我们使用 QuTiP 来模拟控制量子比特的状态准备:
>>> import qutip as qt
>>> from qutip.qip.operations import hadamard_transform
>>> H = hadamard_transform() ❶
>>> H
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper,
➥ isherm = True
Qobj data =
[[ 0.70710678 0.70710678] ❷
[ 0.70710678 -0.70710678]]
>>> control_state = H * qt.basis(2, 0) ❸
>>> control_state
Quantum object: dims = [[2], [1]], shape = (2, 1),
➥ type = ket ❹
Qobj data =
[[0.70710678]
[0.70710678]]
❶ 虽然 Q# 中的 H 是一条指令,但 QuTiP 中的 hadamard_transform 给我们一个单位矩阵,我们可以用它来模拟 H 指令如何转换状态。
❷ 1 / √2 ≈ 0.707,所以这个输出告诉我们
。
❸ 在 QuTiP 中,我们可以通过调用 basis(2, 0) 获取 |0〉 状态的向量。2 告诉 QuTiP 我们想要一个量子比特(|0〉的必要维度),而 0 表示我们想要状态具有 |0〉的值。由于 |+〉 = H|0〉,这会将 control_state 设置为 |+〉。
❹ 使用 1 / √2 ≈ 0.707,我们将这解读为告诉我们 |+〉 = (|0〉 + |1〉) / √2。
这很简单:控制量子比特现在处于 |+〉 状态。现在让我们看看如何准备下一个片段中的目标量子比特。
>>> target_state = H * (qt.sigmax() * qt.basis(2, 0)) ❶
>>> target_state
Quantum object: dims = [[2], [1]], shape = (2, 1), type = ket ❷
Qobj data =
[[ 0.70710678]
[-0.70710678]]
❶ 与之前相同的 H 操作,但这次是在 X|0〉 = |1〉 上进行。
❷ QuTiP 告诉我们 |−〉 = (|0〉 − |1〉) / √2:与 |+〉 相同,但 |1〉 的符号被翻转了。
现在我们已经看到了如何准备每个量子比特,让我们让 QuTiP 帮助我们编写输入 寄存器 的状态。
>>> register_state = qt.tensor(control_state, target_state) ❶
>>> register_state
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket ❷
Qobj data =
[[ 0.5]
[-0.5]
[ 0.5]
[-0.5]]
❶ 正如第四章中所述,我们使用张量函数将不同量子比特的状态组合起来,以获得整个量子比特寄存器的状态。
❷ QuTiP 告诉我们 |+〉 ⊗ |−〉 = |+−〉 = (|00〉 − |01〉 + |10〉 − |11〉) / 2。也就是说,我们在所有四个可能的计算基态上有一个相等的叠加,当目标量子比特处于 |1〉 状态时,计算基态前面有一个负号。
注意:正如我们在第四章中看到的,在编写多量子比特系统的状态时,张量积可能会变得有点冗长。因此,我们经常通过将它们的标签连接在单个基矢量内部来编写多量子比特状态,例如 |01〉。同样,|+−〉 与 |+〉 ⊗ |−〉 相同。
D.3 步骤 2:应用预言机
准备好输入后,让我们回到 Deutsch-Jozsa 算法的核心,在那里我们调用我们的预言机:
oracle(control, target);
正如我们可以通过编写 control 量子比特的状态并应用幺正算子 H 来理解操作 H(control) 一样,我们可以通过分析其对传递给它的状态的作用来理解预言机 U[f] 做了什么。
回想第八章中的游戏设置,其中 Nimue 和 Merlin 正在玩国王制造者游戏。我们的量子预言机作用于两个量子比特,这引发了如何解释每个量子比特的问题。在经典情况下,从 f 中输入和输出的经典比特的解释是清晰的:Nimue 提出一个单比特问题并得到一个单比特答案。
要了解每个量子比特为我们做了什么,回想一下,当我们使用可逆的经典函数时,我们还需要两个输入:第一个在不可逆情况下类似于我们提出的问题,第二个输入给我们一个放置答案的地方(见图 D.2 以获得提醒)。

图 D.2 从不可逆的经典函数构造可逆的经典函数和幺正矩阵
我们可以大致以相同的方式思考预言机:第一个量子比特(在之前的片段中为 control)代表我们的问题,而第二个量子比特(target)为 Merlin 应用他的答案提供了一个地方。这种解释在 control 以 |0〉 或 |1〉 状态开始时是有意义的,但如何解释我们向预言机传递处于 |+−〉 状态的量子比特的情况呢?我们的控制量子比特以 |+〉 状态开始,但 f(+) 没有意义。由于 f 是一个经典函数,它的输入必须是 0 或 1——我们不能将 + 传递给经典函数 f。这似乎是一个死胡同,但幸运的是,有一种方法可以找出答案。
量子力学是线性的,这意味着我们可以通过将其分解为其对一组代表性状态的行动来理解量子操作的作用。
提示:正如我们在第二章中看到的,一组可以以这种方式使用的状态被称为 基。
要理解当控制量子比特处于 |+〉 状态时我们的奥秘做了什么,我们可以使用 |+〉 = (|0〉 + |1〉) / √2 的事实,将奥秘的作用分解为对 |0〉 加上 对 |1〉 的作用,然后将两部分相加(确保在最后除以 √2)。这有助于我们避免对“f (+)”的含义感到困惑,我们可以将 U f 的作用简化为我们知道如何计算的情况,如 f(0) 和 f(1)!
计算基状态
在量子编程中,根据量子操作对 |0〉 和 |1〉 的作用来展开量子操作是非常常见的。鉴于这一点非常有用,我们为这两个输入状态使用一个特殊的名称,并将 |0〉 和 |1〉 称为 计算基,以将它们与其他我们可能使用的基,如 |+〉 和 |−〉 区分开来。
使用线性来理解量子操作不仅限于单个量子比特,正如我们在附录的其余部分中看到的。例如,对于两个量子比特,计算基由状态 |00〉, |01〉, |10〉 和 |11〉 组成。
如果我们有更多(比如说,五个)量子比特,我们可以像写字符串一样写出 |1〉 ⊗ |0〉 ⊗ |0〉 ⊗ |1〉 ⊗ |0〉 这样的状态,得到 |10010〉。我们可以将五个量子比特的计算基写为 {|00000〉, |00001〉, |00010〉, ..., |11110〉, |11111〉}。
更普遍地说,如果我们有 n 个量子比特,计算基由所有 n 个经典比特的字符串组成,每个字符串作为基矢的标签。换句话说,多量子比特系统的计算基由所有 |0〉 和 |1〉 的张量积组成:也就是说,所有由经典比特字符串标记的状态。
通过分解奥秘如何工作的这种方法,让我们看看我们在第八章中实现的奥秘的一些例子。
D.3.1 示例 1:“id”奥秘
假设我们被给定一个实现梅林选择亚瑟为王的策略(第 8.2 节)的奥秘。回想一下,代表这种策略的经典单比特函数是 id。从表 D.1 中,我们知道这意味着 U[f] 是通过 CNOT 指令实现的,所以让我们看看它对 register_state 做了什么。
表 D.1 将单比特函数表示为双量子比特奥秘
| 函数名称 | 函数 | 奥秘输出 | Q# 操作 |
|---|---|---|---|
| id | f(x) = x | |x〉|y ⊕ x〉 | CNOT(control, target) |
提示:回想一下,受控-NOT 指令在第一个量子比特处于 |1〉 时翻转其第二个量子比特。
列表 D.2 id 奥秘如何转换其输入状态
>>> cnot = qt.cnot(2, 0, 1) ❶
>>> cnot
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper,
➥ isherm = True
Qobj data = ❷
[[1\. 0\. 0\. 0.]
[0\. 1\. 0\. 0.]
[0\. 0\. 0\. 1.] ❸
[0\. 0\. 1\. 0.]]
>>> register_state = cnot * register_state
>>> register_state ❹
Quantum object: dims = [[2, 2], [1, 1]],
➥ shape = (4, 1), type = ket
Qobj data =
[[ 0.5]
[-0.5]
[-0.5]
[ 0.5]]
❶ 询问 QuTiP 以使用 cnot 函数通过 CNOT 指令进行模拟的矩阵。在这里,2 表示我们想要在双量子比特寄存器上模拟 CNOT,0 表示第 0 个量子比特是我们的控制位,1 表示第 1 个量子比特是我们的目标位。
❷ 记住,单位算符对于量子计算来说,就像真值表对于经典逻辑一样。表中的每一行都告诉我们计算基态会发生什么。
❸ 例如,索引为 2 的行(零索引)可以写成二进制的 10。因此,如果我们的输入是|10〉,这将是我们得到的向量,它告诉我们 CNOT 指令将我们的量子比特保持在|11〉(十进制中的 3,因此第三列有一个 1)。
❹ QuTiP 告诉我们,我们的控制比特和目标比特的寄存器现在处于状态(|00〉 − |01〉 − |10〉 + |11〉)/ 2。
现在我们已经解决了id预言者的作用,让我们看看not预言者对我们的输入状态做了什么。
D.3.2 示例 2:“not”预言者
让我们重复使用not预言者,另一个平衡函数的分析。表示梅林选择莫德雷德的预言者是通过一系列X和CNOT操作实现的,如表 D.2 所示。
表 D.2 一比特函数not作为双比特预言者
| 函数名 | 函数 | 预言者的输出 | Q#操作 |
|---|---|---|---|
| not | f(x) = ¬x | |x〉|y ⊕ ¬x〉 | X(control); CNOT(control, target); X(control); |
让我们跳到 Python,看看如何分解not预言者的操作。
列表 D.3 再次使用 QuTiP,这次使用not预言者
>>> control_state = *H* * qt.basis(2, 0) ❶
>>> target_state = *H* * qt.basis(2, 1)
>>> register_state = qt.tensor(control_state, target_state)
>>> I = qt.qeye(2) ❷
>>> X = qt.sigmax()
>>> oracle = qt.tensor(X, I) * qt.cnot(2, 0, 1) *
... qt.tensor(X, I) ❸
>>> oracle
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper,
➥ isherm = True
Qobj data = ❹
[[0\. 1\. 0\. 0.] ❺
[1\. 0\. 0\. 0.]
[0\. 0\. 1\. 0.] ❻
[0\. 0\. 0\. 1.]]
>>> register_state = oracle * register_state
>>> register_state
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket ❼
Qobj data =
[[-0.5]
[ 0.5]
[ 0.5]
[-0.5]]
❶ 正如之前一样,准备控制比特和目标比特在|+−〉状态
❷ 正如第五章中一样,定义变量 I 和 X 作为单位矩阵(qt.qeye)和表示 X 操作的矩阵的简称是有帮助的。
❸ 这次我们的预言者是“not”预言者,我们按照表 D.2 中的指令序列 X(control); CNOT(control, target); X(control);来实现。
❹ 这次预言者操作的单位算符看起来有些不同:当控制比特是|0〉时,它翻转目标比特。
❺ 例如,第 0 行(二进制中的 00)告诉我们|00〉被转换成|01〉。
❻ 类似地,第二行(二进制中的 10)告诉我们|10〉被转换成|10〉;预言者保持那个输入不变。
❼ 应用预言者后的状态是(–|00〉 + |01〉 + |10〉 − |11〉)/ 2 = (–1)|+−〉,与之前完全相同,除了全局相位为–1。
看这两个例子,我们得到了相同的输出状态,除了符号都反转了。这意味着如果我们将一个状态向量乘以一个−1,它们就会相同。将整个向量乘以一个常数被称为添加全局相位。由于全局相位不能通过测量观察到,所以我们从应用id和not预言者得到了完全相同的信息。我们没有学到关于我们是否应用了id或not的任何东西;如果我们能够比较向量,我们只会知道我们应用了一个平衡预言者。
为了比较,让我们看看在应用表示一个常数函数的预言者之后,寄存器看起来像什么。
D.3.3 示例 3:“zero”预言者
再次,充满感情:让我们用 Python 来分解表示常量函数 zero 的求解器是如何工作的。我们想使用 zero 求解器来展示当我们应用表示常量函数的求解器时会发生什么不同。这个求解器特别容易应用,因为它根本不包含任何指令。你可以在表 D.3 中看到所有表示方法。
表 D.3 一比特函数 zero 作为双量子比特求解器
| 函数名称 | 函数 | 求解器输出 | Q# 操作 |
|---|---|---|---|
| zero | f(x) = 0 | |x〉|y ⊕ 0〉 = |x〉|y〉 | (空) |
在列表 D.4 中,我们可以看到在控制量子比特和目标量子比特上都不做任何操作可以通过在整个寄存器上不做任何操作来模拟。因此,我们创建的 oracle 对于 zero 求解器来说是两个量子比特的单位矩阵 𝟙 ⊗ 𝟙。
列表 D.4 计算 zero 求解器转换
>>> control_state = H * qt.basis(2, 0)
>>> target_state = H * qt.basis(2, 1)
>>> register_state = qt.tensor(control_state, target_state)
>>> X = qt.sigmax()
>>> oracle = qt.tensor(I, I)
>>> oracle
Quantum object: dims = [[2, 2], [2, 2]], shape = (4, 4), type = oper,
➥ isherm = True
Qobj data =
[[1\. 0\. 0\. 0.] ❶
[0\. 1\. 0\. 0.]
[0\. 0\. 1\. 0.]
[0\. 0\. 0\. 1.]]
>>> register_state = oracle * register_state
>>> register_state ❷
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1), type = ket
Qobj data =
[[ 0.5]
[-0.5]
[ 0.5]
[-0.5]]
❶ 在控制量子比特和目标量子比特上都不做任何操作可以通过在整个寄存器上不做任何操作来模拟。
❷ 输出状态与身份求解器的全局相位不同。没有标量我们可以乘以全局相位将身份输出转换为零输出。
在这里,我们看到我们与之前的列表的第一个不同点:负号出现在状态向量中的位置不同。之前,我们使用了 id 求解器,得到输出状态是 (|00〉 − |01〉 − |10〉 + |11〉) / 2。使用 zero 求解器,输出状态是 (|00〉 − |01〉 + |10〉 − |11〉) / 2。没有数字我们可以乘以整个向量来将其转换为 [[0.5],[-0.5],[-0.5],[0.5]] 或 [[-0.5],[0.5],[0.5], [-0.5]]。为了了解这种差异如何使我们能够确定我们有一个平衡的或常量求解器,让我们继续到 Deutsch-Jozsa 算法的下一步。
练习 D.1:尝试使用“one”求解器
看看你是否可以使用我们之前使用的 Python 技巧来找出当应用 one 求解器时 target 和 control 量子比特的状态是如何变化的。
练习解答
本书所有练习的解答都可以在配套代码仓库中找到:github.com/crazy4pi314/learn-qc-with-python-and-qsharp。只需进入我们所在的附录文件夹,并打开提及练习解答的 Jupyter 笔记本。
D.4 步骤 3 和 4:撤销目标量子比特的准备,并测量
在这一点上,如果我们撤销我们用来准备 |+−〉 的步骤,使一切回到计算基(|00〉 ... |11〉),那么理解输出就更容易了。为了回顾,表 D.4 包含了所有四个求解器的状态向量(其中三个是我们之前解决的)。
表 D.4 应用求解器后的寄存器状态
| 函数名称 | 应用求解器后的寄存器状态 |
|---|---|
zero |
[[ 0.5], [–0.5], [ 0.5], [–0.5]] |
one |
[[–0.5], [ 0.5], [–0.5], [ 0.5]] |
id |
[[ 0.5], [–0.5], [–0.5], [ 0.5]] |
not |
[[–0.5], [ 0.5], [ 0.5], [–0.5]] |
现在我们想要撤销对目标量子比特的准备步骤。
为什么我们要“未准备”目标量子比特?
在第七章中,我们看到在将量子比特返回目标机器之前,我们需要将它们重置到|0〉状态。在这个时候,我们的目标量子比特总是处于|−〉状态,无论使用哪个预言机。这意味着在应用预言机之后,我们确切地知道如何将其放回|0〉状态。正如第七章所述,这有助于我们避免额外的测量,这在某些量子设备上可能很昂贵。
注意,当我们测量控制量子比特时,我们可以安全地将目标量子比特返回到|−〉状态而不影响结果,因为预言机调用是 Deutsch–Jozsa 算法中唯一的双量子比特操作。正如我们在第五章中看到的,对一个量子比特执行单量子比特操作不会影响另一个量子比特的结果;否则,我们就能以比光速更快的速度传递信息!
让我们通过撤销表示id函数的寄存器的准备步骤来尝试一下。
列表 D.5 计算基下的id预言机输出
>>> I = qt.qeye(2) ❶
>>> register_state_id = qt.cnot(2,0,1) * ❷
... (qt.tensor(H * qt.basis(2, 0), *H* * (qt.sigmax() * qt.basis(2, 0))))
...
>>> register_state_id =
➥ qt.tensor(I, H) * register_state_id ❸
>>> register_state_id
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1),
➥ type = ket ❹
Qobj data =
[[ 0\. ]
[ 0.70710678]
[ 0\. ]
[-0.70710678]]
>>> register_state_id =
➥ qt.tensor(I, qt.sigmax()) *
➥ register_state_id ❺
>>> register_state_id
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1),
➥ type = ket ❻
Qobj data =
[[ 0.70710678]
[ 0\. ]
[-0.70710678]
[ 0\. ]]
>>> qt.tensor(H * qt.basis(2, 1), qt.basis(2, 0))
➥ == register_state_id ❼
True
>>> register_state_id = qt.tensor(H, I) *
➥ register_state_id ❽
>>> register_state_id
Quantum object: dims = [[2, 2], [1, 1]], shape = (4, 1),
➥ type = ket ❾
Qobj data =
[[0.]
[0.]
[1.]
[0.]]
❶ 定义一个简写来表示单位矩阵𝟙是有帮助的,我们用它来表示当我们不对量子比特应用任何指令时它会发生什么。
❷ 在预言机应用后立即重现表示 id 函数的寄存器
❸ 由于我们正在转换一个双量子比特状态,我们需要说明每个量子比特发生了什么以得到我们的矩阵。我们再次使用张量函数来做这件事。
❹ 输出更容易阅读:寄存器处于状态(|01〉 − |11〉)/ √2。
❺ 在我们的 Q#程序中,我们使用了 X 指令来在释放之前将目标量子比特返回到|0〉,这是通过 QuTiP 函数 sigmax()模拟的。
❻ 由于 X 指令翻转其参数,应用X矩阵给我们状态(|00〉 − |10〉)/ √2。
❼ 我们可以使用 QuTiP 来确认另一种表示(|00〉 − |10〉)/ √2 的方法是(H|1〉)⊗|0〉 = |–0〉。
❽ 我们可以通过应用 H 然后测量在Z-基下来模拟 MResetX 操作。
❾ 在测量之前,我们的寄存器状态在第二行有一个 1(二进制中的 10),因此我们的寄存器状态是|10〉,我们将肯定测量到一个 One。
提示:我们使用 Q#标准库中的MResetX操作来在X-基下测量。当其参数在|+〉时,X-基测量返回Zero,当其参数在|−〉时返回One结果。因此,我们可以通过应用H然后测量在z基下来模拟MResetX操作。
查看列表 D.5 中的最终向量,我们可以看到它代表状态 |10〉。如果我们从该状态测量控制量子比特,我们将会 100% 的时间得到经典比特 One。在第八章中我们编写的 Algorithm.qs 文件中,我们向用户返回,如果我们在控制量子比特上测量到 One,则或然子是平衡的,因此我们正确地得出结论,id 是一个平衡的或然子!每次我们都会在控制比特上测量到 One 的这一事实真的很酷。
注意:尽管一些量子算法是随机的,例如第二章中的 QRNG 示例或第七章中 Morgana 和 Lancelot 的游戏,但它们并不需要是随机的。事实上,Deutsch-Jozsa 算法是 确定性的:我们每次运行它都会得到相同的答案。
从这些例子中,我们有一个重要的观察(见表 D.5):将或然子应用于控制量子比特和目标量子比特可以影响控制量子比特的状态。
表 D.5 应用各种或然子后寄存器状态的向量表示
| 函数名 | 测量前的寄存器状态 | 沿 z 测量控制量子比特的结果 |
|---|---|---|
zero |
[[ 1], [ 0], [ 0], [ 0]] | Zero |
one |
[[–1], [ 0], [ 0], [ 0]] | Zero |
id |
[[ 0], [ 0], [ 1], [ 0]] | One |
not |
[[ 0], [ 0], [–1], [ 0]] | One |


浙公网安备 33010602011771号