算法学习笔记(3) : 存图

所谓图(graph),是图论中基本的数学对象,包括一些顶点,和连接顶点的,这里的边只是表示顶点的连接情况,用直线或曲线表示均可。图可以分为有向图无向图,有向图中的边是有方向的,而无向图的边是双向连通的。

img

有向图和无向图

算法竞赛中有一些称为图论题的题目,涉及到对图的处理,为了解决它们,我们至少先得把图存储起来,这个过程我们称为存图


邻接矩阵

谈到存图,最朴素的想法当然是用一个二维数组mat[]存储两个边的连接情况。假如从顶点u到顶点v有一条边,则令mat[u][v] = 1。这种建图方法称为邻接矩阵。例如上面的那张有向图的邻接矩阵是:

\begin{matrix} &\begin{matrix}1&2&3&4&5\end{matrix} \\ \begin{matrix}1\\2\\3\\4\\5\end{matrix} &\begin{bmatrix}0&0&0&0&1\\0&0&0&0&1\\0&0&0&1&0\\0&0&0&0&0\\0&0&1&1&0\end{bmatrix} \end{matrix}

相应地,上面那张无向图的邻接矩阵是:

\begin{matrix} &\begin{matrix}1&2&3&4&5\end{matrix} \\ \begin{matrix}1\\2\\3\\4\\5\end{matrix} &\begin{bmatrix}0&0&0&0&1\\0&0&0&0&1\\0&0&0&1&1\\0&0&1&0&1\\1&1&1&1&0\end{bmatrix} \end{matrix}

这是没有边权的情况,对于有边权(可以理解为边的长度)的图,其实只要把对应的1换成边权即可。

img

有边权的图

代码也很好写:

//这是双向有边权图的写法,其他类型的图写法类似
inline void add(int u, int v, int w)
{
    mat[u][v] = w;
    mat[v][u] = w;
}

邻接矩阵的优点显而易见:简单好写,查询速度快。但缺点也很明显:空间复杂度太高了。 n 个点对应大小 n^2 的数组,如果点的数量达到10000,这种方法就完全不可行了。

事实上,我们可以看到,上面那两个矩阵中有大量的元素是0,有大量空间被浪费了。这虽然使得我们可以迅速判断两个点之间是否没有边,但我们为此付出的代价太大了,我们其实更关注那些确实存在的边。我们希望,可以跳过这些0,直达有边的地方,就像下面这样:

\begin{matrix} &\begin{matrix}1&2&3&4&5\end{matrix}\\ \begin{matrix}1\\2\\3\\4\\5\end{matrix} &\begin{bmatrix}-&-&-&\rightarrow &1\\-&-&-&\rightarrow&1\\-&-&\rightarrow &1&-\\-&-&-&-&-\\-&\rightarrow&1&1&-\end{bmatrix} \end{matrix}


邻接表

上面那张表可以认为是邻接表的雏形。我们把邻接矩阵的数组替换为链表。当然上面那张表并不准确,因为用链表替换数组后,下标也就不复存在了。所以我们需要用一个结构体来同时储存边的终点(相当于邻接矩阵的第二个下标)和权值

//如果没有边权可以不使用结构体,只存储终点即可
struct Edge
{
    int to, w;
};

那么文中的第一张图的邻接表(无边权)应该长这个样子:

\begin{matrix} \begin{matrix}1:\\2:\\3:\\4:\\5:\end{matrix} \begin{matrix}&5\\&5\\&4\\\\&3&\rightarrow&4\end{matrix} \end{matrix}

上面那张有边权的图的邻接表则长这个样子:

\begin{matrix} \begin{matrix}1:\\2:\\3:\\4:\\5:\end{matrix} \begin{matrix}&5(8)\\&5(6)\\&4(10)&\rightarrow&5(2)\\&3(10)&\rightarrow&5(4)\\&3(2)&\rightarrow&4(10)&\rightarrow&2(6)&\rightarrow&1(8)\end{matrix} \end{matrix}

换句话说,邻接表存储每个顶点能够到达哪些顶点。注意这里链表的顺序是无关紧要的,取决于存图的顺序。

接下来按理说我们该实现链表了,但在算法竞赛上手写链表这种动态数据结构,又费时又容易写错,所以我们一般采取以下两种方法代替链表:

std::vector

STL里的vector容器,作为动态数组,既拥有链表节省内存的优点,但又可以以类似数组的方式访问,而且写法也很简便。

std::vector<Edge> edges[MAXN];
inline void add(int from, int to, int w)
{
    Edge e = {to, w};
    edges[from].push_back(e);  //向vector的最后添加一条边
}

对于无向图,调用两次add()即可:

//这对本文所有数据结构都适用
inline void add2(int u, int v, int w)
{
    add(u, v, w);
    add(v, u, w);
}

遍历图时用通常遍历数组的方法即可,注意vector的size()方法可以返回其包含元素的个数。

// 遍历2号点能到达的所有点
for (int i = 0; i <= edges[2].size(); ++i)
    printf("%d ", edges[2][i].to); 

链式前向星

另一种思路是用数组模拟链表,这样的存图方法有一个听上去很高端的名字:链式前向星。因为STL常数大,我个人更喜欢这种方法。不过它写起来稍微复杂一点。

struct Edge
{
    int to, w, next;
}edges[MAXM];
int head[MAXN], cnt; // cnt为当前边的编号
inline void add(int from, int to, int w)
{
    edges[++cnt].w = w;    //新增一条编号为cnt+1的边,边权为w
    edges[cnt].to = to;    //该边的终点为to
    edges[cnt].next = head[from];  //把下一条边,设置为当前起点的第一条边
    head[from] = cnt;  //该边成为当前起点新的第一条边
}

我们为每条边额外储存一个属性next,并赋予每条边一个编号。head数组则用于储存每个起点对应的第一条边

为了理解链式前向星存图的过程,我们用一张无权值有向图来举个例子:

一开始,没有点,也没有边,所有数组为空且cnt=0。现在我们add(1,2)

img

这时我们拥有了一条编号为1的边(注意1是编号不是权值),1号边的起点是1号顶点,现在1号顶点没有连接任何边,于是head[1]自然为1。然后1号边通往2号顶点,所以edges[1].to=2。head[1]原本为0,于是edges[1].next=0,这其实就是遍历结束的标志

然后我们add(2,3)。

img

这时新增一条编号为2的边,通往3号顶点。这条新的边“鸠占鹊巢”成为新的head[1],原来的head[1]成为它的next。然后我们add(2,4)。

img

到这里已经很明显了,如果你有关注图片最右边的那张表,会发现那就是邻接表。它跟std::vector的一个区别在于,它会把新元素添加到最前面而不是最后面。(也许这就是叫“前”向星的原因?)

遍历链式前向星的时候稍微复杂一点,类似于链表的遍历,例如:

//打印2号顶点能到达的所有点
for (int e = head[2]; e != 0; e = edges[e].next)
    printf("%d ", edges[e].to);

本文介绍了三种存图的方法,除了邻接矩阵对内存的消耗太大外,另两种方法在大部分题目都可以互换使用,主要取决于个人喜好。当然,存图只是图论题基础中的基础,具体的图论算法还需要后续慢慢学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341