首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

临接矩阵描述的图

图\(G = (V, E)\)的临接矩阵形式使用一个\(|V| \times |V|\)的矩阵来描述节点之间的链接关系,我们用符号\(M\)来标记图\(G\)的临界矩阵。 矩阵的第\(i\)行第\(j\)列的元素\(M_{i,j}\)描述了第\(i\)个节点与第\(j\)个节点之间的链接关系,可以形式化表示为: $$ M_{i,j} = \begin{cases} 1 & if (i,j) \in E \\ 0 & else \end{cases} $$ 如右图所示,是一个有向图的临接矩阵示例。图中矩阵的1表示对应行的节点有一条边指向了对应列的节点,0则表示不存在相应的边。 对于有向图而言,当\((i,j) \in E, i \neq j\)并且\((j,i) \notin E\)时,其临接矩阵就不是对称的。而无向图,\((i,j)\)与\((j,i)\)实际上是同一条边, 所以其临接矩阵是对称的。

本文中,我们以C++模板的形式实现图的临接矩阵表示。

1. 临接矩阵的定义

下面的代码片段是临接矩阵的主要成员变量定义。因为对于临接矩阵而言,其行和列索引分别对应着不同的节点,而矩阵的元素描述了边。所以,我们需要一个存储节点信息的线性表, 和一个存储边的二维数组。下面的代码片段中的成员变量nodes和matrix分别用于存储节点和边。

        template <class NodeType, class EdgeType>
        class GMatrix {
            protected:
                DataArray<NodeType> nodes;
                Array2D<EdgeType*> matrix;
        };

在上述代码片段中,成员matrix的元素是指针类型。如果两个节点之间存在连边关系,我们就构建一个EdgeType的对象来记录边的权重,并把对象的地址记录在临接矩阵中对应的位置上。 如果两个节点之间不存在连边关系,我们就将临接矩阵中对应的元素赋为空指针(NULL)。

目前我们为GMatrix提供了两个构造函数。其默认构造函数是什么也没有做。另一个构造需要一个输入参数指定图的节点数量,在成员变量的初始化列表中,我们指定了节点数组和临接矩阵的尺寸, 并在其函数体中,依次将matrix中的每个元素都赋为空指针。

        GMatrix() {}
        GMatrix(int n) : nodes(n), matrix(n, n) {
            for (int i = 0; i < n; i++)
                for (int j = 0; j < n; j++)
                    matrix(i, j) = NULL;
        }

此外,我们还重载了运算符"()",提供临接矩阵的下角标访问形式。分别为节点和边提供了一个下角标操作,只需要一个节点索引就可以获得对应节点的权重对象的引用。 如果提供了两个节点索引,那么将返回连接这两个节点的边对象的指针。如果得到的是空指针则说明对应节点之间不存在连边关系,这正是函数IsConnected的功能。 需要注意的是,这三个函数并没有检查输入数据的合理性,在使用过程中应当格外注意。

        NodeType & operator () (int i) { return nodes(i); }
        EdgeType * operator () (int i, int j) { return matrix(i, j); }
        bool IsConnected(int i, int j) const { return NULL != matrix(i, j); }

2. 临接矩阵的增删改

临接矩阵的增删改操作是指,向图中新增节点或者边、删除原图中的节点或边、修改节点的索引顺序或者节点和边的键值。 相比于临接表而言, 临接矩阵的增删操作比较繁琐。因为矩阵的行列索引分别对应着不同的节点,改变图的节点表述,往往需要大量的数据搬运操作。 用于存储节点和连边信息的数据结构DataArrayArray2D, 完成了这些数据搬运操作的封装。

2.1 增

        bool add_node(int i, NodeType const key) {
            int nv = nodes.size();
            if (i > nv || 0 != nodes.insert(i, key))
                return false;
            if (!matrix.insert_row(i, 1) || matrix.insert_column(i, 1))
                return false;
            nv = nodes.size();
            for (int idx = 0; idx < nv; idx++) {
                matrix(idx, i) = NULL;
                matrix(i, idx) = NULL;
            }
            return true;
        }
        bool add_edge(int i, int j, EdgeType const key) {
            int nv = nodes.size();
            if (i >= nv || j >= nv || IsConnected(i, j))
                return false;
            matrix(i, j) = new EdgeType(key);
            return true;
        }

右边是向临接矩阵描述的图中增加一个节点的函数,它有两个参数,其中i表示插入的节点索引,key则是节点的权重。该函数返回一个bool类型的数据,用于告知用户操作是否成功。

首先在第2行中,获取当前节点的数量。在第3行的if条件中先检查输入的索引,确保不会超出存储界限。 然后通过节点的数组容器接口,在第i个索引上插入新增节点的键值。 如果成功插入键值,nodes对象的成员函数insert将返回0,出错将返回错误编号。 紧接着,我们在第5行的条件中,先后对二维数组matrix插入新行和新列。

至此,我们已经完成了新增节点后的内存扩充。看起来,我们只是简单的在4个if语句中完成了这一过程,实际上进行了大量的数据搬运操作,只是容器的封装隐藏了很多细节。

接下来,我们还需要对矩阵的新增元素赋予初值。第7行获取扩容后的节点数量,之后在一个for循环中依次将各个新增元素赋为空指针。

新增边的操作就相对简单很多,因为矩阵中的每个元素都对应着一条边,我们只要保证节点索引不要越界,并且原来不存在连边关系即可。如右边的代码所示,在通过了第16行的if条件判定之后, 我们就直接构建了一个边对象并将地址赋给了矩阵中的对应元素。

2.2 删

如下面的代码所示,对于临接矩阵的删操作还是比较简单的。删除边时,我们确认节点索引没有越界后,就可以释放对应边的内存并将之赋为空指针。 删除节点时,我们只需要先检查节点索引不要越界,然后释放相关连边的对象,最后从数组nodes中移除节点的键值, 并依次删除矩阵的行和列。

        bool remove_node(int i) {
            int nv = nodes.size();
            if (i > nv || 0 != nodes.remove(i, i+1))
                return false;
            for (int idx = 0; idx < nv; idx++) {
                remove_edge(idx, i);
                remove_edge(i, idx);
            }
            if (!matrix.remove_row(i, 1) || !matrix.remove_column(i, 1))
                return false;
            return true;
        }
        bool remove_edge(int i, int j) {
            int nv = nodes.size();
            if (i >= nv || j >= nv)
                return false;

            if (NULL != matrix(i, j))
                delete matrix(i, j);
            matrix(i,j) = NULL;

            return true;
        }

2.3 改

修改节点或者边的键值很简单,我们只需要通过重载的运算符"()"获取待修改的对象,然后赋值即可。类似如下的代码片段,修改对象gmatrix的元素。

        GMatrix<double, double> gmatrix(3);
        gmatrix(0) = 3.1415926:
        *gmatrix(1,2) = 1.41421;

有时因为计算的需要,我们会交换图的两个节点。此时,除了要交换节点数组nodes的相关元素外,还需要交换临接矩阵matrix对应的行和列,如下面的函数所示。

        bool swap_nodes(int i, int j) {
            int nv = nodes.size();
            if (i >= nv || j >= nv || 0 != nodes.swap(i, j) || !matrix.swap_row(i, j) || !matrix.swap_column(i, j))
                return false;
            return true;
        }

3. 完

本文中,我们用C++模板提供了临接矩阵形式的图。矩阵的行索引和列索引分别对应着图的节点。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1