图结构和相关问题

Posted hhhuang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图结构和相关问题相关的知识,希望对你有一定的参考价值。

图的定义:图由顶点和边组成,每条边的两端是图的两个顶点。记作G(V,E),V是顶点集,E 为边集。

一般图分为有向图和无向图。
顶点的度是指和该顶点相连的边的条数。特变的对于有向图,顶点的出边条数成为出度,顶点的蠕变条数成为入度。顶点和边都可以由一些属性,称为点权和边权。

图的存储:

图可以使用两种存储方式:邻接矩阵和邻接表。

邻接矩阵:适合顶点数目不多的稠密图。

设图G(V,E)的顶点编号为0-N-1,那么可以令二维数组G[N][N]的两维分别表示图的顶点标号,及如果G[i][j]的值为1,表示i和j之间有变。这个二维矩阵被称为邻接矩阵。并且如果存在边权可以让G[N][N]中存放边权。
无向图的邻接矩阵是对称矩阵。

邻接表:适合顶点个数较多的稀疏图。

设图G(V,E)的顶点标号为0,1,… ,N-1,每个顶点都有可能有若干条出边,如果把同一个顶点的所有出边放在一个列表中,那么N个顶点就会有N个列表(没有出边,对应空表)。这N个列表被称为图的邻接表。记作Adj[N]。
邻接表实现可以使用变长数组vector,开一个vector数组Adj[N], N是顶点个数,每个顶点对应一个变长数组,存储其出边。
如下所示:
<Vector<int> Adj[N];>
如果节点还有权值,我们可以定义一个结构体:

Struct Node{
?????Int v;
?????Int w;
};

然后vector邻接表中的元素类型就是Node型的。
<Vector<Node> Adj[N];>

图的遍历:

是指对图的所有顶点按一定顺序进行访问,遍历方法一般有两种:深度优先搜索(DFS)和广度优先搜索(BFS)。

深度优先搜索以“深度”作为第一关键词,每次都沿着路径到不能再前进时才退回到最近的岔路口。

DFS的具体实现,首先介绍两个概念:
连通分量。在无向图中,如果两个顶点之间可以相互到达(可以是通过一定路径间接到达),那么就称这两个顶点连通。如果图G(V,E)的任意两个顶点都连通,则通图G为连通图,否则称G为非连通图,且称其中的极大连通子图为连通分量。
强连通分量。在有向图中,如果俩ing个顶点可以各自通过一条有向路径到达另一个顶点,就称这两个顶点强连通。如果图G(V,E)的任意两个顶点都强连通,则称图G为强连通图;否则称G为非强连通图,且称其中的极大强连通子图为强连通分量。
如果要遍历一个图就要对所有的连通块进行遍历,如果已知的图是连通图,则只需要一次DFS遍历就可以完成。
DFS的伪代码:(可以使用临界矩阵和邻接表实现)

DFS(u){//访问顶点u
    vis[u] = true; //设置u为已访问
    for(从u出发能到达的所有顶点v){  //枚举从u出发可以到达的所有顶点v
        if(vis[v] == false){
        DFS(v);
    }
}
DFSTrave(G){ //遍历图
    for(G 的所有顶点u) //对G的所有顶点u
        if vis[u] == false //如果u未被访问
            DFS(u); //访问u所在的连通块
    
}

广度优先搜索(BFS)遍历图

广度优先搜索以“广度”作为关键词,每次以扩散的方式向外访问顶点。和树的遍历一样,使用BFS遍历图需要使用一个队列,通过反复取出队首顶点,将该顶点可到达的未曾加入过队列的顶点全部入队,(而不是未被访问)直到队列为空时遍历结束。
可以查看下面的伪代码,根据思路可以使用邻接表和临界矩阵进行实现。

BFS(u){ //遍历u所在的连通块
    queue q;//定义队列q
    将u入队;
    inq[u] = true;
    while(q 非空){
        取出队首元素u进行访问;
        for(从u出发可达到的所有顶点v)
            if( inq[v] == false) { //如果v未曾加入过队列
                将v入队;
                inq[v] = true;
            }
    }
}
BFSTrave(G){
    for(G 的所有顶点u)
        if(inq[u] == false){ //如果u未曾加入过队列
            BFS(u); //遍历u所在的连通块
        }
}

最短路径问题:

最小生成树(Minimum Spanning Tree,MST):

是在一个给定的无向图G(V,E)中求一棵树T,使这棵树拥有图G中的所有顶点,且所有边都来自图G中,并且满足整棵树的边权之和最小。
最小生成树有三个性质需要掌握:
1. 最小生成树是树,因此其边数等于顶点数减一,且树内一定不会有环。
2. 对给定的图G(V,E),其最小生成树可以不唯一,但其边权之和一定是唯一的。
3. 由于最小生成树是在无向图上生成的,因此其根节点可以是这棵树上的任意一个结点。一般为了输出唯一,会指定一个结点作为根节点。
常用的算法有:Prim(普利姆算法)和 Kruskal算法(克鲁斯卡尔算法)

Prim算法

伪代码如下,其时间复杂度为O(V^2),如果图用邻接表实现,可以使用堆优化即使用优先级队列将复杂度降低为O(VlogV + E):

G为图,S是以及加入图中的顶点集,数组d为顶点与集合S的最短距离
Prim(G,d[]){
    初始化G[],d[],d[1] = 0;
    for(循环n次)
    {
        u = 使d[u]最小的还未被访问的顶点的标号;
        记录u已被访问;
        for(从 u 出发能到达的所有顶点v){
            if(v 未被访问 && 以u为中介点使得v与集合S的最短距离d[v]更优){
                将G[u][v]赋值给d[v];
            }
        }
    }
}

Kruskal算法

伪代码如下,其时间负责度主要在拍于函数上,是O(ElogE),其中E是图的边数。

int kruskal(){
    令最小生成树的边权之和为ans,最小生成树的当前边数Num_edge;
    将所有边按照边权从小到达排序;
    for(从小到大枚举所有边)
    {
        if(当前测试边的两个端点在不同的连通块中){ //判断是否在一个联通块中可以使用并查集
            将该测试边加入最小生成树;
            ans += 测试边的边权;
            最小生成树的当前边数num_edge+1;
            当前边数num_edge 等于定点数减一时结束循环;
        }
    }
    return ans;
}

从上面的时间复杂度分析可知,Prim 算法的时间复杂度与V相关,适合稠密图(顶点少边多),而kruskal算法的实际复杂度与E的数目有关,适合稀疏图(顶点多,边少)。

拓扑排序:

关键路径:

参考内容:《算法笔记》 胡凡 曾磊主编

以上是关于图结构和相关问题的主要内容,如果未能解决你的问题,请参考以下文章

从零开始配置vim(27)——代码片段

从零开始配置vim(27)——代码片段

RexNet片段记录

RexNet片段记录

图结构和相关问题

Wagtail - 在页面上呈现带有相关片段和标签的数据时遇到问题