图结构和相关问题

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

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

图的存储:

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

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

设图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的伪代码:(可使用临界矩阵和邻接表实现)spa

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遍历图须要使用一个队列,经过反复取出队首顶点,将该顶点可到达的不曾加入过队列的顶点所有入队,(而不是未被访问)直到队列为空时遍历结束。
能够查看下面的伪代码,根据思路可使用邻接表和临界矩阵进行实现。code

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的数目有关,适合稀疏图(顶点多,边少)。

拓扑排序:

关键路径:

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

相关文章
相关标签/搜索