总结
图的性质
此处只列举了我觉得几个不太好记的几个。区分有向无向问题。
简单图: 边不重复,没有单环。
多重图:
简单完全图: 任意两点都有边;无向/有向。
子图: 边是子边,点是子点。
连通,连通图,连通分量: 任意两点可到,条件。
强连通图,强连通分量: 两两点直链。
生成树/森林:
度/入度/出度: 无向等,有向2。
简单路径/回路: 定点不重复出现
有向树: 一个顶点入度为0,其余顶点入度都是1的有向图。
存储结构
邻接矩阵
#define MaxVertexNum 100
typedef char VertexType;
typedef int EdgeType;
typedef struct{
VertexType Vex[MaxVertexNum]; // 顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum];
int vexnum , arcnum; //当前的顶点数和弧数
}MGraph;
邻接表
//邻接表定义//
typedef struct VNode{
VertexType data; //顶点编号//
ArcNode *firstedge;//顶点的第一个指针//
}VNode , AdjList[MaxVertexNum]; //用一个一维数组表示//
//结点定义//
typedef struct ArcNode{
int adjvex; //指向顶点位置//
struct ArcNode *next; //指向下一条弧的指针//
}ArcNode;
十字链表
//顶点结点定义//
typedef struct VNode{
VertexType data;
ArcNode *firstin , *firstout;
}VNode;
//边表结点定义//
typedef struct ArcNode{
int tailvex , headvex;//弧头指向下表,弧尾指向下标//
struct ArcNode *hlink , *rlink;//下一个相同弧头,下一个相同弧尾的边//
}ArcNode;
//十字链表定义//
typedef struct{
VNode xlist[MaxVertexNum];
int vexnum , arcnum;//顶点数,弧数//
};
图的遍历
DFS
基本思路:递归
void DFS(Graph G , int v){
ArcNode *p; // 每次的指针p都是内部定义的 , 每次重新开始的时候就会设置一次//
visit(v); //拜访该结点//
vis[v] = true; //标记为访问过//
p = G->adjlist[v].firstarc; // 设置为第一个结点//
while(p != NULL){ //结点不为空的时候//
if(!vis[p->adjvex]){ //如果没有访问过//
DFS(G , p->adjvex); // 递归访问这个结点//
}
p = p->nextarc; // 移动到下一个结点//
}
}
void DFS(Graph G){
for(int i = 0 ; i < vexnum ; i ++){
vis[i] = false;
}
for(int i = 0 ; i < vexnum ; i ++){ // 为了防止非连通图的出现//
if(!vis[i])
DFS(G , i);
}
}
BFS
基本思路:其基本操作类似于树的层序遍历,设置一个队列。代码以邻接表的形式给出。
#define MaxSize 100;
bool vis[MaxSize];
void BFS(Graph G , int v){
ArcNode *p;
queue<int> q;
visit(v); // 具体的一个访问结点的操作//
vis[v] = true; //访问的开始设置为True//
q.push(v);
while(!q.empty()){ //当queue中不为空//
int t = q.front(); // 降低一个数据拿出来//
q.pop(); // 弹出第一个数据//
p = G->adhList[v].firstedge; // 将第一个孩子结点拿出来//
while(p){ //当结点不为空的时候//
if(!vis[p->adjvex]){ // 如果没有被访问过
visit(p->adjvex);
vis[p->adjvex] = true; // 设置为拜访过//
q.push(p->adjvex); // 将其插入队列中//
}
p = p->next; // 结束之后移向下一个结点//
}
}
}
void BFS(Graph G){
for(int i = 0 ; i < G->vexnum ; i ++){ // 初始化//
vis[i] = false;
}
for(int i = 0 ; i < G->vexnum ; i ++){ // 为了防止非连通图的问题//
if(!vis[i]){
BFS(G , i);
}
}
}
图的应用
1.最小生成树MST
含义: 一个图中的极小连通子图,包含所有顶点,并且边尽可能少。最小生成树不一定是唯一的,但是权值之和必是唯一的。
特殊方法: 破圈法。任取一圈,去掉边中最大的那个边,依次将所有的圈都干掉,得到答案。
Prim算法
基本思路:在集合中找这几个里面最接近的那个。(点少边多图)
注意:当图中的所有闭环中边的大小都不一样,此时生成的MST是唯一的;无符号整型的int最大的数值是65535 , 而普通的int占32位的时候,最大可以赋值为:21 4748 3647,约为20亿,2*10^9。
void Prim(MGraph G){
int adjvex[Maxvex]; // adjvex代表此时访问这个点是从那个点来的。0代表访问结点已经访问过了 //
int lowcost[Maxvex];// lowcost是访问当前结点的数值和距离。0代表已经加入生成树,无穷代表无关,有数据代表与当前 i 的距离
lowcost[0] = 0;//最初的时候
adjvex[0] = 0;
//初始化操作
for(int i = 1 ; i < G.vexnum ; i ++){ /
lowcost[i] = G.arc[0][i]; //这个代表的是从初始结点开始到各个结点的路径长
adjvex[i] = 0; // 保存邻接顶点下标
}
//Core
for(int i = 0 ; i < G.vexnum ; i ++){
int min = 65535; // 无符号整型的最大值为65535
int k = 0;
//找顶点
for(int j = 0 ; j < G.vexnum ; j ++){
if(lowcost[j] != 0 && lowcost[j] < min){ //尚未加入生成树,并且是当前最短的
min = lowcost[j]; // 距离当前 i 的最短的路径
k = j; // 最短路径到达的位置
}
}
printf("%d --> %d\n" , adjvex[k] , k);
lowcost[k] = 0; //标记为访问过
//更新表数据
for(int j = 0 ; j < G.vexnum ; j ++){
if(lowcost[j] != 0 && G.arc[k][j] < lowcost[j]){ //尚未加入生成树 , 并且可以更新为更短的
lowcost[j] = G.arc[k][j];
adjvex[j] = k; // 那些修改的点的下一坐标都改成从当前 k 开始
}
}
}
}
Kruskal算法
基本思想:利用并查集实现算法。打碎然后找复合条件的边(边少点多图。)
typedef struct{
int a , b;
int weight;
}Edge; //边结构体
//并查集:并
int Find(int *parent , int x){ //指针
while(parent[x] >= 0){
x = parent[x];
}
return x;
}
Edge edges[MaxVex];
int parent[MaxVex];
void Kruskal(MGraph G){
sort(edges);// 堆排序:权值递增排序
//并查集:初始化
for(int i = 0 ; i < G.vexnum ; i ++){
parent[i] = -1;
}
//搜索所有的边//
for(int i = 0 ; i < G.arcnum ; i ++){
n = Find(parent , edges[i].a);
m = Find(parent , edges[i].b);
if(n != m){ //当边的结点不在一个圈子内,合并起来
parent[n] = m; //并查集:并
printf("%d --> %d" , edges[i].a , edges[i].b);
}
}
}
2.最短路径
Dijkstra算法
基本思路:不断更新,记得不能负权边。
注意:Dijkstra区别于Prim算法,所以Dijkstra不能得到一棵最小生成树。
Dijkstra 算法可以视为一个带有权重版本的 BFS。
#define INF 65535
/*
* v代表当前访问开始的结点
* path[]当前结点的来源
*/
void Dijkstra(MGraph G , int v , int path[] , int dist[]){
bool vis[Maxsize]; //那些被访问最短路径,访问过的设置为true , 未访问的false
int u;
//初始化记录表
for(int i = 0 ; i < G.vexnum ; i ++){
dist[i] = G.edge[v][i]; // 到初始的结点的距离
vis[i] = false; // 所有的结点都是尚未访问的
if(G.edge[v][i] < INF)
path[i] = v; // 与初始结点联通的数据写入path
else
path[i] = -1
}
vis[v] = true; // 根结点已经访问过
path[v] = -1;
//核心
for(int i = 0 ; i < G.vexnum ; i ++){
int min = INF;
//核心:找最小值
for(int j = 0 ; j < G.vexnum ; j ++){
if(vis[j] == false && dist[j] < min){ //找未访问过的结点中的最小值
min = dist[j];
u = j;
}
}
//核心:更新数组
vis[u] = true; // 该结点访问过
for(int j = 0 ; j < G.vexnum ; j ++){
if(vis[j] == false && dist[u] + G.edges[u][j] < dist[j]){ // 找那些能与当前结点
dist[j] = dist[j] + G.edges[u][j];
path[j] = u; // 更新,代表这条路径的来源
}
}
}
}
Floyd算法
基本思想:每次拉入一个新的点更新,但是不允许有负值环路。
void Floyd(MGraph G , int Path[][]){
int A[Maxsize][Maxsize];
//初始化
for(int i = 0 ; i < G.vexnums ; i ++){
for(int j = 0 ; j < G.vexnums ; j ++){
A[i][j] = G.Edges[i][j];
Path[i][j] = -1;
}
}
//更新数值
for(int k = 0 ; k < G.vexnums; k ++){
for(int i = 0 ; i < G.vexnums ; i ++){
for(int j = 0 ; j < G.vexnums ; j ++){
if(A[i][j] > A[i][k] + A[k][j]){ // 从 i 到 k 到 j 的距离 < i 到 j 的距离
A[i][j] = A[i][k] + A[k][j];
Path[i][j] = k; // 中间穿插的路径
}
}
}
}
}
3.拓扑排序AOV
基本思路: 入度为0,然后依次删除。
时间复杂度: O(n + e)
bool TopologicalSort(Graph G){
stack<int> s;
//找到一个入度为 0 的结点
for(int i = 0 ; i < G.vexnum ; i ++){
if(indrgee[i] == 0){
s.push(i);
}
}
int count = 0; // 记录当前已经输出的顶点数
while(!s.empty()){
int k = s.top();
s.pop();
count ++;
printf("%d " , G.adlist[k]);
for(ArcNode *p = G.vertices[i].firstarc ; p ; p = p->nextarc){ //遍历这个结点的孩子链表
v = p->adjvex;
if(!( --indegree[v])) // 如果它的结点的数值减到 0 了 入栈
s.push(v);
}
}
if(count < G.vexnum) // 输出的结点数比原来的少,则存在回路
return false; // 有环的话,入度结点不肯能有1 -> 栈空了 -> count的个数不如原有的多
else
return true;
}
4.关键路径(AOE)
基本思路:
加速某个关键活动不一定缩短整个工程的工期,因为关键路径可能不止一条。需要缩短那些关键路径公共部分的那些步骤,才能缩短关键路径。
计算要点:
所属分类 | 内容 | 计算 |
---|---|---|
事件 | 最早开始时间 | 前往后,max |
事件 | 最迟发生时间 | 后往前,min |
活动 | 最早开始时间 | 事件开始时间 |
活动 | 最迟发生时间 | 最迟发生时间 - 花费时间 |
活动 | 机动时间 | 最迟看开始时间 - 最早开始时间 |