数据结构——图

目录

-

正文

什么是

是由两个集合V(vertex)和E(edge)组成的,其中:V代表顶点的有限集合。E代表链接V中两个不同顶点的边的有限集合。

按照边的有无方向可以分为有向图和无向图;
在这里插入图片描述
显然有向图就是有一个指向表示(一条边为 A -> B 而向图则表示A与B是连通的:即A -> B && B -> A对于无向图都是成立的)

基本术语

1. 端点和邻接点:

假设存在一条边 A ---> B 那么其中的两个点AB 就是这条边的端点,他们两个互为邻接点
由于这是一个有向图的边因此:A 称作起始端点,B称作终止端点

2. 顶点的度、出度和入度

图的度和树的度存在差异,树的度主要表示他的子节点个数
而图的度则表示有多少条边连接在这个点上如下图

A
B
C
D

这是一个无向图,我们可以观察到有俩条边连接在A点上因此A的度为2
因为无向图是不分起点和终点的因此存在关系度 / 2 = 出度 = 入度但是有向图就不是这样了;

!!!假设一个图为有向图那么就要严格的区分出度和入度
假如有这样的俩条边A -> B -> C

出度入度
A10
B11
C01

这就是这个边的出入度描述
在有向图中存在度 = 出度 + 入度
注意:出度为0的点一般就是有向图的终点,入度为0的点就是有向图的起点

3. 完全图

若无向图的每两个顶点之间都存在着一条边,有向图的每两个顶点之间都存在着相反的俩条边,则称此图为完全图

这就是一个有向完全图

a
b

这就是一个无向完全图

A
B

如果我们把这个图拓展开就可以得到(n表示结点的个数)
无向完全图的边: n ( n − 1 ) / 2 n(n -1)/2 n(n1)/2
有向完全图的边: n ( n − 1 ) n(n - 1) n(n1)

4. 稀疏图和稠密图

当一个图接近完全图时,称为稠密图。相反,当一个图含有较少的边数时就称作稀疏图。

5. 子图
设有两个图G=(V,E)G'=(V',E'),如果V’是V的子集,即V' ⊆ \subseteq VE'E的子集即E' ⊆ \subseteq E,则称G’是G的子图
注意:并非V和E的任何子集都能构成G的子图,因为这样的子集可能不是图,即E的子集中的某些边关联的顶点可能不在这个V的子集中。

6. 路径和路径长度
路径:是一个顶点序( i i i, i 1 i_1 i1, i 2 i_2 i2, i 3 i_3 i3 …, i m i_m im, j j j)。
路径长度:就是一条路径上经过的边的数目例如a -> b -> c -> d中a 到 d 的路径长度为 3。

其中 如果一条路径上除了开始点和结束点可以相同以外,其余的点都不相同那么就称这个路径为简单路径

7. 回路或环

  • 若一条路径上的开始点和结束点为同一个顶点,则此路径称为回路或环
  • 开始点和结束点相同的简单路径为简单回路或者简单环

注意:不同点
1、环:图中有个点最后通过边能绕回该点即可。
2、回路:有专指有向图,从某点出发,最终又有边回到该点,注意一个边出一个边入,如果某点只有输出或输入,那该点就没有回路。

8. 连通、连通图和连通分量

在无向图中,若从顶点 v v v到顶点 w w w有路径存在,则称 v v v w w w连通的
若图 G G G中任意两个顶点都是连通的,则称图 G G G连通图,否则称为非连通图
无向图中的极大连通子图称为连通分量
若一个图有 n n n个顶点,并且边数小于 n − 1 n − 1 n1, 则此图必是非连通图

9. 强连通图和强连通分量

在有向图中,若从顶点 v v v到顶点 w w w和从顶点 w w w到项点 v v v之间都有路径,则称这两个顶点是强连通的
若图中任何一对顶点都是强连通的,则称此图为强连通图。
有向图中的极大强连通子图称为有向图的强连通分量

注意:强连通图、强连通分量只是针对有向图而言的。一般在无向图中讨论连通性,在有向图中考虑强连通性。

10. 权和网

在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称

图的存储

  • 邻接矩阵
    邻接矩阵就是使用一个二维数组来存储这个图的点和边的关系

第一维度就是 起点, 第二维度就是 终点

  1. 如果G是不带权无向图
    A [ i ] [ j ] = { 1 若(i, j) ∈  E(G) 0 其他 A[i][j]= \begin{cases} 1& \text{若(i, j)$\in$ E(G)}\\ 0& \text{其他} \end{cases} A[i][j]={10(i, j) E(G)其他
  2. 如果G是带权无向图
    A [ i ] [ j ] = { 1 若i  ≠  j AND (i, j) ∈  E(G),该边的权为 w i 0 i == j ∞ 其他 A[i][j]= \begin{cases} 1& \text{若i $\neq$ j AND (i, j)$\in$ E(G),该边的权为$w_i$}\\ 0& \text{i == j}\\ ∞& \text{其他}\\ \end{cases} A[i][j]=10= j AND (i, j) E(G),该边的权为wii == j其他
  3. 如果G是不带权有向图
    A [ i ] [ j ] = { 1 若<i, j> ∈  E(G) 0 其他 A[i][j]= \begin{cases} 1& \text{若<i, j>$\in$ E(G)}\\ 0& \text{其他} \end{cases} A[i][j]={10<i, j> E(G)其他
  4. 如果G是带权有向图
    A [ i ] [ j ] = { 1 若i  ≠  j AND <i, j> ∈  E(G),该边的权为 w i 0 i == j ∞ 其他 A[i][j]= \begin{cases} 1& \text{若i $\neq$ j AND <i, j>$\in$ E(G),该边的权为$w_i$}\\ 0& \text{i == j}\\ ∞& \text{其他}\\ \end{cases} A[i][j]=10= j AND <i, j> E(G),该边的权为wii == j其他

如果存在一组不带权有向边<1, 2> 、<4, 3>、<2, 4>、< 3, 2>、<1, 4>、<1, 3>
我们以 数组的每一行作为起点,每一列作为终点就可以
那么我们就可以得到邻接矩阵 !!!此处的数组下标从1开始
[ 0 1 1 1 0 0 0 1 0 1 0 0 0 0 1 0 ] \begin{bmatrix} 0&1&1&1\\ 0&0&0&1\\ 0&1&0&0\\ 0&0&1&0\\ \end{bmatrix} 0000101010011100

对应的不带权无向图为(1, 2) 、(4, 3)、(2, 4)、( 3, 2)、(1, 4)、(1, 3)
我们以 数组的每一行作为起点,每一列作为终点就可以
那么我们就可以得到邻接矩阵 !!!此处的数组下标从1开始
[ 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 0 ] \begin{bmatrix} 0&1&1&1\\ 1&0&1&1\\ 1&1&0&1\\ 1&1&1&0\\ \end{bmatrix} 0111101111011110

由此可见 无向图的邻接矩阵一定是 关于对角线对称的

存储方式

#const int N = MaxSize
#typedef Elemtype elseType
typedef struct{
	int no;	//顶点的编号
	elseType info;	//其他信息
}VetexType;
typedef struct
{
	int edges[MaxSize][MaxSize]; //邻接矩阵
	int n, e;	//顶点数,边数
	VetexType vexs[MaxSize];//存放顶点信息	
}MatGraph;

实际使用时可以使用简单的存储方式

const int N = 100010;
int graph[5005][5005];

while(cin >> v >> w >> c, v && w && c)
graph[v][w] = c;
//graph[w][v] = c;无向图存俩次

注意邻接矩阵的数不能太大,不然会发生内存超限的错误

  • 邻接表
    图的邻接表就是一种顺序和链式存储相结合的存储方法。

存储方式

找出所有的起点;
将该起点能够到达的点 连成一个链存储起来
比如存在 <1, 2> | <1, 3>这俩条边的存储方式就是

1 -> 2 -> 3 -> nullptr
2 -> nullptr
3 -> nullptr

真正的样子
在这里插入图片描述

这就是邻接表

因为邻接表可以分为俩个部分
在这里插入图片描述

其中:在有必要的时候可以在边表中添加一个 weight表示边的权重

存储方式:

const int MaxSize = 100010;
typedef struct Anode{
	int adjvex;
	Anode *nextarc;
	int weight 		//必要时定义即可
}edgeNode; //边结点类型
typedef struct Vnode{
	ElseType data;	//顶点的其他信息
	Vnode *firstarc;// 指向第一个边结点
}VNode;
typedef struct{
	Vnode adjlist[MaxSize]; // 头结点数组
	int n, e; //顶点数n 边数 e
}Edge;
const int N = 100010;
int e[N], ne[N], w[N], h[N], idx = 0;
void add(int a, int b, int c)
{
	e[idx] = b;
	ne[idx] = h[a];
	w[idx] = c;
	idx++;
}

图的遍历

  • 广度优先遍历

就是每一次输出每一层的边,一般的广度优先搜索都要使用队列辅助
此处给大家演示链式前向星的BFS遍历方式

const int N = 100010;
int e[N], ne[N], w[N], h[N], idx = 0;
void add(int a, int b, int c)
{
	e[idx] = b;
	ne[idx] = h[a];
	w[idx] = c;
	idx++;
}

bool visited[100010];
memset(visited, 0, sizeof visited);
memset(h, -1, sizeof h);
void bfs()
{
    queue<int> q;
    visited[1] = true;
    cout << 1 << ' ';
    q.push(1);
    while( q.size() )
    {
        int t = q.front();
        q.pop();
        for(int i = h[t]; i != -1; i = ne[i])
        {
            j = e[i];
            if(visited[j] == false)
            {
                cout << j << ' ';
                q.push(j);
                visited[j] = true;
            }
        }
    }
}
  • 深度优先遍历

就是从每个起点开始一直找到他的终点,然后标记找到了点防止二次查找即可

bool visited[100010];
memset(visited, 0, sizeof visited);
void dfs(AdjGraph *G,int v)
{
	edgeNode *p;
	visited[v] = true;
	print("%d ", v);
	p = G -> adjlist[v].firstarc;
	while(p)
	{
		if(visited[p -> adjvex] == false)
			dfs(G, p -> adjvex);
		p = p-> nextarc;
	}
}

生成树和最小生成树

在图论的数学领域中,如果连通图G的一个子图是一棵包含G的所有顶点的树,则该子图称为G的生成树(SpanningTree)。生成树是连通图的包含图中的所有顶点的极小连通子图。图的生成树不惟一。从不同的顶点出发进行遍历,可以得到不同的生成树。

最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。

算法流程:
1.随机选取一个点作为一个新的集合
2.循环找到当前集合的点到非集合内的一个点的最短距离,将非集合内的点加入到集合中
再次循环找到另外一个非集合内点能够到这个集合的最小距离,然后执行(2)
如此往复即可

例题:

在这里插入图片描述

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;
const int N = 520;
const int INF = 0x3f3f3f3f;
int g[N][N];
int dist[N];
int st[N];
int n, m;

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    int res = 0;
    for (int i = 0; i < n; ++i)
    {
        int t = -1;
        for (int j = 1; j <= n; ++j)    
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
                
        st[t] = true;
        
        if(dist[t] == INF && i) return INF;
        
        if(i) res += dist[t];
        
        for (int j = 1; j <= n; ++j) dist[j] = min(g[t][j], dist[j]);
    }
    return res;
}
int main()
{
    memset(g, 0x3f, sizeof g);
    scanf("%d%d",&n, &m);
    for (int i = 0; i < m; ++i)
    {
        int a, b, c;
        scanf("%d%d%d",&a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    int t = prim();
    if(t == INF) cout << "impossible" << endl;
    else cout << t << endl ;
    return 0;
}

算法流程:
1.将所有的边先进行排序
2.先找到一条最小的边,然后将边连接的两点放到同一个集合内表示这两个点已经在新集合内了;
3.再次循环找到集合中的边(此时的边点的两个点不能同时在新集合内部),如果端点都在集合内了继续执行,否则就执行(2);
此处要使用到上一篇文章 树 中的并查集算法来判断是否处于同一个集合内部

例题:

在这里插入图片描述
代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 200010;
const int INF = 0x3f3f3f3f;
int p[100010];
int find(int x)
{
    if(x != p[x])
    return p[x] = find(p[x]);
}

struct edge{
    int a, b, w;
}Ed[N];

bool cmp(edge a, edge b)
{
    return a.w < b.w;
}
int n, m;

int kruskal()
{
    for (int i = 1; i <= n; ++i) p[i] = i;

    int cnt = 0, res = 0;
    for (int i = 0; i < m; ++i)
    {
        int a = find(Ed[i].a), b = find(Ed[i].b);
        if(a != b)
        {
            cnt++;
            p[a] = b;
            res += Ed[i].w;
        }
    }
    if(cnt < n - 1) return INF;
    else return res;
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < m; ++i)
        {
            int a, b, c;
            scanf("%d%d%d",&a, &b, &c);
            Ed[i] = {a, b, c};
        }

    sort(Ed, Ed + m, cmp);
    
    int ans = kruskal();

    if(ans == INF) cout << "impossible";
    else cout << ans;
    
    return 0;
}

最短路

最短路就是表示从一个点到达另外一个点的距离最短的长度就是最短路问题

注意: 下面的算法均以链式前向星或者邻接矩阵的方式存图

算法过程:
1.创建一个新的数组来记录到达某个点 i i i的距离d[i]
2.循环遍历所有的点
3.如果某个点的距离小于当前到的d[i],更新到最小的距离即可

例题:

在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 520;
int n,m;
int g[N][N];
bool st[N];
int d[N];
void dijkstra()
{
    memset( d, 0x3f ,sizeof d);
    d[1] = 0;
    for ( int i = 1; i <= n; i++ )
    {
        int t = -1;
        for( int j = 1; j <= n; j++ )
            if(!st[j] && (t == -1 || d[t] > d[j]))
            t = j;
            st[t] = true;
        for( int j = 1; j<= n; j++ )
        d[j] = min( d[j] , g[t][j] + d[t] );
    }
    
    if( d[n] == 0x3f3f3f3f ) printf("%d\n",-1);
    else printf("%d\n",d[n]);
}
int main()
{
    memset( g, 0x3f ,sizeof g);
    scanf("%d%d",&n,&m);
    for ( int i = 1; i <= m; i++ )
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b] = min(g[a][b] , c);
    }
    dijkstra();
    return 0;
}

算法过程:
> 1.
此思路来自于------- 简书

例题

在这里插入图片描述

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 500 , INF = 0x3f3f3f3f;
int graph[N][N];
int n,m,k;

void floyd()
{
    for ( int k = 1; k <= n; k++ )
        for ( int i = 1; i <= n; i++ )
            for ( int j = 1; j <= n; j++ )
                graph[i][j] = min( graph[i][j] , graph[i][k] + graph[k][j] );
}
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    
    for ( int i = 1; i <= n; i++ )
        for ( int j = 1; j <= n; j++ )
                if( i == j ) graph[i][j] = 0;
                else graph[i][j] = INF;
            
    for ( int i = 1; i <= m; i++ )
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        graph[a][b] = min(graph[a][b] , c ); 
    }
    floyd();
    for ( int i = 1; i <= k; i++ )
    {
        int x,y;
        scanf("%d%d",&x,&y);
        if( graph[x][y] > INF / 2 ) puts("impossible");
        else printf("%d\n",graph[x][y]);
    }
    return 0;
}

拓扑排序

对一个有向无环图(Directed Acyclic Graph简称DAG) G G G进行拓扑排序,是将 G G G中所有顶点排成一个线性序列,使得图中任意一对顶点 u u u v v v,若边 < u , v > ∈ E ( G ) <u,v>∈E(G) <u,v>E(G),则 u u u在线性序列中出现在 v v v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

在这里插入图片描述

实现代码:

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N=1e5+10;

int ne[N],e[N],h[N],idx;
int top[N],cnt,d[N];
int n,m;

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

bool topu()
{
    queue<int> s;
    for( int i=1 ;i<=n ;i++ )
    {
        if(!d[i]) s.push(i);
        else continue;
    }
    while(s.size())
    {
        int t = s.front();
        top[ cnt ++ ] = t;
        s.pop();
        
        for( int i = h[t]; i != -1; i = ne[i] )
        {
            int j = e[i];
            if( --d[j] == 0 ) s.push(j);
            
        }
    }
    return cnt == n ;
}
int main()
{
    cin >> n >> m;
    memset( h , -1 , sizeof h);
    for( int i = 0; i <= m; i++ )
    {
        int a,b;
        cin >> a >> b;
        add(a,b);
        
        d[b]++;
    }
    if (topu())
    {
        for( int i = 0 ; i < cnt ; i++ )
         cout << top[i] << ' ';
         cout<<endl;
    }
    else
    cout<<-1;
    
    return 0;
}

Thanks

部分例题和代码均来自AcWing
作者:yxc
链接:https://www.acwing.com/activity/content/code/content/48773/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

部分定义来自 百度百科

  • 30
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

He_xj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值