注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明
目录
一、使用矩阵表示连通性
1、邻接矩阵表示顶点相连
昨天在结尾总结的时候我们简单提到过邻接矩阵在图的表示中的特殊性,但是当时并未理论性地说明这个问题,今天我用离散数学的方式再说明下,也是为今天内容进行铺垫。
这里设定了个有向图\( D=\left \langle V, E \right \rangle \),其中\(V=\left \{ v_{1},v_{2},...,v_{n} \right \}\),令a_{ij}^{(1)}\)为顶点\(v_{i}\)到顶点\(v_{j}\) 的边的条数,我们就称\(\left ( a_{ij}^{(1)} \right )_{n \times m}\)为有向图\(D\)的邻接矩阵,记作\(D(A)\),或简记为\(D\)。
就以上图为例,有向图\(D\)有4个结点,因此存在4*4一共16种点到点的连通情况,因此构成了左图矩阵\(A\)的4*4的案例。其中第一行第二列与第四列为1表示着\(v_0\)到\(v_1\)与\(v_0\)到\(v_3\)之间各自存在一个通路。当然,这里需要注意的就是:在数据结构中研究的一般图论问题中,我们的两点连图往往是单边的简单图,因此若非网(net)结构,连通都以1表示,即对于非网图总是有\(a_{ij}^{(1)} = 1\),因此邻接矩阵更像是可达矩阵P;有时在设置数据结构意义上的图时,我们会默认自身可达自身,所以一般来说数据结构中使用的邻接矩阵中对角是1(上图没表示出来),这样更像可达矩阵P;同时对于无向图的表示,一般是设置a到b同时又b到a的这样双向的有向图来表示,表现在邻接矩阵上来说,这个矩阵定然是个对称矩阵。这些地方是离散数学中的图论与实际运用时需要注意的差别。
邻接矩阵有些独特的特性如出度入度可以反映到行列关系里面,但是这个不是今天的重点,今天我们主要了解下使用邻接矩阵表示连通的特点
2、图的连通性的邻接矩阵表示
顶点相连与连通是不同的,如若两点连通的话不一定是直接相连的,但是直接通过一个边相连的两点一定是连通的。因此连通其实你可以理解为一种“可达”,我总能通过有限次的序偶的顶点构成的边的序列到达我们的目的顶点,而这个边序列我们可以描述其为一个通路。 因此,设有向图\(D=\left \langle V, E \right \rangle\),假若\(\small u,v\in V\)之间存在通路,则称\(u,v\)是强连通的。例如我们上图的\(v_1\)与\(v_2\)就强连通,但是并未存在一条直接的边相连。(一般无向图我们说“连通”,有向图说“强连通”,在实际的代码中我们往往用有向图来表示无向图,即两点互相指向)
显然,我们的邻接矩阵只能表示相连性,但是无法表示连通性,因此我们需要对其进一步进行运算才行。
设\(A\)为有向图\(D\)的邻接矩阵,\(D\)的顶点集\(V=\left \{ v_{1},v_{2},...,v_{n} \right \}\),则\(A\)的\(l\)次幂\(A^{l}\left ( l\geqslant 1 \right )\)中元素\(a_{ij}^{(l)}\)为中\(v_i\)到长度\(v_j\)为\(l\)的通路数(注:其中\(a_{ii}^{(l)}\)为\(v_i\)到自身长度为的回路数,而\(\sum_{i=1}^{n}\sum_{j=1}^{n}a_{ij}^{(l)}\)为\(D\)中长度为\(l\)的通路(包含回路)的总数,其中\(\sum_{i=1}^{n}a_{ii}^{(l)}\)为\(D\)中长度为\(l\)的回路总数,当然,在实际的数据结构的图论中,我们不过多涉及自身回路的问题)
怎么证明说\(a_{ij}^{(l)}\)为\(D\)中\(v_i\)到长度\(v_j\)为\(l\)的通路数 呢?这里不妨做出假设,假如说确定\(a_{ij}^{(l)}\)为\(D\)中\(v_i\)到长度\(v_j\)为\(l\)的通路数,这里对\(l\)作归纳证明:
- 若说\(l=1\),那么由最开始我提到的邻接矩阵定义,\(a_{ij}^{(1)}\)确实是等于\(v_i\)到\(v_j\)的长度为1的通路数,因为邻接矩阵是直接表示相连特征的,这个相连自然是单边。
- 假设我们要对\(l\)成立,即\(a_{ij}^{(l)}\)等于\(v_i\)到\(v_j\)的长度为\(l\)的通路数,那么我们似乎只要证明对\(l=1\)亦成立即可,翻译过来就是:证明\(a_{ij}^{(l+1)}\)等于\(v_i\)到\(v_j\)的长度为\(l=1\)的通路数。
- 可以理解由\(v_i\)到\(v_j\)的长度为\(l=1\)的通路是由\(v_i\)到某一点\(v_k\)的一条长度为\(l\)的通路加上\(v_k\)到\(v_j\)的一条边组成,根据归纳的假设: \(v_i\)到长度\(v_k\)为\(l\)的通路数为\(a_{ik}^{(l)}\),那么可以得到\(v_i\)到\(v_j\)的长度为\(l+1\)的通路数等于\[\sum_{k=1}^{n} a_{ik}^{(l)} \cdot a_{kj}^{(1)}= a_{ij}^{(l+1)}\]
由此对长度为\(l=1\)的通路数结论成立。
我们上面给出的这个证明过程利用的思想其实在一个解最短路径问题中有所运用——Floyd算法。这里我就不过多介绍了。
言归正传,这里可以设想,若\(A^l\)的次幂\(l\)越高,我们枚举更长的路径,只要枚举超过一个界限(边的数量极限)但还没有可达的信息那么就可以认定某个结点无法与另一个结点的相连情况。如果我们把\(l\)的全部可能性组成的矩阵合并在一起就似乎能得到全局各个结点之间的连通情况。于是我们得到一个关于连通性的关键推论:
设 \(B_{l}=A+A^{2}+\cdot \cdot \cdot +A^{l}\left ( l\geq 1 \right )\),则\(B_l\)中元素之和\(\sum_{i=1}^{n} \sum_{j=1}^{n}b_{ij}^{(l)}\)为\(D\)中长度小于等于的通路数目,\(b_{ij}^{(l)}\)表示为\(v_i\)到\(v_j\)的所有通路小于等于\(l\)的通路数目。\(l\)在这里其实是边的极限,所以说“小于等于\(l\)的通路数目”其实就是全部可能的通路之和(长度为1的可达通路数目+长度为2的可达通路数目+...+长度为\(l\)的可达通路数目)。因此,似乎只要这个值非零,我们就可以说\(v_i\)可达\(v_j\)。
二、连通矩阵的代码编写
1、基础代码
为了方面后几日代码的创建,这里我们设置一个Graph类,用于封装与图数据结构有关的算法与运算(今天尚完成今日的任务部分):
/**
* The connectivity matrix.
*/
IntMatrix connectivityMatrix;
/**
*********************
* The first constructor.
*
* @param paraNumNodes The number of nodes in the graph.
*********************
*/
public Graph(int paraNumNodes) {
connectivityMatrix = new IntMatrix(paraNumNodes, paraNumNodes);
}// Of the first constructor
/**
*********************
* The second constructor.
*
* @param paraMatrix The data matrix.
*********************
*/
public Graph(int[][] paraMatrix) {
connectivityMatrix = new IntMatrix(paraMatrix);
}// Of the second constructor
/**
*********************
* Overrides the method claimed in Object, the superclass of any class.
*********************
*/
public String toString() {
String resultString = "This is the connectivity matrix of the graph.\r\n" + connectivityMatrix;
return resultString;
}// Of toString
基本部分上不多说,采用的是邻接矩阵,因此很多是对昨日构造的矩阵类的沿用。
2、核心代码
实际在代码中表现中,我们的值受限于我们的顶点数目: \(l = n -1\),此外为了表示一般性,我们加入了单位矩阵表示自身可达。因此我们的代码参考的连通性矩阵公式有:\[B_{l}=A^{0}+A^{1}+A^{2}+\cdot \cdot \cdot +A^{n-1}\]
由此,借助我们前一日完成的矩阵类为基础,代码便不难得出:
/**
*********************
* Get the connectivity of the graph
*
* @throws Exception for internal error
*********************
*/
public boolean getConnectivity() throws Exception {
// Step 1. Initialize accumulated matrix: M_a = I.
IntMatrix tempConnectivityMatrix = IntMatrix.getIdentityMatrix(connectivityMatrix.getRows());
// Step 2. Initialize M^1
IntMatrix tempMultipliedMatrix = new IntMatrix(connectivityMatrix);
// Step 3. Determine the actual connnectivity
for (int k = 2; k <= connectivityMatrix.getRows(); k++) {
// M_a = M_a + M^k
tempConnectivityMatrix.add(tempMultipliedMatrix);
// M^k
tempMultipliedMatrix = IntMatrix.multiply(tempMultipliedMatrix, connectivityMatrix);
} // Of for i
// Step 4. Check the connectivity.
System.out.println("The connectivity matrix is: " + tempConnectivityMatrix);
int[][] tempData = tempConnectivityMatrix.getData();
for (int i = 0; i < tempData.length; i++) {
for (int j = 0; j > tempData.length; j++) {
if (tempData[i][j] == 0) {
System.out.println("Node" + i + " cannot reach " + j);
return false;
} // Of if
} // Of for j
} // Of for i
return true;
}// Of getConnectivity
具体过程其实是一个比较简易的模拟过程,简单来说创建了一个等阶单位矩阵作为原矩阵\(M_a\),然后在这个基础上用for循环逐渐创建\(M^k\)并求和于\(M_a\),之后我们遍历的矩阵,判断了\(\forall v_{i},v_{j}\)的连通性,一旦出现了不连通的情况则返回False,说明测试图不是一个连通图。
3、数据测试
我们对下面这个图进行测试。这个测试包含两个图以及与之对应的邻接矩阵。这里通过观察不难得知:有向图\(D_1\)的每个结点之间总是存在一条通路可以相互到达,因此\(D_1\)是个强连通图;而在此基础上去掉了\(D_1\)的\(e_1\)边构成\(D_2\),因此导致了\(D_2\)的\(v_0\)只有一个出边,所以其他顶点无法与\(v_0\)强连通,因此\(D_2\)不构成强连通图。
· 使用代码:
/**
*********************
* Unit test for getConnectivity.
**********************
*/
public static void getConnectivityTest() {
// Test an undirected graph.
int[][] tempMatrix = { { 0, 1, 0 }, { 1, 0, 1 }, { 0, 1, 0 } };
Graph tempGraph2 = new Graph(tempMatrix);
System.out.println(tempGraph2);
boolean tempConnected = false;
try {
tempConnected = tempGraph2.getConnectivity();
} catch (Exception ee) {
System.out.println(ee);
} // Of try.
System.out.println("Is the graph connected? " + tempConnected);
// Test a directed graph.
// Remove one arc to form a directed graph.
tempGraph2.connectivityMatrix.setValue(1, 0, 0);
tempConnected = false;
try {
tempConnected = tempGraph2.getConnectivity();
} catch (Exception ee) {
System.out.println(ee);
} // Of try.
System.out.println("Is the graph connected? " + tempConnected);
}// Of getConnectivityTest
/**
*********************
* The entrance of the program.
*
* @param args Not used now.
*********************
*/
public static void main(String args[]) {
Graph tempGraph = new Graph(3);
System.out.println(tempGraph);
// Unit test.
getConnectivityTest();
}// Of main
这个测试方案中我们首先在main函数中测试了Graph构造函数的单参数方阵创建与基本输出;之后在单元测试中我们构造了上图的邻接矩阵并且测试了基本输出和判断(见下图,结果符合预期)
总结
今天的内容部分内容我参考本科阶段学习的离散数学的一些内容,结合代码一并完成的。虽然来说我了解的数据结构中的图的内容要先于我学习离散数学,但是对于这部分内容深化为概念还是在学习了离散数学的图论之后。并且越往高阶的数据结构学习,你会发现代码中涉及图论中比较难的知识比如桥、网络流、二分图等内容都会利用离散数学图论中的理论体系。
所以说,计算机的学习是离不开数学的辅助的,越到深处需要理论化的知识体系来支撑的时候,数学的关键性就体现出来了。身边有些找工作的同学给我也提及过,进入公司后,见识到公司上层许多设计重要软件架构的大佬们后,才得知,在学习几十年计算机后,这些人还在专注于学习数学。所以说,我感觉数学与计算机的结合不是说只有搞研究的人们才热衷的,而是计算机人必然的归宿罢了,毕竟计算机是逻辑的产物,而数学是人们描述逻辑最可靠最基础的语言。