如何用 np.block 递归构建超立方体邻接矩阵?

来源:XML-XSL教程作者:下班再修头衔:程序员
导读:本期聚焦于下班再修创作的《如何用 np.block 递归构建超立方体邻接矩阵?》,敬请观看详情。超立方体 Q_n 的顶点可以用长度为 n 的二进制串表示,两个顶点相邻当且仅当它们的汉明距离为 1。按最后一位分组时,邻接矩阵会自然分裂成四个同尺寸块:左上和右下继承 Q_{n-1},右上和左下是单位矩阵。要把这个递归定义变成 NumPy 代码,np.block 是最直接的翻译工具,它接收嵌套列表,按行列块语义拼接,几乎就是矩阵分块表达式的代码版。本文从这一递归结构出发,展示如何用 np.block 正确实现从 Q_0 到任意 n 的超立方体邻接矩阵,同时覆盖 dtype 保持、形状校验、递归基准和内存边界等容易忽视的细节。相比手写索引或 np.kron,这种写法更贴近数学定义,也更容易排查分块顺序问题。

超立方体图 Q_n 是网络拓扑和组合优化中经常出现的一类结构,它的每个顶点都可以用一个长度为 n 的 0/1 二进制串表示。两个顶点之间有边,当且仅当这两个串在恰好一个位置上不同,也就是汉明距离等于 1。要把这种关系转化为 NumPy 邻接矩阵,最容易出错的地方在于顶点顺序。若按照最后一个二进制位划分顶点集合,0 组和 1 组各自包含一个 Q_{n-1},而跨组边只连接低 n-1 位完全相同的顶点对。这个观察直接给出了一个干净的分块形式。

如何用 np.block 递归构建超立方体邻接矩阵?

设 A_{n-1} 表示 Q_{n-1} 的邻接矩阵,I 表示同阶单位矩阵。按照上述分组排序,Q_n 的邻接矩阵可以写成 [[A_{n-1}, I], [I, A_{n-1}]] 这样的 2×2 分块矩阵。左上角和右下角对应两个子立方体内部的边,右上角和左下角对应两条跨组连接。因为跨组连接只发生在低 n-1 位相等的顶点之间,所以非对角块不是任意置换矩阵,而是单位矩阵。

递归块结构是怎么来的

先看一个具体例子。Q_2 有四个顶点,顺序取为 00、01、10、11。00 与 01、10 相邻;01 与 00、11 相邻;10 与 00、11 相邻;11 与 01、10 相邻。把它写成邻接矩阵,就是如下形式:

# Q_2 的邻接矩阵,顶点顺序为 00、01、10、11
A2 = np.array([
    [0, 1, 1, 0],
    [1, 0, 0, 1],
    [1, 0, 0, 1],
    [0, 1, 1, 0]
], dtype=int)

把 Q_1 的邻接矩阵记为 A_1 = [[0, 1], [1, 0]],单位矩阵记为 I_2。上面的 Q_2 矩阵恰好可以拆成四个 2×2 的块:左上和右下都是 A_1,右上和左下都是 I_2。这不是巧合,而是由分组方式决定的。按最后一位分成两组后,组内的顶点仍然只在一个位上不同,所以组内连接继承 Q_{n-1};跨组的两个顶点如果低 n-1 位相同,它们恰好只差最后一位,也会相连,因此跨组连接就是一一对应的恒等关系。

把这个结构推广到一般 n,就得到递归式 A_n = [[A_{n-1}, I], [I, A_{n-1}]]。递归基准可以取 Q_0,也就是只有一个顶点、没有边的图,其邻接矩阵为 [[0]]。如果希望只处理到 n=1 以上,也可以从 A_1 开始;但从 Q_0 开始的好处是递归函数更加统一,不需要额外区分第一层。

这里要特别注意顶点顺序。矩阵分块的位置取决于你如何排列二进制串。如果按最高位分组而不是最后一位分组,同样也成立,只是分块方式的解释略有不同。关键是每一轮递归使用同一种分组策略,并且跨组块必须是单位矩阵。如果分组顺序混乱,得到的矩阵可能仍然是某个同构超立方体的邻接矩阵,但行和列的顶点顺序会不一致。

np.block 的分块拼接语义

np.block 接收一个嵌套列表,列表的每一层对应分块矩阵的一行块,而每一个元素对应一个列块。例如 np.block([[A, I], [I, A]]) 会先拿第一行块中的 A 和 I 做水平拼接,再拿第二行块中的 I 和 A 做水平拼接,最后把两行块垂直堆叠。这个行为与数学上的分块矩阵记法几乎一一对应,这也是它适合实现超立方体邻接矩阵的原因。

使用 np.block 时最容易踩的坑是把嵌套列表写成展平列表。比如 np.block([A, I, I, A]) 并不会得到 2×2 分块,而会尝试把四个块水平拼成一行。由于在这个问题中 A 和 I 都是方阵且同阶,展平拼出来的形状会是 2^{n-1} 行、4 × 2^{n-1} 列,显然不是邻接矩阵。另一个常见问题是 dtype 被悄悄改变。默认 np.eye 返回浮点数组,如果 A_{n-1} 是整型,np.block 的结果可能被提升为浮点。邻接矩阵通常只包含 0 和 1,用整型存储可以减少内存并保持后续索引、位运算等操作的直观性。

还有一类错误出现在递归实现时没有保持单位矩阵的阶数。每一层递归的单位矩阵必须与 A_{n-1} 同阶,也就是 2^{n-1}。如果写成固定大小 I_2,在 n=3 时四个块尺寸不一致,np.block 会抛出形状不匹配错误。正确做法是使用 prev.shape[0] 动态生成单位矩阵。

下面给出正确和错误写法的对比:

# 正确:嵌套列表表达 2×2 分块
A = np.block([[prev, eye], [eye, prev]])

# 错误:展平列表会拼接成一行四块
# wrong = np.block([prev, eye, eye, prev])

完整实现与形状验证

把前面的递归结构翻译成 Python 函数并不复杂。递归基返回一个 1×1 的零矩阵,每一层递归先用上一层的邻接矩阵生成对应阶数的单位矩阵,再交给 np.block 完成拼接。代码如下:

import numpy as np

def hypercube_adj(n):
    if n < 0:
        raise ValueError('n must be non-negative')
    if n == 0:
        return np.array([[0]], dtype=int)
    prev = hypercube_adj(n - 1)
    eye = np.eye(prev.shape[0], dtype=int)
    return np.block([[prev, eye], [eye, prev]])

实现完成后,应当验证矩阵的几个基本性质。超立方体 Q_n 的邻接矩阵必须是对称的,对角线必须全为零,每一行的和应等于 n,因为每个顶点在 n 个二进制位上各有一条边。下面的循环同时检查形状、对称性、零对角线和行和:

for n in range(1, 6):
    A = hypercube_adj(n)
    size = 1 << n
    assert A.shape == (size, size)
    assert np.array_equal(A, A.T)
    assert np.all(np.diag(A) == 0)
    assert np.all(A.sum(axis=1) == n)
    print(f'n={n}, shape={A.shape}, edge_count={A.sum() // 2}')

运行后可以看到,n=1 时得到两个顶点的单边,n=2 时得到正方形的四条边,n=3 时得到立方体的十二条边。随着 n 增大,顶点数和边数按指数增长,但因为邻接矩阵是稠密存储,内存占用为 4^n 个元素。n=10 时矩阵为 1024×1024,约 800 万个元素,使用 int64 大约需要 8MB;n=12 时约为 128MB;再往上就很容易超过普通机器内存。因此 n 较大时,应当考虑稀疏矩阵或边列表表示。

递归构造本身也会产生额外的临时数组。每一层返回时都会创建一个新矩阵,同时保留上一层的矩阵。对于不是特别大的 n,这种开销可以接受;但如果在循环中反复构建多个 n 值,可以显式释放中间变量,或者使用迭代方式逐层扩增,以减少峰值内存。

与 np.kron 方案和稀疏方案的取舍

除了 np.block,另一种常见做法是利用笛卡尔积图的 Kronecker 积公式。超立方体 Q_n 是两个图的笛卡尔积:Q_n = Q_{n-1} □ K_2。对应邻接矩阵可以写成 A_n = A_{n-1} ⊗ I_2 + I_{2^{n-1}} ⊗ A_1,其中 A_1 是 Q_1 的邻接矩阵。用 NumPy 可以这样实现:

def hypercube_kron(n):
    if n < 0:
        raise ValueError('n must be non-negative')
    if n == 0:
        return np.array([[0]], dtype=int)
    I2 = np.eye(2, dtype=int)
    A1 = np.array([[0, 1], [1, 0]], dtype=int)
    prev = hypercube_kron(n - 1)
    return np.kron(prev, I2) + np.kron(np.eye(2 ** (n - 1), dtype=int), A1)

np.block 和 np.kron 最终都能得到正确的邻接矩阵,但二者的表达重点不同。np.block 直接对应顶点按最后一位分组的排列方式,分块结构清晰,调试时更容易看出矩阵块顺序是否正确。np.kron 则更贴近图论中的笛卡尔积表示,适合需要推广到其他积图或使用矩阵运算推导性质时使用。

从性能上看,两个方案在中小规模下差距不大,因为它们都需要分配新矩阵并复制数据。不过 np.kron 实现中先计算两个 Kronecker 积再相加,理论上会多出两个较大的临时矩阵,峰值内存更高一些。对于 n=12 这种中等规模,这种差异可能变得明显。实际工程中,如果只需要做图遍历、连通性分析或稀疏特征计算,使用 scipy.sparse 的块矩阵构造会更合适。例如用稀疏单位矩阵和稀疏邻接矩阵递归拼接,可以避免稠密矩阵的 4^n 存储压力,但这是另一个话题。

总结来说,如果目标是教学演示、小规模谱分析或快速验证图论性质,用 np.block 递归构造超立方体邻接矩阵是最容易理解和维护的方式。只要把握住嵌套列表结构、单位矩阵阶数和 dtype 三个要点,就能得到完全正确的结果。

NumPy超立方体邻接矩阵修改时间:2026-10-07 01:54:40

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1007/66679.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。