背景:
本来以前学过的东西都懒得写了,不过小徒弟学图论好像碰到瓶颈了,就写一篇大致讲一下。
概念:
首先,我们要了解图的定义。
什么是图?
一张图
G
G
G由两个集合组成的二元组,点集
V
V
V和边集
E
E
E。即
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)。
边就是连接点和点之间的关系,分成有向边和无向边两种。
无向图就是都是无向边的图。
有向图就是有向边组成的图。
具体题目中,边和点可能由权值,也就是点权和边权。
路径是一个点到另一个点经过的边的序列。
简单路径指没有经过重复边的路径。
环,也叫回路,指的是一个点经过一条简单路径回到自身。
度一个点连的边的数量。
出度一个点连的出边的数量。
入度一个点连的入边的数量。
重边是两条起点和终点相同的边。
自环是自己连到自己的边,理论上也叫环。
因为这两个东西的存在比较奇怪,有时候题目里会特意注明不存在重边和自环,如果没有的话就要小心了。
还有竞赛中常见的一种图有向无环图(Directed Acyclic Graph),顾名思义,就是有方向的没有环的图(都是有向边,没有无向边)。俗称DAG。
再常见的一种图,树。题目太常见,这里入门就不介绍了。由多棵树组成的图叫森林,名字很形象了。
还有二分图,一个图是二分图有一个重要的等价条件是这个图没有奇环。(奇环:奇数个点构成的环)
还有一些会出到题目的高级图:弦图,基环树(基环内向树和基环外向树),仙人掌(圆方树)。
也许还有……
建图:
既然知道了图,我们需要学习怎么在代码中构建一张图。
那么构建一张图需要有什么操作呢。
1.
1.
1.加边:我们需要在图中添加一条边。
2.
2.
2.遍历一个点连接的边。
用数组把所有边存起来的办法就不说了。
这里介绍三种我常用的建图方式。
一、矩阵存图
就是用一个二维数组,行列都代表点,矩阵内的值表示边权。
那么解决上面两个问题,我们要添加一条边
(
a
,
b
)
(a,b)
(a,b),边权为
w
w
w,那么就
t
u
[
a
]
[
b
]
=
w
tu[a][b]=w
tu[a][b]=w。(这里不考虑重边)
如果是无向边,那其实就是添加两条有向边,
t
u
[
a
]
[
b
]
=
t
u
[
b
]
[
a
]
=
w
tu[a][b]=tu[b][a]=w
tu[a][b]=tu[b][a]=w。
那么另一个问题,怎么去遍历一个点 x x x连的边,其实就是第 x x x行扫过去。
int tu[maxn][maxn];//图
memset(tu,0,sizeof(tu));//初始化图
tu[a][b]=w;//添加一条a到b的边权为w的有向边
tu[a][b]=tu[a][b]=w;//添加一条a到b的边权为w的无向边
//遍历点x连的边
for(int i=1;i<=n;i++)if(i!=x&&tu[x][i]!=0)
{
printf("点x到点%d,边权为%d的边\n",i,tu[x][i]);
}
这样建图有两个缺陷。一是内存占用是
o
(
n
2
)
o(n^{2})
o(n2),当点数为
1
0
5
10^5
105的时候就开不下了。二是我们遍历一个点连的边复杂度是
o
(
n
)
o(n)
o(n),但这个点连的边可能不足
n
n
n,造成了浪费。
当然也是有好处的,比如删边和加边可以o(1),也可以快速查找某两个点之间的边。
为了有时候对上面两个缺陷的需要,就有了后面两种建图的方式。
二、vector存图(邻接表存图)
数据结构课里的邻接表存图还是老老实实地写链表,这里只介绍竞赛里常用的。
大概介绍一下邻接表存图,我们对每一个点建一个点结点
[
a
]
[a]
[a],当我们要加一条有向边(a,b)的时候,就把它接在
[
a
]
[a]
[a]的后面。(如果是无向边,那就
[
a
]
[a]
[a]和
[
b
]
[b]
[b]后面都接一个,即加入有向边(a,b)和(b,a))
比如加入有向边
(
a
,
b
)
(a,b)
(a,b),
(
a
,
c
)
(a,c)
(a,c),
(
a
,
d
)
(a,d)
(a,d)后,
[
a
]
[a]
[a]的情况如下。
[
a
]
−
>
(
b
)
−
>
(
c
)
−
>
(
d
)
[a]->(b)->(c)->(d)
[a]−>(b)−>(c)−>(d)
(
)
()
()内的表示边的信息,如果还有边权,就变成下面这样了。
[
a
]
−
>
(
b
,
w
b
)
−
>
(
c
,
w
c
)
−
>
(
d
,
w
d
)
[a]->(b,wb)->(c,wc)->(d,wd)
[a]−>(b,wb)−>(c,wc)−>(d,wd)
我们放到
v
e
c
t
o
r
vector
vector里面,就是
v
[
a
]
=
{
(
b
,
w
b
)
,
(
c
,
w
c
)
,
(
d
,
w
d
)
}
v[a]=\{(b,wb),(c,wc),(d,wd)\}
v[a]={(b,wb),(c,wc),(d,wd)}。
或者不想写结构体,也可以用两个
v
e
c
t
o
r
vector
vector,
v
[
a
]
=
{
b
,
c
,
d
}
v[a]=\{b,c,d\}
v[a]={b,c,d}
g
[
a
]
=
{
w
b
,
w
c
,
w
d
}
g[a]=\{wb,wc,wd\}
g[a]={wb,wc,wd}
上下一一出点和边权对应就行了。
//第一种写法,用结构体
struct E
{
int to,w;
};
vector<E>v[maxn];//图
for(int i=0;i<maxn;i++)v[i].clear();//初始化图
v[a].push_back(E{b,w});//添加一条a到b的边权为w的有向边
//添加一条a到b的边权为w的无向边
v[a].push_back(E{b,w});
v[b].push_back(E{a,w});
//遍历点x的连的出边
for(int i=0;i<v[x].size();i++)
{
int to=v[x][i].to,w=v[x][i].w;
printf("点x到点%d,边权为%d的边\n",to,w);
}
//第二种写法,不用结构体
vector<int>v[maxn],g[maxn];//图
for(int i=0;i<maxn;i++)v[i].clear(),g[i].clear();//初始化图
v[a].push_back(b);//添加一条a到b的边权为w的有向边
g[a].push_back(w);
//添加一条a到b的边权为w的无向边
v[a].push_back(b);
g[a].push_back(w);
v[b].push_back(a);
g[b].push_back(w);
//遍历点x的连的出边
for(int i=0;i<v[x].size();i++)
{
int to=v[x][i],w=g[x][i];
printf("点x到点%d,边权为%d的边\n",to,w);
}
三、链式前向星
存图方式和前面的一样,都是接在后面。
不过这个是用链表实现,这个写的时候链表也不用指针,用下标索引实现。
具体理解看代码吧。
struct
{
int to,w,next;
}edge[maxm];
int head[maxn],tot;//图
void init()//初始化图
{
tot=0;
memset(head,-1,sizeof(head));
}
void addedge(int a,int b,int w)//添加一条a到b边权为w的有向边,无向边就正反各加一次
{
edge[tot].to=b;
edge[tot].w=w;
edge[tot].next=head[a];
head[a]=tot++;
}
//遍历点x的连的出边
for(int i=head[x];i!=-1;i=edge[i].next)
{
int to=edge[i].to,w=edge[i].w;
printf("点x到点%d,边权为%d的边\n",to,w);
}
讲完了。