数据结构--图

1. 术语

  1. V(Vertex): 顶点
  2. E(Edge): 边
  3. (v, w): v顶点与w顶点双向连接
  4. <v, w>: v顶点指向w顶点的单项连接

2. 图的表示

2.1 邻接矩阵法

G[i][j] = 1: <vi, vj>是G的边

​ =0: 无边

0 1 ... n
0 0 1 0
1 1 0
... 0 0
n ... ... ... 0
  1. 无向图用全阵浪费一半空间

    方案:用链表(单行)表示元素下表:(i*(i+1)/2+j)

    ps:整个表看起来:对角线为0,对称

    0
    ... 0
    ... ... 0
    ... ... ... 0
    ... ... ... ... 0
  2. 对于网络:值表示权重

    网络:有权重的图


2.2 邻接表法

ps:用这种方法表示的图够稀疏才划算,且:图的表示并非只有这两种,要根据实际情况来设计

G[0]: 指针数组

G[0] --> |下标|指针域| --> |下标|指针域| --> ...

G[1] --> |下标|指针域| --> |下标|指针域| --> ...

...

G[n-1] --> |下标|指针域| --> |下标|指针域| --> ...

ps: 对于带权的网络,还要另加一个域表示权重

特点:

  1. 方便找临界点
  2. 节约稀疏图的空间(N个头指针+2E个节点)
  3. 计算“度”
    • 无向图:方便
    • 有向图:不方便,需构造“你邻接表”来计算“入度”
  4. 不方便检查对顶点是否存在边

3. 图的遍历

3.1 DFS

void DFS(Vertex V){
    visited[V] = true;  //点亮第一盏灯
    for(v的每一个邻接点W){  //观察与v相连的灯
        if(!visited[W])  //若未被点
            DFS(W);  //点亮,进入堆栈
    }
}

对于N个顶点,E条边的图,时间复杂度:

  1. 邻接表:O(N+E)
  2. 邻接矩阵:O(N2)

自然语言描述:往下走,遇到路口挑第一个走,走到不通就往回退,退到起点遍历完成


3.2 BFS

类似树的BFS,树是特殊的图,借助队列来实现

void BFS(Vertex V){
    visited[V] = true;//点灯
    Enqueue(V, Q);//将与V相连的点入队
    while(!IsEmpty(Q)){//队列不空
        V = Dequeue(Q);//出队
        for(V的每个邻接点W){
            visited[w] = true;//电灯
            Enqueue(W,Q);//入队
        }
    }
}

对于N个顶点,E条边的图,时间复杂度:

  1. 邻接表:O(N+E)
  2. 邻接矩阵:O(N2)

3.3 总结

  1. 为何需要两种遍历

    每种遍历适用于不同情况,故两种皆有用

    • BFS:优点是可以得到最优解,缺点是在树的层次较深并且子节点个数较多的情况下,消耗内存现象十分严重。因此,BFS适用于节点的子节点个数不多,并且树的层次不太深的情况。
    • DFS:优点是消耗内存小,缺点是难以寻找最优解,仅仅只能寻找有解。
  2. 图不连通怎么办

    //每一个DFS都是在把图的一个连通分量遍历(不连通的部分为一个连通分量)
    void ListComponent(Graph G){
        for(each V in G)//每个连通分量处理一遍
            if(!visited[V])
                DFS(V);
    }
    

4. 最短路径

源点:起点

最短路径

  1. 单源最短路径问题(从某点到其他所有点的最短路径)
    • 无权图单源
    • 有权图单源
  2. 多源最短路径问题(求任意的顶点的最短路径)

4.1 无权单源

按递增(非递减)的顺序找出到各个顶点的最短路

类似BFS一样利用队列向外一圈一圈拓展开来,以计算和记录圈层和最短路径经过的顶点

v3 --> v1 --> v4

(无法上传图,只能创一个像链表一样的图)

0: v3

1: v1

2: v4

代码:

//dist[W] = S到W的最短路径
//dist[S] = 0
//path[W] = S到W的路上经过的某顶点

//与BFS类似,只不过这里不用点亮而是用记录的方式得到最短路径。
void Unweighted(Vertex S)
{
	Enqueue(S,Q);
    while(!Empty(Q)){
        V = Dequeue(Q);
        for(V的每个邻接点W) 
            if(dist[W] == -1)  //如果dist[W]等于初始值-1
            	dist[W] = dist[V] + 1;  //距离+1
        		path[W] = V;  //记录路径上一个点,利用堆栈可以得到路径上所有的点
        		Enqueue(W,Q); 
    }
}

4.2 有权单源(Dijkstra算法)

按递增(非递减)的顺序找出到各个顶点的最短路

Dijkstra算法

  • 令S = {源点s + 已经确定了最短路径的顶点vi}
  • 对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径仅经过S中的顶点。即路径{s --> (vi in S) --> v 的最小长度}
  • 若路径是按照递增(非递减)的顺序生成的,则
    • 真正的最短路必须只经过S中的顶点
    • 每次从为收录的顶点中选一个dist最小的记录(贪心算法的思想)
    • 增加一个v进入S,可能影响另外一个w的dist值(新增的这个v一定在s到w的路径上,更即s是w的邻接点,v新增进S,只能影响v的邻接点的dist值,如果不是,那么dist值就不是S内的最短路了,而是全部点的最短路了,这冲突了)
      • dist[w] = min
/* 应该将源点和第一圈层的顶点的值正确赋值(如源点的dist从初始值无穷大赋值为0后),然后将源点收录,再进入Dijkstra算法 */
void Dijkstra(Vertex s)
{
    while(1){
        V = 未收录顶点中dist最小者;
        if( 这样的V不存在 )
            break;
        collected[V] = true;  //表示V边已被收录
        for( V的每个邻接点W )
            if( collected[W] == false )  //未被收录的点
                if( dist[V]+E<v,w> < dist[W] ){  //dist的初始值应该定义为无穷大
                    dist[W] = dist[V] + E<v,w>;
                    path[W] = V;  //记录W的上一个点是V
                }
    }
}
/* 不能解决有负边的情况 */
//dist 初始值应该是无穷大
//path 初始值应该是-1

算法案例在这里

计算算法复杂度方法:

  1. 直接扫描所有未收录顶点--O(|V|)
    • T = O(|V|2) + |E|)
    • 对于稠密图效果好
    • 稠密图(V 与 E 的数量不在同一个数量级的差别上)
  2. 将dist存在最小堆中--O(log|V|)
    • 更新dist[w]的值 - O(log|V|)
    • T = O(|V|log|V| + |E|log|V|) = O(|E|log|V|)
    • 对于稀疏图效果好

4.3 多源最短路算法

  • 方法一:直接将单源最短路算法调用|V|遍

    • T = O(|V|3 + |E|*|V|) ——对于稀疏图效果好
  • 方法二:Floyd算法

    • T = O(|V|3) ——对于稠密图效果好

Folyd算法:每一条最短路是逐步成型的

  • 定义一个矩阵,Dkij = 路径 {i -> { l <= k } -> j}的最小长度
  • D0, D1, ..., D|V|-1 ij,慢慢一步一步的增加一条边,直到最后即给出了 i 到 j 的真正最短距离
  • 最初的D-1是一个零阶矩阵,对角线是零,Dij 表示的是 i 到 j 的路径长度,如果两点没有直接边的话,必须初始化为正无穷
  • 当 Dk-1 已经完成,递推到 Dk 时:
    • 或者 k 不属于最短路径 { i -> { l <= k } -> j },则 Dk = Dk-1
    • 或者 k 属于最短路径 { i -> { l <= k } -> j },则该路径必定由两段最短路径组成:Dkij = Dk-1 ij + Dk-1 kj
void Floyd(){
    for(i = 0; i < N; i++)
        for(j = 0; j < N; j++){
            D[i][j] = G[i][j];
            path[i][j] = -1; //path矩阵是用来记录最短路径的
        }
    for(k = 0; k < N; k++)
        for(i = 0; i < N; i++)
            for(j = 0; j < N; j++)
                if(D[i][j]) + D[k][j] < D[i][j]){
                    D[i][j] = D[i][k] + D[k][j];
                    path[i][j] = k;
                }
}
// T = O(|V|3次方)

5. 最小生成树问题

  • 是一棵

    • 无回路
    • |V|个顶点一定有|V|-1条边
  • 生成

    • 包含全部顶点
    • |V|-1条边都在图里
  • 边的权重和最小

ps:

  1. 向生成树中任加一条边都一定构成回路
  2. 最小生成树存在 <---> 图连通

贪心算法是解决这个问题的唯一算法,不管你是Prim算法还是什么别的,本质上都是一种贪心算法。

贪心算法

  • 什么是“贪”:每一步都要最好的
  • 什么是“好”:权重最小的边
  • 需要约束:
    • 只能用图里有的边
    • 只能正好用掉|V|-1条边
    • 不能有回路

5.1 Prim算法——让一棵小树长大

  1. 初始化:收入一个顶点
  2. 在与该顶点相连的边中选择一条权重最小的,把边对面的顶点收录进来
  3. 在与已经收录进来的顶点相邻的边中,选择权重最小的,收录进来,注意,每一步收录都要避免有回路生成
  4. 循环第三步,直至所有的顶点都收录进来
void Prim(){
    MST = (s); //把s根节点收进生成树
    while(1){
        V = 未收录顶点中dist最小者; //令与生成树相邻的权重最小的点为V
        if( 这样的V不存在 ) //已经没有这样连接的点,则退出循环
            break; 
        将V收录进MST:dist[v] = 0; //为0表示这个点已经被收录进生成树里了
        for( V 的每个邻接点 W ) //因为收录了V进去,所以要更新与V邻接的点到生成树的距离、权重
            if(dist[W] != 0)
                if(E(v,w) < dist[W]){ //如果原来W到树的距离大于收录后V到W的距离
                    dist[W] = E(v,w); //刷新W到数的距离为V到W的距离
                    parent[W] = V; //令W的父节点为V
                }
    }
    if( MST中收的顶点不到|V|个 ) //有顶点与生成树不连接
        Error("生成树不存在");//生成树不存在,因为没有把所有的点纳入进来
}

// dist[V] = E(s,V) 表示的是V节点到已经收录的图的距离
// dist[V] = 正无穷 表示的是节点没有直接与已经收录的图相连
// parent[s] = -1 表示s点是生成树的根节点,别的值则代表这个点的父节点
// T = O(|V|的平方)

这个算法适合稠密图


5.2 Kruskal算法——将森林合并成树

  1. 初始时,每一个顶点都是独立的,都看作一棵树
  2. 然后将全部的点中把距离最近的点都连上
  3. 然后继续连,当然,树不能成环
  4. 直到所有的点都连通起来,循环结束
void Kruskal(Graph G){
    MST = { };
    while( MST中不到 |V|-1 条边 && E 中还有边 ){
        从 E 中取一条权重最小的边 E(V,W); //最小堆
        将 E(V,W) 从 E 中删除;
        if( E(V,W) 不在 MST 中构成回路) //并查集,检验如果V和W在同一棵树上,那么连接上就会构成回路
            将 E(V,W) 加入 MST;
        else
            彻底无视 E(V,W);
    }
    if( MST 中不到 |V|-1 条边 )
        Error("生成树不存在");
}

// T = O(|E| log|E|)

这个算法适合稀疏图

posted @ 2021-04-16 23:21  何-某人  阅读(135)  评论(0)    收藏  举报