数据结构 第七章 图

目录

7.1 图的定义和术语

图的分类

关于一条边或弧的表示方法

重要结论

重要概念

7.2 图的存储结构

数组(邻接矩阵)表示法

无向图的邻接矩阵表示法

有向图的邻接矩阵表示法

网(即有权图)的邻接矩阵表示法

邻接矩阵表示法的特点

邻接表(链式)表示法

无向图的邻接表表示

有向图的邻接表表示

邻接矩阵与邻接表表示法的关系

十字链表---用于有向图

十字链表---用于无向图

7.3 图的遍历

深度优先搜索( DFS - Depth_First Search)

DFS与BFS算法效率比较

7.4 图的连通性问题

最小生成树

Prim算法: 归并顶点,与边数无关,适于稠密网

克鲁斯卡尔算法的基本思想-归并边

最短路径

Floyd算法

dijkstra算法

拓扑排序

7.5有向无环图及其应用

拓扑排序算法的思想-重复选择没有直接前驱的顶点

关键路径

实例:求事件结点的最早发生时间

实例:求事件结点的最迟发生时间


7.1 图的定义和术语

图:Graph=(V,E)

  V:顶点(数据元素)有穷非空集合;

  E:边的有穷集合。

图的分类

无向图:对于(vi,vj),必有(vj,vi),并且偶对中顶点的前后顺序无关。每条边都是无方向的

有向图:若<vi,vj>ÎE是顶点的有序偶对。每条边都是有方向的

网络:与边有关的数据称为权,表明从一个顶点到另一个顶点的距离或耗费。边上带权的图称为网络。

邻接:有边/弧相连的两个顶点之间的关系。

          存在(vi, vj),则称vivj互为邻接点

          存在<vi, vj>,则称vi邻接到vjvj邻接于vi

关联(依附)/弧与顶点之间的关系。

     存在(vi, vj)/ <vi, vj>, 则称该边/弧关联于vivj

关于一条边或弧的表示方法

(1)  用图形 (2)  用符号 (3) 用语言

重要结论

  1. 对于具有n个顶点,e条边的图,有e=1/2\sum_{i=1}^{n}TD(v_{i} )
  2. 具有n个顶点的无向图最多有n(n-1)/2条边。
  3. 具有n个顶点的有向图最多有n(n-1)条边。

边的数目达到最大的图称为完全图。边的数目达  到或接近最大的图称为稠密图 ,否则,称为 稀疏图。

重要概念

完全图:任意两个点都有一条边相连

无向完全图有n(n-1)/2 条边 有向完全图有n(n-1) 条边

顶点的度:与该顶点相关联的边的数目,记为TD(v)

有向图, 顶点的度等于该顶点的入度出度之和。

顶点 v 的入度是以 v 为终点的有向边的条数, 记作 ID(v)

顶点 v 的出度是以 v 为始点的有向边的条数, 记作OD(v)

问:当有向图中仅1个顶点的入度为0,其余顶点的入度均为1,此时是何形状?答:是树!而且是一棵有向树!

路径:接续的边构成的顶点序列。

路径长度:路径上边或弧的数目/权值之和。

回路()第一个顶点和最后一个顶点相同的路径。

简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径。

简单回路(简单环)除路径起点和终点相同外,其余顶点均不相同的路径。

连通图(强连通图)在无(有)向图G=( V, {E} )中,若对任何两个顶点 vu 都存在从v u 的路径,则称G是连通图(强连通图)。

子图 设有两个图G=V{E})、G1=V1{E1}),若V1\subseteq  VE1 \subseteq E,则称 G1G的子图。
连通分量(强连通分量)无向图G 的极大连通子图称为G的连通分量。极大连通子图意思是:该子图是 G 连通子图,将G 的任何不在该子图中的顶点加入,子图不再连通。

有向图G 的极大强连通子图称为G的强连通分量。极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。

极小连通子图:该子图是G 的连通子图,在该子图中删除任何一条边,子图不再连通。
生成树:包含无向图G 所有顶点的极小连通子图。生成森林:对非连通图,由各个连通分量的生成树的集合

7.2 图的存储结构

对于一个图,需要存储的信息应该包括:

(1所有顶点的数据信息;

(2顶点之间关系(边或弧)的信息;

(3权的信息(对于网络)。

顺序存储结构:数组表示法(邻接矩阵)

链式存储结构:多重链表(邻接表、邻接多重表、十字链表

数组(邻接矩阵)表示法

建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)

设图 A = (V, E) n 个顶点,则图的邻接矩阵是一个二维数组 A.Edge[n][n],定义为:

无向图的邻接矩阵表示法

分析1无向图的邻接矩阵是对称的;

分析2顶点i =第 i 行 (列) 1 的个数

特别:完全图的邻接矩阵中,对角元素为0,其余1

有向图的邻接矩阵表示法

注:在有向图的邻接矩阵中, 第i行含义:以结点vi为尾的弧(即出度边);i列含义:以结点vi为头的弧(即入度边)。

分析1有向图的邻接矩阵可能是不对称的。

分析2顶点的出度=i行元素之和顶点的入度=i列元素之和;顶点的=i行元素之和+i列元素之       

网(即有权图)的邻接矩阵表示法

邻接矩阵表示法的特点

优点:容易实现图的操作,如:求某顶点的度、判断顶点之间是否有边、找顶点的邻接点等等。

缺点:n个顶点需要n*n个单元存储边;空间效率为O(n2)对稀疏图而言尤其浪费空间。

邻接表(链式)表示法

v对每个顶点vi 建立一个单链表,把与vi有关联的边的信息链接起来,每个结点设为3个域;

每个单链表有一个头结点(设为2个域),存vi信息;每个单链表的头结点另外用顺序存储结构存储。

无向图的邻接表表示

注:邻接表不唯一,因各个边结点的链入顺序是任意的

空间效率为O(n+2e)

若是稀疏图(e<<n2),比邻接矩阵表示法O(n2)省空间。

TD(Vi)=单链表中链接的结点个数

有向图的邻接表表示

空间效率为O(n+e)

出度:OD(Vi)=单链出边表中链接的结点数

入度:ID(Vi)=邻接点域为Vi的弧个数

度:TD(Vi) = OD( Vi )  +  I D( Vi )

优点:空间效率高,容易寻找顶点的邻接点;缺点:判断两顶点间是否有边或弧,需搜索两结点对应的单链表,没有邻接矩阵方便。

邻接矩阵与邻接表表示法的关系

1. 联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。

2. 区别:

① 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。

② 邻接矩阵的空间复杂度为O(n2),而邻接表的空间复杂度为O(n+e)

3. 用途:邻接矩阵多用于稠密图;而邻接表多用于稀疏图

十字链表---用于有向图

结点表中的结点的表示:

data结点的数据,保存结点的数据值。

firstin:   结点的指针,给出自该结点出发的的第一条边的边结点的地址。

firstout结点的指针场,给出进入该结点的第一条边的 边结点的地址。

边结点表中的结点的表示:

info:边结点的数据,保存边的权值等。

tailvex:   本条边的出发结点地址。

headvex:本条边的终止结点的地址。

hlink:终止结点相同的边中的下一条边的地址。

tlink:出发结点相同的边中的下一条边的地址。

十字链表---用于无向图

结点表中的结点的表示:

data:结点的数据,保存结点的数据值。

firstedge: 结点的指针,给出自该结点出发的的第一条边的边结点的地址。

边结点表中的结点的表示:

ivex: 本条边依附的一结点的地址

ilink: 依附于该结点(地址由ivex给出)的边中的下一条边的的地址。

jvex: 本条边依附的另一个结点的地址

jlink: 依附于该结点(地址由jvex给出)的边中的下一条边的的地址。

info:  边结点的数据,保存边的权值等。

mark:边结点的标志域,用于标识该条边是否被访问过。

7.3 图的遍历

遍历定义:从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算

遍历实质:找每个顶点的邻接点的过程。

图的特点:图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点

深度优先搜索( DFS Depth_First Search)

简单归纳:

访问起始点v;

v1邻接点没访问过,深度遍历此邻接点;

若当前邻接点已访问过,再找v的第2个邻接点重新遍历。

#include <stdio.h>
#define MaxVertexNum 10  /* 最大顶点数设为10 */
#define INFINITY 65535   /* ∞设为双字节无符号整数的最大值65535*/
typedef int Vertex;      /* 用顶点下标表示顶点,为整型 */
typedef int WeightType;  /* 边的权值设为整型 */

typedef struct GNode *PtrToGNode;
struct GNode{
    int Nv;  /* 顶点数 */
    int Ne;  /* 边数   */
    WeightType G[MaxVertexNum][MaxVertexNum]; /* 邻接矩阵 */
};
typedef PtrToGNode MGraph; /* 以邻接矩阵存储的图类型 */
bool Visited[MaxVertexNum]; /* 顶点的访问标记 */

MGraph CreateGraph(); /* 创建图并且将Visited初始化为false;裁判实现,细节不表 */

void Visit( Vertex V )
{
    printf(" %d", V);
}

void DFS( MGraph Graph, Vertex V, void (*Visit)(Vertex) )
{
    Visited[V] = true;
    Visit(V);
    for(int i = 0; i<Graph->Nv; i++)
    {
        if(Graph->G[V][i] == 1 && !Visited[i])
        {
            DFS(Graph,i,Visit);
        }
    }
}


int main()
{
    MGraph G;
    Vertex V;

    G = CreateGraph();
    scanf("%d", &V);
    printf("DFS from %d:", V);
    DFS(G, V, Visit);

    return 0;
}

广度优先搜索( BFS Breadth_First Search)

简单归纳:

在访问了起始点v之后,依次访问 v的邻接点

然后再依次访问这些顶点中未被访问过的邻接点

直到所有顶点都被访问过为止。

广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有回退的情况。

因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。

1)从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。

2)只要队列不空,则重复下述处理。

   ① 队头顶点u出队。

   ② 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队。

(懒得写了,用的POJ3279的AC代码)

#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
struct node{
    int x;
    int s;
};
int book[100010],step;
struct node curr,t;
int n,k;
int main()
{
    while(scanf("%d %d",&n,&k) != EOF)
    {
        memset(book,0,sizeof(book));
        curr.x = n;
        curr.s = 0;
        queue<node> q;
        q.push(curr);
        book[curr.x] = 1;
        while(!q.empty())
        {
            curr = q.front();
            q.pop();
            if(curr.x == k)
            {
                step = curr.s;
                break;
            }
            int next[3] = {curr.x-1,curr.x+1,2*curr.x};
            for(int i = 0; i<3; i++)
            {
                t.x = next[i];
                if(t.x>=0 && t.x<=100010 && book[t.x] != 1 )
                {
                    book[t.x] = 1;
                    t.s = curr.s + 1;
                    q.push(t);
                }
            }
        }
        printf("%d\n",step);
    }
    return 0;
}

如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行( n 个元素),总的时间代价为O(n2)

用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为O(n+e)

DFSBFS算法效率比较

空间复杂度相同,都是O(n)(借用了堆栈或队列);

时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。

7.4 图的连通性问题

最小生成树

极小连通子图该子图是G 的连通子图,在该子图中删除任何一条边,子图不再连通。

生成树:包含图G所有顶点的极小连通子图(n-1条边

求最小生成树

首先明确

使用不同的遍历图的方法,可以得到不同的生成树

从不同的顶点出发,也可能得到不同的生成树。

按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。

目标:

在网的多个生成树中,寻找一个各边权值之和最小的生成树

构造最小生成树的准则

v必须只使用该网中的边来构造最小生成树;

v必须使用且仅使用n-1条边来联结网络中的n顶点

v不能使用产生回路的边

Prim算法: 归并顶点,与边数无关,适于稠密网

设连通网络 N = { V, E }

1. 从某顶点 u0 出发,选择与它关联的具有最小权值的边(u0, v),将其顶点加入到生成树的顶点集合U

2. 每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(u, v),把它的顶点加入到U

3. 直到所有顶点都加入到生成树顶点集合U为止

int mp[maxn][maxn],in[maxn],d[maxn];
//d[maxn]数组记录从树出发到i点的距离最小值,in[maxn]记录i点是否在集合中
int V;
int prim()
{
    for(int i = 1; i<=V; i++)
    {
        in[i] = 0;
        d[i] = INF;
    }
    d[1] = 0;
    int res = 0,sum = 0;
    while(true)
    {
        int pos = -1;
        for(int i = 1; i<=V; i++)
        {
            if(!in[i] && (pos == -1 || d[i]<d[pos]))
                pos = i;
        }
        if(pos == -1 || d[pos] == INF) break;
        in[[pos] = 1;
        res += d[pos];
        sum++;
        for(int i = 1; i<=V; i++)
        {
            d[i] = min(d[i],d[pos]+mp[pos][i]);
        }
    }
    if(sum == V) return res;
    else return -1;
}

克鲁斯卡尔算法的基本思想-归并边

设连通网络 N = { V, E }

1. 构造一个只有 n 个顶点,没有边的非连通图 T = { V, Æ }, 每个顶点自成一个连通分量

2. E 中选最小权值的边,若该边的两个顶点落在不同的连通分量上,则加入 T 中;否则舍去,重新选择

3. 重复下去,直到所有顶点在同一连通分量上为止

int n,m,fa[maxn],cnt;
struct edge{
    int u,v,w;
};
bool cmp(edge e1,edg2 e2)
{
    return e1.w<e2.w;
}
void init(int m)
{
    cnt = 0;
    for(int i = 0; i<=m; i++)
        fa[i] = i;
}
int findfa(int i)
{
    while(i != fa[i])
        i = fa[i];
    return i;
}
int merge(int i, int j)
{
    i = findfa(i);
    j = findfa(j);
    if(i != j)
    {
        fa[i] = j;
        return 1;
    }
    return 0;
}
int main()
{
    while(scanf("%d %d",&n,&m) != EOF)
    {
        edge mp[maxn];
        init(m);
        for(int i = 0; i<n; i++)
        {
            int u,v,w;
            scanf("%d %d %d",&u,&v,&w);
            mp[i].u = u;
            mp[i].v = v;
            mp[i].w = w;
        }
        sort(mp,mp+n,cmp);
        int res = 0;
        for(int i = 0; i<n; i++)
        {
            if(merge(mp[i].u,mp[i].v))
                res += mp[i].w;
        }
        for(int i = 0; i<=m; i++)
        {
            if(fa[i] == i) cnt++;
        }
        if(cnt == 1) printf("%d\n",res);
        else printf("No\n");
    }
}

最短路径

典型用途:交通问题。如:城市A到城市B有多条线路,但每条线路的交通费(或所需时间)不同,那么,如何选择一条线路,使总费用(或总时间)最少?

问题抽象:带权有向图A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。

(注:最短路径与最小生成树不同,路径上不一定包含n个顶点)

Floyd算法

1.复杂度:O(n3)

2.适用范围:数据范围小的情况下可用于计算所有点间的最短路,且可以解决带有负权边的图,但不能用于解决负权环(带有负权边的回路)问题。

3.思想:中转。比较 点i和点j间的现有最短路径 与 点i通过点k中转到达点k的路径 的大小,从而更新最短路。注意在三层for循环中,中转点k的for循环要放在最外层,从而保证现有的ij点间的路径长度已是被点1~k-1中转过后的最短路,这样才能保证结果的正确性。
 

for(int k = 1; k<=n; k++)
    for(int i = 1; i<=n; i++)
        for(int j = 1; j<=n; j++)
{
       mp[i][j] = min(mp[i][j],mp[i][k]+mp[k][j])
}

dijkstra算法

1.复杂度:O(n2)

2.适用范围:计算单源最短路,不能处理负权边

3.思想:松弛。每次找到离源点最近的点,以该点为中心进行扩展(这是一个循环的过程,找到距离u点最短的点,以该点为中心进行扩展,而后继续再找距离该点距离最短的点…………),最终得到源点至所有点的最短路径。

int mp[maxn][maxn],d[maxn],used[maxn];
void dijkstra()
{
    for(int i = 0; i<maxn; i++)
    {
        d[i] = mp[0][i];
        used[i] = 0;
    }
    while(true)
    {
        int pos = -1;
        for(int u = 0; u<maxn; u++)
        {
            if(!used[u] && (pos == -1 || d[u]<d[pos]))
                pos = u;
        }
        if(pos == -1) break;
        used[pos] = 1;
        for(int j = 0; j<maxn; j++)
            d[j] = min(d[j],d[pos]+mp[pos][j]);
    }
}

拓扑排序

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <iostream>
#include <map>
#include <vector>
#include <queue>
#include <set>
#define eps 1e-8
const double PI = acos(-1.0);
const int maxn = 505;
const int INF = 1e9;
using namespace std;
int cnt;
int ind[maxn],mp[maxn][maxn];
map<string,int>m;
void topsort()
{
    int p = 0;
    for(int i = 1; i<=cnt; i++)
    {
        if(ind[i] == 0)
            p++;
    }
    if(p == 1)
        cout<<"Yes"<<endl;
    else
        cout<<"No"<<endl;
}
int main()
{
    int k;
    while(scanf("%d",&k)!=EOF && k)
    {
        memset(mp,0,sizeof mp);
        m.clear();
        cnt = 0;
        memset(ind, 0, sizeof ind);
        while(k--)
        {
            string s,t;
            cin>>s>>t;
            if(!m[s])
                m[s] = ++cnt;
            if(!m[t])
                m[t] = ++cnt;
            if(!mp[m[s]][m[t]])
            {
                mp[m[s]][m[t]] = 1;
                ind[m[t]]++;
            }
        }
        topsort();
    }


    return 0;
}

7.5有向无环图及其应用

用有向图来描述一个工程或系统的进行过程。

一个工程可以分为若干个子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成。

AOV(Activity  On Vertices)顶点表示活动的网络

AOE(Activity  On Edges)表示活动的网络

拓扑排序算法的思想-重复选择没有直接前驱的顶点

1.输入AOV网络。令 n 为顶点个数。 

2.AOV网络中选一个没有直接前驱的顶点, 并输出之;

3. 从图中删去该顶点, 同时删去所有它发出的有向边;

4. 重复以上 23 , 直到:

u全部顶点均已输出,拓扑有序序列形成,拓扑排序完成;或:

u图中还有未输出的顶点,但已跳出处理循环。这说明图中还剩下一些顶点,它们都有直接前驱,再也找不到没有前驱的顶点了。这时AOV网络中必定存在有向环。

关键路径

•   用途:估算工程项目完成时间

    AOE网络:定义结点为事件,有向边的指向表示事件的执行次序。单位是时间(时刻)。有向边定义为活动,它的权值定义为活动进行所需要的时间。

•   术语:

    源点:表示整个工程的开始点,也称起点。

    收点:表示整个工程的结束点,也称汇点。

    事件结点:单位时间,表示的是时刻。

    活动(有向边):它的权值定义为活动进行所需要的时间。方向表示起始结点事件先发生,而终止结点事件能发生。

    事件的最早发生时间(Ve(j)):从起点到本结点的最长的路径。意味着事件最早能够发生的时刻。

    事件的最迟发生时间(V l (j)):不影响工程的如期完工,本结点事件必须发发生的时刻。

    活动的最早开始时间:e( ai ) = Ve( j )

    活动的最迟开始时间:l( ai ) = V l( k ) - dut( j , k )

    事件的最早发生时间(Ve(j)):从起点到本结点的最长的路径。意味着事件最早能够发生的时刻。

    事件的最迟发生时间(V l (j)):不影响工程的如期完工,本结点事件必须发发生的时刻。

    活动的最早开始时间:e(ai ) = Ve( j )

    活动的最迟开始时间: l (ai ) = V l( k ) - dut( j , k )

    关键活动:最早开始时间 = 最迟开始时间的活动

    关键路径:从源点到收点的最长的一条路径,或者全部由关键活动构成的路径。

实例:求事件结点的最早发生时间

利用拓扑排序算法求事件结点的最早发生时间的执行步骤:

1、设每个结点的最早发生时间为0,将入度为零的结点进栈。

2、将栈中入度为零的结点V出,并压入另一栈,用于形成逆向拓扑排序的序列。。

3、根据邻接表找到结点V的所有的邻接结点,将结点V的最早发生时间 + 活动的权值 得到的和同邻接结点的原最早发生时间进行比较;如果该值大,则用该值取代原最早发生时间。另外,将这些邻接结点的入度减一。如果某一结点的入度变为零,则进栈。

4、反复执行 23;直至栈空为止。 

Status  Topologicalsort( ALGraph G,  Stack &T)
{  FindinDegree(G,indegree); 
// 对各顶点求入度,建立入度为零的栈 S,
Initstack(T);count = 0;
    ve [ 0 .. G.vexnum - 1 ] = 0;
while (!StackEmpty(S))
       {  Pop(S,j);Push(T,j); ++count;
           for (p=G.vertices[i]. firstarc; p; p=p->nextarc);
	{ k = p->adjnexr;
	   if (!(- - indegree [ k ])) Push(S, k);
	   if (ve[ j ]+ *( p->info)> ve[  k ] )
                               ve[ k ]  = ve[ j ] +  *( p->info); }
                     }
        }
     if (count < G.vexnum)return ERROR;
     else return OK;
} //  栈 T 为求事件的最迟发生时间的时候用。

实例:求事件结点的最迟发生时间

利用逆向拓扑排序算法求事件结点的最迟发生时间的执行步骤:

1、设每个结点的最迟发生时间为收点的最早发生时间。

2、将栈中的结点V取出

3、根据逆邻接表找到结点V的所有的起始结点,将结点V的最迟发生时间 - 活动的权值得到的差同起始结点的原最迟发生时间进行比较;如果该值小,则用该值取代原最迟发生时间。

4、反复执行 23;直至栈空为止。 

•   注意:关键路径可有多条缩短工期必须缩短关键活动所需的时间

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值