1. 基本概念

1.1 矩阵的图表示

对于 n \times n 对称矩阵 A = [a_{ij}],定义其无向图 G(A) = (V, E)

  • 顶点集 V = \{1, 2, \ldots, n\} 对应矩阵的行/列索引

  • 边集 E = \{(i, j) \mid i \neq j, a_{ij} \neq 0\}

由于A 对称,(i, j) \in E \iff (j, i) \in E 。

例子

A = \begin{bmatrix} * & a_{12} & 0 & a_{14} \\ a_{12} & * & a_{23} & 0 \\ 0 & a_{23} & * & a_{34} \\ a_{14} & 0 & a_{34} & * \end{bmatrix}

对应的图:边集 \{(1,2), (1,4), (2,3), (3,4)\},形成一条路径 1-2-3-4 加上 1-4 边,构成一个4-cycle加一条对角线。

2. Cholesky分解与填充图

2.1 填充现象

当对稀疏矩阵 A 进行 Cholesky 分解 A = LL^T 时,L 中可能出现一些 A 中原本为零的位置变成非零。这些新出现的非零元称为填充(fill-in)

填充图 G^F = G(L + L^T)  定义为Cholesky因子L 的对称模式对应的无向图

  • 顶点集不变

  • 边集包含所有原始边以及所有填充边

2.2 填充产生的图论解释

定理:在 Cholesky 分解过程中,当消去顶点 k 时,会在其所有邻居之间产生新的边(填充)。即如果 i 和 j 都是 k 的邻居,且 i < k < j,则边 (i, j) 会出现在填充图中。

这个过程的图论解释是:消元过程将图逐步转化为弦图

3. 弦图与填充图的关系

3.1 弦图定义

        一个无向图称为弦图(chordal graph),如果它不包含长度≥4的无弦环。换句话说,任何长度 \ge 4 的环都至少有一条对角线(弦)。

3.2 重要定理

定理Cholesky 分解的填充图 G^F 是一个弦图。而且,它是包含原始图 G(A) 的最小弦图(即弦图的完备化)。

定理原始图 G(A) 是弦图当且仅当存在一个排列(消元顺序)使得 Cholesky 分解不产生任何填充

4. 消去树

4.1 定义

消去树(elimination tree)是描述 Cholesky 分解中依赖关系的有向树(或森林):

  • 顶点集与矩阵相同

  • 对每个 j < i,如果 l_{ij} \neq 0 且对所有 k > jl_{ik} = 0,则 j 是 i  的父节点

  • 更简单的定义:顶点jj的父节点是 \min\{i > j \mid l_{ij} \neq 0\}

4.2 性质

  1. 消去树反映了矩阵分解的数据依赖关系

  2. 如果 l_{ij} \neq 0,则 j 是 i 的祖先(不一定是父节点)

  3. 消去树可以用于指导并行计算

5. 在Cholesky分解中的应用

5.1 符号分解

在实际计算前,先进行符号分解,预测填充位置:

def symbolic_factorization(G, n):
    """
    符号分解:预测Cholesky因子的非零模式
    G: 原始图的邻接表
    n: 顶点数
    返回:L的非零模式(邻接表)
    """
    # 初始化L的非零模式为原始图的下三角部分
    L_pattern = [set() for _ in range(n)]
    for i in range(n):
        for j in G[i]:
            if j < i:
                L_pattern[i].add(j)
    
    # 模拟消元过程
    reach = [set() for _ in range(n)]  # 可达集
    for k in range(n):
        # 计算顶点k的可达集
        reach_set = set()
        for j in L_pattern[k]:
            reach_set.update(reach[j])
        reach_set.update(L_pattern[k])
        reach[k] = reach_set
        
        # 填充产生:k的邻居之间产生新边
        neighbors = [j for j in L_pattern[k]] + [k]
        for i in neighbors:
            for j in neighbors:
                if i > j and j not in L_pattern[i]:
                    L_pattern[i].add(j)
    
    return L_pattern

 

5.2 重排序

通过重新排列矩阵的行列(图的重标号),可以减少填充:

最小度算法(Minimum Degree Algorithm)

def minimum_degree_ordering(G, n):
    """
    最小度排序:每次选择度最小的顶点消去
    返回:新的顶点顺序
    """
    import heapq
    
    degree = [len(G[i]) for i in range(n)]
    order = []
    eliminated = [False] * n
    
    # 使用优先队列按度排序
    heap = [(degree[i], i) for i in range(n)]
    heapq.heapify(heap)
    
    while heap:
        deg, v = heapq.heappop(heap)
        if eliminated[v] or degree[v] != deg:
            continue
            
        order.append(v)
        eliminated[v] = True
        
        # 更新受影响顶点的度
        for u in G[v]:
            if not eliminated[u]:
                # 找到v的邻居中未消去的
                new_neighbors = set()
                for w in G[v]:
                    if not eliminated[w] and w != u:
                        # u和w可能成为新邻居
                        new_neighbors.add(w)
                
                # 更新u的邻接表
                G[u].update(new_neighbors)
                degree[u] = len(G[u])
                heapq.heappush(heap, (degree[u], u))
    
    return order

 

5.3 嵌套分割排序

def nested_dissection(G, n):
    """
    嵌套分割排序:递归地将图分割成两个大致相等的部分
    返回:新的顶点顺序
    """
    if n <= 1:
        return list(range(n))
    
    # 找到图的顶点分割(简化版本)
    separator = find_separator(G, n)
    
    # 将图分成三部分:左、分隔符、右
    left = [v for v in range(n) if v not in separator]
    right = [v for v in range(n) if v in separator]
    
    # 递归排序
    left_order = nested_dissection(restrict_graph(G, left), len(left))
    right_order = nested_dissection(restrict_graph(G, right), len(right))
    
    # 组合顺序:先左,再右,最后分隔符
    return [left[i] for i in left_order] + \
           [right[i] for i in right_order] + \
           list(separator)

 

5.4 并行Cholesky分解

利用消去树进行并行计算:

def parallel_cholesky(A, etree):
    """
    基于消去树的并行Cholesky分解
    etree: 消去树,每个节点记录其父节点和子节点列表
    """
    n = len(A)
    L = np.zeros((n, n))
    
    # 按拓扑顺序处理节点(从叶节点到根)
    def process_node(i):
        # 等待所有子节点完成
        for child in etree.children[i]:
            wait(child)
        
        # 执行当前节点的分解
        # 1. 更新当前行/列(使用子节点的结果)
        for child in etree.children[i]:
            update_from_child(L, i, child)
        
        # 2. 计算当前节点的对角元
        L[i, i] = sqrt(A[i, i] - sum(L[i, :i]**2))
        
        # 3. 计算当前节点所在列的非对角元
        for j in range(i+1, n):
            if A[j, i] != 0 or has_fill(j, i):
                L[j, i] = (A[j, i] - sum(L[j, :i] * L[i, :i])) / L[i, i]
        
        # 标记当前节点完成
        signal_completion(i)
    
    # 并行启动所有叶节点
    for i in range(n):
        if not etree.children[i]:  # 叶节点
            spawn(process_node, i)
    
    return L

 

6. 实际例子

6.1 原始图

考虑矩阵:

A = \begin{bmatrix} * & * & 0 & 0 \\ * & * & * & 0 \\ 0 & * & * & * \\ 0 & 0 & * & * \end{bmatrix}

对应的图是一条路径 1-2-3-4。

6.2 自然顺序的填充

消元顺序 1\rightarrow 2\rightarrow 3\rightarrow 4

  • 消去1:无填充

  • 消去2:2的邻居{1,3},1和3之间产生填充边(1,3)

  • 消去3:3的邻居{2,4}(以及填充的1),产生填充边(2,4)和(1,4)

填充图是完全图K4。

6.3 重排序减少填充

按顺序 2\rightarrow 1\rightarrow 3\rightarrow 4 :

  • 消去2:邻居{1,3},产生填充(1,3)

  • 消去1:邻居{2,3}(2已消),无新填充

  • 消去3:邻居{1,2,4},1和2已消,只可能产生(1,4)和(2,4)

填充图比完全图少一些边。

7. 算法复杂度

算法时间复杂度空间复杂度
符号分解O(\text{nnz}(L))O(\text{nnz}(L))
最小度排序O(n^2) 平均O(n^2)
嵌套分割O(n \log n)O(n)
并行分解O(n^3/p)O(\text{nnz}(L))

8. 总结

    图论在Cholesky分解中的应用:

  1. 预测填充,即通过符号分解提前知道L的非零模式

  2. 减少填充,即通过图重排序算法优化消元顺序

  3. 并行计算,利用消去树实现任务并行

  4. 内存管理,预分配L的存储空间

  5. 数值稳定性,图的信息可用于指导选主元策略

        这些技术是高性能稀疏直接求解器的核心,在有限元分析、电路仿真、优化问题等领域有广泛应用。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐