1.图的定义、实现与基本操作
1.1 有关图的重要定义
- 稀疏图,密集图,完全图
- 标号图
- 相邻的,邻接点
- 权,带权图
- 路径,简单路径,路径长度
- 回路,简单回路
- 子图
- 连通的,连通分量(最大连通子图)
- 无环图,有向无环图
- 自由树
1.2 图的两种表示方式、ADT及其实现
使用哪种方式取决于边的数目
密集图:相邻矩阵
稀疏图:邻接表
1.2.2 图的ADT
- 没有使用模板
- 顶点用索引值描述
- ※:所示代码假设了图的顶点数是定值
class Graph {
private:
void operator=(const Graph&) {}
Graph(const Graph&) {}
public:
Graph() {}
virtual ~Graph() {}
virtual void Init(int n) = 0;
virtual int n() = 0;
virtual int e() = 0;
//访问当前顶点的第一个邻居
virtual int first(int v) = 0;
//访问当前顶点的下一个邻居
virtual int next(int v,int w) = 0;
//设置一条边的权,权必须>0
virtual void setEdge(int v1,int v2,int wght) = 0;
virtual void delEdge(int v1,int v2) = 0;
//查看在顶点i和顶点j之间的边是否存在
virtual bool isEdge(int i,int j) = 0;
virtual int weight(int v1,int v2) = 0;
virtual int getMark(int v) = 0;
virtual void setMark(int v) = 0;
};
1.2.2 图的两种实现
1.2.2.1 使用相邻矩阵表示图
#include<iostream>
#include<cstdio>
#define UNVISITED 0
#define Assert(a,b) assert((a)&&(b))
using namespace std;
class Graph {
private:
void operator=(const Graph&) {}
Graph(const Graph&) {}
public:
Graph() {}
virtual ~Graph() {}
virtual void Init(int n) = 0;
//顶点和边的数目
virtual int n() = 0;
virtual int e() = 0;
//访问当前顶点的第一个邻居
virtual int first(int v) = 0;
//访问当前顶点的下一个邻居
virtual int next(int v,int w) = 0;
//设置一条边的权,权必须>0
virtual void setEdge(int v1,int v2,int wght) = 0;
virtual void delEdge(int v1,int v2) = 0;
//查看在顶点i和顶点j之间的边是否存在
virtual bool isEdge(int i,int j) = 0;
virtual int weight(int v1,int v2) = 0;
virtual int getMark(int v) = 0;
virtual void setMark(int v,int val) = 0;
};
//邻接表
class Graphm : public Graph {
private:
int numVertex, numEdge;
int **matrix; //二维的表结构
int *mark;
public:
Graphm(int numVertex) { Init(numVertex); }
virtual ~Graphm() {
delete [] mark;
// for(int i=0;i<numVertex;i++)
// for(int j=0;j<numVertex;j++)
// delete matrix[i][j];
for(int i=0;i<numVertex;i++)
delete [] matrix[i];
delete [] matrix;
}
void Init(int n) {
int i;
numVertex=n;
numEdge=0;
for(i=0;i<numVertex;i++)
mark[i]=UNVISITED;
for(i=0;i<numVertex;i++)
matrix[i] =(int**) new int*[numVertex]; //
for(i=0;i<numVertex;i++)
for(int j=0;j<numVertex;j++)
matrix[i][j]=0;
}
virtual int n() { return numVertex; }
virtual int e() { return numEdge; }
//访问当前顶点的第一个邻居
virtual int first(int v) {
for(int i=0;i<numVertex;i++)
if(matrix[v][i]!=0) return i;
return numVertex;
}
//访问当前顶点的下一个邻居
virtual int next(int v,int w) {
for(int i=w+1;i<numVertex;i++)
if(matrix[v][i]!=0) return i;
return numVertex;
}
//设置一条边的权,权必须>0
virtual void setEdge(int v1,int v2,int wt) {
Assert(wght>0,"Illegal weight value");
if(matrix[v1][v2]==0) numEdge++;
matrix[v1][v2]=wt;
}
virtual void delEdge(int v1,int v2) {
if(matrix[v1][v2]!=0) numEdge--;
matrix[v1][v2]=0;
}
//查看在顶点i和顶点j之间的边是否存在
virtual bool isEdge(int i,int j) {
return matrix[i][j]!=0;
}
virtual int weight(int v1,int v2) {
return matrix[v1][v2];
}
virtual int getMark(int v) {
return mark[v];
}
virtual void setMark(int v,int val) {
mark[v]=val;
}
};
1.2.2.2 使邻接表表示图
- 邻接表:一个以链表为元素的数组(数组内存放指向链表的指针)
※:是树结构“子节点表”表示法的推广 - 链表内结点是图中顶点所指向的顶点,其顺序为按顶点序号排列
1.3 图的遍历
- 从一个起始顶点出发,试图访问其余顶点。
- 需处理的情况:
- 从起点出发可能到不了所有顶点(如非连通图)
- 确保算法不会因为回路而陷入循环
=> 设置标志位mark
(遍历结束后检查mark数组中是否还有顶点UNVISITED,然后从未被标记的顶点再开始遍历)
图的遍历函数:
void graphTraverse(Graph* G) {
int v;
for(int v=0;v<numVertex;v++)
G->setMark(v,UNVISITED);
for(v=0;v<G->n;v++) {
if(G->getMark(v)==UNVISITED)
doTraverse(G,v);
}
}
1.3.1 深度优先搜索(DFS)
- 栈结构
- 将产生一棵深度优先搜索树
- 适用于有向图和无向图
void PreVisit(Graph* G,int v) {
}
void PostVisit(Graph* G,int v) {
}
//深度优先DFS
void DFS(Graph* G,int v) {
PreVisit(G,v);
G->setMark(v,VISITED);
for(int w=G->first(v);w<G->n();w=G->next(v,w))
if(G->getMark(v)==UNVISITED)
DFS(G,v);
PostVisit(G,v);
}
1.3.2 广度优先搜索(BFS)
- 队列
- 由顶到底逐层访问(树的层次遍历)
在这里插入代码片
1.3.3 拓扑排序
- 有向无环图(DAG)建模:
(※:只有DAG有拓扑排序)
- 有向:任务间存在相互依赖关系
- 无环:回路中隐含了相互冲突的依赖条件
- 拓扑排序定义:将一个DAG中所有顶点在不违反前置依赖条件规定的基础上,排成线性序列
(将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。) - 拓扑排序应用场景:在进行步骤v之前,必须完成步骤u
1.3.3.1 用DFS实现拓扑排序
- 得到逆拓扑序列
void printout(int v) {}
void topsort(Graph* G) {
int i;
for(i=0;i<G->n();i++)
G->setMark(i,UNVISITED);
for(i=0;i<G->n();i++)
if(G->getMark(i)==UNVISITED)
tophelp(G,i);
}
//DFS实现拓扑排序
void tophelp(Graph* G,int v) {
G->setMark(v,VISITED);
for(int w=G->first(v);w<G->n();w=G->next(v,w))
if(G->getMark(w)== UNVISITED)
tophelp(G,w);
printout(v);
}
1.3.3.2 队列实现拓扑排序
- 不使用递归
void topsort(Graph* G) {
int cnt[G->n()];
int v,w;
for(v=0;v<G->n();v++) cnt[v]=0;
for(v=0;v<G->n();v++)
for(w=G->first(v);w<G->n();w=G->next(v,w))
cnt[w]++;
queue<int> q;
for(v=0;v<G->n();v++)
if(cnt[v]==0)
q.push(v);
while(q.size()) {
int v=q.front();
q.pop();
printout(v);
for(w=G->first(v);w<G->n();w=G->next(v,w)) {
cnt[w]--;
if(cnt[w]==0) q.push(w);
}
}
}
2. 最短路径问题
- 单源最短路径:在图G中给定一顶点s,找出从s到G的任何一个顶点的最短路径
2.1 单源最短路径:Dijkstra算法
- 每次最短路径所经过的顶点,总在已经确认的最短路径收录顶点集中取
- 每次从未收录的顶点中取距离最小的顶点收录进去
按顶点编号从小到大,每次扩一条对于起始顶点而言路径最短的边 -> 把与该边连接的顶点放入顶点集 -> 将该顶点能够到达的顶点所在的最短路径更新(若经过该顶点的最短路径更小,则用经过该顶点的最短路径替换原算出的最短路径) -> 将图的所有顶点全部按照上述方法遍历一遍 -> 单元最短路径计算完成
下述代码只算了路径长度,没有记录实际路径,但可以改代码实现实际路径的记录
//单源最短路径:Dijkstra算法
void Dijkstra(Graph* G,int* D,int s) { //D为从s到各个点的最短路径长度
int i,v,w;
for(i=0;i<G->n();i++) { //顺序处理每个顶点
v=minVertex(G,i);
if(D[v]==INFINITY) return;
G->setMark(v,VISITED);
for(w=G->first(v);w<G->n();w=G->next(v,w))
if(D[w]>(D[v]+G->weight(v,w)))
D[w]=D[v]+G->weight(v,w);
}
//此时D中已经存储了s到所有顶点的最短路径
}
//找出相对顶点i具有最短距离的顶点
int minVertex(Graph* G,int *D) {
int i;
int v=-1;
for(i=0;i<G->n();i++)
if(G->getMark(i)==UNVISITED) {
v=i;
break;
}
for(i++;i<G->n();i++)
if((G->getMark(i)==UNVISITED)&&(D[i]<D[v]))
v=i;
return v;
}
改动后:可记录路径和路径长度
void initD(int* D,Graphm& G) {
D[0]=0;
for(int i=1;i<G.n();i++) {
if(G.isEdge(0,i)) {
D[i]=G.weight(0,i);
}
else D[i]=INFINITY;
}
}
void initP(int* path,int n) {
for(int i=0;i<n;i++)
path[i]=0;
}
//单源最短路径:Dijkstra算法
void Dijkstra(Graphm& G) { //D为从s到各个点的最短路径长度
int i,v,w;
unvisit(G);
int *D,*path;
D=new int[G.n()];
path=new int[G.n()];
initD(D,G);
initP(path,G.n());
for(i=0;i<G.n();i++) { //顺序处理每个顶点
v=minVertex(G,D);
if(D[v]>=INFINITY) { cout<<0<<endl; return; }
G.setMark(v,VISITED);
for(w=G.first(v);w<G.n();w=G.next(v,w))
if(D[w]>(D[v]+G.weight(v,w))) {
D[w]=D[v]+G.weight(v,w);
path[w]=v;
}
}//此时D中已经存储了s到所有顶点的最短路径
for(int i=1;i<G.n();i++) {
printPath(path,i);
cout<<D[i]<<endl;
}
}
//找出未被访问过的、相对起始顶点具有最短距离的顶点
int minVertex(Graphm& G,int *D) {
// cout<<"minVertex"<<endl;
int i,v=-1;
for(i=0;i<G.n();i++)
if(G.getMark(i)==UNVISITED) { v=i; break; }
for(i++;i<G.n();i++)
if((G.getMark(i)==UNVISITED)&&(D[i]<D[v])) v=i;
return v;
}
2.2 多源最短路径:Floyd算法
3. 最小支撑树(minimum-cost spanning tree,MST)
- 别名:最小生成树
- 输入:一个每条边都带权的连通无向图G
- 输出:MST,其特点为:
- 包括图G中的所有顶点和一部分边
- 是连通图
- 所有边权之和最小
- 没有回路,是有 |V|-1 条边的自由树的结构
3.1 Prim算法
3.2 Kruskal算法
路径压缩
判断当前节点是否为根,如果不是(即 没有找到),就把以当前节点为根的子树整体向上挪,挪到当前节点的父亲的父亲
,下次再判断当前节点的父亲(即 原来的祖先)是否为根 …
相当于跳了一步,所以叫 路径压缩