专题整理:图论(1)
本博客包含:图存储(邻接矩阵、vector、前向星)、拓扑排序
一、引入概念:
什么是图?
简单地说,用一组点和他们之间相连的若干线组成的图形称为图。
同时可以简单分为有向图和无向图,有向图两类图。
有向图,指点和点之间的线有明确的方向,从某个节点指向另一个节点。
无向图,指点和点之间的线没有明确方向,也可以看做线是双向的。
同时,根据实际应用需要,有些图有一定的权值,有些图具有点值,有些图则什么都没有。
该图为无向图
二、图的存储
虽然图看上去神秘莫测变化多端,但是编程软件毕竟不是SAI,计算机不可能直接识别你画的图像,因此,如何将图存储给电脑,就有极大的学问。
所谓一张图,在我们不考虑整体的情况下,我们实际上就是保存若干的点,以及点和点之间的连接关系,许多的点和他们的链接关系一起,就形成了一张图。
以下所有的边给出的方式都是x,y,z,表示x和y之间存在权值为z的有向边(无向边)
2-1、邻接矩阵
利用两个数组对图的信息进行存放,一个一位数组用于存放点权,一个二维数组用于存放边和边权。
例如
输入 1,2,5,就让mp[1][2]=5,表示点1和点2之间有一条权值为5的边。
这样子的存放方式优点是能够比较直观地展现出点和边的关系,缺点也很明显,那就是浪费了大量空间,假设题目中提到有10个点,但是却只给出了一条有效的边的关系,那么开出的10*10的空间只用了一个单位,造成了大量空间上的浪费。
2-2、vector存图
之前在数据结构里介绍过强大的STL,在STL中有一个存在vector,我们可以将其简单理解为可改变大小动态数组。
我们用结构体来保存边的信息(终点,权值),然后将每个点发出的边压到相应的vector数组下,这样子就完成了一次存图。
2-3、前向星&&链式前向星
2-3-1、前向星
vector存图依然存在一定的空间浪费问题,当给定的点比较多而边的信息较少的时候,还是会有空间浪费。
有没有一种以存储边的信息为第一要义的存图方式呢?
前向星是一种特殊的边集数组,所谓边集数组,就是利用一维数组存储图中所有边的一种图的表示方法。
我们还是用一个结构体数组存储边的信息,然后将边进行排序,排序依据是先比较起点大小后比较终点大小。
然后我们用一个head数组将起点为i的第一个边的位置存入head[i]中,用另一个数组len,将起点为i的边的数量存入len[i]中,由于之前已经进行了排序操作,因此我们所存的起点相同边一定是连续的,由此完成的一个图的存储。
2-3-2、链式前向星
前向星确实很好地解决了空间浪费的问题,但是问题又来了,排序的操作怎么看怎么不顺眼,假如出题人心地善良帮忙把数据排序好那还好,假如出题人心狠手辣,那可浪费了不少时间,有没有什么办法让排序的操作消失呢?
还记得数据结构里有一种不用在乎空间是否连续的结构吗?没错,链表。
这就是链式前向星,我们在边值结构体增加一个next,ed[i].next指向上一个和边i相同起点的边的位置信息,然后用head[j]来保存最后一个以j为起点的边,这样ed[i].next和head[j]就有了传递性,当读取下一个以i为起点的边的时候,将当前边的next指向当前的head[起点]所指向的边,然后将当前的head指向当前边。
2-4、代码
#include#include #include<string> #include #include #include #include #include #include #include #define N 100001 #define ll long long using namespace std; int n,m; //链式前向星 struct node{ int to,next,w; }ed[N]; int heads[N*10]; int tot inline void qxx(int a,int b,int v) //一条从a连向b权值为v的边。 { ed[++tot].to=b; ed[tot].v=v; ed[tot].next=heads[a]; heads[a]=tot; } inline void dfs(int a) //遍历所有与a相连的边的方法 { for(int i=heads[a];i!=0;i=ed[i].next) { node t=ed[i]; } } //vector存图 vector mp[N]; inline void vec(int a,int b,int v) { ed[++tot].to=b; ed[tot].w=v; mp[a].push_back(tot); } //邻接矩阵 int m[N][N]; inline void jz(int a,int b,int v) { m[a][b]=v; }
注:对于无向图,实际上就是看作双向图建立一条起点终点互换的边。
三、拓扑排序
3-1、题目链接:https://www.luogu.com.cn/problem/P1983
3-2、题目简介:
3-3、思路
3-3-1、什么是拓扑排序?
拓扑排序的对象是有向无环图,它期望找到一个走完所有节点的相对顺序。
举个生活中的例子,做完所有家务,打五把CSGO,然后上床睡觉。
我们将不同家务作为不同的节点,每把CSGO作为不同的节点,上床睡觉作为一个节点。
我们可以看出一个大概的顺序来,那就是只有将所有的家务完成(将家务节点走完),之后再打CSGO(将游戏节点走完),最后再上床睡觉。
这就是拓扑排序的用法,它经常被用于对事物相对顺序的考察以及分级类的问题上。
具体的做法就是从入度(指向该节点的边的数量)为0的点开始(显然这些点的等级为1),指向其他的点,然后将该点为起点的边删除,再找入度为0的点,这些点的等级就是2,然后重复这样的过程。
3-3-2、分级
我们来考察一下这道题。
简单理解一下题意,就是如果这趟车次停靠了火车站 x,则始发站、终点站之间所有级别大于等于火车站 x 的都必须停靠。
那么停靠的火车站一定要比未停靠的等级要高,于是我们就用箭头将未停靠站连向停靠站。
最后连接出来的是一个有向无环图,我们在图上进行拓扑排序,最少的等级就是拓扑排序的次数。
3-3-3、代码
#include#include #include #include #include #include<string> #include #define N 1020 using namespace std; int degree[2*N],dep[2*N]; int ans; struct node{ int next,to; }edge[N*1000]; bool vis[1010][1010]; int heads[2*N],tot=0; void addin(int x,int y) { tot++; edge[tot].to=y; edge[tot].next=heads[x]; heads[x]=tot; } int stop[N],disstop[N],hash[N]; inline void init() { memset(hash,0,sizeof(hash)); int n,m=0; scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&stop[i]); hash[stop[i]]=1; } for(int i=stop[1];i<=stop[n];i++) if(0==hash[i]) disstop[++m]=i; for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(!vis[stop[i]][disstop[j]]) { vis[stop[i]][disstop[j]]=1; addin(stop[i],disstop[j]); degree[disstop[j]]=1; } } } return; } inline void dfs(int u,int num) { if(num<=dep[u]) return ; dep[u]=num; for(int i=heads[u];i!=-1;i=edge[i].next) dfs(edge[i].to,num+1); } int main() { int n,m; scanf("%d%d",&n,&m); memset(heads,-1,sizeof(heads)); memset(vis,false,sizeof(vis)); for(int i=1;i<=m;i++) init(); for(int i=1;i<=n;i++) if(!degree[i]) dfs(i,1); //这里的思路是找到所有入度为0的边,一直走到终点,不断更新每个点的等级最大值 for(int i=1;i<=n;i++) if(dep[i]>ans) ans=dep[i]; printf("%d",ans); return 0; }