Loading

数据结构系列4——图

图的概念

线性表可以是空表,树可以是空树,但是图不可以是空图,图的顶点集V一定非空,但边集E可以为空,此时的图中只有顶点而没有边

基本定义

  • 有向图(尖括号)
  • 无向图(圆括号)
  • 简单图和多重图
    • 简单图:不存在重复边,不存在顶点到自身的边
    • 多重图与简单图相对
  • 完全图(完全简单图)
    • 对无向图,$ | E | \(的取值范围是\) 0 \(到\) n ( n - 1 ) / 2 \(,有\) n (n - 1 ) / 2 \(条边的无向图称为完全图(边数为\) n - 1, n - 2, \cdots , 1 $)
    • 对有向图,$ | E | \(的取值范围是\) 0 \(到\) n ( n - 1 ) \(,有\) n (n - 1 ) \(条边的无向图称为完全图,有向完全图中任意两个顶点之间都存在方向相反的两条弧(边数为\) n $个 $ n - 1 $)
  • 子图
    • 生成子图,顶点一样,只有边不同,并非所有E和V的子集都有可能构成子图,可能E的某些边中的顶点不在V中
  • 连通、连通图和连通分量
    • 连通:顶点v和顶点w有路径存在
    • G中任意两个顶点都是连通的称为连通图。否则称为非连通图
    • 连通分量:无向图中的极大连通子图(注意是子图,即所有的边和顶点都在,生成树不是连通分量),通俗理解成联通的一块,顶点不能缺也不能多
    • 区分极大连通子图极小连通子图,连通图只有一个极大联通子图,就是它自身,非连通图有多个极大联通子图,称为极大连通子图是因为再加入一个点就会导致其不连通,一个连通图的生成树是极小连通子图注意是生成树,而不是最小生成树,这里只关注边,而不关注边上的权值),去掉一条边就会导致其不连通

非连通情况下边最多的情况:由 n - 1 个顶点(n - 2 条边)构成一个完全图,此时任意再加入一条边则变成连通图

  • 强连通、强连通图和强连通分量
    • 强连通:一对顶点v和w,从v到w和从w到v之间都有路径,注意:有路径不一定就强连通,可能只满足部分到达
    • G中任何一对顶点都是强连通(互相都有路径)则称为强连通图
    • 强连通分量:有向图中的极大强连通子图

有向图强连通情况下边最少的情况:至少需要n条边,构成一个环路(必须是环路,否则仍不满足连通即任意两边可达)

  • 生成树和生成森林
    • 生成树:包含图中全部顶点的一个极小连通子图,n - 1 条边,砍去一条边就会变成非连通图,再加上一条边就会形成一个回路
    • 生成森林:非连通图中,连通分量的生成树构成了非连通图的生成森林
  • 顶点的度、入度和出度
    • 无向图的度之和 = 边的条数的两倍
    • 有向图的入度 = 出度 = 边的条数(可以理解为无向图没有入度和出度的概念,仅强调度)
  • 稠密图和稀疏图
    • 稀疏图,$ | E | < | V | log | V | $,反之则为稠密图
  • 路径长度
    • 路径上边的数目
  • 简单路径和简单回路
    • 简单路径,顶点不重复出现的路径
    • 简单回路,除第一个和最后一个顶点外,其余顶点不重复出现的回路
  • 有向树
    • 一个顶点的入度为0,其余顶点的入度为1的有向图,称为有向树

图的存储

邻接矩阵法

  • 行代表出边,对应出度,列代表入边,对应入度
  • 设图G对应的矩阵为$ A $, $ A^n[i][j] \(对应从顶点\) i \(到顶点\) j \(的长度为\) n $的路径的数目
const int MAXN = 1e3;
int G[MAXN][MAXN];

// 常用
const int INF = 0x7fffffff;
fill(G[0], G[0] + MAXN * MAXN, 0); // init
fill(G[0], G[0] + MAXN * MAXN, INF); // init

邻接表法

  • 表示指向的下一个顶点
  • 对于无向图,所需的存储空间为$ O(|V| + 2 |E|) \(,对于有向图,所需的存储空间为\) O(|V| + |E|) $,前者的倍数是两倍是因为每条边在邻接表中出现了两次(对每条边来说由于重复存储,复杂度是数量的两倍)
  • 邻接表表示不唯一
const int MAXN = 1e3;
vector<int> Adj[MAXN];
\\ new edge 1 -> 3
Adj[1].push_back(3)

\\ 带权图
struct node {
    int v, w;
    node(int _v, int _w) : v(_v), w(_w) {}
}
vector<node> Adj[MAXN];
\\ new edge 1-> 3, value equals 0
Adj[1].push_back(node(3, 0));

十字链表

  • 针对有向图,每条弧都有有一个结点(仅对应一个结点),每个顶点也有一个结点
    • 弧结点中,尾域tailvex和头域headvex分别表示弧尾和弧头两个顶点,链域hlink 指向弧头相同的下一条弧,链域tlink指向弧尾相同的下一条弧,info指向该弧的相关信息(权值等),注意,弧头表示指向的结点,弧尾表示出发的结点,不要混淆
    • 顶点结点中,data域存放顶点相关信息(顶点名称)firstin指向该顶点)和firstout该顶点指出)分别指向以该顶点为弧头和弧尾的第一个弧结点
    • 图的十字链表表示法不唯一,但一个十字链表表示唯一的图

20220717114824
20220717114838

注意,这里的表示中,结点编号比实际位置大一

邻接多重表

  • 针对无向图,在邻接表中,删除两顶点之间的边要分别在两个顶点中执行遍历操作,效率较低,在邻接多重表中,每条边用一个结点表示,每个顶点也用一个结点表示
    • 弧结点中,mark为标志域,可用来标记该条边是否被搜索过,ivexjvex为该边依附的两个顶点在图中的位置,ilink指向下一条依附于该顶点ivex的边,jlink指向下一条依附于顶点jvex的边,info指向和边相关的各种信息的指针域
    • 顶点节点中,data存储该顶点的相关信息,firstedge域指示第一条依附于该顶点的边
    • 在邻接多重表中,所有依附于同一个顶点的边串联在同一个链表中,每个边结点同时连接在两个链表中
    • 与邻接表的差别,同一条边在邻接表中用两个结点表示,在邻接多重表中用一个结点表示

20220717114850
20220717114928
20220717114940

图的遍历

与树不同,的结点之间有层级关(父子结点),因此结点的访问有一定顺序,而图不同,顶点之间可能重复访问,因此需要记录是否访问(vis[MAXN]inq[MAXN]

概念

  • 图的遍历与图的连通性的关系
    • 对于无向图,bfsTrave()dfsTrave()中调用bfs()dfs()的数量(一次dfs()bfs()访问该(顶点所在的)连通分量所能到达的所有顶点)
    • 对有向图,不能用dfs()bfs()的数量表示,因为可能存在如下情况,连通但是不是强连通分量,非强连通分量不能通过一次bfs()dfs()访问所有顶点
  • 广度优先的生成树、广度优先的生成树和各自的生成森林
    • 一给定图的邻接矩阵表示唯一,其广度优先或深度优先的生成树也是唯一的,但是邻接表存储表示不唯一,故其广度优先或深度优先的生成树是不唯一
    • 连通图才有生成树,否则是生成森林
  • 基于矩阵的遍历所得到的dfs和bfs序列是唯一的,基于邻接表的遍历所得到的dfs和bfs序列是不唯一的
  • 注意,各边权值相等时,广度优先算法可以解决单源最短路问题,搜索的层数可以对应到达的路径长度,维护cnt控制层数

20220717114953

广度优先搜索(BFS)

  • 需要一个辅助队列,空间复杂度$ O(|V|) \(,时间复杂度,采用邻接表时\) O(|V| + |E|) \(,采用邻接矩阵时\) O(|V|^2) $
  • 邻接矩阵
const int MAXN = 1e3;
const int INF = 0x7fffffff;
int n, G[MAXN][MAXN];
// 记录是否入队而不是是否访问,可以减少不必要的重复访问(入队了但是还未访问,会重复入队该顶点相邻的顶点)
bool inq[MAXN] = {false};

void bfs(int u) {
    queue<int> q;
    q.push(u);
    inq[u] = true;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v = 0; v < n; v++) {
            if (inq[v] == false && G[u][v] != INF) {
                q.push(v);
                inq[v] = true;
            }
        }
    }
}

void bfsTrave() {
    for (int u = 0; u < n; u++) {
        if (inq[u] == false) bfs(u);
    }
}
  • 邻接表
const int MAXN = 1e3;
const int INF = 0x7fffffff;
vector<int> Adj[MAXN];
int n;
// 记录是否入队而不是是否访问,可以减少不必要的重复访问(入队了但是还未访问,会重复入队该顶点相邻的顶点)
bool inq[MAXN] = {false};

void bfs(int u) {
    queue<int> q;
    q.push(u);
    inq[u] = true;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i = 0; i < Adj[u].size(); i++) {
            int v = Adj[u][i];
            if (inq[v] == false) {
                q.push(v);
                inq[v] = true;
            }
        }
        // c++ 11
        // for (auto v : Adj[u]) {
        //      if (inq[v] == false) {
        //          q.push(v);
        //          inq[v] = true;
        //      }
        // }
        }
    }
}

void bfsTrave() {
    for (int u = 0; u < n; u++) {
        if (inq[u] == false) bfs(u);
    }
}

深度优先搜索(DFS)

  • 需要一个递归空间栈,空间复杂度$ O(|V|) \(,时间复杂度,采用邻接表时\) O(|V| + |E|) \(,采用邻接矩阵时\) O(|V|^2) $
  • 邻接矩阵
const int MAXN = 1e3;
const int INF = 0x7fffffff;
int n, G[MAXN][MAXN];
bool vis[MAXN] = {false};

void dfs(int u, int depth) {
    vis[u] = true;
    for (int v = 0; v < n; v++) {
        if (vis[v] == false && G[u][v] != INF) {
            dfs(v, depth + 1);
        }
    }
}

void dfsTrave() {
    for (int u = 0; u < n; u++) {
        if (vis[u] == false) dfs(u, 1);
    }
}
  • 邻接表
const int MAXN = 1e3;
const int INF = 0x7fffffff;
vector<int> Adj[MAXN];
int n;
bool vis[MAXN] = {false};

void dfs(int u, int depth) {
    vis[u] = true;
    for (int i = 0; i < Adj[u].size(); i++) {
        int v = Adj[u][i];
        if (vis[v] == false) dfs(v, depth + 1);
    }
    // c++ 11
    // for (auto v : Adj[u]) {
    //     if (vis[v] == false) dfs(v, depth + 1)
    // }
}

void dfsTrave() {
    for (int u = 0; u < n; u++) {
        if (vis[u] == false) dfs(u, 1);
    }
}
  • bfs标记层号(从出发顶点依次向外扩散)
const int MAXN = 1e3;
const int INF = 0x7fffffff;

struct node {
    int v;
    int layer;

    node() {}
    node(int _v, int _layer) : v(_v), layer(_layer) {}
};
vector<node> Adj[MAXN];
int n;
// 记录是否入队而不是是否访问,可以减少不必要的重复访问(入队了但是还未访问,会重复入队该顶点相邻的顶点)
bool inq[MAXN] = {false};

// 为该子图,若要整个图,加一个bfsTrave
void bfs(int s) {
    queue<node> q;
    node start;
    start.v = s;
    start.layer = 0;
    q.push(start);
    inq[start.v] = true;
    while (!q.empty()) {
        node cur = q.front();
        q.pop();
        int u = cur.v;
        for (int i = 0; i < Adj[u].size(); i++) {
            node next = Adj[u][i];
            next.layer = cur.layer + 1;
            if (inq[next.v] == false) {
                q.push(next);
                inq[next.v] = true;
            }
        }
        // c++ 11
        // for (auto next : Adj[u]) {
        //     next.layer = cur.layer + 1;
        //     if (inq[next.v] == false) {
        //         q.push(next);
        //         inq[next.v] = true;
        //     }
        // }
    }
}
posted @ 2022-07-17 11:50  Patrickhao  阅读(108)  评论(0)    收藏  举报