1.DFS—深度优先遍历
DFS的核心部分:
①将问题转换成递归树的形式
②明确递归的终止条件
③分析回溯过程的状态
④如何保存每次DFS到最底层的状态
问题1 树的重心
问题描述
给定一颗树,树中包含 n个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围
1 ≤ n ≤ 10^5
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例
4
代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 2 * N;
int n;
int h[N], val[M], ne[M], idx;
int ans = N; //最终答案
bool st[N]; //状态数组
void add(int a, int b){
val[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
// u表示最开始选择的点, 这里u的初始值没有限制, 因为最终对每个点都会求最大连通块数
// dfs返回某个点的子节点数量
int dfs(int u){
st[u] = true;
//sum表示子节点数量
//size表示去掉该节点后,其子节点中最大连通块数量
int sum = 0, size = 0;
for(int i = h[u]; i != -1; i = ne[i]){
int j = val[i];
if(!st[j]) {
int s = dfs(j);
sum += s;
size = max(size, s);
}
}
//连通块分为两部分, 一部分是来自该节点的子节点, 另一部分来自该节点父节点
//父节点构成的连通块数量用总数减去sum即可
size = max(size, n - sum - 1);
ans = min(ans, size);
return sum + 1;
}
int main(){
memset(h, -1, sizeof h);
scanf("%d", &n);
for(int i = 0; i < n - 1; i ++){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
add(b, a);
}
dfs(1);
printf("%d", ans);
return 0;
}
解题思路
对每个结点进行分析,求每个结点去掉后的最大连通块数量,最后返回所有最大连通块数量中的最小值即可
(1) 如何求每个结点的最大连通块数量?
每个结点去掉后的连通块数量分为两个部分,一个是该结点的子结点构成的连通块,另一个是该结点父结点构成的连通块。其子节点构成的连通块可以通过DFS得到,其父结点构成的连通块可用总结点树减去以该结点为父结点构成的子树的数量
(2) 如何存储整个树的信息
树和图的存储可以用邻接表来表示,以下是表示过程中用到的数组及变量的含义:
N:结点的数量 M:边的数量,因为树是一个无向图,所以每条边需要在邻接表中存两次
h[N]: h存的是每个点对应的链表的表头坐标,h[2]=0代表2这个点对应的链表表头坐标为0
e[M]:e存的是每个结点的值,代表坐标为0的点对应的值是2
ne[M]:ne存的是每个点的下一个指针指向的点,ne[0]=2代表坐标为0的点的下一个点的坐标为2
idx:idx指的是当前用到的点
DFS各个步骤:
①递归参数:u表示起始选择的结点的编号,这里u选择任意结点均可,因为最终对每个结点都进行了讨论
②终止条件:dfs返回值为以该结点为父结点的树的总结点数量
③回溯状态:这里的状态数组st[u]记录的是结点u有没有被更新过,一旦更新过就标记成true,防止以后被重复遍历。因此一旦标记后,就不能再改成false。注意与搜索问题的区别,搜索问题需要回溯,因为一个数字可能被多次使用
细节
①无向图中的边需要存两次,比如边ab,在a和b的邻接表中都需要存储
②采用全局变量存储最优解,在递归过程中更新即可
③起始DFS参数的选择不影响最终结果
2.BFS—宽度优先遍历
BFS常用于解决图或树中最短距离、最短路径等问题,因为采用BFS第一次遍历到某个结点的距离一定是最短距离(前提:边的权重相同)
BFS的核心部分:
①状态表示,如何表示某个结点的状态,常用数组记录某个结点到起点的距离,在BFS过程中更新
②队列使用,BFS采用队列来维护每一层遍历的结果
解决问题时需要根据问题描述将问题抽象成图或树的最短距离问题,采用BFS宽搜得到最优解
问题1 走迷宫
问题描述
给定一个 n×m的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1)处和 (n,m)处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
1 ≤ n,m ≤ 100
输入样例
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例
8
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#define x first
#define y second
using namespace std;
const int N = 110;
int n, m;
int g[N][N];
int dist[N][N]; //记录某个点到起点的距离
pair<int, int> st[N * N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int bfs(){
int hh = 0, tt = 0;
st[0] = {0, 0};
memset(dist, -1, sizeof dist);
dist[0][0] = 0;
while(hh <= tt)
{
pair<int, int> temp = st[hh ++];
// 遍历四个方向
for(int i = 0; i < 4; i ++){
int x = temp.x + dx[i], y = temp.y + dy[i];
if(x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && dist[x][y] == -1){
dist[x][y] = dist[temp.x][temp.y] + 1;
st[++tt] = {x, y};
}
}
}
return dist[n - 1][m - 1];
}
int main(){
cin >> n >> m;
for(int i = 0; i < n; i ++)
for(int j = 0; j < m; j ++)
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
解题思路
采用dist数组记录每个结点到起始结点的距离,首先将起始结点入队,每次搜索时,从队头取出结点,遍历该结点四个方向的结点,若符合要求则更新其dist数组对应的值然后入队,遍历完整个迷宫后,最后一个点的dist数组对应的值即为所求
细节
①采用数组模拟队列,注意写法
②遍历四个方向时,采用偏移量的方法
问题2 八数码
问题描述
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x
恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x
与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 x
例如,示例中图形就可以通过让 x
先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
x 4 6 4 x 6 4 5 6 4 5 6
7 5 8 7 5 8 7 x 8 7 8 x
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式
输入占一行,将 3×3 的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出 −1 。
输入样例
2 3 4 1 5 x 7 6 8
输出样例
19
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<unordered_map>
using namespace std;
int dx[4] = {0, 1, -1, 0}, dy[4] = {1, 0, 0, -1};
int bfs(string state){
queue<string> q;
unordered_map<string, int> dist; //用map存储每个网格状态对应的交换次数
q.push(state);
dist[state] = 0;
string res = "12345678x"; //结果状态
while(!q.empty()){
auto t = q.front();
q.pop();
if(t == res) return dist[t]; //结果状态直接返回
int distance = dist[t];
int k = t.find('x'); //查找x的下标
int x = k / 3, y = k % 3; //一维映射到3×3常用技巧
for(int i = 0; i < 4; i ++){
int tx = x + dx[i], ty = y + dy[i];
if(tx >= 0 && tx < 3 && ty >= 0 && ty < 3){
swap(t[tx * 3 + ty], t[k]);
if(!dist.count(t)) { //当前状态还没遍历过
dist[t] = distance + 1;
q.push(t);
}
swap(t[tx * 3 + ty], t[k]); //恢复现场
}
}
}
return -1;
}
int main(){
//采用string数组记录网格状态
//如"1234X5678"
string start;
for(int i = 0; i < 9; i ++){
char c;
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
解题思路
本题的难点在于状态表示,如何表示每次交换的过程中整个网格的状态
思路是将网格的坐标表示转换成字符串表示,对网格内元素的交换相当于对string内的元素进行交换,同时距离的状态存储采用map来存,用map存储每个string状态对应的交换距离
具体步骤如下:
①初始化起始状态,并记录最终的结果状态res
②每次从队列头部取出一个状态,判断该状态是否等于res
③若不等于res,查找'x'在该状态内的下标,并映射到网格中
④根据偏移量计算可以交换的网格内的位置,并将该位置映射带string中的下标
⑤交换string内的两个元素位置,并将交换结果入队
⑥恢复现场
细节
①状态表示,因为网格上每次交换的状态不好用数组表示,采用一维string表示
②距离表示,对每个string的距离存储可用map来存储
③核心部分:一维和二维坐标转换时的下标计算
问题3 图中结点的层次
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
数据范围
1 ≤ n,m ≤ 10^5
输入样例
4 5
1 2
2 3
3 4
1 3
1 4
输出样例
1
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int h[N], val[N], ne[N], idx;
int dist[N], q[N]; //dist[N]记录当前点距起点的距离, q[N]采用数组模拟队列
void add(int a, int b){
val[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int bfs(){
int hh = 0, tt = 0;
memset(dist, -1, sizeof dist);
q[0] = 1;
dist[1] = 0;
while(hh <= tt){
int t = q[hh ++];
for(int i = h[t]; i != -1; i = ne[i]){
int j = val[i];
if(dist[j] == -1){
dist[j] = dist[t] + 1;
q[++tt] = j;
}
}
}
return dist[n];
}
int main(){
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i ++){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
printf("%d", bfs());
return 0;
}
解题思路
对图的存储方式采用邻接表存储,dist数组记录的是每个结点到初始结点的距离,其他过程均为常规BFS过程
细节
本题需要注意的是,在对队列头的每个结点的下一个结点遍历时的for循环,使用ne数组来进行搜索,同时每次入队的不是i,而是val数组内记录的val[i]