赞
踩
图是常见的抽象模型,由点和连接点的边(edge)组成。图是点和边构成的网。图描述了事物之间的连接。图最典型的应用场景是地图,地图由地点和道路组成,它的特征如下。
根据边有无方向、有无权值、有无环路,可以把图分成很多种,例如:
图算法的复杂度显然和边的数量 E E E、点的数量 V V V相关。如果一个算法的复杂度是线性时间 O ( V + E ) O(V+E) O(V+E),这几乎是图问题中能达到的最好程度了。如果能达到 O ( V l o g 2 E ) O_{(Vlog₂E)} O(Vlog2E)、 O ( E l o g 2 V ) O(Elog₂V) O(Elog2V)或类似的复杂度,则是很好的算法。如果是 O ( V 2 ) O(V²) O(V2)、 O ( E 2 ) O(E²) O(E2)或更高,在图问题中不算是好的算法。
对图的任何操作都需要基于一个存储好的图。图的存储结构必须是一种有序的存储,能让程序很快定位结点
u
u
u 和
v
v
v 的边
(
u
,
v
)
(u,v)
(u,v),最好能在
O
(
1
)
O(1)
O(1)的时间内只用一次或几次就定位到。
一般用3种数据结构存储图:
用二维数组存储即可:
i
n
t
g
r
a
p
h
[
N
]
[
N
]
int\ graph[N][N]
int graph[N][N],每个节点
g
r
a
p
h
[
i
]
[
j
]
graph[i][j]
graph[i][j] 表示从
i
i
i 到
j
j
j 的距离。
对于无向图有:
g
r
a
p
h
[
i
]
[
j
]
=
g
r
a
p
h
[
j
]
[
i
]
graph[i][j]=graph[j][i]
graph[i][j]=graph[j][i]。
对于有向图有:
g
r
a
p
h
[
i
]
[
i
]
!
=
g
r
a
p
h
[
j
]
[
i
]
graph[i][i]!=graph[j][i]
graph[i][i]!=graph[j][i]。
权值:如图则有:
g
r
a
p
h
[
1
]
[
2
]
=
3
、
g
r
a
p
h
[
2
]
[
1
]
=
5
…
…
graph[1][2]=3、graph[2][1]=5……
graph[1][2]=3、graph[2][1]=5……;用
g
r
a
p
h
[
i
]
[
j
]
=
+
∞
graph[i][j]=+\infty
graph[i][j]=+∞表示
i
i
i 和
j
j
j 之间无边。
优点:
邻接表是一种常用的图的存储方式,它可以用链表表示图的结构。在邻接表中,每个节点都对应一个链表,链表中存储了与该节点相连的所有节点。规模大的稀疏图一般用邻接表存储。
有点:
vector<int>G[N]; //邻接表
void init(){ //初始化
for(int i = 0; i < N; i++)
G[i].clear();
}
void addEdge(int x, int y){ //插入路径:x -> y
G[x].push_back(y);
}
对于上图所成的邻接表如下:
i = 1 i=1 i=1 | 2 | 4 | ||
---|---|---|---|---|
i = 2 i=2 i=2 | 1 | 3 | 4 | 5 |
i = 3 i=3 i=3 | n u l l null null | |||
i = 4 i=4 i=4 | 1 | 5 | ||
i = 5 i=5 i=5 | 2 | 6 | ||
i = 6 i=6 i=6 | 3 |
用邻接表存图非常节省空间,一般的大图也够用了。那如果空间极其紧张,有没有更紧凑的存图方法呢?邻接表有没有改进的空间?
分析邻接表的组成:存储一个结点
u
u
u 的邻接边,其方法的关键是先定位第
1
1
1个边,第
1
1
1个边再指向第
2
2
2个边,第
2
2
2个边再指向第
3
3
3个边……根据这个分析,可以设计一种极为紧凑、没有任何空间浪费、编码非常简单的存图方法。下图是对前面的图生成的存储空间,其中,
h
e
a
d
[
N
U
M
]
head[NUM]
head[NUM]是一个静态数组,
s
t
r
u
c
t
e
d
g
e
struct\ edge
struct edge是一个结构的静态数组,至少包含
t
o
和
n
e
x
t
to和next
to和next 两项元素。
以结点
2
2
2为例:
从点
2
2
2出发的边有
4
4
4条,即
(
2
,
1
)
(2,1)
(2,1)、
(
2
,
3
)
(2,3)
(2,3)、
(
2
,
4
)
(2,4)
(2,4)、
(
2
,
5
)
(2,5)
(2,5),邻居是
1
、
3
、
4
、
5
1、3、4、5
1、3、4、5。
s
t
r
u
c
t
e
d
g
e
struct\ edge
struct edge的
t
o
to
to参数记录这个边的邻居结点。例如
e
d
g
e
[
8
]
.
t
o
=
5
edge[8].to=5
edge[8].to=5,第一个邻居是点
5
5
5;然后
e
d
g
e
[
6
]
.
t
o
=
4
edge[6].to=4
edge[6].to=4,
e
d
g
e
[
4
]
.
t
o
=
3
edge[4].to=3
edge[4].to=3,
e
d
g
e
[
1
]
.
t
o
=
1
edge[1].to=1
edge[1].to=1,得到邻居是
1
、
3
、
4
、
5
1、3、4、5
1、3、4、5。
上述存储方法被称为“链式前向星”,它是空间效率最高的存储方法,因为它用静态数组模拟邻接表,没有任何浪费。
const long long N = 1e7 + 6; //百万节点百万边 struct Edge{ int next, to, w; //终点to,下一条边next,权值w }; Edge edge[N]; int head[N]; //head[i]表示指向i节点的第一条边的位置 int cnt = 0; //cnt记录的是edge的末尾,新加入的位置都在末尾 void init(){ cnt = 0; for(int i = 0; i < N; i++){ edge[i].next = -1; head[i] = -1; } } void addEdge(int u, int v, int w){ //添加u到v边,权值为w edge[cnt].to = v; edge[cnt].w = w; edge[cnt].next = head[u]; head[u] = cnt; cnt++; } for(int i = head[u]; i != -1; i = edge[i].next){ //遍历i的所有邻居 }
优点:
图的遍历与之前提过的树遍历类似。但是用DFS(递归)来搜索图,比BFS更难理解,但是一旦理解之后,编程将十分便利。图论中的很多算法,例如拓扑排序、强连通分量等,都建立在DFS之上。
下面是DFS的示例程序,其中用vector邻接表来存图。
vector<int>G[N]; //G[i][j]表示点i和点j之间的距离 int vis[N]; //vis = 0 表示这个点没有被访问过 //vis = 1 表示访问过了 //vis = -1 表示正在被访问中(拓扑排序跳出死循环可用) bool dfs(int u){ vis[u] = 1; if(……){……;return true;} //出现目标,返回正确 if(……){……;return false;} //处理并返回错误 for(int i = 0; i < G[u].size(); i++){//u的邻居有G[u].size个 int v = G[u][i]; //v是第i个邻居 if(!vis[v]){ //没有访问过就从这里继续 return dfs(v); } } {……………………} //后续处理 }
对需要的点执行
d
f
s
dfs
dfs,就能找到它连通的点。例如找图中
e
e
e 点的连通性,执行
d
f
s
(
e
)
dfs(e)
dfs(e),访问过程见图结点上面的数字,顺序是
e
→
b
→
d
→
c
→
a
e\rightarrow b\rightarrow d\rightarrow c\rightarrow a
e→b→d→c→a。递归返回的结果见结点下面画线的数字,顺序是
a
→
c
→
d
→
b
→
e
a\rightarrow c\rightarrow d\rightarrow b\rightarrow e
a→c→d→b→e。虚线指向的结点表示不再访问,因为前面已经被访问过。
上面DFS的结果生成了一棵树,称为深搜优先生成树(
d
e
p
t
h
−
f
i
r
s
t
s
p
a
n
n
i
n
g
t
r
e
e
depth-firstspanning\ tree
depth−firstspanning tree),有以下概念:
图的
B
F
S
BFS
BFS和
D
F
S
DFS
DFS的一个直接应用是拓扑排序。
在现实生活中,人们经常要做一连串事情,这些事情之间有顺序关系或者有依赖关系,在做一件事情之前必须先做另一件事,比如安排客人的座位、穿衣服的先后、课程学习的先后等。这些事情都可以抽象为图论中的拓扑排序。
设有
a
、
b
、
c
、
d
a、b、c、d
a、b、c、d等事情,其中
a
a
a 有最高优先级,
b
、
c
b、c
b、c 优先级相同,
d
d
d 是最低优先级,表示为
a
→
(
b
,
c
)
→
d
a→(b,c)→d
a→(b,c)→d,那么
a
b
c
d
abcd
abcd 或
a
c
b
d
acbd
acbd 都是可行的排序。把事情看成图的点,把先后关系看成有向边,问题转化为在图中求一个有先后关系的排序,这就是拓扑排序。如下图左所示。
显然,一个图能进行拓扑排序的充要条件是它是一个有向无环图(
D
A
G
DAG
DAG)。有环图不能进行拓扑排序。
拓扑排序需要用到点的入度和出度的概念。
基于
B
F
S
BFS
BFS的拓扑排序有两种思路:1. 无前驱的顶点优先、2. 后继的顶点优先。
下面先讲解无前驱的顶点优先拓扑排序。其方法是先输出出度为
0
0
0 (无前驱,优先级最高)点,具体操作如下图所示,其中
Q
Q
Q 是
B
F
S
BFS
BFS的队列:
步骤简述如下:
以上是“无前驱”的思路。读者很容易发现,这个过程可以反过来执行,即“无后继的顶点优先”:从出度为
0
0
0(无后继,优先级最低)的点开始,逐步倒推。其示意图如下所示,请读者自己分析过程。最后输出的是逆序
[
d
,
b
,
c
,
a
]
[d,b,c,a]
[d,b,c,a]。
在初始化时,查找入度为 0 0 0的点,需要检查每个边,复杂度为 O ( E ) O(E) O(E);在队列操作中,每个点进出队列一次,需要检查它直接连接的所有邻居,复杂度是 O ( V + E ) O(V+E) O(V+E)。其总复杂度是 O ( V + E ) O(V+E) O(V+E)
D
F
S
DFS
DFS天然适合拓扑排序。
回顾
D
F
S
DFS
DFS深度搜索的原理:沿着一条路径一直搜索到最底层,然后逐层回退。这个过程正好体现了点和点的先后关系,天然符合拓扑排序的原理。
一个有向无环
D
A
G
DAG
DAG图,如果只有一个点
u
u
u是
0
0
0入度的,那么从
u
u
u开始
D
F
S
DFS
DFS,
D
F
S
DFS
DFS递归返回的顺序就是拓扑排序(是一个逆序)。
D
F
S
DFS
DFS递归返回的首先是最底层的点,它一定是
0
0
0出度点,没有后续点,是拓扑排序的最后一个点;然后逐步回退,最后输出的是起点
u
u
u;输出的顺序是一个逆序。
以上图为例,从
a
a
a 开始,递归返回的顺序见点旁边画线的数字,即
[
c
,
d
,
b
,
a
]
[c,d,b,a]
[c,d,b,a],是拓扑排序的逆序。为了按正确的顺序打印出拓扑排序,编程时的处理是定义一个拓扑排序队列
l
i
s
t
list
list,每次递归输出的时候把它插到当前
l
i
s
t
list
list的最前面,最后从头到尾打印
l
i
s
t
list
list,就是拓扑排序。这实际上是一个栈,直接用STL的stack即可。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。