前言
[kuangbin带你飞]是ACMer中名气最大的题单之一,起源是上海大学的kuangbin大佬,是从基础到进阶阶段的最好题单之一,虽然题量适中,题解繁多,不过大多冗杂,注释偏少。
本专栏致力于用尽量简短的描述,以及大量的代码注释,帮助同学们真正理解题目的含义与解题方法,详细地解析代码,从而帮助同学们提高代码能力(当然还是要自己动手),让同学们体验到算法的乐趣。
本专栏会持续更新迭代,欢迎各位算法爱好者提供意见与建议,从而督促本人做出更好的题解。能够迭代升级的,才是最强的题解。
相信大多数看这个题解的同学均是已经接触到kuangbin专题的了,但是为了让同学们学习路径更平滑,我还是想稍作补充:
包含kuangbin专题在内的,ACMer学习路径(必看):ACM 的正确入门方式是什么?
kuangbin专题合集,最全的合集:[kuangbin带你飞]专题1-23,本专题所有题目均在上面提交
kuangbin专题合集,ACwing评测(好处是不用忍受老OJ的老旧评测机,坏处是暂时没有更新完所有专题)
注意!kuangbin每个专题需要一定的知识基础,所以在学习之前希望各位同学有一定的学习途径。
以下是我常用的学习途径:
- (首推)ACwing算法基础课 + 算法提高课,性价比最高的网课,包含大量基础到进阶的知识点,强烈推荐入手,既可当算法百科查阅,也可系统学习打好基础。
- 《算法竞赛进阶指南》,非常契合kuangbin专题的一本书,既有很多知识点讲解,同时也提供了很多例题帮助理解,也是个很不错的题单,牛客 和 ACwing 上都可以刷题。
- OIWIKI,相当全的知识点介绍网站,可以通过这个网站初步了解知识点的原理与基本实现。
注意!本专栏的所有题解都会写出所有题目的主要知识点,方便同学们查找学习
A.棋盘问题
主要知识点:DFS,枚举
题意概括:
给定一个 n × n n\times n n×n 正方形的迷宫,其中每一行和每一列只能放一个棋子,而且只有特定的 “#” 号格子,才允许放一颗棋子
解题思路:
类似经典的“八皇后”问题,只不过加上了只有特定位置才能放棋子这一特点
最朴素的方式是用DFS枚举所有 “#” 号格子,如果同一行或同一列没有放过棋子(用一维数组记录这一行/列是否放过棋子进行标记),才会放下棋子
也可以像我这样,仅枚举每行的 ”#“ 号格子,节约了记录行的空间。
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10 + 10;
int n, k;
char g[N][N]; // 记录迷宫
int col[N]; // 对列上是否存在棋子进行标记
int ans;
void dfs(int row, int cnt) // row是当前行,cnt是已使用的棋子数
{
if (cnt == k) // 已经使用k颗棋子,该分支无需继续(剪枝)
{
ans ++;
return;
}
if (row == n) return; // 所有行都枚举完了,而棋子数还未达条件
for (int i = 0; i < n; i ++ )
{
// 枚举从所有列上的区域
// 只有当前格子可以放棋子,且当前列上没有棋子,才能尝试放下这颗棋子
if (g[row][i] == '#' && !col[i])
{
col[i] = 1; // 在当前列做标记
// 放下这颗棋子,并枚举下一行
dfs(row + 1, cnt + 1);
col[i] = 0; // 这列不放棋子,回到原状态
}
}
// 这一行不放棋子,继续枚举下一行
dfs(row + 1, cnt);
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
while(cin >> n >> k && n != -1 )
{
ans = 0;
memset(col, 0, sizeof col);
// 行数从0 ~ n - 1
for (int i = 0; i < n; i ++ ) cin >> g[i];
// 初始值,第0行开始,当前一共使用了0颗棋子
dfs(0, 0);
cout << ans << endl;
}
// fclose(stdout);//输出结束
return 0;
}
B.Dungeon Master(地牢大师)
主要知识点:BFS求最短路
题意概括:
给定一个 L × R × C L \times R \times C L×R×C 大小的3D地牢,其中 “S” 点为起点, “E” 点为终点,每单位时间能够向一个方格移动一格,包括上下层,无法进入 "#"号 格子。求能否到达终点,如果能到达,最小的步数是多少?
解题思路:
很明显的BFS问题,从起点开始,一步一步扩散,直到遍历到终点或者遍历完所有可到达的点而不能到达终点。
这道题除了代码量稍微大一点之外,就是一道正常的BFS题
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 30 + 10;
struct Position
{
int floor, x, y;
};
int L, R, C;
char g[N][N][N];
int dist[N][N][N];
int bfs(Position st, Position ed) // st为起点,ed为终点
{
// 前后左右方向的向量
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
// 用队列存储当前坐标
queue<Position> q;
q.push(st);
int floor = st.floor, x = st.x, y = st.y;
dist[floor][x][y] = 0;
while (q.size())
{
Position t = q.front(); q.pop();
floor = t.floor, x = t.x, y = t.y;
if (g[floor][x][y] == 'E') return dist[floor][x][y];
for (int i = -1; i <= 1; i += 2)
{
if (floor + i < 0 || floor + i >= L) continue;
if (dist[floor + i][x][y] != -1) continue;
if (g[floor + i][x][y] == '#') continue;
dist[floor + i][x][y] = dist[floor][x][y] + 1;
// 由于POJ的古老评测机,这里必须有一个中间变量才能编译
Position k = {floor + i, x, y};
q.push(k);
}
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= R || b < 0 || b >= C) continue;
if (g[floor][a][b] == '#') continue;
if (dist[floor][a][b] != -1) continue;
dist[floor][a][b] = dist[floor][x][y] + 1;
// 由于POJ的古老评测机,这里必须有一个中间变量才能编译
Position k = {floor, a, b};
q.push(k);
}
}
return -1;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
while(cin >> L >> R >> C && L && R && C)
{
memset(dist, -1, sizeof dist);
for (int i = 0; i < L; i ++ )
for (int j = 0; j < R; j ++ )
cin >> g[i][j];
Position st, ed;
for (int i = 0; i < L; i ++ )
for (int j = 0; j < R; j ++ )
for (int k = 0; k < C; k ++ )
{
// 由于POJ的古老评测机,这里必须有一个中间变量才能编译
Position t = {i, j, k};
if (g[i][j][k] == 'S') st = t;
if (g[i][j][k] == 'E') ed = t;
}
// 用一个变量存储函数返回值
int distance = bfs(st, ed);
// -1 表示到达不了终点,其他表示到达终点的时间
if (distance == -1) puts("Trapped!");
else printf("Escaped in %d minute(s).\n", distance);
}
// fclose(stdout);//输出结束
return 0;
}
C.Catch That Cow(抓住那头牛)
主要知识点:BFS求最短路
题意概括:
牛和农夫都在一个数轴上,假设牛不动,而农夫有以下三种移动方式
(1)向正方向移动,X + 1
(2)向负方向移动,X - 1
(3)自身坐标翻倍,X * 2
求农夫需要移动多少次,才能到达牛的位置,抓住牛?
解题思路:
一维BFS问题,模拟,秒杀一气呵成。
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
int n, k;
int dist[2 * N]; // 这里需要二倍长度,否则倍增的时候有可能会爆空间
int bfs()
{
queue<int> q;
q.push(n);
dist[n] = 0;
while (q.size())
{
int t = q.front(); q.pop();
if (t == k) return dist[k];
// t + 1 不得超过 牛的极限坐标
if (t + 1 < N && dist[t + 1] == -1)
{
dist[t + 1] = dist[t] + 1;
q.push(t + 1);
}
// t - 1 不得小于 0
if (t - 1 >= 0 && dist[t - 1] == -1)
{
dist[t - 1] = dist[t] + 1;
q.push(t - 1);
}
// t * 2 不得超过 2倍 牛的极限坐标
if (t * 2 < 2 * N && dist[2 * t] == -1)
{
dist[2 * t] = dist[t] + 1;
q.push(2 * t);
}
}
return -1;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
cin >> n >> k;
memset(dist, -1, sizeof dist);
cout << bfs();
// fclose(stdout);//输出结束
return 0;
}
D.Fliptile(枚举)
主要知识点:状态压缩,枚举,递推,模拟
题意概括:
给定一个 M × N M \times N M×N 的0-1矩阵,要将这个矩阵转换成完全只有0的矩阵,每次翻转会连带着翻转相邻的四个格子(如果有的话),求最小翻转数方案中字典序最小的。
解题思路:
首先排除枚举所有节点的方案, 2 15 × 15 2 ^ {15 \times 15} 215×15 必定超时,这是一道有点思维难度的题,我们必须具有递推思想,要想明白:在第一行的方案确定的情况下,为了保证上一行能够全为0,后面的所有行都可以由前一行的状态递推出来,也就是说,我们唯一需要枚举的,仅仅是第一行。
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 15 + 10;
int m, n;
// st为每个方格的状态, backup用于记录初始状态,方便回到初始状态
int st[N][N], backup[N][N];
// way代表当前操作的具体矩阵,last代表最终结果的操作矩阵
int way[N][N], last[N][N];
// ans记录最小操作个数
int ans = 0x3f3f3f3f;
// 翻转操作
void turn(int x, int y)
{
way[x][y] = 1; // 记录该操作
// 一系列翻转操作
if (x - 1 > 0) st[x - 1][y] = !st[x - 1][y];
if (x + 1 <= m) st[x + 1][y] = !st[x + 1][y];
st[x][y] = !st[x][y];
if (y - 1 > 0) st[x][y - 1] = !st[x][y - 1];
if (y + 1 <= n) st[x][y + 1] = !st[x][y + 1];
}
// 进行第一行的操作
int count (int state)
{
int cnt = 0; // cnt记录操作个数
for (int i = 1; i <= n; i ++ )
{
if (state & (1 << i - 1))
{
// 字典序从小到大的顺序,i应该从右往左进行操作
turn(1, n - i + 1);
cnt ++;
}
}
return cnt;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
cin >> m >> n;
// 输入初始状态
for (int i = 1; i <= m; i ++ )
for (int j = 1; j <= n; j ++ )
cin >> st[i][j];
// 将初始状态做备份
memcpy(backup, st, sizeof st);
// 枚举的是第一行的操作(状态压缩,字典序从小到大)
for (int k = 0; k < 1 << n; k ++ )
{
// 每次回到初始状态
memcpy(st, backup, sizeof st);
memset(way, 0, sizeof way);
// count函数:进行第一行的操作
int t = count(k);
// 由第一行的状态,递推之后m - 1行的状态
for (int i = 2; i <= m; i ++ )
{
for (int j = 1; j <= n; j ++ )
{
// 如果前一行的这一列的状态为1
// 那么必须在当前一行的这一列进行一次翻转
// 从而保证上一行全部为0
if (st[i - 1][j])
{
turn(i, j);
t ++;
}
}
}
int ok = 1; // 用于标记是否成功将最后一行也变为0
for (int i = 1; i <= n; i ++ )
if (st[m][i]) // 只要最后一行有状态1存在
ok = 0; // 就标记为不成功
if (ok)
// 更新最小解,因为第一行是按字典序从小到大枚举的
// 所以操作次数相同,第一次更新的就是最小字典序解
if (t < ans)
{
ans = t;
memcpy(last, way, sizeof way);
}
}
// 找不到可行解
if (ans == 0x3f3f3f3f) puts("IMPOSSIBLE");
else
{
// cout << ans << endl;
for (int i = 1; i <= m; i ++ )
{
for (int j = 1; j <= n; j ++ )
cout << last[i][j] << ' ';
cout << endl;
}
}
// fclose(stdout);//输出结束
return 0;
}
E.Find The Multiple(找倍数)
主要知识点:BFS,完全二叉树,枚举
题意概括:
给定 1 — — 200 1——200 1——200 的整数 n n n ,求 n n n 的倍数,这个倍数需要满足,在十进制表示中,仅由1和0两个数字组成,这个倍数在十进制上不超过100位。
解题思路:
这道题最基本的思路是BFS一位数一位数地拓展,直到找到能够整除的方案。但是 i n t int int 类型的数组不能存储足够大的数, l o n g long long l o n g long long 类型也是如此,只有 u n s i g n e d unsigned unsigned l o n g long long l o n g long long 才能勉强存下。
我们可以用同余进行优化,只存储每种方案的余数,这样我们就可以使用 i n t int int ,从而可以扩展更多方案。
这是一道数据很模糊的题,复杂度分析很困难,多项输入,但不知道项数,倍数的100位也完全是干扰项,最终测出的数据,最大为 2 19 = 524288 2^{19} = 524288 219=524288 ,但为了求稳,还是选择了将数组开到足够大 。
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1e8 + 10;
int n;
int mod[N]; // mod[i]代表的是第i个数 MOD n的余数
// 第i个数是指完全二叉树的第i个节点
// 通过构建完全二叉树来枚举
int bfs()
{
queue<int> q;
q.push(1);
while(q.size())
{
int t = q.front(); q.pop();
if (mod[t] == 0) return t;
// 拓展左孩子
if (2 * t < N)
{
mod[2 * t] = mod[t] * 10 % n;
q.push(2 * t);
}
// 拓展右孩子
if (2 * t + 1 < N)
{
mod[2 * t + 1] = (mod[t] * 10 + 1) % n;
q.push(2 * t + 1);
}
}
return 0;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
while (cin >> n && n)
{
mod[1] = 1 % n;
// 利用BFS,构建完全二叉树,每一层代表一位数字,最高位为1
// 当枚举到mod == 0的情况停止
int i = bfs(); // 提取mod == 0的那一个节点
// 知道这个节点编号就可以求出他所有的祖先编号
vector<int> ans;
while(i)
{
ans.push_back(i % 2);
i /= 2; // 访问父亲节点
}
reverse(ans.begin(), ans.end());
for (i = 0; i < ans.size(); i ++ ) cout << ans[i];
cout << endl;
}
// fclose(stdout);//输出结束
return 0;
}
F.Prime Path(质数路径)
题意概括:
给定两个四位数的质数 a a a 和 b b b , a a a 为初状态, b b b 为末状态,要想从初状态变成末状态,可以进行以下操作:
选择当前质数,可以通过更换某一位的数字,来将这个质数变成另一个四位质数。
求最少需要多少次操作,才能将初状态变成末状态。
解题思路:
BFS问题,将每个四位质数作为一个状态,并通过条件判断能否转移,
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 1e4 + 10;
int a, b;
bool st[N];
int dist[N]; // 距离数组,记录需要几步到这个状态(质数)
int primes[N], p[N];
int cnt = 0, num = 0;
// 求出所有的四位质数(质数筛)
void init()
{
for (int i = 2; i < N; i ++ )
{
if (!st[i])
{
primes[cnt ++] = i;
if (i >= 1000 && i <= 9999) p[num ++] = i;
}
for (int j = 0; primes[j] <= N / i; j ++ )
{
st[primes[j] * i] = 1;
if (i % primes[j] == 0) break;
}
}
}
// 判断是否能够转移
bool ok(int x, int y)
{
int ans = 0;
while (x)
{
int t1 = x % 10;
int t2 = y % 10;
x /= 10, y /= 10;
if (t1 != t2) ans ++;
}
return ans == 1;
}
void solve()
{
// 记得初始化距离数组
memset(dist, -1, sizeof dist);
cin >> a >> b;
queue<int> q;
while(q.size()) q.pop();
q.push(a);
dist[a] = 0;
while(q.size())
{
int t = q.front(); q.pop();
if (t == b)
{
cout << dist[b] << endl;
return;
}
// 枚举所有状态(四位质数)
for (int j = 0; j < num; j ++ )
{
// 判断能否转移 以及 是否到达过该状态
if (ok(p[j], t) && dist[p[j]] == -1)
{
dist[p[j]] = dist[t] + 1;
q.push(p[j]);
}
}
}
if (dist[b] == -1) cout << "Impossible\n";
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
int t;
cin >> t;
init();
while (t --)
{
solve();
}
// fclose(stdout);//输出结束
return 0;
}
G.Shuffle’m Up(洗牌)
主要知识点:BFS求最短路
题意概括:
给定两个字符串s1,s2,对其进行洗牌操作,即以下方式进行重组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9lmtCeRJ-1653023066918)(C:\Users\86133\AppData\Roaming\Typora\typora-user-images\image-20220520083415986.png)]
并将重组后的数组对半分,前半为s1,后半为s2,即可继续进行洗牌操作。
求最少需要多少次洗牌,才能将初状态变成末状态。
解题思路:
BFS问题,将s1+s2(合并)的长字符串作为状态,用洗牌操作更新状态,直到出现重复状态或目标状态
AC代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
using namespace std;
const int N = 1e5 + 10;
int t;
int c;
string s1, s2, s;
map<string, bool> st; // 记录状态是否出现过
int bfs()
{
st.clear();
string state = s1; string end = s;
st[state] = 1;
int ans = 0;
while (state != end) // 洗牌,直到出现目标状态
{
string cur = "";
for (int i = 0; i < c; i ++ )
{
// 先加上s2的牌
cur += state[i + c];
// 再加上s1的牌
cur += state[i];
}
if (st[cur]) return -1; // 出现重复状态,不可能达到目标状态
st[cur] = 1;
ans ++;
state = cur;
}
return ans;
}
void solve()
{
cin >> c;
cin >> s1 >> s2 >> s;
s1 += s2; // 将s1,s2合并成为一个字符串作为状态
cout << t << ' ' << bfs() << endl;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
int cnt;
cin >> cnt;
for (t = 1; t <= cnt; t ++ )
{
solve();
}
// fclose(stdout);//输出结束
return 0;
}
H.Pots(水罐)
主要知识点:BFS求最短路
题意概括:
有两个水罐,可以进行以下三类操作(六种):
(1) 装满某个水罐(1号 或 2号)
(2) 清空某个水罐(1号 或 2号)
(3) 将某个水罐的水倒到另一个水罐(直到其中一个水罐满 或者 空)
状态设置是两个水罐中的水。
求最少需要多少次操作,才能将初状态变成末状态。
解题思路:
BFS问题,将两个水罐的水设为状态,枚举所有可能的操作,并记录前移状态以及其操作种类,通过末状态进行反向递推得到操作路径。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> State; // 用一对整数来代表状态
const int N = 1e5 + 10;
int a, b, c;
struct Ways // 操作方式
{
int op, a, b; // 操作种类,操作对象
State st; // 前一个状态
};
map<State, Ways> pre; // 记录一个状态的前一个,反向递推出路径
State ed;
int bfs()
{
State t;
map<State, int> dist; // 初状态到这个状态所需的步数
for (int i = 0; i <= a; i ++ )
for (int j = 0; j <= b; j ++ )
{
t = {i, j};
dist[t] = -1;
}
queue<State> q;
t = {0, 0};
q.push(t);
dist[t] = 0;
pre[t] = {0, 0, 0, t};
while(q.size())
{
State t = q.front(); q.pop();
// 达到目标状态时,返回步数
if (t.x == c || t.y == c)
{
ed = t; // 记录最后状态,方便反推
return dist[t];
}
State next;
// 操作1,将第一个水罐装满
if (t.x < a)
{
next = {a, t.y};
if (dist[next] == -1)
{
q.push(next);
dist[next] = dist[t] + 1;
pre[next] = {1, 1, 0, t};
}
}
// 操作2,将第二个水罐装满
if (t.y < b)
{
next = {t.x, b};
if (dist[next] == -1)
{
q.push(next);
dist[next] = dist[t] + 1;
pre[next] = {1, 2, 0, t};
}
}
// 操作3,将第一个水罐清空
if (t.x > 0)
{
next = {0, t.y};
if (dist[next] == -1)
{
q.push(next);
dist[next] = dist[t] + 1;
pre[next] = {2, 1, 0, t};
}
}
// 操作4,将第二个水罐清空
if (t.y > 0)
{
next = {t.x, 0};
if (dist[next] == -1)
{
q.push(next);
dist[next] = dist[t] + 1;
pre[next] = {2, 2, 0, t};
}
}
// 操作5,将第一个水罐的水倒到第二个水罐
if (t.x > 0 && t.y < b)
{
int delta = min(b - t.y, t.x);
next = {t.x - delta, t.y + delta};
if (dist[next] == -1)
{
q.push(next);
dist[next] = dist[t] + 1;
pre[next] = {3, 1, 2, t};
}
}
// 操作6,将第二个水罐的水倒到第一个水罐
if (t.y > 0 && t.x < a)
{
int delta = min(a - t.x, t.y);
next = {t.x + delta, t.y - delta};
if (dist[next] == -1)
{
q.push(next);
dist[next] = dist[t] + 1;
pre[next] = {3, 2, 1, t};
}
}
}
return -1;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
cin >> a >> b >> c;
int n = bfs(); // n为达到目标状态的操作数
if (n == -1)
{
puts("impossible");
return 0;
}
cout << n << endl;
vector<Ways> v;
for (int i = 0; i < n; i ++ )
{
v.push_back(pre[ed]); // 递推n个操作
ed = pre[ed].st; // 并得到前一个状态
}
// 反向输出,才是正确的顺序,因为我们是从末状态反向推出来的操作
for (int i = n - 1; i >= 0; i -- )
{
Ways path = v[i];
int op, x, y;
op = path.op;
x = path.a;
y = path.b;
if (op == 1)
{
printf("FILL(%d)\n", x);
}else if (op == 2)
{
printf("DROP(%d)\n", x);
}else
printf("POUR(%d,%d)\n", x, y);
}
// fclose(stdout);//输出结束
return 0;
}
I.Fire Game(玩火)(本题提交与ACwing评测机)
主要知识点:BFS求最短路
题意概括:
两个小孩在玩火,只有草丛 “#” 才能烧起来,两个小孩会在同时点燃两个草丛(也可能是同一个),求把地图上所有草丛都烧着的最短时间。
解题思路:
BFS问题,首先需要判断连通块个数,如果连通块大于2,则两个小孩不能通过一次点火点燃所有草丛;如果连通块等于2,则两个小孩需要分开点燃两堆草丛;如果连通块等于1,则两个小孩可以在这一堆里面任选 1~2 个草丛点燃。
点燃之后,计算所有草丛被点燃时的时间dist,最大的时间就是所有草丛都烧着的时间,枚举所有方案,并记录其中的最小值。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 100 + 10;
int n, m;
char g[N][N];
int dist[N][N]; // 当前草丛烧起来的所花的最短时间(顺带记录是否烧起来过)
vector<PII> p[N]; // 记录连通块中的节点
int cnt; // 连通块(草丛)的个数cnt
int bfs(int flag, int x1, int y1, int x2, int y2)
{
int mx = 0;
// 判断连通块时千万不能每次都更新dist(记录是否访问)!
if (flag > 1) memset(dist, -1, sizeof dist);
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
queue<PII> q;
q.push({x1, y1});
dist[x1][y1] = 0;
if (flag == 1 || (x1 == x2 && y1 == y2));
else
{
q.push({x2, y2}); // 有时需要两个火源进行BFS
dist[x2][y2] = 0;
}
while (q.size())
{
PII t = q.front(); q.pop();
int x = t.x, y = t.y;
if (flag == 1)
{
p[cnt].push_back(t);
}
mx = max(mx, dist[x][y]);
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
PII next = {a, b};
if (a <= 0 || a > n || b <= 0 || b > m) continue;
if (g[a][b] == '#' && dist[a][b] == -1)
{
q.push(next);
dist[a][b] = dist[x][y] + 1;
}
}
}
// cout << mx << endl;
return mx;
}
// 判断连通块
void check()
{
cnt = 0;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
if (g[i][j] == '#' && dist[i][j] == -1)
{
cnt ++;
if (cnt > 2) return;
bfs(1, i, j, 0, 0); // 对当前连通块进行标记
}
}
// 枚举两个连通块的情况
int both()
{
int mn = 1e9;
// 两个小孩必须分别在两个连通块中放火
for (int i = 0; i < p[1].size(); i ++ )
for (int j = 0; j < p[2].size(); j ++ )
{
PII t1 = p[1][i], t2 = p[2][j];
mn = min(mn, bfs(2, t1.x, t1.y, t2.x, t2.y));
}
return mn;
}
// 枚举一个连通块的情况
int only()
{
int mn = 1e9;
// 两个小孩在同一个连通块中放火,可以是同个点
for (int i = 0; i < p[1].size(); i ++ )
for (int j = 0; j < p[1].size(); j ++ )
{
PII t1 = p[1][i], t2 = p[1][j];
mn = min(mn, bfs(3, t1.x, t1.y, t2.x, t2.y));
}
return mn;
}
int solve()
{
memset(dist, -1, sizeof dist);
p[1].clear();
p[2].clear();
cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> g[i] + 1;
check(); // 判断连通块(草丛)的个数cnt
int ans;
if (cnt > 2) ans = -1;
else if (cnt == 2) ans = both();
else if (cnt) ans = only();
else ans = -1;
return ans;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
int t;
cin >> t;
for (int i = 1; i <= t; i ++ )
{
int ans = solve();
printf("Case %d: %d\n", i, ans);
}
// fclose(stdout);//输出结束
return 0;
}
I.Fire!(火灾)
主要知识点:BFS求最短路
题意概括:
在一个迷宫里,有复数个火源 和 一位逃生者,逃生者和火源都会以1格每单位时间的速度移动,逃生者需要从迷宫边缘逃生,求最短时间。
解题思路:
BFS问题,先对所有火源进行多源BFS,求出每个点被火焰燃烧的时间点,然后让逃生者进行模拟逃生(BFS),每个点,只有在火焰燃烧到之前到达,才能更新到达时间,否则就无法到达。
当BFS拓展到任何一个边界时,下一步就可以直接走出迷宫。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1000 + 10;
int n, m;
char g[N][N];
// dist是火焰到达的时间点, d是逃生者到达的时间点
int dist[N][N], d[N][N];
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
// 火源先拓展
void bfs1()
{
// 这里必须用一个较大的值来表示这个点火焰没有到达
memset(dist, 0x3f, sizeof dist);
queue<PII> q1;
while (q1.size()) q1.pop();
// 导入所有火源
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
if (g[i][j] == 'F')
{
PII t = {i, j};
q1.push(t);
dist[i][j] = 0;
}
// 正常的BFS拓展
while (q1.size())
{
PII t = q1.front(); q1.pop();
int x = t.x, y= t.y;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (g[a][b] == '#') continue;
if (a <= 0 || a > n || b <= 0 || b > m) continue;
if (dist[a][b] == 0x3f3f3f3f)
{
dist[a][b] = dist[x][y] + 1;
PII next = {a, b};
q1.push(next);
}
}
}
}
// 逃生者模拟逃生
int bfs2()
{
// 这里就不需要用大数标记,用-1表示是否到达过就行了。
memset(d, -1, sizeof d);
queue<PII> q;
while (q.size()) q.pop();
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
if (g[i][j] == 'J')
{
PII t = {i, j};
q.push(t);
d[i][j] = 0;
}
while (q.size())
{
PII t = q.front(); q.pop();
int x = t.x, y = t.y;
// 第一次到达边界,答案就是加一步出迷宫
if (x == 1 || x == n || y == 1 || y == m) return d[x][y] + 1;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (g[a][b] == '#') continue;
if (a <= 0 || a > n || b <= 0 || b > m) continue;
// 只有dist大于d,才能进行拓展(这就是为什么要用大数标记dist)
if (d[x][y] + 1 < dist[a][b] && d[a][b] == -1)
{
PII next = {a, b};
q.push(next);
d[a][b] = d[x][y] + 1;
}
}
}
return -1;
}
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> g[i] + 1;
bfs1();
int ans = bfs2();
if (ans == -1) puts("IMPOSSIBLE");
else cout << ans << endl;
// for (int i = 1; i <= n; i ++ )
// {
// for (int j = 1; j <= n; j ++ )
// cout << dist[i][j] << ' ';
// cout << endl;
// }
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
int t;
cin >> t;
for (int i = 1; i <= t; i ++ )
{
solve();
}
// fclose(stdout);//输出结束
return 0;
}
J.迷宫问题
主要知识点:BFS求最短路
题意概括:
给定一个 $5 \times 5 $ 的迷宫矩阵,求左上角 ( 0 , 0 ) (0,0) (0,0) 到 右下角 ( 4 , 4 ) (4,4) (4,4) 的最短路径
注意,输出所有走过的点,即为路径
解题思路:
BFS问题,只不过需要记录路径。
在水罐那道题我们已经见识了反推路径,这道题我们来用个新的方法,从终点开始走到起点,这样直接得到的就是正向路径了。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 10 + 10;
int g[N][N];
int st[N][N]; // 用一个数组记录是否重复走过就行了
map<PII, PII> pre; // 记录前一个节点
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
void bfs(PII ed)
{
memset(st, 0, sizeof st);
queue<PII> q;
q.push(ed);
st[ed.x][ed.y] = 1;
while(q.size())
{
PII t = q.front(); q.pop();
int x = t.x, y = t.y;
if (x == 0 && y == 0) return;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
PII next;
next = {a, b};
if (a < 0 || b < 0 || a >= 5 || b >= 5) continue;
if (g[a][b] == 1) continue;
if (st[a][b] == 1) continue;
q.push(next);
pre[next] = t;
st[a][b] = 1;
}
}
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
for (int i = 0; i < 5; i ++ )
for (int j = 0; j < 5; j ++ )
cin >> g[i][j];
PII ed;
ed = {4, 4};
bfs(ed);
PII state;
state = {0, 0};
while(state.x != 4 || state.y != 4)
{
// 得到节点,直接输出
printf("(%d, %d)\n", state.x, state.y);
state = pre[state];
}
// 最后一个节点手动输出
printf("(%d, %d)\n", state.x, state.y);
// fclose(stdout);//输出结束
return 0;
}
K.Oil Deposits(石油储量)
主要知识点:BFS求最短路
题意概括:
可以认为,在以某个石油的九宫格内的石油,都是同一批石油,否则则认为不是同一批;给定一个地图,求地图上有多少批不同的石油。
解题思路:
BFS判断连通块问题,判断有多少个连通块即可,连通块的注释可以看下"玩火"那道题
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 100 + 10;
int n, m;
char g[N][N];
int st[N][N];
int cnt;
void bfs(int i, int j)
{
queue<PII> q;
q.push({i, j});
st[i][j] = cnt;
while (q.size())
{
PII t = q.front(); q.pop();
int x = t.x, y = t.y;
for (int i = -1; i <= 1; i ++ )
for (int j = -1; j <= 1; j ++ )
{
int a = x + i, b = y + j;
if (a <= 0 || a > n || b <= 0 || b > m) continue;
if (st[a][b]) continue;
if (g[a][b] != '@') continue;
PII next = {a, b};
q.push(next);
st[a][b] = 1;
}
}
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
while (cin >> n >> m && n)
{
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 1; i <= n; i ++ ) cin >> g[i] + 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if (g[i][j] == '@' && st[i][j] == 0)
{
cnt ++;
bfs(i, j);
}
}
cout << cnt << endl;
}
// fclose(stdout);//输出结束
return 0;
}
L.非常可乐
主要知识点:BFS求最短路
题意概括:
一瓶可乐的容量为s,有两个空杯子容量分别为n,m;
三个杯/瓶可以相互倒,每次倒可乐算一步,求恰好平分可乐的最小步数。
解题思路:
类似于水罐那道题,只不过这次我们需要自己分析操作的种类。
将三个杯/瓶分别称为 a, b, c。
(1)a -> b
(2)a -> c
(3)b -> a
(4)b -> c
(5)c -> a
(6)c -> b
倒水的方式跟水罐那道题相互倒水几乎一样。
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
typedef pair<int, pair<int, int>> PIII;
const int N = 100 + 10;
int s, n, m;
int dist[N][N][N]; // 记录到某个状态的最短步数
int bfs()
{
memset(dist, -1, sizeof dist);
queue<PIII> q;
PIII start = {s, {0, 0}}; // 这里可以用结构体定义,可读性会强点
q.push(start);
dist[s][0][0] = 0;
while (q.size())
{
PIII t = q.front(); q.pop();
int a = t.x, b = t.y.x, c = t.y.y;
// 终止状态
// 如果其中一杯已经满足有一半的可乐了,那么另一半一定在另外两杯中
if (a == s / 2 || c == s / 2 || b == s / 2)
{
int ans = dist[a][b][c];
// 如果没有一个为空(则另外一个也是一半)
// 则需要再多加一步将两个合并为另一半可乐
if (a && b && c) ans ++;
return ans;
}
if (a < s)
{
// (1)a -> b
if (b > 0)
{
int da = min(s - a, b);
PIII next = {a + da, {b - da, c}};
if (dist[a + da][b - da][c] == -1)
{
dist[a + da][b - da][c] = dist[a][b][c] + 1;
q.push(next);
}
}
// (2)a -> c
if (c > 0)
{
int da = min(s - a, c);
PIII next = {a + da, {b, c - da}};
if (dist[a + da][b][c - da] == -1)
{
dist[a + da][b][c - da] = dist[a][b][c] + 1;
q.push(next);
}
}
}
if (b < n)
{
// (3)b -> a
if (a > 0)
{
int db = min(n - b, a);
PIII next = {a - db, {b + db, c}};
if (dist[a - db][b + db][c] == -1)
{
dist[a - db][b + db][c] = dist[a][b][c] + 1;
q.push(next);
}
}
// (4)b -> c
if (c > 0)
{
int db = min(n - b, c);
PIII next = {a, {b + db, c - db}};
if (dist[a][b + db][c - db] == -1)
{
dist[a][b + db][c - db] = dist[a][b][c] + 1;
q.push(next);
}
}
}
if (c < m)
{
// (5)c -> a
if (a > 0)
{
int dc = min(m - c, a);
PIII next = {a - dc, {b, c + dc}};
if (dist[a - dc][b][c + dc] == -1)
{
dist[a - dc][b][c + dc] = dist[a][b][c] + 1;
q.push(next);
}
}
// (6)c -> b
if (b > 0)
{
int dc = min(m - c, b);
PIII next = {a, {b - dc, c + dc}};
if (dist[a][b - dc][c + dc] == -1)
{
dist[a][b - dc][c + dc] = dist[a][b][c] + 1;
q.push(next);
}
}
}
}
return -1;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
while (cin >> s >> n >> m && s && n && m)
{
if (s % 2)
{
cout << "NO\n";
continue;
}
int ans = bfs();
if (ans == -1) puts("NO");
else cout << ans << endl;
}
// fclose(stdout);//输出结束
return 0;
}
M.Find a way(找"去KFC的"路)
主要知识点:BFS求最短路
题意概括:
地图上标记了两个人和若干个KFC的位置。
这两个人想去同一个KFC,求两个人花的总时间最少为多少。
解题思路:
BFS问题,两次BFS分别预处理两个人到所有KFC的距离,最后求距离之和最小的即可
AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
typedef pair<int, pair<int, int>> PIII;
const int N = 200 + 10;
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
int n, m;
char g[N][N];
// dis:第一个人到地图上点的最短距离
// d:第二个人到地图上点的最短距离
int dis[N][N], d[N][N];
PII a, b; // 记录两个人的坐标
// 预处理第一个人到地图上点的最短距离
void bfs1()
{
// 求最短距离的时候最好还是用大数标记,否则有的KFC走不到就麻烦了
memset(dis, 0x3f, sizeof dis);
queue<PII> q1;
q1.push(a);
dis[a.x][a.y] = 0;
while (q1.size())
{
PII t = q1.front(); q1.pop();
int x = t.x , y = t.y;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a <= 0 || a > n || b <= 0 || b > m) continue;
if (g[a][b] == '#') continue;
if (dis[a][b] == 0x3f3f3f3f)
{
dis[a][b] = dis[x][y] + 1;
q1.push({a, b});
}
}
}
}
// 预处理第二个人到地图上点的最短距离
void bfs2()
{
memset(d, 0x3f, sizeof d);
queue<PII> q;
q.push(b);
d[b.x][b.y] = 0;
while (q.size())
{
PII t = q.front(); q.pop();
int x = t.x , y = t.y;
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a <= 0 || a > n || b <= 0 || b > m) continue;
if (g[a][b] == '#') continue;
if (d[a][b] == 0x3f3f3f3f)
{
d[a][b] = d[x][y] + 1;
q.push({a, b});
}
}
}
}
void solve()
{
vector<PII> ans; // ans 记录KFC的位置
ans.clear();
for (int i = 1; i <= n; i ++ ) cin >> g[i] + 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if (g[i][j] == 'Y') a = {i, j};
if (g[i][j] == 'M') b = {i, j};
if (g[i][j] == '@') ans.push_back({i, j});
}
bfs1();
bfs2();
int mn = 1e9;
// 分别求所有
for (int i = 0; i < ans.size(); i ++ )
{
int x = ans[i].x, y = ans[i].y;
mn = min(mn, dis[x][y] + d[x][y]);
}
cout << mn * 11 << endl;
}
int main()
{
// freopen("data.in","r",stdin); // 文件输入
// freopen("data.out","w",stdout);// 文件输出
while (cin >> n >> m)
{
solve();
}
// fclose(stdout);//输出结束
return 0;
}