【图论】2 图的建立与遍历
在c++中我们应如何表示一张图呢?
表示完成后又应如何调用呢?
1.图的建立
我们有许多方法存住一张图,在csp-s考试范围常用的方法有:
1.邻接矩阵 2.数组模拟链表(前向星) (当然还有许多其他方法)
邻接矩阵理解很简单:
对于一个二维数组 a [i] [j],a [i] [j]的值即为 点 i 到点 j 的边的边权【注释1】。
就是说我们总是将 从i 到 j 的单向边的边权赋给 用以存这个边的二维数组的对应位置。
void add(int start,int end,int length) { len[start][end]=length; len[end][start]=length; return; }
这便是邻接矩阵的存边法,其实就是将i j之间无向边的“长度”权值赋给了len[i] [j]和len[j] [i]。
#注意这里存在两次赋值,仔细观察发现其实是存了两条有向边 i j和j i。【注释2】我们通过构建两条反向有向边达到了模拟无向边的效果。
#自然,如果存无向图只对一个len赋值即可。
分析:
我们不难发现,对于邻接矩阵,无论图(假设其有n点)是稀疏还是稠密【注释3】,我们都需要高达 n^2 的空间去储存它,这无疑是相当浪费的。
我们不难发现,当出现重边【注释4】时基础的邻接矩阵就无从下手了。
我们不难发现,当初始状态时我们是无法区分边权为0的边和无边的,这有些时候意味着一次初始化,要格外留意。
对于一个已经建好的图,我们总是能知道 从 i 到 j 的边的边权,但也就仅限如此了。
如果我们要遍历邻接矩阵存的图,往往要枚举所有的可能 点对 ,具体点如常与邻接矩阵结合的 floyd 算法(这个后续会讲)时间复杂度高达 O(n^3),这无疑在很多情况下是不可接受的。
但这并不意味这邻接矩阵一无是处,特定情况还是能发挥奇特用处(何况不难),这个在我们讲floyd的时候还会提到。
前向星又叫数组模拟链表,是非常实用的存图法。
前向星是巧妙的存图法,常规的前向星像是这个样子:
struct EDGE{ int l; int to; int weight; }edge[200]; int head[100]; int cnt;
#这里用到了结构体的有关知识,友链(https://blog.csdn.net/Jessica_Shao/article/details/104007982)。
如何理解?
我们预设一个情景——像玩 “电子积木”一样把边摞到点上(这里不存在广告嫌疑)
EDGE型就是边,也就是说,我们的边 edge[i] 中包含:
l——第i条边在起点处“压住”的边的编号——与 i 同起点的上一条边。
to——第i条边的终点。
weight——第i条边的有关权重。
head[i]就是以i节点为起点的最后一条(最上面一条)边
cnt则是当前的总边数。
#如果某边是以某一点为起点的第一条边,那么它覆压的边就是 “虚无”——0;这个性质在遍历时得到了应用。
那么加边规则是什么呢?
void add(int start,int end,int w) { cnt++; edge[cnt].to=end; edge[cnt].weight=w; edge[cnt].l=head[start]; head[start]=cnt; }
add就是加边函数,加一条边(常规)需要三个(或2个)元素 终点end,权w,起点start
首先上首一句就是边的总数+1;
然后是第cnt条边(最后一条)(当前)的终点设为end 权重设为w 上一条边设为head记录的同起点的最后一条边
同时head更新,起点的最后一条边为cnt。
#应结合接下来的遍历看。
分析:
不难发现它只是单向边,故无向边加法为
add(a,b,c);
add(b,a,c);
是故存无向图存变数(edge数组容量)至少应为无向边数的两倍。
不难发现对于重边或自环【注释5】前向星都无问题。
不难发现它比邻接矩阵难。
对于大多数图,前向星的空间更优。
2.图的遍历
这里只讲前向星的遍历。
对于如图的图
黑序号序号表示节点号,红序号表示边号,默认权值皆为1。
那么对于这张图,假设我们存好了,他应该是什么情况呢?
边: 编号 l to
1 2 4 2 0 1 3 0 3 4 3 5 点: 编号 head
1 0 2 1 3 0 4 4 5 0
(当然 情况不唯一)
对于这个有向图 我们先选择起点(这里对于起点的选择先跳过,通常题目中的信息会使你明白如何选择起点),以2为起点进行遍历。
这里用到了搜索的有关知识,如果有友链我会贴到这里的。()
我们知道了以2为起点的最后一条边 head[2]=1,又知道了1覆压的边edge[1].l = 2,那么我们就可以遍历一遍以点2为起点的边,进而遍历出这些边的终点。
以这些边的终点为起点进行上述的操作,便可遍历整张图。
#注意此时我们并非随便选起点就能遍历整张图,上述例图很普通以至于没有很好的性质。不同的图应有不同的遍历方法,稍后会给出说明。
void dfs(int x)//x是当前节点(起点) { for(int i=head[x];i;i=edge[i].l)//这里的i是边号,从最上面逐渐往下,当编号是0(最下面一条边覆压的实际不存在的边)时停止 { int p=edge[i].to;//p是当前选中的边的终点 dfs(p);//以终点为起点dfs } }
若是一个无向图,则每次我们总能找到一条返回刚才访问过的点的边。
这样遍历无疑会死循环。
所以我们进行一下修改:
void dfs(int x) { vis[x]=1; for(int i=head[x];i;i=edge[i].l) { int p=edge[i].to; if(vis[p]==1)continue; dfs(p); } }
用一个vis[]数据记录我们是否经过点i,若经过则不跑。
明显,对于一个无向连通图,其遍历起点的选择可以随意。
图具体的遍历方法,应结合图本身的性质选择。