图-知识点(未完,ohno)

图是一种用于表示对象之间的存在关系的方式。即图是对象的一个集合。

定义

(复杂的概念你给我好好记住!

图是有限集 V 和 E 的有序对,即 G = (V, E),其中 V 的元素被称为顶点(也称节点或点),E 的元素称为边(也称弧或线)。对于边,有些边是带方向的,有些边是不带方向的。故图也可区分为:

  • 有向图:其中的边都具有方向, (u, v) 和 (v, u ) 不同。

  • 无向图:其中的边都没有方向, (u, v) 和 (v, u) 相同。

当且仅当 (i, j) 是图的边,称顶点 i 和 j 是邻接的。边 (i, j ) 关联与顶点 (i, j)。

对于有向图当中,定义更加精确。有向边 (i, j) 是关联至顶点 j 而关联于顶点 i ,顶点 i 邻接至顶点 j。

在图的一些应用当中,我们可能需要为每条边赋予一个表示成本的值(可将原有的值看作1),我们称之为权。这样的图称为加权有向图加权无向图。一个网络经常指一个加权有向图和加权无向图。

简单路径:一个路径,如果除第一个和最后一个顶点之外,其余所有顶点均不同,那么该路径称为一条简单路径。

图或有向图中的每一条边都可以有长度,一条路径的长足则是该路径中所有边的长度之和。

  • 连通图:G连通的当且仅当每一对顶点之间都有一条路径。
  • 非连通图:存在两个顶点之间未被联通。

子图:如果 H 的顶点和边的集合分别是 G 的顶点和边的集合的子集,那么称图 H 是 G的子图。

环路:一条始点和终点相同的简单路径称为环路。

生成树:一个G 的子图,如果包含 G 的所有顶点,且是一棵树,则称为 G 的生成树。

度:在一个无向图中,与一个顶点 i 相关联的边数称为该顶点的度。若G是有向图,则顶点 i 的入度是关联至该顶点的边数(指向它),顶点 i 的初读是指关联于该顶点的边数(指出)。

完全图:一个具有 n 个顶点和 n(n - 1 )/ 2 条边的无向图是一个完全图,有 n 个顶点的有向图有 n(n - 1) 条边,则此图为完全有向图

强连通:一个有向图是强连通的,当且仅当对于每一对不同顶点 i 和 j,从 i 到 j 和从 j 到 i 都有一条有向路径。非强连通图的极大强连通子图被叫做强连通分量。

图的特性

  • 设 G = (V, E) 是一个无向图,令 n = V ,e = E ,则:
    • $\sum_{i = 1}^{n}d_i = 2e$;
    • $0 <= e <= n(n - 1) / 2$。

(在无向图中,每一条边都与 2 个顶点相关联,因此顶点的度之和是边数的两倍。一个顶点的度在0 到 n - 1 之间,因此度的和在 0 到 n(n - 1)之间)

  • 设 G = (V, E )是一个有向图,令 n = V ,e = E ,则:
    • $0 <= e <= n(n - 1)$;
    • $\sum_{i = 1}^{n}d_i^{in} = \sum_{i = 1}^{n}d_i^{out} = e$。

ADT

对于一个图,应该具备以下的操作:

functions description
vertex_count() 返回图中的顶点的数目
vertices() 迭代返回图中的所有顶点
edge_count() 返回图中的边的数目
edges() 迭代返回图中的所有边
get_edge(u, v) 返回从顶点u到顶点v的边,如果不存在则返回None;对于无向图,get_edge(u, v)和get_edge(v, u) 之间没有区别
degree(v, out = True) 对于一个无向图,返回其他边入射到 v 的数目。对于一个有向图,返回入射到顶点 v 的输出边的数目
incident_edges(v, out = True) 返回所有边入射到顶点 v 的迭代循环。在有向图的情况下,默认是输出边。
insert_vertex(x = None) 创建和返回一个新的存储元素 x 的 vertex
insert_edge(u, v, x = None) 创建和返回一个新的从顶点 u 到顶点 v 的存储元素 x 的 Edge.
functions description
remove_vertex(v) 移除顶点 v 和图中它的所有入射边
remove_edge(e) 移除图中的边 e

图的存储表示

无权图的表示

邻接矩阵

一个 n 顶点图 G = (V, E) 的邻接矩阵(adjacent matrix) 是一个 n x n 的矩阵,其中每个元素是0 或1。如果 G 是无向图,那么其中的元素定义如下:

  • A(i, j) = 1,如果 $(i, j )∈ E 或 (j, i) ∈ E$;
  • A(i, j) = 0,其他。

如果 G 是有向图,那么其中的元素定义如下:

  • A(i, j) = 1,如果$(i, j )∈ E$ ;
  • 0,其他。

由上述定义中的描述,我们可以有如下的描述:

  • 对于 n 个顶点的无向图,有 $A(i, i) = 0,即对角线上的值为0$;
  • 无向图的邻接矩阵是对称的,即$A(i, j) = A(j, i)$,根据这个特性,在存储无向图时,可以选择一种更加节约空间的方法。
  • 对于 n 个顶点的有向图,有$\sum_{j = 1}^{n}A(i, j) = d_i^{out}$ 和 $\sum_{j = 1}^{n}A(j, i) = d_i^{in}$。

在存储时,可以把 n x n 邻接矩阵A 映射到一个 $(n + 1) * (n + 1)$ 的布尔型数组,因为所有的对角线元素都是零而不需要存储,所以可以进一步的减少 n 字节的存储空间。把对角线元素去掉,即可以得到一个上/下三角矩阵,这些三角矩阵可以压缩到一个 $(n - 1) * n$ 的矩阵中。

然而,上述方法虽然减少了部分的存储空间,但是却付出了不少代价,因为顶点的外部索引和在图中的内部描述不一致。如此,代码的访问容易出错,且访问边的时间也会增加。

typedef struct
{  int no;
   InfoType info;
}VertexTtype
typedef  struct
{ int edges[MAXV][MAXV]; /*各顶点之间的关系或权值*/
   int  n,e; /*顶点数,边(或弧)的个数*/
   VertexType  vexs[MAXV];  /*存储顶点元素*/
 }MGraph;
  

邻接表/邻接链表

在一个图的邻接表描述当中,图的每一个顶点都有一个邻接表。当邻接表用链表表示时,就是邻接链表(liked-adjacency-list)。

一个指针和一个整数各需要 4 字节的存储空间,因此用邻接链表描述一个 n 顶点图需要 8(n + 1)字节来存储 n + 1 个 firstNode 指针和 listSize 域,需要 4 * 2 * m 字节来存储 m 个链表节点。

邻接数组

在邻接数组中,每一个邻接表用一个数组线性表而非链表来描述。可以选择的是每个节点后连接一个数组线性表。另一个选择是使用二位不规则数组,其中 aList[i] 容量等于顶点 i 的邻接表长度。 邻接数组必邻接链表少用了 4m 字节,因为不需要 next 指针域。而对于大部分的图操作,无论是用邻接链表,还是用邻接数组,其渐近时间复杂度是相同的。

十字链表

有邻接表不同,十字链表法适用于存储有向图(当然无向图也有对应的存储方法)。

所谓十字链表,就是各个链表相互串联,错综复杂。那么将各个节点和链表连接起来的就是图中的顶点之间的关系。

首节点:

首节点中有一个数据域和两个指针域:

  • fisrtin 指针用于连接以当前顶点为弧头的其他顶点构成的链表;
  • firstout 指针用于连接以当前顶点为狐尾的其他顶点构成的链表;
  • data 用于存储该顶点上的数据。

普通节点:

  • tailvex 用于存储以首元节点为弧尾的顶点位于数组中的位置下标;
  • headvex 用于存储以首元节点为弧头的顶点位于数组中的位置下标;
  • hlink 指针:用于链接下一个存储以首元节点为弧头的顶点的节点;
  • tlink 指针:用于链接下一个存储以首元节点为弧尾的顶点的节点;
  • info 指针:用于存储与该顶点相关的信息,例如量顶点之间的权值;
#define  MAX_VERTEX_NUM 20
#define  InfoType int//图中弧包含信息的数据类型
#define  VertexType int
typedef struct ArcBox{
    int tailvex,headvex;//弧尾、弧头对应顶点在数组中的位置下标
    struct ArcBox *hlik,*tlink;//分别指向弧头相同和弧尾相同的下一个弧
    InfoType *info;//存储弧相关信息的指针
}ArcBox;
typedef struct VexNode{
    VertexType data;//顶点的数据域
    ArcBox *firstin,*firstout;//指向以该顶点为弧头和弧尾的链表首个结点
}VexNode;
typedef struct {
    VexNode xlist[MAX_VERTEX_NUM];//存储顶点的一维数组
    int vexnum,arcnum;//记录图的顶点数和弧数
}OLGraph;
int LocateVex(OLGraph * G,VertexType v){
    int i=0;
    //遍历一维数组,找到变量v
    for (; i<G->vexnum; i++) {
        if (G->xlist[i].data==v) {
            break;
        }
    }
    //如果找不到,输出提示语句,返回 -1
    if (i>G->vexnum) {
        printf("no such vertex.\n");
        return -1;
    }
    return i;
}
//构建十字链表函数
void CreateDG(OLGraph *G){
    //输入有向图的顶点数和弧数
    scanf("%d,%d",&(G->vexnum),&(G->arcnum));
    //使用一维数组存储顶点数据,初始化指针域为NULL
    for (int i=0; i<G->vexnum; i++) {
        scanf("%d",&(G->xlist[i].data));
        G->xlist[i].firstin=NULL;
        G->xlist[i].firstout=NULL;
    }
    //构建十字链表
    for (int k=0;k<G->arcnum; k++) {
        int v1,v2;
        scanf("%d,%d",&v1,&v2);
        //确定v1、v2在数组中的位置下标
        int i=LocateVex(G, v1);
        int j=LocateVex(G, v2);
        //建立弧的结点
        ArcBox * p=(ArcBox*)malloc(sizeof(ArcBox));
        p->tailvex=i;
        p->headvex=j;
        //采用头插法插入新的p结点
        p->hlik=G->xlist[j].firstin;
        p->tlink=G->xlist[i].firstout;
        G->xlist[j].firstin=G->xlist[i].firstout=p;
    }
}

有权图的表示

对于有权图,唯一改变的就是边具有了权值,因此将无权图的描述进行简单的扩充(节点域)就可以得到加权图的描述。

成本邻接矩阵

图的遍历

深度优先搜索(DFS)

(老生常谈了,贴个图就行了

广度优先搜索(BFS)

最小生成树

要想了解最小生成树,先要知道什么是生成树:

定义:一个连通图的生成树是一个极小的连通子图,它包含着图中全部的 n 个节点,但只有构成树的 n - 1 条边。

最小生成树:一个带权图的最小生成树,就是原图中边的权值的和最小的生成树。

我们下文要讲的 Prim 算法和 Kruskal 算法,均是求解最小生成树的方法。

Prim 算法

Prim 角度主要从点来出发,其将点划分为2类,已经使用过的和未使用过的。

算法大致步骤如下:

  • 选取一个初始节点,将其添加至 MST 中;
  • 找到MST 中所有元素的相邻的节点,并选取其中权值最小的那个节点;
  • 重复上述的第二个步骤,直至所有的节点都被选取。

#define MAX  100000
#define VNUM  10+1                                             //这里没有ID为0的点,so id号范围1~10

int edge[VNUM][VNUM]={/*输入的邻接矩阵*/};
int lowcost[VNUM]={0};                                         //记录Vnew中每个点到V中邻接点的最短边
int addvnew[VNUM];                                             //标记某点是否加入Vnew
int adjecent[VNUM]={0};                                        //记录V中与Vnew最邻近的点


void prim(int start)
{
     int sumweight=0;
     int i,j,k=0;

     for(i=1;i<VNUM;i++)                                      //顶点是从1开始
     {
        lowcost[i]=edge[start][i];
        addvnew[i]=-1;                                         //将所有点至于Vnew之外,V之内,这里只要对应的为-1,就表示在Vnew之外
     }

     addvnew[start]=0;                                        //将起始点start加入Vnew
     adjecent[start]=start;
                                                 
     for(i=1;i<VNUM-1;i++)                                        
     {
        int min=MAX;
        int v=-1;
        for(j=1;j<VNUM;j++)                                      
        {
            if(addvnew[j]!=-1&&lowcost[j]<min)                 //在Vnew之外寻找最短路径
            {
                min=lowcost[j];
                v=j;
            }
        }
        if(v!=-1)
        {
            printf("%d %d %d\n",adjecent[v],v,lowcost[v]);
            addvnew[v]=0;                                      //将v加Vnew中

            sumweight+=lowcost[v];                             //计算路径长度之和
            for(j=1;j<VNUM;j++)
            {
                if(addvnew[j]==-1&&edge[v][j]<lowcost[j])      
                {
                    lowcost[j]=edge[v][j];                     //此时v点加入Vnew 需要更新lowcost
                    adjecent[j]=v;                             
                }
            }
        }
    }
    printf("the minmum weight is %d",sumweight);
}

Kruskal 算法

相对于 Prim 算法, Kruskal 算法要更加”高级“一点。

定义:Start with an empty structure for the MST Place all edges into a priority queue based on their weight (cost). While the priority queue is not empty: Dequeue an edge e from the priority queue. If e’s endpoints aren’t already connected, add that edge into the MST. Otherwise, skip the edge.

简单描述就是:

  • 将所有边的权值添加到一个优先队列当中;
  • 从小到大依次进行选择,若该选择会导致环的产生,则舍弃;
  • 重复上述步骤,直至满足最小生成树的边的条件。

(ps: 在判断环的产生时,可以判断其是否能回到自己)

Dijkstra 算法

定义:consider every vertex to have a cost of infinity, except v1 which has a cost of 0. create a priority queue of vertexes, ordered by cost, storing only v1 . while the pqueue is not empty: dequeue a vertex v from the pqueue, and mark it as visited. for each unvisited neighbor, n, of v, we can reach n with a total cost of (v’s cost + the weight of the edge from v to n). if this cost is cheaper than n’s current cost, we should enqueue the neighbor n to the pqueue with this new cost, and remember v was its previous vertex. when we are done, we can reconstruct the path from v2 back to v1 by following the path of previous vertices.

拓扑排序

应用