☰
MATLAB量子计算模拟:从状态向量到Grover与相位估计
2026/11/2 8:42:26 网站建设 项目流程

简介:面向量子计算入门者以及需要快速验证算法的高校学生和工程师,这份 MATLAB 实现包用代码示例串联量子位、量子门与量子电路的基本操作,并覆盖量子傅里叶变换、Grover 搜索、Shor 因数分解、Deutsch-Jozsa 与 Simon 等典型算法,适合课程实验、自学演练和小规模仿真验证。压缩包共 5 个文件,由 4 个 .m 脚本和 1 个 Markdown 说明组成,体积约 5KB,轻量便携;脚本可用于搭建 Hadamard 门、受控门、量子测量等基础模块,说明文档则帮助理解项目结构与运行思路。已有 1883 人学习或下载,说明其在入门练习中具有一定参考价值。读者可借助现成代码对照算法流程,观察量子态演化与测量结果,并在此基础上改写参数、扩展电路,形成自己的实验记录与排错经验。

1. 量子计算模拟:为什么一台笔记本就能跑量子算法

很多人第一次听说“用量子计算机做算法”,下意识以为必须在真机上才能验证,或者至少要搭一套低温制冷环境。反直觉的地方在于:量子算法的正确性,本质上由线性代数决定,而不是由物理硬件决定。一个 n 量子比特系统的状态就是一个 2^n 维复向量,所有门操作都是作用在这个向量上的酉矩阵乘法。只要你能把矩阵乘法和复数运算老老实实算对,你得到的结果和真机在数学上是同一个东西。

这就是“量子计算算法的 MATLAB 实现”这类资源真正想解决的事:把量子力学的物理外衣剥掉,露出底下那层线性代数骨架,让你在熟悉的数值计算环境里把 Deutsch-Jozsa、Grover、Shor 这些算法的逻辑跑通、把振幅变化打印出来看。搞懂这套机制,你才能明白量子算法的加速到底加在哪、模拟器为什么一到二三十个比特就集体崩掉。这也是理解量子计算底层最常见的一条路径。

2. 状态向量与门矩阵:量子算法的数据到底长什么样

2.1 从 2^n 维复向量说起

一切从状态表示开始。单个量子比特的状态是两个复数振幅组成的向量[α; β],满足 |α|² + |β|² = 1。两个量子比特不是两个独立向量,而是它们的张量积,维度变成 4;三个比特变 8;n 个比特就是 2^n。这一点是所有实现里最容易翻车的地方:新手会本能地想用长度为 n 的数组存 n 个比特,然后发现纠缠怎么都表达不出来。

纠缠之所以无法用“每个比特一个数”表示,是因为它需要 n 个比特联合概率分布中那些不可分解的相关性。像贝尔态 (|00⟩+|11⟩)/√2,你没法写成“比特 A 是某某态、比特 B 是某某态”的形式。所以状态必须是整块 2^n 维向量,MATLAB 里就是一个2^n × 1的复数列向量。

function psi = init_state(n) % 初始化 n 量子比特系统为全零态 |00...0> dim = 2^n; psi = zeros(dim, 1); psi(1) = 1; % 索引约定:第 k 位对应从 0 开始的二进制 end

这里有个约定必须交代清楚:MATLAB 是从 1 开始索引的,而量子态习惯用 0 开始的二进制基矢。通常的做法是让第 1 个元素对应 |00…0⟩,第 2 个对应 |00…1⟩,也就是“索引值减一”的二进制表示。这个映射一旦定错,后面所有门矩阵的位置都会错乱,且往往不会报错,只会给你一个看着合理但完全错误的答案——真正的黑匣子型 bug。

2.2 单比特门怎么作用到整块状态上

单比特门本身是个 2×2 矩阵,比如 Hadamard:

H = (1/sqrt(2)) * [1 1; 1 -1];

要把它作用在 n 比特系统的第 k 位上,核心操作是“其他比特不动,只在这一个比特的二维子空间里做旋转”。数学上等价于对整块状态做I ⊗ … ⊗ H ⊗ … ⊗ I,但如果真的去算 2^n × 2^n 的克罗内克积,内存会瞬间爆掉。常见的实现思路是直接按索引分块重排数据:把状态按第 k 位的取值拆成偶索引组和奇索引组,各做一次线性组合再写回。

function psi = apply_single_qubit_gate(psi, U, k, n) % U: 2x2 酉矩阵; k: 作用的比特位(1 表示最高位); n: 总比特数 N = 2^n; step = 2^(n - k); % 该比特在索引中变化的步长 psi = reshape(psi, step, 2, []); % 把第 k 位单独拆成一维 a = psi(:, 1, :); b = psi(:, 2, :); psi(:, 1, :) = U(1,1)*a + U(1,2)*b; psi(:, 2, :) = U(2,1)*a + U(2,2)*b; psi = reshape(psi, N, 1); end

reshape那一步是全篇最值钱的一句。把向量按step × 2 × 剩余三块重排后,第 k 位恰好落在中间那一维上,于是“只作用一位”就变成了对中间维度的 2×2 线性变换,完全避开了大矩阵。参数step = 2^(n-k)体现的是位序约定:k = 1 时该比特每隔 2^(n-1) 个位置翻转一次,k = n 时相邻索引交替。改k就能把门打到任意一位上,这套写法在比特数上到二十多也还能撑住,比克罗内克积实际太多了。

2.3 受控门与纠缠是怎么长出来的

受控门是量子算法里唯一能制造纠缠的结构。CNOT 的逻辑是:控制位为 1 时才对目标位取反,否则不动。实现上它比单比特门更简单,因为它本质上只是对索引做条件判断,连线性组合都不需要。

function psi = apply_cnot(psi, ctrl, targ, n) N = 2^n; for idx = 0:N-1 bit_c = bitget(idx, n - ctrl + 1); % 控制位 if bit_c == 1 partner = bitxor(idx, bitshift(1, n - targ)); % 翻转目标位 if idx < partner % 只交换一次,避免翻回来 psi([idx+1, partner+1]) = psi([partner+1, idx+1]); end end end end

这段代码里idx < partner的判断是个血泪经验点。如果不加,循环对同一对索引处理两次,振幅会被换回原位,程序不报错,输出看起来“没啥问题”,但纠缠根本没建立起来。另外bitget的位序参数n - ctrl + 1必须和前面apply_single_qubit_gate的位序约定严格一致,否则会出现“门打在了对的比特上但控制关系反了”这种极难排查的错位。

纠缠一旦产生,状态就无法再拆成各部分独立的乘积。可以顺手验证一下:对贝尔态计算任一比特的约化密度矩阵,如果它不是纯态(迹的平方小于 1),说明纠缠成立。这个检查在实际调试中比盯着 2^n 维向量看有用得多。

3. 算法层:Deutsch-Jozsa 与 Grover 的振幅到底怎么被搬动

3.1 量子并行性不是“同时算所有输入”

Deutsch-Jozsa 算法的经典叙事是“一次查询判断函数是常量还是平衡”,很多人由此得出“量子计算机同时算了所有输入”的结论。这个说法是不准确的,也容易让人在后面理解 Grover 时彻底迷路。真正的机制是:Hadamard 把 |0⟩ 打成所有基矢的等幅叠加,一次酉变换作用在这个叠加态上,相当于把函数值编码进了每个分支的相位,再靠第二次 Hadamard 把这些相位差干涉回可测量的振幅差。

换句话说,并行性在制备叠加态那一步就完成了,真正的“读出”靠的是干涉。相位信息你直接测不出来,必须通过干涉把它转成振幅差异,最后测量才有意义。这也是为什么量子算法设计里到处是“前后各一层 H”的对称结构——那不是装饰,是让相位重新变回概率的必要步骤。

3.2 用一个酉门模拟任意布尔函数

在模拟器里,量子预言机就是一个把|x⟩|y⟩映射到|x⟩|y ⊕ f(x)⟩的酉矩阵。实现方式是对角结构:遍历所有基矢索引,按 f 的取值决定是否翻转辅助位。

function psi = apply_oracle(psi, f, n) % f: 函数句柄, 输入 x(整数), 输出 0/1; 最后一位为辅助位 N = 2^n; for idx = 0:N-1 x = bitshift(idx, -1); % 去掉辅助位得到输入 y = bitget(idx, 1); % 最低位是辅助位 if f(x) == 1 partner = bitxor(idx, 1); if y == 0 % 只在 y=0 时交换,等价于异或 psi([idx+1, partner+1]) = psi([partner+1, idx+1]); end end end end

bitshift(idx, -1)取输入、bitget(idx, 1)取辅助位,这个约定必须和调用方的比特排列对齐。Deutsch-Jozsa 的完整流程就是:初始化 → 辅助位取反(变成 |1⟩ 以便相位回踢)→ 全体 H → oracle → 全体 H → 测量前 n-1 位。如果函数平衡,前 n-1 位测出来必然全零之外的结果;如果常量,则全是零。调试时不要只测最终结果,把中间态打印出来看振幅的实部和虚部怎么随每一步变化,很多“结果不对”其实是辅助位初始化漏了取反。

3.3 Grover:振幅放大是一个几何旋转

Grover 迭代常被描述成“标记正确答案”,但它的机制更接近在二维子空间里的旋转。整个状态可以拆成“正确答案方向”和“其余答案方向”两部分,每次迭代由 oracle 翻转答案分支的相位,再由扩散算子(关于平均值的反射)把振幅从均匀分布往答案方向掰一点。每次迭代旋转角度约 2arcsin(1/√N),所以最优迭代次数是 π√N/4 量级——这个数字不是拍脑袋来的,是旋转角度决定的。

function psi = grover_diffusion(psi, n) % 扩散算子: 关于平均值反射, 2|s><s| - I N = 2^n; avg = mean(psi); psi = 2*avg - psi; end

mean(psi)对复数向量求平均,2*avg - psi就是把每个振幅关于平均值做镜像。这段三行代码是整个 Grover 的核心,也是最能体现“量子算法本质是线性代数”的地方。注意mean在 MATLAB 里对复数是逐元素求平均,行为正确,但如果换成某些语言或自己写循环,很容易把共轭搞错——扩散算子里的 |s⟩ 应该是 ⟨s| 的共轭转置,作用在状态上是投影,写错共轭方向结果会缓慢发散,且小规模测试时可能看不出问题。

3.4 相位估计:Shor 的骨架其实是一个求特征值问题

Shor 算法被讲得神乎其神,但它的量子部分核心只有一块:相位估计。给定酉算子 U 和它的一个特征向量 |u⟩,相位估计能把对应的特征值 e^(2πiφ) 中的 φ 以二进制精度提取出来。机制是:用一组辅助比特制备均匀叠加,做受控 U^(2^j) 操作把相位逐位“踢”进辅助寄存器,最后做逆量子傅里叶变换把相位信息转成可测的二进制串。

模拟层面,量子傅里叶变换(QFT)就是一个固定的酉矩阵,乘积形式为各比特的 Hadamard 加受控相位门。实现时如果比特数不大,直接构造 QFT 矩阵做矩阵乘法最省事;比特数上去后要改成分解形式逐位施加,避免 O(4^n) 的内存开销。这里的取舍很典型:能直接乘大矩阵就别硬拆,但一旦维度超过几千,拆解形式是唯一能跑的路。相位估计的精度由辅助比特数决定,辅助比特每多一个,φ 的有效位数就多一位,这个关系在调试时是最好的“正确性标尺”。

4. 模拟器的边界:为什么二三十个比特就跑不动了

4.1 2^n 的内存在现实中意味着什么

状态向量大小是 2^n 个复数,每个复数在双精度下 16 字节。20 个比特是 1M 个复数,约 16 MB,轻松;25 个比特约 512 MB,还算能忍;30 个比特就是 16 GB,单机已经很吃力;40 个比特是 16 TB,已经不是“优化一下”能解决的问题了。这不是代码写得不够好,而是状态向量表示法的根本代价。

提示:评估一个量子模拟器能不能用在你的场景时,先算 2^n × 16 字节这个数,再对比可用内存,比看任何宣传参数都准。

门操作的复杂度也跟着涨。单比特门看起来是 O(2^n),但受控门如果写成逐索引循环,就是 O(2^n) 次迭代加上 MATLAB 解释器的开销,实际比理论慢一个量级。有经验的写法是把循环向量化,用逻辑索引一次性筛出所有控制位为 1 的索引再做交换,速度差异在小规模测试里不明显,规模一上去就是几分钟和几小时的差别。

4.2 稀疏与张量网络:绕开指数墙的两条路

既然 2^n 撞墙了,业界有两条常见的绕行路线。第一条是利用状态的稀疏性:很多算法中间态只有少数基矢非零,用稀疏向量存储能把内存降到与支撑集大小成正比;但一旦进入均匀叠加(比如刚打完 H 门),稀疏性瞬间消失,这条路只对特定算法阶段有效。

第二条是张量网络,把整块状态表示成一组小张量的缩并,用纠缠度换取内存。对纠缠很低的态(比如一维近邻电路),它能表示远超 2^n 的状态数;但 Grover 这类算法全程处于高纠缠状态,张量网络的优势会被吃掉。所以没有通用解法,选哪种表示取决于你要跑什么算法、态在中间过程里纠缠有多重——这是设计取舍,不是性能调参。

4.3 MATLAB 在这里的独特优势与代价

用 MATLAB 做量子模拟,好处在于复数矩阵、张量重排、稀疏存储都是原生能力,写出来的代码和数学表达式几乎一一对应,调试时disp(psi)直接看复振幅,abs(psi).^2直接看概率分布。代价是解释型语言的循环很慢,逐索引遍历的受控门在大比特数下会成为瓶颈。

一个现实的做法是:小规模(十几比特)用逐位逻辑清晰实现,便于对照论文验证;规模上去后把热点循环改写成向量化的索引操作。这套思路和经典数值计算的优化路径完全一致,量子模拟并没有更玄学,只是把“数组重排”这个基本功用到了极致。

5. 避坑与排查:量子模拟里最容易翻车的几处

5.1 现象:结果概率合计不等于 1

原因通常有两种。一是门矩阵本身不酉,比如把 Hadamard 写成了[1 1; 1 1]漏了 1/√2,或者相位门的符号写反导致矩阵行列式模长不为 1。二是多比特门作用时索引映射错了位,把某些振幅乘了两次、另一些一次没乘到。解决方式是每施加一个门后立刻做一次归一化检查:abs(norm(psi) - 1) < 1e-10,以及把门矩阵单独测U'*U是否等于单位矩阵。这个检查成本极低,但能挡掉绝大多数低级错误。

5.2 现象:Deutsch-Jozsa 对常量函数也返回非零

原因多半出在辅助位的初始状态。相位回踢要求辅助位是 (|0⟩-|1⟩)/√2,也就是初始化后先对辅助位做 X 再 H,或者等价地把辅助位直接置为 |1⟩ 后统一打 H。如果辅助位停留在 |0⟩,oracle 的异或操作作用在零上什么都不改,相位踢不出来,整个算法退化。解决方式是检查初始化顺序,并在 oracle 前后各打印一次辅助位的振幅,确认它确实在 |0⟩ 和 |1⟩ 之间有等幅分量。

5.3 现象:Grover 迭代次数越多结果越差

这不是 bug,是机制。振幅放大是旋转,转过最优角度后继续转会转回来,概率反而下降。经典翻车场景是“为了保险多迭代几次”。解决方式是按 π√N/4 估算最优次数,并且在小规模时直接扫描迭代次数画出成功概率曲线,你会看到一条明显的拱形。这个曲线的存在本身,就是对“量子加速不是无限的”最直观的证明。

5.4 现象:程序跑通了但和论文数值对不上

最常见的元凶是位序约定不一致。MATLAB 从 1 开始索引、量子文献从 0 开始基矢、有的资料把最低位当第一个比特、有的把最高位当第一个比特。只要有一处不一致,矩阵本身没错、算法流程没错,结果就是不一样。解决方式是:固定一套约定写进注释,并设计一个“已知答案”的单元测试,比如对 |01⟩ 施加 X 到第一位,看结果是不是 |11⟩,用这个小测试把约定钉死再往上搭。

5.5 现象:大规模时内存瞬间耗尽或卡死

原因是状态向量的 2^n 增长,加上中间变量复制。MATLAB 在函数传参时对数组做写时复制,psi = reshape(psi, ...)这类操作在某些情况下会产生临时副本,峰值内存可能是理论值的两到三倍。解决方式是尽量原地操作、复用缓冲区,并在跑大规模前用2^n * 16 * 3估算峰值需求。如果估算已经超了,就别硬撑,换稀疏表示或者缩小比特数验证逻辑。

6. 设计取舍:什么时候该自己写模拟器,什么时候该换思路

聊到这里,一个绕不开的判断是:这套 MATLAB 实现的模拟器,到底适合拿来干什么。我的看法是把它定位成“理解与验证工具”而不是“生产级量子计算平台”。它的核心价值在于每一步的中间态都摊在你面前,你可以把振幅、相位、纠缠度全部打印出来对照公式,这是真机和封装好的库都给不了的透明度。学习 Deutsch-Jozsa 的相位回踢、看 Grover 的成功概率随迭代次数拱起再落下,这种“看得见”的体验是它最值钱的地方。

代价也很明确:指数墙是硬的,三十比特左右基本就是单机 MATLAB 的实用上限,再往上必须换表示方法或者换架构。所以判断标准很清晰——如果你要验证的是算法逻辑、做教学演示、或者在论文里对着小规模实例核对数值,自己用 MATLAB 写一遍是最扎实的路径;如果你要跑几十上百比特的电路,那属于另一个工程问题,需要张量网络或分布式方案,不是把这份代码优化几下能解决的。

另一个值得想的取舍是“直接构造酉矩阵”还是“逐门作用”。前者写起来快、和教科书公式一一对应,适合比特数极小的验证;后者内存友好、可扩展,但调试时需要更多中间检查。我一般的习惯是先用小矩阵版本把算法跑对、把期望结果记录下来,再改写成逐门版本,用前者当基准做回归测试。这个顺序反过来做过一次,结果是两边都错、互相不知道谁错,排查了一整晚。

从那以后我每次动手写这类数值模拟,都强制先用两三个比特、已知答案的例子把位序约定和门矩阵钉死,再往上加规模。量子算法看着玄,翻车点其实全在索引和共轭这些最朴素的地方。这份资源把从状态向量、门作用到 Deutsch-Jozsa、Grover、相位估计的完整链路都落成了可运行的 MATLAB 代码,想真正把量子计算的底层机制摸透,拿它逐行跑一遍、把中间态打印出来看,比读十篇综述都管用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询