图的基本概念
图的定义 G=(V,E)
- 用线(边)连接起来的顶点(节点)的集合
- V:顶点的有限集合
- E:边的有限集合
基本概念
- 无向边
- 有向边
- 关联至:被指向
- 关联于:指向
- 无向图:所有边都是无向边的图
- 有向图:所有边都是有向边的图
- 完全图:边数达到最大的图 n(n-1)/2
- 稀疏图:有很少边的图
- 稠密图:有较多边的图
- 加权有向图、加权无向图:每条边赋予一个权重 ➡️网络
- 子图:G=(V,E) , G'=(V',E'), V'是V的子集,E'是E的子集 ➡️G'是G的子图
- 顶点v的度:与v关联的边的数目
- 有向图中顶点v的入度:关联至v的边的数目
- 有向图中顶点v的出度:关联于v的边的数目
- 路径
- 路径长度:路径上所有边的长度之和
- 简单路径:序列中顶点不重复出现的路径
- 回路:第一个顶点和最后一个顶点相同的路径
- 连通图:图中任意两个顶点都是联通的
- 连通分量:无向图中的极大连通子图
- 强连通图:有向图中任意两个顶点Vi、Vj,从Vi到Vj和从Vj到Vi都有路径
- 强连通分量:有向图中的极大连通子图
- 生成树:包含G中所有项点且是G的子图的树
图的特性
- 设G=(V, E)为无向图,|V|=n, |E|=e,di为顶点i的度,则有
- 设G=(V, E)为有向图,则有
图的存储
邻接矩阵
特性
- 对于n顶点的无向图,
- A(i,i)=0
- 邻接矩阵是对称的
- 行、列之和均等于顶点的度
- 对于n顶点的有向图,
- 行之和等于顶点的出度
- 列之和等于顶点的入度
数组实现邻接矩阵
- 对角线无需存储
- 矩阵元素取值只能是0或1,只需一个二进制位即可保存
- 无向图是对称矩阵,只保存上/下三角即可
- 缺点:图较为稀疏时,邻接矩阵空间浪费
邻接压缩表
- 使用一维数组l[0:x], h[0:n+1]
- 有向图:x=e-1,无向图:x=2e-1
- l:保存顶点的邻接顶点的集合
- h[i]:顶点i邻接顶点集合在l中的起始位置
- 优化:数组元素的压缩
- h: 位即可
- l: 位即可
- 缺点:插入、删除复杂
邻接链表
- 链表数组h:h[i]存储顶点i的邻接表
十字链表
耗费邻接矩阵
- 记录对应边的权重
- 如果不存在边,则为∞
图的遍历
宽度优先搜索
//从顶点v开始的宽度优先搜索
把顶点v标记为已到达顶点;
初始化队列Q,其中仅包含一个元素v;
while (Q不空) {
从队列中删除顶点w;
令u为邻接于w的顶点;
while (u) {
if(u尚未被标记){
把u加入队列;
把u标记为已到达顶点;
}
u=邻接于w的下一个顶点;
}
宽度优先构造生成树
深度优先搜索
1 ➡️ 2 ➡️ 5 ➡️ 8
➡️ 3
➡️ 4 ➡️ 6
➡️ 7 ➡️ 9
深度优先搜索构造生成树
最小生成树
Kruskal算法
- 每个步骤选择一条边加入生成树
- 贪心准则:不会产生环里,且耗费最小
- 可按耗费递增顺序考察每条边,若产生环路则丢弃,否则加入
//在一个具有n个顶点的网络中找到一棵最小生成树
令T为所选边的集合,初始化T=Φ
令E为网络中边的集合
while (E≠Φ) && (|T|≠n-1) {
令(u,v)为E中代价最小的边
E=E-{ (u,v) } //从E中删除边
if ((u,v)加入T中不会产生环路)
将(u,v)加入T
}
if (|T| == n-1)
T是最小耗费生成树
else
网络不是连通的,不能找到生成树
Prim算法
贪心准则
- 耗费最小
- 始终保持树的结构
算法过程
- 从单一顶点的树开始
- 不断加入耗费最小的边,使加入后仍为树
//假设网络中至少具有一个顶点
设T为所选择的边的集合,初始化T=Φ
设TV为已在树中的顶点的集合,置TV={1}
令E为网络中边的集合
while (E<>Φ) && (|T|<>n-1) {
令(u,v)为最小代价边,其中u∈TV,v∈TV
if (没有这种边)
break E=E-{(u,v)} //从E中删除此边
在T中加入边(u,v)
}
if (|T|==n-1)
T是一棵最小生成树
else
网络是不连通的,没有最小生成树
有向带权图的最短路径
单源最短路径:Dijkstra算法
数组法
- 集合S:已经求出最短路径的顶点的集合
- 数组d:从S集合内的顶点,一步可达到相应顶点的路径长度,自己记为0,无法到达记为∞
- 数组p:到达的相应顶点的前一个顶点
画图法
表格法
- 第一竖列:除第一个顶点以外的顶点
- 最后一横行:当前竖列得出的最短路径
- 中间的表格:
- 已经得出最短路径的顶点可以一步到达相应顶点的最短长度和路径,无法到达记为∞
- 每一列记得更新最短长度和最短路径
代码实现
- 初始化d[i]=a[s][i](1≤i≤n) 对于邻接于s的所有顶点i,置p[i]=s对于其余的顶点置p[i]=0,对于p[i]≠0的所有顶点建立L表
- 若L为空,终止,否则转至3
- 从L中删除d值最小的顶点
- 对于与i邻接的所有还未到达的顶点j,更新d[j] 值为min{d[j], d[i]+a[i][j]} 若d[j]发生了变化且j还未在L中,则置p[j]=i, 并将j加入L,转至2
template<class T>
void AdjacencyWDigraph<T>::ShortestPaths(int s,T d[], int p[]){
if (s < 1 || s > n)
throw OutOfBounds();
Chain<int> L;
ChainIterator<int> I;
for (int i = 1; i <= n; i++){
d[i] = a[s][i];
if (d[i] == NoEdge)
p[i] = 0;
else {
p[i] = s;
L.Insert(0,i);
}
}
while (!L.IsEmpty()) {
int *v = I.Initialize(L);
int *w = I.Next();
while (w) {
if (d[*w] < d[*v])
v = w;
w = I.Next();
}
int i = *v;
L.Delete(*v);
for (int j = 1; j <= n; j++) {
if (a[i][j] != NoEdge && (!p[j] ||d[j] > d[i] + a[i][j])) {
d[j] = d[i] + a[i][j];
if (!p[j])
L.Insert(0,j);
p[j] = i;}
}
}
}
}
每一对点的最短路径:Floyd算法
- 所有点对间的最短路径:最多n(n-1)条
- 简单算法:每个顶点执行单源最短路径算法 ➡️缺陷:太复杂
Floyd算法描述
- c ( i , j , k ):i ➡️ j的“最短路径”长度,路径中顶点的最大编号不超过k
- 存在直连边:c ( i , j , k )=< i , j >的长度
- 不存在直连边:c ( i , j , k )=∞
- c ( i , i , n )=0
- c ( i , j , n )=最短路径长度
- 路径中顶点的最大编号不超过k
- 没有实际过k:简化为c ( i , j , k-1 )
- 实际过k:
- 从i ➡️ k,路径中顶点的最大编号不超过k-1
- 从k ➡️ j,路径中顶点的最大编号不超过k-1
- c ( i , j , k )=min { c ( i , j , k-1 ) , c ( i , k , k-1 )+c ( k , j , k-1 ) }
template<class T>
void AdjacencyWDigraph<T>::AllPairs(T **c, int **kay) {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
c[i][j] = a[i][j];
kay[i][j] = 0;
}
for (i = 1; i <= n; i++)
c[i][i] = 0;
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
T t1 = c[i][k];
T t2 = c[k][j];
T t3 = c[i][j];
if (t1!=NoEdge&&t2!=NoEdge&&(t3==NoEdge||t1+t2<t3)) {
c[i][j] = t1 + t2;
kay[i][j] = k;
}
}
void outputPath(int **kay, int i, int j) {
if (i == j) return;
if (kay[i][j] == 0)
cout << j << ' ';
else {
outputPath(kay, i, kay[i][j]);
outputPath(kay, kay[i][j], j);
}
}
template<class T>
void OutputPath(T **c, int **kay, T NoEdge,int i, int j){
if (c[i][j] == NoEdge) {
cout << "There is no path from " << i << " to "<< j << endl;
return;
}
cout << "The path is" << endl;
cout << i << ' ';
outputPath(kay,i,j);
cout << endl;
}
拓扑排序
- 一个工程可以分解为多个活动,活动之间有先后顺序 ➡️ 制约条件
- 工程能否顺利进行?
- 如果能够顺利进行,应当以什么样的顺序,使工程最快最简单地完成?
相关概念
- 偏序:集合中仅有部分成员之间可比较
- 全序:集合中全体成员之间均可比较
- 拓扑排序:由某个集合上的一个偏序得到该集合上的一个全序
AOV图
- 用顶点表示活动
- 用箭头表示活动间优先关系:当某个顶点的入度为0时,可以开始工作
算法描述
bool Network::Topological(int v[]) {
int n = Vertices();
int *InDegree = new int [n+1];
InitializePos();
for (int i = 1; i <= n; i++)
InDegree[i] = 0;
for (i = 1; i <= n; i++) {
int u = Begin(i);
while (u) {
InDegree[u]++;
u = NextVertex(i);
}
}
LinkedStack<int> S;
for (i = 1; i <= n; i++)
if (!InDegree[i])
S.Add(i);
i = 0;
while (!S.IsEmpty()) {
int w;
S.Delete(w);
v[i++] = w;
int u = Begin(w);
while (u) {
InDegree[u]--;
if (!InDegree[u])
S.Add(u);
u = NextVertex(w);
}
}
DeactivatePos();
delete [] InDegree;
return (i == n);
}
关键路径
AOE网
- 用顶点表示里程碑:起始顶点一定是V0,结束顶点一定是Vn
- 用边表示活动
- 用权表示活动需要的时间
关键路径的定义
- 花费时间最久的一条路径
- 工程意义:优化时间最久的路径,才能提高整个工程的效率
具体描述
- Ve:每个顶点的最早开始时间
- 从上往下
- 算每个顶点的时间时,找路径上的前一个顶点,看它的时间,再加上前一个顶点到该顶点的活动时间
- 不同的路径有不同的时间,选取最长的时间
- Vl:每个顶点的最晚开始时间
- 注意最后一个顶点的最早和最晚开始时间相同
- 从下往上看
- 算每个顶点的时间时,找路径上的后一个节点,看它的时间,再减去该顶点到后一个顶点的活动时间
- 不同的路径有不同的时间,选取最短的时间
- e:每个活动的最早开始时间,即为它的出发顶点的最早开始时间
- l:每个活动的最晚开始时间,即为它的到达顶点的最晚开始时间减去它需要的时间
- 关键路径:e和l相等的活动构成的路径
作业10
利用广度优先的方式在途中寻找一条路径
/* 1- 3 7
| | \ |
2- 4- 5-6
*/
template<typename T>
class chain;
template<typename
T>
class linkedGraph;
template<typename T>
class node{
friend chain<T>;
friend linkedGraph<T>;
private:
T data;
node *next;
};
template<typename T>
class chain{
friend linkedGraph<T>;
public:
chain(){
head=NULL;
}
chain& insert(T t){
node<T> *q=new node<T>;
q->data=t;
q->next=NULL;
if(!head)
head=q;
else{
node<T> *p=head;
while(p->next)
p=p->next;
p->next=q;
}
return *this;
}
T getHead(){
if(head)
return head->data;
else
return NULL;
}
void output(){
if(!head){
cout<<"Empty chain!"<<endl;
return;
}
node<T> *p=head;
cout<<p->data<<": ";
p=p->next;
while(p){
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
private:
node<T> *head;
};
template<typename T>
class linkedGraph{
public:
linkedGraph(int n){
size=n;
vertex=new chain<T>[size+1];
}
linkedGraph& initialize(){
cout<<"Please input "<<size<<" vertexes!"<<endl;
T t;
for(int i=1;i<=size;i++){
cin>>t;
vertex[i].insert(t);
}
int edge;
cout<<"Please input how many edges this graph has!"<<endl;
cin>>edge;
T start,end;
cout<<"Please input "<<edge<<" edges of this graph!"<<endl;
for(int i=1;i<=edge;i++){
cin>>start>>end;
vertex[start].insert(end);
vertex[end].insert(start);
}
return *this;
}
int find(T v){
for(int i=1;i<=size;i++){
if(vertex[i].getHead()==v){
return i;
}
}
return 0;
}
void findPath(T start, T end){
int length=0;
T *path=new T[size];
T *reach=new T[size];
T *label=new T[size];
int r=0;
bfs(start,reach,label,r);
int i=0;
for(;i<r;i++){
if(reach[i]==end){
break;
}
}
if(i==r){
cout<<"There's no path between vertex "<<start<<" and "<<end<<"!"<<endl;
return;
}
path[length]=end;
length++;
while(label[i]!=start){
path[length]=label[i];
length++;
for(int j=0;j<i;j++){
if(reach[j]==label[i]){
i=j;
break;
}
}
}
path[length]=start;
length++;
for(int i=length-1;i>=0;i--){
cout<<path[i];
if(i!=0)
cout<<",";
else
cout<<endl;
}
cout<<"The length of the path is: "<<length<<endl;
}
void bfs(T start,T *reach,T *label,int &r){
reach[r]=start;
if(r==0)
label[r]=0;
r++;
queue<T> q;
q.push(start);
while(!q.empty()){
T w=q.front();
q.pop();
int i=find(w);
node<T> *p=vertex[i].head;
p=p->next;
T u;
while(p){
u=p->data;
bool reachable=false;
for(int j=0;j<r;j++)
if(reach[j]==u){
reachable=true;
break;
}
if(!reachable){
q.push(u);
reach[r]=u;
label[r]=w;
r++;
}
p=p->next;
}
}
}
void output(){
for(int i=1;i<=size;i++)
vertex[i].output();
}
private:
chain<T> *vertex;
int size;
};
int main(){
int n;
cout<<"Please input how many vertexes this graph has!"<<endl;
cin>>n;
linkedGraph<int> graph(n);
graph.initialize();
graph.output();
int start,end;
cout<<"Please input the start and end vertexes!"<<endl;
cin>>start>>end;
graph.findPath(start,end);
return 0;
}
计算无向图的传递闭包
/* 1-2 4-5- 6
| | /
3 7
*/
//和上面的代码类似
//把findPath函数的返回值改为length
//增加函数getTC
int** getTC(){
int **tc=new int*[size];
for(int i=0;i<size;i++)
tc[i]=new int[size];
for(int i=0;i<size;i++)
for(int j=0;j<size;j++)
tc[i][j]=0;
for(int i=1;i<size;i++){
for(int j=0;j<i;j++){
int l=findPath(vertex[i+1].getHead(),vertex[j+1].getHead());
if(l>=1){
tc[i][j]=1;
tc[j][i]=1;
}
}
}
return tc;
}
//主函数中增加
int main(){
//……
int **tc=new int*[n];
for(int i=0;i<n;i++)
*tc=new int[n];
tc=graph.getTC();
cout<<"The transitive closure of this graph is:"<<endl;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
cout<<tc[i][j]<<" ";
}
cout<<endl;
}
return 0;
}