数据结构:图的算法和应用

tips

•避免陷入‘定义’和‘概念’沼泽,尽量尝试用直觉和自然的认知来理解

•对算法思想和过程的直觉理解>>细节

•建议使用完整的时间去学习和一次性掌握

一.图的基本概念(多对多的逻辑结构)

•假设ABCDEFG是七个电话,之间的连线表示修有通信线路

•电话就是图的顶点,通信线路是边就是一个图。

 

•只要两个电话间有线路,就可以互相通话=>无向图

•电话(顶点)连接的线路(边)数量:度

•ABCDE和GF之间消息无法传递:不连通

•ABCDE和GF是两个连通分量

•假设ABCDE是五个电话,之间的连线表示修有通信线路,数字表示该线路的电话费

 •不同通信线路上的电话费不同:加权图

 •假设ABCDE是五个城市,带箭头连线表示该方向上有航班运营

•例如航班A——>只能支持A飞往D,边是单向的=>有向图

•飞来某地的航班数量:入度

•从某地起飞的航班数量:出度

•图并非只能表示地理数据,只要数据元素间满足多对多关系即可:

•可表示几个学生之间的朋友关系=>无向无权图

•可表示社交媒体的关注/被关注关系=>有向无权图

•可表示多个耗时不同的任务之间的依赖关系=>有向加权图

....

*key1:在一个无向图中,所有顶点的度数之和为边数量的2倍

*key2:在于一个有向图中,所有顶点的出度之和==所有顶点的入度之和

1.无向图G={V,E}中,|V|=n,则|E|最大为?()

A.n        B.n的平方        C.n(n-1)        D.n(n-1)/2

分析:

n个结点,每个结点都与除自己外其他结点间有一条边相连,,而一条边连接了两个结点,因此答案为n(n-1)/2

2.从某一城市出发,可以沿着高速公路经或不经中转抵达所有其他城市,则这几个城市是(D)

A.有向的        B.强连通的        C.加权的        D.连通的

二.图的存储结构:邻接矩阵和邻接表

邻接矩阵

•设|V|=n,则图可用一个n*n方阵表示

 •既一个二维数组AdjMat[n][n]

 •AdjMat[i][j]表示vi和vj的邻接情况

无向无权图:

•AdjMat[i][j]为1表示有边相连,为0表示无边

•AdjMat是对称的

 邻接表

•每个顶点用一个链表存下自己的邻接

•|V|=n,有n个链表,既图可用一个链表的数组AdjList[n]存储

•AdjList[i]表示顶点为vi的链表(头)

•从AdjList[i]开始可以遍历所有vi的邻居

邻接矩阵和邻接表的比较

•设G={V,E},|V|=n.

•邻接矩阵无论如何都需要一个二维数组[n][n],而邻接表中每条链表长度取决于它有多少邻居

•邻接矩阵访问AdjList[i][j]是O(1)的,但邻接表访问特定边需要顺着起点的链表向后查找。

•邻接表的优点:

        •在边较少时节省许多空间=>适用于稀疏图

•邻接表的缺点:

        •无法直接获得某条边信息,需要vi链表进行从头顺序存取,最坏情况为O(n)

三.图DFS和BFS遍历

Depth First Search

DFS:深度优先遍历

•遇到新的邻居就进去..直到没有可以进的邻居了再返回

•优先进入后来遇到的邻居=>递归/栈

Breath First Search

BFS:广度优先遍历

•先把当前结点的邻居都遍历完,再按先来后到遍历邻居的邻居们,逐层向外扩张。

•优先进入先访问的邻居的邻居=>队列

•先把当前结点的邻居都遍历完,再按先来后到遍历邻居的邻居们,逐层向外扩张。

•优先进入先访问的邻居的邻居=>队列

*key1:DFS每步操作:进入当前结点下一个未访问的邻居,如无则返回

*key2:BFS每步操作:进入当前队首结点并让其出队,将其未访问邻居入队

1.给定如下邻接矩阵,写出由v0出发的DFS序列:_________

A.0243156

B.0136542

C.0134256\wa

D.0361542

分析:

1.无需画出图结构

2.标注已遍历过的结点


2.给定如下邻接表,则由v0出发的深度优先遍历结果为(D),广度优先遍历结果为(D)。

A.0132                     

B.0231

C.0321

D.0123 

四.最小生成树(Prim算法)

生成树

•对于含n个结点的一个无向连通图,其边数最多为n(n-1)/2条,最少为n-1条。

•保持连通性的情况下,选n-1条边出来,剔除其他边,它就变成了一棵树。

•生成树里没有环

 最小生成树MST

•在加权图中选出n-1条边来构成其生成树,且这些边的权值之和最小。

求最小生成树:Prim算法“加点法” 

•在加权图中选出n-1条边来构成一颗生成树,且它们权值之和最小,用什么策略选?

•每次在连接已完成结点和未完成结点的边中,选一条权值最小的,重复n-1遍

•算法利用了贪心思想:选择局部最优

*key1:Prim算法是加点法,逐步增加n-1个点来形成MST

*key2:Prim算法每次加点满足1(这个点所属边的权值最小2)加点不会形成环

1.给定如下图结构,求最小生成树。

 五.迪杰斯特拉算法

单源点最短路径:Dijkstra算法

•加权图中求从一个顶点s出发到图上其他各点的最短距离

•|V|=n,算法循环n-1次,每次循环中:

        1.找到未完成结点中,s->距离最短的t,将t标注为已完成

        2.以t为中转更新s至t的邻居们的距离

六.AOV图求拓扑排序

拓扑排序

•假设以下有向图中的顶点表示不同课程,边表示课程之间的依赖关系,请问能否顺利毕业?

•能。因为顺着箭头走,不会产生环=>有向无环图(DAG)

•一个可行的选课顺序:1,2,5,4,3,6,7,8,9

•如何求拓扑排序?

•简而言之,把当前能上(没有前序依赖)的课上了,然后将此与之有关的依赖关系删掉

•如果当前还有剩余课程未上,然而没有能上的课了=>无法拓扑排序=>有向图存在环

•一个可行的选课顺序:1,2,5,4,3,6,7,8,9

七.AOE网路求解关键路径

AOE网路,关键活动,关键路径

•设计师,架构师,前端,后端和测试五人共同开发一个网站:

        1.设计师设计好前端才能开工,架构师搭好框架后端才能开工,前后端都完成了测试才能开工

        2.设计师需要3天,架构师需要5天,前端需要4天,后端需要8天,测试需要2天

•至少需要多少天?哪些人比较关键?

 •一般会给一个AOE网络,求关键活动和关键路径

•方法:求每个结点的最早/最晚发生时间VE和VL,以及每个活动的最早/最晚开始时间eE和eL

        1.按照拓扑排序,求出所有结点的VE

        2.令终点的VL=VE,按照拓扑排序的逆序,求出所有结点的VL

        3.所有活动(边)的eE等于其起点的VE(一个活动最早在起点状态达成后即可开始)

        4.所有活动(边)的eL等于其终点的VL-Wi(也可以拖到最后一刻开始)

        5.eE=eL的即为关键活动,关键活动构成了关键路径

 

 练习:

 

 

 

 

代码: 

/*
 图
*/

#include <cstdio>

/* (无向)图 */
// 邻接矩阵表示
#define N 100
typedef struct GraphAdjMatrix {
    int mat[N][N];
} AdjMat;

// 邻接表表示
typedef struct LinkListNode {
    int v;
    struct LinkListNode *next;
} ListNode;
typedef struct GraphAdjList {
    ListNode *list[N];  // 指针数组, 存放每个结点的邻接表头
} AdjList;

// 图的DFS,用邻接矩阵表示
void helper(AdjMat &G, int v, bool *visited) {
    // helper是一个递归函数,表示当前访问结点v
    printf("%d\n", v);
    for (int i = 0; i < N; ++i) {
        if (G.mat[v][i] == 1 &&
            !visited[i]) {  // 如果v和i之间联通,且i未被访问过
            helper(G, i, visited);  // 访问结点i
        }
    }
}
void DFS(AdjMat &G) {
    bool visited[N] = {false};  // visited记录哪些结点已经被访问过了
    for (int i = 0; i < N; ++i) {
        if (!visited[i]) {  // 这个循环保证了能访问G中的每个联通分量
            helper(G, i, visited);
        }
    }
}

// 图的DFS,用邻接表表示
void helper(AdjList &G, int v, bool *visited) {
    // helper是一个递归函数,表示当前访问结点v
    bool visited[N] = {false};  // visited记录哪些结点已经被访问过了
    printf("%d\n", v);
    for (ListNode *p = G.list[v]; p; p = p->next) {  // 遍历所有邻居
        if (!visited[p->v]) {
            helper(G, p->v, visited);  // 如果这个邻居未被访问过则访问它
        }
    }
}
void DFS(AdjList &G) {
    bool visited[N] = {false};  // visited记录哪些结点已经被访问过了
    for (int i = 0; i < N; ++i) {
        if (!visited[i]) {  //        这个循环保证了能访问G中的每个联通分量
            helper(G, i, visited);
        }
    }
}

// 图的BFS, 用邻接矩阵表示

#include <queue>
void BFS(AdjMat &G) {
    bool visited[N] = {false};  // visited记录哪些结点已经被访问过了
    std::queue<int> q;          // 队列内存放结点编号
    for (int i = 0; i < N; ++i) {  // 这个循环保证了能访问G中的每个连通分量
        if (!visited[i]) {
            q.push(i);  // 开始遍历一个新的连通分量
            visited[i] = true;
            while (!q.empty()) {
                int cur = q.front();  // 获得队首结点编号
                printf("%d\n", cur);  // 访问之
                q.pop();              // 队首出队
                for (int i = 0; i < N; ++i) {
                    int v = G.mat[cur][i];
                    if (!visited[v]) {
                        q.push(v);  // 遍历cur所有邻居, 将未访问顶点入队
                        visited[v] = true;  // 设置为已访问,避免重复入队
                    }
                }
            }
        }
    }
}

// 迪杰斯特拉算法(邻接矩阵实现)(仅考虑G为全连通图)
void Dijkstra(AdjMat &G, int start) {
    int Dist[N];     // s->t的距离数组
    bool finish[N];  // 标记已求出s->t最短路径的t

    // 初始化距离数组和已完成数组
    for (int i = 0; i < N; ++i) {
        finish[i] = false;
        for (int j = 0; j < N; ++j) {
            Dist[i] = G.mat[start][i];
        }
    }

    int count = 1;  // 记录已完成结点数量, count == N 时算法结束
    finish[start] = true;
    while (count < N) {
        int v = -1, d = INT_MAX;
        for (int i = 0; i < N; ++i) {
            if (!finish[i] && Dist[i] < d) {
                v = i;
                d = Dist[i];
            }
        }
        finish[v] =
            true;  // 求得v是当前未完成结点中路程最短的,这趟确定它的答案
        count++;
        // 接着用v中转来更新起点到其他结点的最短距离
        for (int i = 0; i < N; ++i) {
            int newDist = d + G.mat[v][i];
            Dist[i] = newDist < Dist[i] ? newDist : Dist[i];
        }
    }

    // 展示答案(以下与本算法无关,仅作展示)
    for (int i = 0; i < N; ++i) {
        printf("%d -> %d: %d\n", start, i, Dist[i]);
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光少年.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值