从两位地址开始
先把四个位置排好,依次标上 00、01、10、11。两位地址恰好能区分它们,因为每一位都有 0、1 两种取值,合起来就是 2² = 4 种;推广到 n 位,就能为 2ⁿ 个位置编号。
这次选中的是 10。读的时候,先看左边的 1,再看右边的 0。它表示二进制的 10,也就是十进制的 2。于是,我们已经知道要找哪一个位置,接下来只差一条到达它的路线。
- 00
- 01
- 本例10
- 11
按顺序读出路径
把这些位置画成一棵两层分叉的树,地址就可以变成行走路线。我们先约定:遇到 0 向左,遇到 1 向右。从根节点出发,按顺序读完 10,便会先向右,再向左。下面这小段 Python 把刚才的过程写了下来,可以先对照输出读一遍。
address = "10"
turn = {"0": "左", "1": "右"}
path = []
for bit in address:
path.append(turn[bit])
print(" → ".join(path))
print(f"存储单元 {address}")右 → 左 存储单元 10
看到“存储单元 10”,说明我们已经找对位置了。现在还没有读取内容,也没有改变数据量子位;这个位置究竟保存 0 还是 1,等拿到后面的存储表,就能继续往下算。
沿树形结构找到位置 10
- 1第一位:1
从最上方的根节点出发,先沿右侧分支走到下一层。
- 2第二位:0
到了右侧节点,再沿它的左侧分支走,就到达位置 10。
回头看这张图,第一位决定第一层怎么走,第二位决定第二层怎么走,两层分叉就能到达四个存储单元。蓝线画出了确定地址 |10⟩ 的路线。
稍后遇到叠加地址时,这幅图仍能帮我们辨认每个地址对应的位置。不过,那时要同时考虑各个量子分支,并保留它们之间的相位关系;随机挑一条蓝线走,无法表达这样的查询。
在四个位置各存入一位数据
位置找到了,现在给这四格放上具体的内容。我们约定它们分别保存下面这些 0 或 1,作为这次讲解的存储表。先留意两格就够了:刚刚找到的地址 10 保存 1,稍后会一起用到的地址 01 保存 0。
| 地址 | 存储位 |
|---|---|
| 00 | 1 |
| 01 | 0 |
| 10 | 1 |
| 11 | 1 |
先读固定地址 10
先做一次最容易跟上的查询:把地址准备为 |10⟩,把接收结果的数据位准备为 |0⟩。表里告诉我们,地址 10 存的是 1。查询结束后,左边的地址保留下来,右边的数据位变成 1,整个过程可以这样读:
如果想把这一步写成适用于任意地址的规则,我们用 a 表示地址,b 表示数据位原来的值,mₐ 表示该地址存储的位。下面的 ⊕ 是异或:存储位为 0 时保持数据位,存储位为 1 时把数据位翻转。
这里多写一个异或,是为了让查询可以撤销。量子电路中的相干查询必须可逆,而直接用存储值覆盖数据位,会丢掉它原来的值。仍以地址 10 为例:数据位从 0 开始,就翻成 1;从 1 开始,就翻回 0。因此 |10⟩|1⟩ 会变成 |10⟩|0⟩。对同一地址再查一次,数据位便回到原值,你可以用这个办法检查自己是否读懂了规则。
再读两个地址的叠加
固定地址的过程清楚了,就可以再向前走一步。让 01 和 10 组成等振幅叠加态,数据位仍从 0 开始。存储表没有变,查询规则也没有变:01 这一支查得 0,10 这一支查得 1。量子变换具有线性性质,因此对叠加态做同一次理想查询,就得到这两个分支各自变换后的叠加。
读下面的式子时,可以先只看两个分支各自发生了什么。√2 用来归一化,让两个分支的测量概率各为 50%;⊗ 则表示把地址和数据位组合在一起。有了这些记号,就能把刚才的叙述完整写下来:
再看一遍输出:地址 01 和数据 0 在同一支,地址 10 和数据 1 在另一支。加号把两个量子分支写成叠加,地址和数据的数值本身没有相加。
现在,地址与数据位形成了纠缠:它们的状态需要一起描述,无法拆成两个彼此独立的状态。查询把这种对应关系留在了量子态中,后续电路可以在测量前继续处理它。这个小例子就先走到这里,接下来用程序看看刚刚推导出的关联态。
一次测量会得到什么
手上的存储表和查询规则已经足够写出一个小程序了。我们用 PivotQ 构造可逆查询电路;它的电路接口直接复用 Qiskit 对象,因此接着就能交给 Qiskit Statevector,计算查询后的理想量子态。
对照代码前,先认清三个量子位:q2、q1 负责地址,q0 负责数据。Qiskit 按 q2q1q0 写出的三位标签,会被程序拆成 |地址⟩|数据⟩ 的样子,这样就能和前面的公式一一对应。四个函数也沿着同一条思路展开:准备输入、执行查询,再查看状态。
- 先看
fixed_case():它用X(2)把地址准备为 |10⟩,数据位保持 |0⟩,正好对应第一种输入。 - 再看
superposition_case():从地址 |01⟩ 出发,H(2)产生 01 和 11 两个等振幅分支,随后CX(2, 1)把其中的 11 变成 10。这样就准备好了第二种输入。 - 两种输入都交给
append_query()。它照着存储表,只在值为 1 的地址上用CCX翻转数据位。遇到地址中的 0,会临时对相应地址位加X来匹配控制条件,查询后再撤销这些X。 - 最后,
show_components()用Statevector算出非零基态的振幅,并把振幅模的平方打印为理想测量概率。到这里,我们看到的是计算出的状态与概率,程序没有进行随机抽样。
为了把例子完整跑通,我们把四个存储位直接编进了门电路。这样可以演示可逆查询规则;可动态读写的量子随机存储器器件还需要更多实现,这段代码没有创建这样的器件。
如果想亲手试一遍,可以使用 Python 3.12,在 PivotQ 仓库根目录运行 python -m pip install ./packages/framework,也可以直接使用仓库已配置的统一环境。接着把下方代码复制并保存为 qram_query.py,在同一环境中运行 python qram_query.py,然后对照下面的输出,看看它是否和刚才的推导一致。
#!/usr/bin/env python3
"""用 PivotQ 构造电路,以 Qiskit 理想态矢量重现页面中的四位置查询。
四个存储位是教学假设;此脚本不提交 PivotQ 任务,也不访问真实 QRAM。
"""
from __future__ import annotations
from pivotq import QuantumCircuit
from qiskit.quantum_info import Statevector
# Qiskit 的三位基态标签按 q2 q1 q0 排列;前两位是地址,末位是数据。
MEMORY = {"00": 1, "01": 0, "10": 1, "11": 1}
def append_query(circuit: QuantumCircuit) -> None:
"""在三量子位电路上追加 |a>|b> -> |a>|b XOR MEMORY[a]> 查询。
q2、q1 是地址位,q0 是数据位。存储位为 1 时,用受控 X 翻转 q0;
对地址中的 0 临时加 X,查询后撤销,保留地址和分支间的相位关系。
"""
for address, stored_bit in MEMORY.items():
if stored_bit == 0:
continue
zero_controls = [wire for bit, wire in zip(address, (2, 1)) if bit == "0"]
for wire in zero_controls:
circuit.x(wire)
circuit.ccx(2, 1, 0)
for wire in reversed(zero_controls):
circuit.x(wire)
def fixed_case() -> QuantumCircuit:
"""准备 |10>|0>,追加查询后返回电路。"""
circuit = QuantumCircuit(3)
circuit.x(2)
append_query(circuit)
return circuit
def superposition_case() -> QuantumCircuit:
"""准备 (|01> + |10>)|0>/sqrt(2),再追加同一查询。"""
circuit = QuantumCircuit(3)
circuit.x(1)
circuit.h(2)
circuit.cx(2, 1)
append_query(circuit)
return circuit
def show_components(circuit: QuantumCircuit) -> None:
"""打印非零基态振幅及其理想测量概率。"""
state = Statevector.from_instruction(circuit)
for basis, amplitude in enumerate(state.data):
if abs(amplitude) < 1e-10:
continue
if abs(amplitude.imag) > 1e-10:
raise ValueError("本教程中的计算预期只有实数振幅")
label = f"{basis:03b}"
print(
f"|{label[:2]}⟩|{label[2]}⟩ "
f"振幅 {amplitude.real:+.6f} 概率 {abs(amplitude) ** 2:.0%}"
)
if __name__ == "__main__":
print("固定地址 |10⟩|0⟩:")
show_components(fixed_case())
print("叠加地址 (|01⟩ + |10⟩)|0⟩ / √2:")
show_components(superposition_case())固定地址 |10⟩|0⟩: |10⟩|1⟩ 振幅 +1.000000 概率 100% 叠加地址 (|01⟩ + |10⟩)|0⟩ / √2: |01⟩|0⟩ 振幅 +0.707107 概率 50% |10⟩|1⟩ 振幅 +0.707107 概率 50%
先读固定地址那一行:|10⟩|1⟩ 的概率是 100%,正好对应地址 10 保存的 1。再读叠加地址的两行:|01⟩|0⟩ 与 |10⟩|1⟩ 各自的振幅约为 0.707107。取振幅模的平方,就得到各 50% 的概率。
这时很自然会想到开头留下的问题:既然屏幕上有两行,一次测量能把两行都读出来吗?Statevector 能列出理想态的所有非零分支,是因为我们正在查看模拟器计算出的量子态;这里没有执行随机测量。若直接测量地址位和数据位,单次只会得到其中一组。每次重新准备同样的输入、完成查询再测量,重复多次后,两组结果的出现频率才会趋近各 50%。
于是,从寻找地址 10 到查看叠加查询,我们走完了这个例子的全过程:定位、可逆地读取,再理解测量结果。若继续走向真实设备,还要准备存储数据、实现相干寻址,并处理噪声与读写成本。这些都超出了这张四格存储表的演示范围,因此这里的输入与输出还不能用来判断实际硬件性能或算法加速效果。