量子计算探秘:从零开始的量子编程与算法之旅 · 第五篇

量子算法导论------超越经典的第一个证明:Deutsch-Jozsa算法

它不解决实际问题,却首次向世界宣告:量子计算机真的可以比经典计算机更快。

在前四篇文章中,我们搭建了量子编程环境,学会了操作量子门,理解了测量和退相干的残酷现实。现在,终于到了见证奇迹的时刻------我们要亲手运行第一个真正意义上的量子算法

1992年,David Deutsch和Richard Jozsa提出了一个看似"无聊"的问题:如何判断一个函数是常数函数还是平衡函数?这个问题本身毫无实用价值,但它给出了一个惊人的结论:量子计算机只需要一次函数调用,而经典计算机在最坏情况下需要2ⁿ⁻¹+1次。这是人类历史上第一个严格证明量子计算具有指数级加速能力的算法。

2025年11月,Richard Jozsa教授本人做客武汉大学珞珈讲坛,再次讲述了这段历史。他在演讲中指出,Deutsch-Jozsa算法的意义不在于解决实际问题,而在于它首次揭示了量子计算潜力的存在。今天,我们就沿着这位先驱的足迹,亲手实现这个具有里程碑意义的算法。


1. 问题描述:一个刻意设计的游戏

1.1 神秘的黑盒函数

想象你面前有一个黑盒子,它接受一个n位的二进制字符串(比如"001"、"110"),输出一个比特(0或1):

f: {0,1}\^n \\rightarrow {0,1}

现在,有人向你保证这个函数一定属于以下两种类型之一:

  • 常数函数(Constant):对所有输入,输出都相同。要么全是0,要么全是1。
  • 平衡函数(Balanced):对一半的输入输出0,另一半输出1。注意,当n≥1时,2ⁿ一定是偶数,所以"一半"是严格定义的。

你的任务:只通过查询这个黑盒子,判断它到底是常数函数还是平衡函数

💡 举例:当n=2时,输入有4种可能:00、01、10、11。

  • 常数函数示例:f(00)=0, f(01)=0, f(10)=0, f(11)=0
  • 平衡函数示例:f(00)=0, f(01)=1, f(10)=0, f(11)=1

1.2 为什么这个问题有意义?

你可能会问:这么刻意的问题,有什么用?确实,Deutsch-Jozsa算法本身没有实用价值------现实世界的问题很少会保证函数要么常数要么平衡。但它的理论意义无与伦比:

  1. 第一个证明量子优势 :它首次严格证明了在某些问题上,量子计算机可以指数级快于经典计算机。
  2. 引入了关键概念:量子并行性、相位反冲、量子干涉------这些后来在Shor算法、Grover算法中扮演核心角色的思想,都在这个简单算法中首次亮相。
  3. 测试平台价值:由于实现相对简单,Deutsch-Jozsa算法常被用来验证量子硬件的基本功能和量子编程框架的正确性。

2. 经典解法:运气好时很快,运气差时很慢

2.1 最优情况:两次就能确定

假设我们运气特别好。第一次查询某个输入,得到0;第二次查询另一个输入,得到1。那么立刻就能判断:这是平衡函数!因为常数函数不可能给出两种不同的输出。

2.2 最坏情况:需要查一半以上

但如果运气不好呢?假设我们不断查询,但每次都得到相同的输出(比如全是0):

  • 查了1次:都是0 → 还可能是常数函数(全0)或平衡函数(恰好这1次是0)
  • 查了2次:都是0 → 仍不能确定
  • ...
  • 查了2ⁿ⁻¹次:都是0 → 仍然不能确定!因为平衡函数恰好有2ⁿ⁻¹个0,我们可能恰好查到了所有输出0的输入

直到查了2ⁿ⁻¹+1次,如果还全是0,这时才能肯定:这一定是常数函数,因为平衡函数最多只有2ⁿ⁻¹个0,不可能在第2ⁿ⁻¹+1次还输出0。

所以,最坏情况下,经典算法需要2ⁿ⁻¹+1次查询。当n=10时,这已经是513次;当n=100时,这个数字比宇宙中的原子还多。

2.3 概率算法呢?

如果允许一定概率出错,我们可以提前终止。例如,连续查到k个0后,函数为常数的概率是 (1 - 1/2^{k-1})。当k=10时,这个概率已经超过99.9%。但如果要求100%确定,就必须查2ⁿ⁻¹+1次。


3. 量子解法:一次查询,确定无疑

Deutsch-Jozsa算法的核心思想是:将函数的所有输入同时查询(量子并行性),然后通过干涉让错误结果相消,让正确结果相长

3.1 算法电路图

整个算法的量子电路如图所示:

复制代码
    ┌───┐          ┌───┐┌─┐
q0: ┤ H ├──────■───┤ H ├┤M├
    ├───┤      │   ├───┤├─┤
q1: ┤ H ├──────┼───┤ H ├┤M├
    ├───┤      │   ├───┤├─┤
q2: ┤ H ├──────┼───┤ H ├┤M├
    └───┘┌─────┴────┐└───┘└╥┘
q3: ─────┤ U_f(x)   ├───────╫─
         └──────────┘       ║ 
c: 2/═══════════════════════╩═
                            0 

电路包含两个寄存器:

  • 输入寄存器:n个量子比特,初始化为|0⟩
  • 辅助寄存器:1个量子比特,初始化为|1⟩(注意不是|0⟩!)

3.2 逐步推导

让我们一步步推导算法的状态变化。

步骤0:初始化

\|\\psi_0\\rangle = \|0\\rangle\^{\\otimes n} \|1\\rangle

步骤1:对所有量子比特施加H门

辅助寄存器的|1⟩经过H门变成(|0⟩ - |1⟩)/√2(带负号!)。输入寄存器的n个|0⟩经过H门变成所有计算基态的等幅叠加:

\|\\psi_1\\rangle = \\frac{1}{\\sqrt{2^n}}\\sum_{x=0}^{2\^n-1} \|x\\rangle \\otimes \\frac{\|0\\rangle - \|1\\rangle}{\\sqrt{2}}

步骤2:应用Oracle U_f

Oracle的作用是:(U_f |x\rangle|y\rangle = |x\rangle|y \oplus f(x)\rangle)。当辅助寄存器处于(|0⟩ - |1⟩)/√2时,会发生一个神奇的现象------相位反冲

\\begin{aligned} U_f \\left( \|x\\rangle \\frac{\|0\\rangle - \|1\\rangle}{\\sqrt{2}} \\right) \&= \|x\\rangle \\frac{\|0 \\oplus f(x)\\rangle - \|1 \\oplus f(x)\\rangle}{\\sqrt{2}} \\ \&= \\begin{cases} \|x\\rangle \\frac{\|0\\rangle - \|1\\rangle}{\\sqrt{2}}, \& \\text{if } f(x)=0 \\ \|x\\rangle \\frac{\|1\\rangle - \|0\\rangle}{\\sqrt{2}}, \& \\text{if } f(x)=1 \\end{cases} \\ \&= (-1)\^{f(x)} \|x\\rangle \\frac{\|0\\rangle - \|1\\rangle}{\\sqrt{2}} \\end{aligned}

瞧!函数值f(x)从辅助寄存器"反冲"到了输入寄存器的相位上。现在整个状态变为:

\|\\psi_2\\rangle = \\frac{1}{\\sqrt{2^n}}\\sum_{x=0}^{2\^n-1} (-1)\^{f(x)} \|x\\rangle \\otimes \\frac{\|0\\rangle - \|1\\rangle}{\\sqrt{2}}

步骤3:对输入寄存器再次施加H门

此时辅助寄存器已经完成任务,可以忽略。对输入寄存器施加H门后:

\|\\psi_3\\rangle = \\frac{1}{2^n}\\sum_{x=0}^{2^n-1}\\sum_{y=0}^{2\^n-1} (-1)\^{f(x) + x\\cdot y} \|y\\rangle

其中 (x\cdot y = x_0y_0 \oplus x_1y_1 \oplus \cdots \oplus x_{n-1}y_{n-1}) 是按位点乘(模2加法)。

步骤4:测量输入寄存器

测量得到 (|0\rangle^{\otimes n}) 的概率是:

P(0\\cdots 0) = \\left\| \\frac{1}{2^n}\\sum_{x=0}^{2\^n-1} (-1)\^{f(x)} \\right\|\^2

现在关键来了:

  • 如果 (f) 是常数函数,那么所有 ((-1)^{f(x)}) 都相等(要么全是+1,要么全是-1)。求和结果为 (\pm 2^n),概率为1。
  • 如果 (f) 是平衡函数,恰好一半 ((-1)^{f(x)} = +1),另一半 (= -1),求和结果为0,概率为0。

因此:

  • 测量得到全0 → 常数函数
  • 测量得到任何非0结果 → 平衡函数

一次查询,确定无疑!

3.3 算法的核心:相位反冲

相位反冲(Phase Kickback)是Deutsch-Jozsa算法的核心技巧,也是许多量子算法的基石。它的美妙之处在于:

  • 我们从未直接测量辅助寄存器,却通过它将f(x)的信息"刻"在了输入寄存器的相位上
  • 相位本身不可直接观测,但通过第二次H门和测量,这些相位信息转化为可观测的振幅信息
  • 干涉效应让平衡函数的振幅恰好相消,常数函数的振幅恰好相长

4. 构造Oracle:把函数变成量子电路

在量子算法中,函数 (f) 不能直接调用,必须实现为一个量子Oracle (U_f)。Oracle是量子电路中的一个"黑盒子"模块,它对计算基态的作用是确定性的,但对叠加态的作用则体现了量子并行性。

4.1 常数函数的Oracle

常数函数非常简单:

  • 如果 (f(x) \equiv 0):Oracle就是恒等变换,什么门都不用加
  • 如果 (f(x) \equiv 1):需要对辅助比特施加X门(相当于告诉辅助寄存器"输出永远是1")

注意:即使加了X门,在相位反冲机制下,也会产生正确的相位因子。

4.2 平衡函数的Oracle

构造平衡函数Oracle的经典方法是:用输入寄存器的每个比特作为控制位,对辅助寄存器施加CNOT门

为什么这样是平衡的?以3量子比特为例:

python 复制代码
# 平衡Oracle的经典构造
from qiskit import QuantumCircuit

def create_balanced_oracle(n):
    """创建n比特的平衡函数Oracle(标准构造)"""
    oracle = QuantumCircuit(n+1, name="U_f")
    
    # 用每个输入比特控制辅助比特
    for i in range(n):
        oracle.cx(i, n)  # 第n位是辅助比特
    
    return oracle

这个电路的效果是:

  • 当输入 (x) 中有奇数个1时,辅助比特被翻转奇数次,输出1
  • 当输入 (x) 中有偶数个1时,辅助比特被翻转偶数次,输出0

这正好是奇偶校验函数------一半输入有奇数个1,一半有偶数个1,完美平衡。

4.3 更灵活的平衡Oracle

我们还可以通过添加X门来"掩码"控制位,实现不同的平衡函数:

python 复制代码
def create_masked_balanced_oracle(n, mask):
    """用二进制掩码mask指定哪些输入比特参与控制"""
    oracle = QuantumCircuit(n+1, name="U_f")
    
    # 根据掩码添加X门(将控制位从0反转为1)
    for i in range(n):
        if mask[i] == '1':
            oracle.x(i)
    
    # 添加CNOT门
    for i in range(n):
        oracle.cx(i, n)
    
    # 恢复X门
    for i in range(n):
        if mask[i] == '1':
            oracle.x(i)
    
    return oracle

这个电路的效果是:对于输入 (x),当 ((x \oplus mask)) 的奇偶性为奇数时输出1。改变mask就可以得到不同的平衡函数。


5. Qiskit实战:实现Deutsch-Jozsa算法

现在,让我们用Qiskit完整实现Deutsch-Jozsa算法,分别测试常数函数和平衡函数。

5.1 导入所需模块

python 复制代码
import numpy as np
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt

# 设置随机种子,保证可重复性
np.random.seed(42)

5.2 常数函数测试

python 复制代码
def deutsch_jozsa_constant(n, constant_value=0):
    """
    测试常数函数的Deutsch-Jozsa算法
    constant_value: 0表示全0常数,1表示全1常数
    """
    # 创建电路:n个输入比特 + 1个辅助比特 + n个经典比特用于测量
    qc = QuantumCircuit(n+1, n)
    
    # 步骤0:初始化辅助比特为|1⟩
    qc.x(n)  # 将辅助比特从|0⟩变为|1⟩
    
    # 步骤1:对所有比特施加H门
    for i in range(n+1):
        qc.h(i)
    
    # 步骤2:应用常数函数Oracle
    if constant_value == 1:
        # 如果常数函数输出全1,需要翻转辅助比特
        # 但注意:相位反冲机制下,X门在H门之后会正确产生相位
        qc.x(n)
    
    # 步骤3:对输入寄存器再次施加H门
    for i in range(n):
        qc.h(i)
    
    # 步骤4:测量输入寄存器
    qc.measure(range(n), range(n))
    
    return qc

# 测试n=3的常数函数(全0)
n = 3
qc_const = deutsch_jozsa_constant(n, constant_value=0)
print("=== 常数函数(全0)电路 ===")
print(qc_const.draw())

# 运行模拟
sim = AerSimulator()
job = sim.run(qc_const, shots=1000)
counts = job.result().get_counts()
print("\n测量结果:", counts)

预期输出:应该几乎全是 000(理论概率100%)。

5.3 平衡函数测试

python 复制代码
def deutsch_jozsa_balanced(n):
    """
    测试平衡函数的Deutsch-Jozsa算法
    使用标准奇偶校验Oracle
    """
    qc = QuantumCircuit(n+1, n)
    
    # 步骤0:初始化辅助比特为|1⟩
    qc.x(n)
    
    # 步骤1:对所有比特施加H门
    for i in range(n+1):
        qc.h(i)
    
    # 步骤2:应用平衡函数Oracle(奇偶校验)
    for i in range(n):
        qc.cx(i, n)
    
    # 步骤3:对输入寄存器再次施加H门
    for i in range(n):
        qc.h(i)
    
    # 步骤4:测量输入寄存器
    qc.measure(range(n), range(n))
    
    return qc

# 测试n=3的平衡函数
qc_bal = deutsch_jozsa_balanced(n)
print("\n=== 平衡函数电路 ===")
print(qc_bal.draw())

# 运行模拟
job = sim.run(qc_bal, shots=1000)
counts = job.result().get_counts()
print("\n测量结果:", counts)

预期输出:应该几乎没有 000,而是各种非零结果(理论上概率100%得到非零)。

5.4 可视化对比

python 复制代码
# 绘制结果对比
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

# 常数函数结果
job_const = sim.run(qc_const, shots=1000).result()
counts_const = job_const.get_counts()
plot_histogram(counts_const, ax=ax1, title='常数函数结果')

# 平衡函数结果
job_bal = sim.run(qc_bal, shots=1000).result()
counts_bal = job_bal.get_counts()
plot_histogram(counts_bal, ax=ax2, title='平衡函数结果')

plt.tight_layout()
plt.show()

5.5 封装成通用函数

为了方便测试不同n和不同类型的函数,我们可以封装一个通用函数:

python 复制代码
def run_deutsch_jozsa(n, function_type='constant', shots=1000):
    """
    运行Deutsch-Jozsa算法
    
    参数:
        n: 输入比特数
        function_type: 'constant0', 'constant1', 或 'balanced'
        shots: 运行次数
    """
    qc = QuantumCircuit(n+1, n)
    
    # 初始化辅助比特为|1⟩
    qc.x(n)
    
    # 第一层H门
    qc.h(range(n+1))
    
    # Oracle
    if function_type == 'constant0':
        # 什么都不做
        pass
    elif function_type == 'constant1':
        qc.x(n)
    elif function_type == 'balanced':
        # 标准奇偶校验Oracle
        for i in range(n):
            qc.cx(i, n)
    else:
        raise ValueError("function_type必须是 'constant0', 'constant1', 或 'balanced'")
    
    # 第二层H门(只在输入寄存器上)
    qc.h(range(n))
    
    # 测量
    qc.measure(range(n), range(n))
    
    # 运行
    sim = AerSimulator()
    job = sim.run(qc, shots=shots)
    result = job.result()
    counts = result.get_counts()
    
    # 判断结果
    is_constant = ('0'*n in counts and len(counts) == 1)
    
    return counts, is_constant

# 测试不同情况
for ftype in ['constant0', 'constant1', 'balanced']:
    counts, is_const = run_deutsch_jozsa(4, ftype)
    print(f"{ftype}: 测量结果 {counts}, 判断为常数? {is_const}")

5.6 验证相位反冲机制

为了更直观地理解相位反冲,我们可以直接查看态向量:

python 复制代码
from qiskit_aer import AerSimulator

def inspect_phase_kickback(n):
    """查看相位反冲后的态向量"""
    qc = QuantumCircuit(n+1)
    
    # 初始化辅助比特为|1⟩
    qc.x(n)
    
    # 第一层H门
    qc.h(range(n+1))
    
    # 应用平衡Oracle(奇偶校验)
    for i in range(n):
        qc.cx(i, n)
    
    # 不进行第二层H门,直接保存态向量
    qc.save_statevector()
    
    sim = AerSimulator(method='statevector')
    result = sim.run(qc).result()
    statevector = result.get_statevector()
    
    # 提取相位信息
    print(f"态向量维度: {len(statevector)}")
    print("前8个分量的相位:")
    for i in range(min(8, len(statevector))):
        amp = statevector[i]
        phase = np.angle(amp) / np.pi  # 以π为单位
        print(f"|{i:0{n}b}⟩: {amp:.3f}, 相位 = {phase:.2f}π")
    
    return statevector

# 查看n=3时的相位
state = inspect_phase_kickback(3)

你会看到,有些分量的相位是0,有些是π(即乘了-1),这正是f(x)的信息。


6. 为什么这很重要?算法的深层意义

6.1 指数加速的首次证明

Deutsch-Jozsa算法的伟大之处不在于它的实用性,而在于它首次严格证明了量子计算机可以指数级快于经典计算机。经典算法需要O(2ⁿ)次查询,量子算法只需要O(1)次------这个对比在1992年是震撼性的。

6.2 三个核心量子特性的完美展示

这个简单算法几乎囊括了量子计算的所有核心思想:

量子特性 在算法中的作用
叠加 同时查询所有可能的输入
相位反冲 将函数值编码为量子相位
干涉 让错误结果相消,正确结果相长

后来的Shor算法、Grover算法,本质上都是这些思想的更复杂应用。

6.3 没有纠缠也能加速?

有趣的是,研究表明Deutsch-Jozsa算法即使在没有纠缠的情况下也能实现加速。这提示我们:量子加速的根源可能比我们想象的更深,不完全是纠缠的功劳。

6.4 理论与实践的距离

当然,Deutsch-Jozsa算法也有明显的局限:

  • 问题本身是刻意设计的,没有实用价值
  • 要求函数要么常数要么平衡,现实中很少见
  • Oracle模型假设函数可以完美实现为量子电路,这在复杂函数上并不容易

但这不妨碍它成为每个量子计算学习者的必修课------它是理解更复杂算法的基石。


7. 延伸思考与练习

7.1 思考题

  1. 为什么辅助寄存器要初始化为|1⟩而不是|0⟩? 如果初始化为|0⟩,相位反冲还会发生吗?

  2. 相位反冲的本质:为什么 (U_f |x\rangle(|0\rangle-|1\rangle)) 会产生 ((-1)^{f(x)}) 因子?请用矩阵运算验证。

  3. 经典随机算法:如果允许一定概率出错,经典算法需要多少次查询?当n=100时,要保证99.9%的置信度,需要查多少次?

7.2 编程练习

  1. 自定义平衡函数:用掩码方式实现不同的平衡函数,验证算法是否仍然正确。

  2. 噪声环境测试:给电路添加T₁和T₂噪声(参考第四篇),观察退相干如何影响算法成功率。

  3. 可视化相位 :修改inspect_phase_kickback函数,绘制所有分量的相位分布图。

  4. 比较不同n:测试n从1到10的算法运行时间,验证量子模拟器的时间复杂度(注意:模拟器本身是指数复杂的,但真实量子硬件是线性的)。


8. 小结:从Deutsch-Jozsa到更广阔的世界

今天我们学习了量子计算史上第一个里程碑算法------Deutsch-Jozsa算法。回顾一下关键点:

  • 问题:判断一个函数是常数函数还是平衡函数
  • 经典复杂度:最坏情况需要2ⁿ⁻¹+1次查询
  • 量子复杂度:只需要1次查询
  • 核心技巧:叠加 + 相位反冲 + 干涉
  • 历史意义:首次证明量子指数加速的可能性

这个算法虽然简单,却包含了量子算法设计的全部要素。掌握它,你就掌握了理解Shor算法、Grover算法的基础。

下一篇文章 ,我们将学习另一个重要算法------Grover搜索算法。它能在无序数据库中实现平方级加速,比Deutsch-Jozsa更接近实用,也是NISQ时代最有希望率先落地的量子算法之一。我们将看到如何用振幅放大的思想,在O(√N)时间内找到目标项。


思考题参考答案(下一期公布,欢迎在评论区讨论):

辅助寄存器初始化为|1⟩是为了创造(|0⟩-|1⟩)态,这个态在受控操作下会产生相位反冲。如果初始化为|0⟩,受控操作后得到|f(x)⟩,不会产生相位因子。


参考资料

  • 1\] Origin Quantum:6.2 Deutsch--Jozsa算法介绍

  • 3\] TuringQ:Deutsch-Jozsa 算法详解

  • 5\] CSDN博客:量子计算(二十一):Deutsch-Josza算法

  • 7\] ACM Digital Library:Quantum advantage without entanglement

  • 9\] Qiskit API Documentation:qiskit.aqua.algorithms.DeutschJozsa

相关推荐
灰色小旋风2 小时前
力扣第九题C++回文数
c++·算法·leetcode
vx-bot5556662 小时前
企业微信ipad协议的增量同步算法与差量更新机制
算法·企业微信·ipad
cpp_25012 小时前
P1359 租用游艇
c++·算法·题解·洛谷·线性dp
Naisu Xu2 小时前
数学笔记:最小二乘法(直线拟合)
笔记·算法·最小二乘法
weixin_395448912 小时前
main.c_raw_0311_lyp
前端·网络·算法
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第七章之利用数组求矩阵各行元素之和并输出
c语言·算法·矩阵
智者知已应修善业2 小时前
【输入矩阵将其按副对角线交换后输出】2024-11-27
c语言·c++·经验分享·笔记·线性代数·算法·矩阵
17(无规则自律)2 小时前
C++ 链表修炼指南
数据结构·c++·算法·leetcode·链表
KhalilRuan2 小时前
基于OpenGL实现布料模拟
算法