铺垫
树与图最常见的存储方式就是邻接表。树可以看作是具有 N − 1 N-1 N−1条边的无向图,他们的边都存储在一个邻接表中,邻接表以 h e a d head head数组为表头,使用 v e r ver ver和 e d g e edge edge数组分别存储边的终点和权值,使用 n e x t next next数组模拟链表中的指针。
树与图的深度优先遍历
深度优先遍历,就是就是在某一个节点
x
x
x有多条分支时,任选一条边走下去,进行递归,直到回溯到
x
x
x,再考虑走另一条边,根据上面提到的存储方式,我们可以采用下面的代码,调用
d
f
s
(
1
)
dfs(1)
dfs(1),对一张图进行深度优先遍历。
C
o
d
e
:
Code:
Code:
void dfs(int x)
{
vis[x] = 1;//记录x被访问过
for (int i = head[x]; i; i = nxt[i])//用next编译会报错
{
int y = ver[i];//边的终点
if (vis[y]) continue;//如果访问过,则跳转到下一个
dfs(y);//递归遍历
}
}
这段代码访问每个节点恰好1次(无向边正反各一次),时间复杂度为 O ( N + M ) O(N+M) O(N+M), M M M为边数。
树的DFS序
我们在对树进行深度优先遍历时,对于每个节点,在刚进入递归后以及即将回溯时,各记录一次该点的编号,最后产生的 2 N 2N 2N的节点序列(递归前+回溯前)即称为树的 d f s dfs dfs序
C o d e : Code: Code:
void dfs(int x)
{
a[++m] = x;//a数组存储dfs序
vis[x] = 1;//记录x被访问过
for (int i = head[x]; i; i = nxt[i])//用next编译会报错
{
int y = ver[i];//边的终点
if (vis[y]) continue;//如果访问过,则跳转到下一个
dfs(y);//递归遍历
}
a[++m] = x;
}
d f s dfs dfs序的特点是:每个节点 x x x的编号在序列中恰好出现两次。设这两次出现的位置为 L [ x ] L[x] L[x]与 R [ x ] R[x] R[x],那么闭区间 [ l [ x ] , R [ x ] ] [l[x],R[x]] [l[x],R[x]]就是以 x x x为根的子树的 d f s dfs dfs序(因为在这个子树中第一个递归的是 x x x,最后一个回溯的也是 x x x).所以,可以通过 d f s dfs dfs序把树转化区间进行解题。
树的深度
树中各个节点的深度是一种自顶向下的统计信息。起初,我们已知根节点的深度为 0 0 0,若节点 x x x的深度是 d e e p [ x ] deep[x] deep[x],则它的子结点y的深度就是 d e e p [ y ] = d e e p [ x ] + 1 deep[y]=deep[x]+1 deep[y]=deep[x]+1.在深度优先遍历的过程中结合自顶向下的递推,就可以求出每一个节点的深度d。
C o d e : Code: Code:
void dfs(int x)
{
vis[x] = 1;
for (int i = head[x]; i; i = nxt[i])
{
int y = ver[i];
if (vis[y]) continue;
deep[y]=deep[x] + 1; //从父结点x到子节点y进行递推,计算深度
dfs(y);
}
}
树的重心
也有许多信息是自底向上统计的,比如以每个节点
x
x
x为根的子树大小
s
i
z
e
[
x
]
size[x]
size[x].对于叶子节点,我们知道:以这个叶子节点为根的子树大小为
1
1
1,若节点
x
x
x有
k
k
k个字节点
Y
1
Y_1
Y1 ~
Y
k
Y_k
Yk,并且以
Y
1
Y_1
Y1~
Y
k
Y_k
Yk为根的子树大小分别是
s
i
z
e
[
x
]
=
s
i
z
e
[
Y
1
]
+
s
i
z
e
[
Y
2
]
+
.
.
.
+
s
i
z
e
[
Y
k
+
]
+
1
size[x]=size[Y_1]+size[Y_2]+...+size[Y_k+]+1
size[x]=size[Y1]+size[Y2]+...+size[Yk+]+1
对于一个节点x,如果我们把它从树中删除,那么原来的一棵树可能会分成若干个不相连的部分,其中每部分都是一棵子树。用
M
a
x
N
u
m
(
x
)
MaxNum(x)
MaxNum(x)表示删除x节点后产生的子树中最大一棵的大小。使
M
a
x
N
u
m
(
x
)
MaxNum(x)
MaxNum(x)函数取到最小值的节点p就称为整棵树的重心。
C o d e : Code: Code:
void dfs(int x)
{
vis[x] = 1;size[x] = 1;//子树x的大小
int max_num = 0;//删掉x后分成的最大子树的大小
for (int i = head[x]; i; i = nxt[i])
{
int y = ver[i];
if (vis[y]) continue;
dfs(y);
size[x] += size[y];//从子节点向父节点递推
max_num = max(max_num, size[y]);
}
max_num = max(max_num, n - size[x]);//n为整棵树的节点数目
if (max_num < ans)
{
ans = max_num;//ans是全局变量记录重心所对应的max_num值
pos = x;//pos是全局变量记录重心
}
}
连通块的划分
上面的代码每从x开始一次遍历,就会访问x能够到达得所有节点与边。所以我们可用使用多次dfs,划分出一张图中的各个连通区域。同理,对一个森林进行dfs可以划分出森林中的每棵树,如下面代码,cnt是图包含的连通区域数目,v数组标记每个节点属于哪个连通块。
C o d e : Code: Code:
void dfs(int x)
{
v[x] = cnt;
for (int i = head[x]; i; i = nxt[i])
{
int y = ver[i];//边的终点
if (vis[y]) continue;
dfs(y);//递归遍历
}
}
for (int i = 1; i <= n; i++)//在main函数中
{
if (!v[i])
{
cnt++;
dfs(i);
}
}
树与图的广度优先遍历
广度优先遍历需要使用队列实现。起初,队列只有一个起点元素(例如
1
1
1号节点).在广度优先遍历的过程中,我们从队头不断取出(删除)一个节点
x
x
x,对于
x
x
x面对的多条分支,把沿着每条分支到达的下一节点(当然是未访问过的节点)插入队尾重复执行上述过程知道队空为止即可。
下面的代码就是使用广度优先遍历
(
b
f
s
)
(bfs)
(bfs)历来遍历一张图(本代码使用STL,不懂的可以上网搜寻资料):
C o d e : Code: Code:
void bfs()
{
memset(d, 0, sizeof(d));
queue<int> Q;
Q.push(1);d[1] = 1;
while(!Q.empty())
{
int x = Q.front();
Q.pop();
for (int i = head[x]; i; i = nxt[i])
{
int y = ver[i];
if (d[y]) continue;
d[y] = d[x] + 1;
Q.push(y);
}
}
}
从代码中可以看出,广度优先搜索的性质:
先访问完所有的第
i
i
i层节点后再开始访问第
i
+
1
i+1
i+1层
而且任意时刻,队列中至多有两个层次的节点。其中一部分属于第
i
i
i层,另一部分属于第
i
+
1
i+1
i+1层。并且所有
i
i
i层节点排在所有第
i
+
1
i+1
i+1层节点之前。
和深搜一样复杂度为
O
(
N
+
M
)
O(N+M)
O(N+M)