1. DFS
数据结构:stack
空间复杂度:O(h)
不具最短性
每个DFS
都对应一个搜索树
- 排列数字
给定一个整数 n,将数字 1∼n排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 n。
输出格式
按字典序输出所有排列方案,每个方案占一行。
int n;
int p[N];
bool st[N];
void dfs(int u) {
if (u == n) {
for (int i = 0; i < n; i ++ ) {
cout << p[i] << " ";
}
cout << endl;
return ;
}
for (int i = 1; i <= n; i ++ ) {
if (!st[i]) {
p[u] = i;
st[i] = 1;
dfs(u + 1);
st[i] = 0;
}
}
}
void solve() {
cin >> n;
dfs(0);
}
int main() {
int t = 1;
// cin >> t;
while (t -- ) {
solve();
}
return 0;
}
- n-皇后问题
int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N];
void dfs(int x, int y, int s) { // x,y是横纵坐标,s是放到棋盘的元素个数
if (y == n) { // 搜到每行最后一个,换行继续搜
y = 0;
x ++ ;
}
if (x == n) { // 搜到最后一行,停止
if (s == n) { // 元素个数等于n,找到一组符合题意的解
for (int i = 0; i < n; i ++ ) {
puts(g[i]);
}
puts("");
}
return ;
}
// 不放元素,直接递归到下一个格子
dfs(x, y + 1, s);
// 放元素
if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) { // 反对角线+n是为了防止数组下标越界
g[x][y] = 'Q'; // 赋值
row[x] = col[y] = dg[x + y] = udg[x - y + n] = 1; // 标记
dfs(x, y + 1, s + 1); // 递归到下一个格子
row[x] = col[y] = dg[x + y] = udg[x - y + n] = 0; // 恢复现场
g[x][y] = '.';
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < n; j ++ ) {
g[i][j] = '.';
}
}
dfs(0, 0, 0);
return 0;
}
DFS之连通性模型
- 迷宫
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n∗n的格点组成,每个格点只有2种状态,.
和#
,前者表示可以通行后者表示不能通行。
同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。
如果起点或者终点有一个不能通行(为#
),则看成无法办到。
注意:A、B不一定是两个不同的点。
输入格式
第1行是测试数据的组数 k,后面跟着 k组输入。
每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n∗n 的。
接下来是一个 n∗n的矩阵,矩阵中的元素为.
或者#
。
再接下来一行是 4 个整数 ha,la,hb,lb,描述 A处在第 ha行, 第 la列,B处在第 hb行, 第 lb 列。
注意到 ha,la,hb,lb 全部是从 0 开始计数的。
输出格式
k行,每行输出对应一个输入。
能办到则输出“YES”,否则输出“NO”。
const int N = 110;
char g[N][N];
bool st[N][N];
int n;
int xa, ya, xb, yb;
bool dfs(int x, int y) {
if (g[x][y] == '#') {
return false;
}
if (x == xb && y == yb) {
return true;
}
st[x][y] = true;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
for (int i = 0; i < 4; i ++ ) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) {
continue;
}
if (st[nx][ny]) {
continue;
}
if (dfs(nx, ny)) {
return true;
}
}
return false;
}
void solve() {
memset(st, 0, sizeof st);
cin >> n;
for (int i = 0; i < n; i ++ ) {
cin >> g[i];
}
cin >> xa >> ya >> xb >> yb;
if (dfs(xa, ya)) {
puts("YES");
} else {
puts("NO");
}
}
int main() {
int t;
cin >> t;
while (t -- ) {
solve();
}
return 0;
}
- 红与黑
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。
你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。
请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
输入格式
输入包括多个数据集合。
每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y方向瓷砖的数量。
在接下来的 H行中,每行包括 W个字符。每个字符表示一块瓷砖的颜色,规则如下
1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。
当在一行中读入的是两个零时,表示输入结束。
输出格式
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
char g[N][N];
bool st[N][N];
int n, m;
int bfs(int x, int y) {
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
queue<PII> q;
q.push({x, y});
st[x][y] = 1;
int cnt = 1;
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 4; i ++ ) {
int nx = t.first + dx[i], ny = t.second + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (st[nx][ny]) {
continue;
}
if (g[nx][ny] != '.') {
continue;
}
q.push({nx, ny});
st[nx][ny] = 1;
cnt ++ ;
}
}
return cnt;
}
int main() {
while (cin >> m >> n, n || m) {
memset(st, 0, sizeof st);
for (int i = 0; i < n; i ++ ) {
cin >> g[i];
}
int x, y;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
if (g[i][j] == '@') {
x = i, y = j;
}
}
}
cout << bfs(x, y) << endl;
}
return 0;
}
DFS之搜索顺序
- 马走日
马在中国象棋以日字形规则移动。
请编写一段程序,给定 n∗m大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。
输入格式
第一行为整数 T,表示测试数据组数。
每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。
输出格式
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。
bool st[N][N];
int res;
int n, m;
void dfs(int x, int y, int cnt) {
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
if (cnt == n * m) { // 退出条件
res ++ ;
return ;
}
st[x][y] = 1; // 标记
for (int i = 0; i < 8; i ++ ) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (st[nx][ny]) {
continue;
}
dfs(nx, ny, cnt + 1);
}
st[x][y] = 0; // 恢复现场
}
void solve() {
int x, y;
cin >> n >> m >> x >> y;
memset(st, 0, sizeof st);
res = 0;
dfs(x, y, 1);
cout << res << endl;
}
int main() {
int t;
cin >> t;
while (t -- ) {
solve();
}
return 0;
}
- 单词接龙
单词接龙是一个与我们经常玩的成语接龙相类似的游戏。
现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”,每个单词最多被使用两次。
在两个单词相连时,其重合部分合为一部分,例如 beast 和 astonish ,如果接成一条龙则变为 beastonish。
我们可以任意选择重合部分的长度,但其长度必须大于等于1,且严格小于两个串的长度,例如 at 和 atide 间不能相连。
输入格式
输入的第一行为一个单独的整数 n 表示单词数,以下 n行每行有一个单词(只含有大写或小写字母,长度不超过20),输入的最后一行为一个单个字符,表示“龙”开头的字母。
你可以假定以此字母开头的“龙”一定存在。
输出格式
只需输出以此字母开头的最长的“龙”的长度。
int n;
int g[N][N]; // 存单词之间的重叠部分的最小长度
int used[N]; // 存单词用过几次
string w[N];
int res;
void dfs(string dragon, int last) {
res = max((int)dragon.size(), res); // 最大值,dragon.size()为当前重复合并长度
used[last] ++ ;
for (int i = 0; i < n; i ++ ) {
if (g[last][i] && used[i] < 2) { // 可以连并且使用次数小于2
dfs(dragon + w[i].substr(g[last][i]), i);
}
}
used[last] -- ; // 恢复现场
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++ ) {
cin >> w[i];
}
char start;
cin >> start;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < n; j ++ ) {
string a = w[i], b = w[j];
for (int k = 1; k <= min(a.size(), b.size()); k ++ ) {
if (a.substr(a.size() - k, k) == b.substr(0, k)) {
g[i][j] = k;
break;
}
}
}
}
for (int i = 0; i < n; i ++ ) {
if (w[i][0] == start) {
dfs(w[i], i);
}
}
cout << res << endl;
return 0;
}
- 分成互质组
给定 n 个正整数,将它们分组,使得每组中任意两个数互质。
至少要分成多少个组?
输入格式
第一行是一个正整数 n。
第二行是 n个不大于10000的正整数。
输出格式
一个正整数,即最少需要的组数。
int n;
int p[N];
int group[N][N];
bool st[N];
int ans = N;
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
bool check(int group[], int gc, int i) {
for (int j = 0; j < gc; j ++ ) {
if (gcd(p[group[j]], p[i]) > 1) {
return false;
}
}
return true;
}
void dfs(int g, int gc, int tc, int start) {
if (g >= ans) {
return;
}
if (tc == n) {
ans = g;
}
bool flag = true;
for (int i = start; i < n; i ++ ) {
if (!st[i] && check(group[g], gc, i)) {
st[i] = true;
group[g][gc] = i;
dfs(g, gc + 1, tc + 1, i + 1);
st[i] = false;
flag = false;
}
}
if (flag) {
dfs(g + 1, 0, tc, 0);
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i ++ ) {
cin >> p[i];
}
dfs(1, 0, 0, 0);
cout << ans << endl;
return 0;
}
2. BFS
数据结构:queue
空间复杂度:O(2^h)
最短路
一般思路
queue <-初始
while(队列不空) {
t<-队头
拓展队头
}
-
走迷宫
给定一个 n×m的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1)(1,1) 处和 (n,m)处的数字为 0,且一定至少存在一条通路。
int a[N][N], d[N][N];
int n, m;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int bfs() {
queue<PII> q;
memset(d, -1, sizeof d);
q.push({0, 0});
d[0][0] = 0;
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 4; i ++ ) {
int nx = t.first + dx[i], ny = t.second + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (a[nx][ny] == 1) {
continue;
}
if (d[nx][ny] != -1) {
continue;
}
// if (nx >= 0 && nx < n && ny >= 0 && ny < m && a[nx][ny] == 0 && d[nx][ny] == -1) {
d[nx][ny] = d[t.first][t.second]+ 1;
q.push({nx, ny});
// }
}
}
return d[n - 1][m - 1];
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
cin >> a[i][j];
}
}
cout << bfs() << endl;
return 0;
}
- 八数码
int bfs(string start) {
string end = "12345678x";
queue<string> q;
unordered_map<string, int> d;
q.push(start);
d[start] = 0;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
while (q.size()) {
auto t = q.front();
q.pop();
int dd = d[t];
if (t == end) {
return dd;
}
// 状态转移
int k = t.find('x');
int x = k / 3, y = k % 3;
for (int i = 0; i < 4; i ++ ) {
int nx = x + dx[i], ny = y + dy[i];
if (nx >= 0 && nx < 3 && ny >= 0 && ny < 3) {
swap(t[k], t[nx * 3 + ny]);
if(!d.count(t)) {
d[t] = dd + 1;
q.push(t);
}
swap(t[k], t[nx * 3 + ny]);
}
}
}
return -1;
}
int main() {
string start;
for (int i = 0; i < 9; i ++ ) {
char c;
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
Flood Fill
-
池塘计数
农夫约翰有一片 N∗M的矩形土地。
最近,由于降雨的原因,部分土地被水淹没了。
现在用一个字符矩阵来表示他的土地。
每个单元格内,如果包含雨水,则用”W”表示,如果不含雨水,则用”.”表示。
现在,约翰想知道他的土地中形成了多少片池塘。
每组相连的积水单元格集合可以看作是一片池塘。
每个单元格视为与其上、下、左、右、左上、右上、左下、右下八个邻近单元格相连。
请你输出共有多少片池塘,即矩阵中共有多少片相连的”W”块。
输入格式
第一行包含两个整数 N和 M。
接下来 N行,每行包含 M个字符,字符为”W”或”.”,用以表示矩形土地的积水状况,字符之间没有空格。
输出格式
输出一个整数,表示池塘数目。
char g[N][N];
bool st[N][N];
int n, m;
int dx[8] = {-1, -1, -1, 0, 1, 1, 1, 0};
int dy[8] = {-1, 0, 1, 1, 1, 0, -1, -1};
void bfs(int x, int y) {
queue<PII> q;
q.push({x, y});
st[x][y] = 1;
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 8; i ++ ) {
int nx = t.first+ dx[i], ny = t.second + dy[i];
if (nx >= 0 && nx < n && ny >= 0 && ny < m && g[nx][ny] == 'W' && !st[nx][ny]) {
q.push({nx, ny});
st[nx][ny] = 1;
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) {
scanf("%s", g[i]);
}
int res = 0;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
if (g[i][j] == 'W' && !st[i][j]) {
bfs(i, j);
res ++ ;
}
}
}
printf("%d", res);
return 0;
}
- 城堡问题
1 2 3 4 5 6 7
#############################
1 # | # | # | | #
#####---#####---#---#####---#
2 # # | # # # # #
#---#####---#####---#####---#
3 # | | # # # # #
#---#########---#####---#---#
4 # # | | | | # #
#############################
(图 1)
# = Wall
| = No wall
- = No wall
方向:上北下南左西右东。
图1是一个城堡的地形图。
请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。
城堡被分割成 m∗n个方格区域,每个方格区域可以有0~4面墙。
注意:墙体厚度忽略不计。
输入格式
第一行包含两个整数 m 和 n,分别表示城堡南北方向的长度和东西方向的长度。
接下来 m行,每行包含 n个整数,每个整数都表示平面图对应位置的方块的墙的特征。
每个方块中墙的特征由数字 P来描述,我们用1表示西墙,2表示北墙,4表示东墙,8表示南墙,P为该方块包含墙的数字之和。
例如,如果一个方块的 P为3,则 3 = 1 + 2,该方块包含西墙和北墙。
城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。
输入的数据保证城堡至少有两个房间。
输出格式
共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。
int g[N][N];
int n, m;
bool st[N][N];
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};
int bfs(int x, int y) {
queue<PII> q;
q.push({x, y});
st[x][y] = 1;
int area = 0;
while (q.size()) {
auto t = q.front();
q.pop();
area ++ ;
for (int i = 0; i < 4; i ++ ) {
int nx = t.first + dx[i], ny = t.second + dy[i];
if (nx >= 0 && nx < n && ny >= 0 && ny < m && !st[nx][ny] && !(g[t.first][t.second] >> i & 1)) {
q.push({nx, ny});
st[nx][ny] = 1;
}
}
}
return area;
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
cin >> g[i][j];
}
}
int cnt = 0, area = 0;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
if (!st[i][j]) {
area = max(area, bfs(i, j));
cnt ++ ;
}
}
}
cout << cnt << endl << area << endl;
return 0;
}
- 山峰和山谷
FGD小朋友特别喜欢爬山,在爬山的时候他就在研究山峰和山谷。
为了能够对旅程有一个安排,他想知道山峰和山谷的数量。
给定一个地图,为FGD想要旅行的区域,地图被分为 n×n的网格,每个格子 (i,j)的高度 w(i,j)是给定的。
若两个格子有公共顶点,那么它们就是相邻的格子,如与 (i,j)相邻的格子有(i−1,j−1),(i−1,j),(i−1,j+1),(i,j−1),(i,j+1),(i+1,j−1),(i+1,j),(i+1,j+1)。
我们定义一个格子的集合 S为山峰(山谷)当且仅当:
- S 的所有格子都有相同的高度。
- S 的所有格子都连通。
- 对于 s 属于 S,与 s 相邻的 s不属于 S,都有 ws>ws′(山峰),或者 ws<ws′(山谷)。
- 如果周围不存在相邻区域,则同时将其视为山峰和山谷。
你的任务是,对于给定的地图,求出山峰和山谷的数量,如果所有格子都有相同的高度,那么整个地图即是山峰,又是山谷。
输入格式
第一行包含一个正整数 n,表示地图的大小。
接下来一个 n×n 的矩阵,表示地图上每个格子的高度 w。
输出格式
共一行,包含两个整数,表示山峰和山谷的数量。
int g[N][N];
bool st[N][N];
int n;
int dx[8] = {-1, -1, -1, 0, 1, 1, 1, 0};
int dy[8] = {-1, 0, 1, 1, 1, 0, -1, -1};
void bfs(int x, int y, bool& has_higher, bool& has_lower) {
queue<PII> q;
q.push({x, y});
st[x][y] = 1;
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 8; i ++ ) {
int nx = t.first + dx[i], ny = t.second + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) {
continue;
}
if (g[nx][ny] != g[t.first][t.second]) {
if (g[nx][ny] > g[t.first][t.second]) {
has_higher = true;
} else {
has_lower = true;
}
} else if (!st[nx][ny]) {
q.push({nx, ny});
st[nx][ny] = 1;
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < n; j ++ ) {
scanf("%d", &g[i][j]);
}
}
int peak = 0, valley = 0;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < n; j ++ ) {
if (!st[i][j]) {
bool has_higher = false, has_lower = false;
bfs(i, j, has_higher, has_lower);
if (!has_higher) {
peak ++ ;
}
if (!has_lower) {
valley ++ ;
}
}
}
}
printf("%d %d", peak, valley);
return 0;
}
最短路模型
- 迷宫问题
给定一个 n×n的二维数组,如下所示:
int maze[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,
};
它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。
数据保证至少存在一条从左上角走到右下角的路径。
输入格式
第一行包含整数 n。
接下来 n行,每行包含 n个整数 0 或 1,表示迷宫。
输出格式
输出从左上角到右下角的最短路线,如果答案不唯一,输出任意一条路径均可。
按顺序,每行输出一个路径中经过的单元格的坐标,左上角坐标为 (0,0)(0,0),右下角坐标为 (n−1,n−1)。
int n;
int g[N][N];
PII pre[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
void bfs(int x, int y) {
memset(pre, -1, sizeof pre);
queue<PII> q;
q.push({x, y});
pre[x][y] = {0, 0};
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 4; i ++ ) {
int nx = t.first + dx[i], ny = t.second + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) {
continue;
}
if (g[nx][ny]) {
continue;
}
if (pre[nx][ny].first != -1) {
continue;
}
q.push({nx, ny});
pre[nx][ny] = t;
}
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < n; j ++ ) {
scanf("%d", &g[i][j]);
}
}
bfs(n - 1, n - 1); // 因为要输出路径,所以倒序进行bfs可以在输出的时候直接正序输出
PII end(0, 0);
while (true) {
printf("%d %d\n", end.first, end.second);
if (end.first == n - 1 && end.second == n - 1) {
break;
}
end = pre[end.first][end.second];
}
return 0;
}
- 武士风度的牛
农民 John 有很多牛,他想交易其中一头被 Don 称为 The Knight 的牛。
这头牛有一个独一无二的超能力,在农场里像 Knight 一样地跳(就是我们熟悉的象棋中马的走法)。
虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个 x,y的坐标图来表示。
这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了 The Knight 的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。
现在你的任务是,确定 The Knight 要想吃到草,至少需要跳多少次。
The Knight 的位置用 K
来标记,障碍的位置用 *
来标记,草的位置用 H
来标记。
这里有一个地图的例子:
11 | . . . . . . . . . .
10 | . . . . * . . . . .
9 | . . . . . . . . . .
8 | . . . * . * . . . .
7 | . . . . . . . * . .
6 | . . * . . * . . . H
5 | * . . . . . . . . .
4 | . . . * . . . * . .
3 | . K . . . . . . . .
2 | . . . * . . . . . *
1 | . . * . . . . * . .
0 ----------------------
1
0 1 2 3 4 5 6 7 8 9 0
The Knight 可以按照下图中的 A,B,C,D…… 这条路径用 55 次跳到草的地方(有可能其它路线的长度也是 55):
11 | . . . . . . . . . .
10 | . . . . * . . . . .
9 | . . . . . . . . . .
8 | . . . * . * . . . .
7 | . . . . . . . * . .
6 | . . * . . * . . . F<
5 | * . B . . . . . . .
4 | . . . * C . . * E .
3 | .>A . . . . D . . .
2 | . . . * . . . . . *
1 | . . * . . . . * . .
0 ----------------------
1
0 1 2 3 4 5 6 7 8 9 0
注意: 数据保证一定有解。
输入格式
第 11 行: 两个数,表示农场的列数 C 和行数 R。
第 2…R+1行: 每行一个由 C个字符组成的字符串,共同描绘出牧场地图。
输出格式
一个整数,表示跳跃的最小次数。
int n, m;
char g[N][N];
int dist[N][N];
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
int bfs() {
int x, y;
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
if (g[i][j] == 'K') {
x = i, y = j;
}
}
}
queue<PII> q;
q.push({x, y});
memset(dist, -1, sizeof dist);
dist[x][y] = 0;
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 8; i ++ ) {
int nx = t.first + dx[i], ny = t.second + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (g[nx][ny] == '*') {
continue;
}
if (dist[nx][ny] != -1) {
continue;
}
if (g[nx][ny] == 'H') {
return dist[t.first][t.second] + 1;
}
dist[nx][ny] = dist[t.first][t.second] + 1;
q.push({nx, ny});
}
}
return -1;
}
int main() {
cin >> m >> n;
for (int i = 0; i < n; i ++ ) {
cin >> g[i];
}
cout << bfs() << endl;
return 0;
}
- 抓住那头牛
农夫知道一头牛的位置,想要抓住它。
农夫和牛都位于数轴上,农夫起始位于点 N,牛位于点 K。
农夫有两种移动方式:
- 从 X移动到 X−1或 X+1,每次移动花费一分钟
- 从 X移动到 2∗X,每次移动花费一分钟
假设牛没有意识到农夫的行动,站在原地不动。
农夫最少要花多少时间才能抓住牛?
输入格式
共一行,包含两个整数N和K。
输出格式
输出一个整数,表示抓到牛所花费的最少时间。
int dist[N];
int n, k;
int bfs() {
queue<int> q;
memset(dist, -1, sizeof dist);
dist[n] = 0;
q.push({n});
while (q.size()) {
int t = q.front();
q.pop();
if (t == k) {
return dist[k];
}
if (t + 1 < N && dist[t + 1] == -1) {
dist[t + 1] = dist[t] + 1;
q.push({t + 1});
}
if (t - 1 >= 0 && dist[t - 1] == -1) {
dist[t - 1] = dist[t] + 1;
q.push({t - 1});
}
if (t * 2 < N && dist[t * 2] == -1) {
dist[t * 2] = dist[t] + 1;
q.push({t * 2});
}
}
return -1;
}
int main() {
cin >> n >> k;
cout << bfs() << endl;
return 0;
}
3. 树与图的深度优先遍历
模板代码
vector<int> v[N];
int n, m;
bool st[N];
// 以u为根的子树中点的数量
void dfs(int u) {
st[u] = 1; // 标记一下,已经被搜过了
int sum = 1, res = 0; // sum是当前子树大小,res是删掉当前点每一个连通块大小的最大值
for (int i = 0; i < v[u].size(); i ++ ) { // 遍历u的出边
int k = v[u][i]; // 编号
if (!st[k]) {
// 具体思路
}
}
}
int main() {
cin >> n;
for (int i = 0; i < n - 1; i ++ ) {
int a, b;
cin >> a >> b;
v[b].push_back(a), v[a].push_back(b); // 存图,具体情况具体分析
}
dfs(1);
return 0;
}
4. 树与图的广度优先遍历
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size()) {
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
st[j] = true; // 表示点j已经被遍历过
q.push(j);
}
}
}
- 图中点的层次
int n, m;
int h[N], e[N], ne[N], idx;//邻接表
int d[N];//d是距离
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int bfs() {
queue<int> q;
memset(d, -1, sizeof d);//初始化距离
d[1] = 0;//最开始只有第一个点被遍历过
q.push(1);
while(q.size()) {
int t = q.front();//取队头
q.pop();//弹出队头元素
for(int i = h[t]; i != -1; i = ne[i]) { // 扩展当前点
int j = e[i]; // j表示当前点
if(d[j] == -1) { // j没有遍历过
d[j] = d[t] + 1;
q.push(j);
}
}
}
return d[n];
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++) {
int a, b;
cin >> a >> b;;
add(a, b);
}
cout << bfs() << endl;
return 0;
}
5. 拓扑排序
时间复杂度 O(n+m), n表示点数,m表示边数
bool topsort() {
int hh = 0, tt = -1;
// d[i] 存储点i的入度
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
// 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
return tt == n - 1;
}
6. Dijkstra
朴素dijkstra算法
时间复杂是 O(n^2+m), n表示点数,m表示边数
int g[N][N]; // 存储每条边
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ ) {
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
堆优化版dijkstra —— 模板题 AcWing 850. Dijkstra求最短路 II
时间复杂度 O(mlogn), n表示点数,m表示边数
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > distance + w[i]) {
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
7.Bellman-Ford算法
时间复杂度 O(nm), n表示点数,m表示边数
注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge { // 边,a表示出点,b表示入点,w表示边的权重
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ ) {
for (int j = 0; j < m; j ++ ) {
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
if (dist[b] > dist[a] + w)
dist[b] = dist[a] + w;
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
8. spfa
队列优化的Bellman-Ford算法
时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n表示点数,m表示边数
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size()) {
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
if (!st[j]) { // 如果队列中已存在j,则不需要将j重复插入
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
spfa判断图中是否存在负环
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool spfa() {
// 不需要初始化dist数组
// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
queue<int> q;
for (int i = 1; i <= n; i ++ ) {
q.push(i);
st[i] = true;
}
while (q.size()) {
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
9. Floyd
时间复杂度是 O(n^3), n表示点数
初始化:
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd() {
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
10. Prim
朴素版prim算法
时间复杂度是 O(n^2+m),n表示点数,m表示边数
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim() {
memset(dist, 0x3f, sizeof dist);
int res = 0;
for (int i = 0; i < n; i ++ ) {
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
11. Kruskal
时间复杂度是 O(mlogm), n表示点数,m表示边数
int n, m; // n是点数,m是边数
int p[N]; // 并查集的父节点数组
struct Edge { // 存储边
int a, b, w;
bool operator< (const Edge &W)const {
return w < W.w;
}
}edges[M];
int find(int x) { // 并查集核心操作
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal() {
sort(edges, edges + m);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ ) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) { // 如果两个连通块不连通,则将这两个连通块合并
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}
12. 染色法判定二分图
时间复杂度是 O(n+m), n表示点数,m表示边数
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c) {
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (color[j] == -1) {
if (!dfs(j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check() {
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0)) {
flag = false;
break;
}
return flag;
}
13. 匈牙利算法
时间复杂度是 O(nm), n表示点数,m表示边数
int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x) {
for (int i = h[x]; i != -1; i = ne[i]) {
int j = e[i];
if (!st[j]) {
st[j] = true;
if (match[j] == 0 || find(match[j])) {
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ ) {
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}