数据结构笔记9: 图

图的基本概念

图的定义 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的度,则有
    • \sum_{i=1}^{n}d_{i}=2e
    • 0\leqslant e\leqslant \frac{n(n-1)}{2}
  • 设G=(V, E)为有向图,则有
    • 0\leqslant e\leqslant n(n-1)
    • \sum_{i=1}^{n}{d_{i}}^{in}=\sum_{i=1}^{n}{d_{i}}^{out}=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:\left \lceil {log_{2}}^{(2e+1)} \right \rceil 位即可
    • l:\left \lceil {log_{2}}^{n} \right \rceil 位即可
  • 缺点:插入、删除复杂

邻接链表

  • 链表数组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:到达的相应顶点的前一个顶点

画图法

表格法

  • 第一竖列:除第一个顶点以外的顶点
  • 最后一横行:当前竖列得出的最短路径
  • 中间的表格:
    • 已经得出最短路径的顶点可以一步到达相应顶点的最短长度和路径,无法到达记为∞
    • 每一列记得更新最短长度和最短路径

代码实现

  1. 初始化d[i]=a[s][i](1≤i≤n) 对于邻接于s的所有顶点i,置p[i]=s对于其余的顶点置p[i]=0,对于p[i]≠0的所有顶点建立L表
  2. 若L为空,终止,否则转至3
  3. 从L中删除d值最小的顶点
  4. 对于与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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值