图论基础回顾:从数据结构到资金流网络

做资金流网络挖掘,图论是绕不开的基础。说实话,我刚开始接触这个领域时,觉得图论就是数学课上的抽象概念,跟实际业务八竿子打不着。直到有一次做企业资金链路分析,被一个循环转账的问题卡了整整两天——嗯,从那以后我再也不敢小看图论了。

这一节,我们快速过一遍图论的核心知识点。都是干货,不绕弯子。

图的定义与分类

图是什么?说白了,就是一堆节点和连线的集合。节点代表实体,连线代表关系。在资金流网络中,节点可以是公司、账户、个人,连线就是转账、交易、担保这些行为。

图有三种常见分类,我列个表方便你对比:

类型 定义 资金流场景举例
无向图 边没有方向,A-B 和 B-A 一样 企业之间的担保关系、关联公司
有向图 边有方向,A→B 和 B→A 不同 转账记录、资金流向(A 转给 B)
加权图 边上带有权重(数值) 转账金额、交易频次、风险评分
我的经验:资金流网络几乎都是有向加权图。方向代表资金流向,权重代表金额大小。我曾经遇到一个案子,只看无向关系发现不了异常,加上方向后立刻定位到某个账户是「资金归集中心」——这就是有向图的威力。

图的存储方式

图建好了,怎么存到计算机里?两种主流方式:邻接矩阵和邻接表。我分别说说。

邻接矩阵

用一个二维数组存图。假设有 n 个节点,就开一个 n×n 的矩阵。matrix[i][j] 表示节点 i 到节点 j 是否有边(或者权重是多少)。

# 有向加权图的邻接矩阵示例
# 节点:0, 1, 2, 3
# 边:0→1(权重5), 1→2(权重3), 2→0(权重2), 2→3(权重4)

matrix = [
    [0, 5, 0, 0],  # 节点0
    [0, 0, 3, 0],  # 节点1
    [2, 0, 0, 4],  # 节点2
    [0, 0, 0, 0]   # 节点3
]
避坑指南:邻接矩阵适合稠密图(边很多)。但资金流网络通常很稀疏——几万个节点,每个节点只跟几十个账户有交易。用矩阵存的话,大部分空间都是 0,浪费内存。我曾经用邻接矩阵存 10 万个节点的图,内存直接爆了...

邻接表

每个节点维护一个列表,只存它指向的邻居。空间省很多。

# 同样上面的图,用邻接表存
graph = {
    0: [(1, 5)],        # 节点0 → 节点1,权重5
    1: [(2, 3)],        # 节点1 → 节点2,权重3
    2: [(0, 2), (3, 4)],# 节点2 → 节点0(权重2) 和 节点3(权重4)
    3: []               # 节点3没有出边
}
我建议:做资金流挖掘,默认用邻接表。除非你的图节点数少于 1000 且边非常密集,否则邻接表是更优选择。Python 里用字典套列表,操作起来也很顺手。

图的遍历:BFS 与 DFS

遍历就是「走一遍图」。两种走法:广度优先(BFS)和深度优先(DFS)。

广度优先搜索(BFS)

一层一层往外扩。先访问起点,再访问起点的所有邻居,然后访问邻居的邻居... 用队列实现。

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)
    
    while queue:
        node = queue.popleft()
        print(f"访问节点: {node}")
        
        for neighbor, _ in graph.get(node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

# 运行
graph = {0: [(1,5)], 1: [(2,3)], 2: [(0,2),(3,4)], 3: []}
bfs(graph, 0)
# 输出: 0 → 1 → 2 → 3
实际场景:BFS 适合找「最短路径」。在资金流中,我想查 A 公司转给 B 公司,中间经过了多少层账户——BFS 一层层往外扩,第一次找到目标时就是最短路径。我做过一个反洗钱项目,用 BFS 找资金传递路径,效果很好。

深度优先搜索(DFS)

一条路走到黑,走不通再回头。用栈(或递归)实现。

def dfs(graph, node, visited=None):
    if visited is None:
        visited = set()
    
    visited.add(node)
    print(f"访问节点: {node}")
    
    for neighbor, _ in graph.get(node, []):
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

# 运行
dfs(graph, 0)
# 输出: 0 → 1 → 2 → 3
注意:DFS 递归深度可能很大。如果图有上万个节点,Python 默认递归深度(约 1000)会报错。我建议用栈手动实现,或者设置 sys.setrecursionlimit()。曾经有个同事没注意这个,线上程序直接挂了...

图的连通性与强连通分量

连通性解决一个问题:从节点 A 能不能走到节点 B?

  • 无向图:如果任意两个节点都能互相到达,叫「连通图」。否则叫「非连通图」,里面分成多个「连通分量」。
  • 有向图:情况更复杂。A 能到 B,但 B 不一定能到 A。如果 A 和 B 能互相到达,它们属于同一个「强连通分量」(SCC)。

强连通分量在资金流中特别重要。为什么?因为资金闭环(循环转账)往往就藏在 SCC 里。

我的亲身经历:有一次做企业资金网络分析,发现一批公司之间频繁转账,但金额不大。用 SCC 算法一跑,发现它们属于同一个强连通分量——A 转给 B,B 转给 C,C 又转回 A。这就是典型的「资金空转」模式,后来查实是虚增流水。如果没有 SCC 分析,光看单笔交易根本发现不了。

找 SCC 的经典算法是 Kosaraju 算法或 Tarjan 算法。这里不展开代码,你只需要记住:SCC 是资金流网络中找闭环、找团伙的核心工具

本章知识体系总览

下面这张图总结了本章的核心内容,我手绘了一个结构图:

图论基础 图的分类 无向图 有向图 加权图 存储方式 邻接矩阵 邻接表 图的遍历 BFS DFS 连通性与强连通分量(SCC) 无向图连通分量 有向图强连通分量 Kosaraju/Tarjan 资金流网络:找路径、找闭环、找团伙

嗯,图论基础就这些。别小看这些概念,后面每一章都会用到它们。尤其是 SCC 和 BFS,在资金流路径挖掘中几乎是天天打交道的工具。

一句话总结:图是资金流网络的骨架,邻接表是存储的首选,BFS 找路径,SCC 找闭环。把这四个点吃透,后面实战就顺了。

专注资料整理