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]);
}
}