【图论】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,若经过则不跑。

明显,对于一个无向连通图,其遍历起点的选择可以随意。

图具体的遍历方法,应结合图本身的性质选择。

posted @ 2020-01-01 12:17  Return_dirt  阅读(318)  评论(0编辑  收藏  举报