最近有点忙,没来得及写博客,让大家久等啦。
上节课蒟蒻君给大家讲了dfs如何解决枚举类型的题,这节课咱们将会讲到它的另一种用途——图论上的用途(针对图和树的算法)。
大家看到标题肯定会想:什么叫计算思维?
计算思维就是将不好表示对东西用数来表示,比如以下几道题:
铺垫1
有四位同学其中对一位多次扶老奶奶过马路 (老奶奶们很善良,不会碰瓷) ,不留名,表扬信下来后,老师问是哪位同学做的好事。
我们如果不从数学对角度去考虑 (因为数学的太简单了 ) ,要完成本任务,我们首先要将这四个人说的自然语言改变成计算机能看懂的可计算的式子。
在本题里,这个式子用的就是所谓的“布尔代数”。
我们先假设每个人是做好事的那位同学,并且带入,判断是否矛盾即可。
#include
using namespace std;
int main() {
for (int k = 0; k < 4; ++k) {
char thisman = 'A' + k;
// 如果3句话为真,则输出当前可能性所假定的人为做好事者
if ((thisman != 'A') + (thisman == 'C') + (thisman == 'D') + (thisman != 'D') == 3) {
cout << thisman << "做了好事\n";
return 0;
}
}
cout << "无人做了好事\n";
return 0;
}
铺垫2
const int like[5][5] = {
{
0, 0, 1, 1, 0},
{
1, 1, 0, 0, 1},
{
0, 1, 1, 0, 1},
{
0, 0, 0, 1, 0},
{
0, 1, 0, 0, 1}};
step2:饲料状态用一个一维数组描述。
int assigned[5];
数组元素存储的是分配到下标对应饲料的小猪猪编号。若assigned[book_id] == -1 则表示book_id这袋饲料没有分配。
注意:数组下标是的编号。
开始,所有饲料都未分配,我们要做出以下的预处理:
memset(assigned, -1, sizeof(assigned));
#include
using namespace std;
const int like[5][5] = {
{
0, 0, 1, 1, 0},
{
1, 1, 0, 0, 1},
{
0, 1, 1, 0, 1},
{
0, 0, 0, 1, 0},
{
0, 1, 0, 0, 1}};
int sum; // 总方案数
int assigned[5];
void Try(int pig) {
// 也就是所谓的dfs函数
// 递归终止条件:所有猪都分配到了合适的饲料
if (pig == 5) {
cout << "第" << ++sum << "个方案:";
for (int i = 0; i < 5; ++i) {
cout << assigned[i] << ' ';
}
cout << '\n';
return ;
}
// 为每袋饲料找到合适的猪
for (int feed = 0; feed < 5; ++feed) {
// 判断是否满足分饲料的条件
if (like[pig][feed] != 1 || assigned[feed] != -1) {
continue;
}
// 记录这袋饲料的分配情况
assigned[feed] = pig;
// 为下一只猪找到合适的饲料
Try(pig + 1);
// 回溯,尝试另一袋饲料
assigned[feed] = -1;
}
}
int main() {
memset(assigned, -1, sizeof(assigned));
Try(0); // 从编号为0的猪开始寻找方案
return 0;
}
#include
using namespace std;
const int like[5][5] = {
{
0, 0, 1, 1, 0},
{
1, 1, 0, 0, 1},
{
0, 1, 1, 0, 1},
{
0, 0, 0, 1, 0},
{
0, 1, 0, 0, 1}};
int sum; // 总方案数
void Try(int pig, int assigned[]) {
// 递归终止条件:所有猪都分配到了合适的饲料
if (pig == 5) {
cout << "第" << ++sum << "个方案:";
for (int i = 0; i < 5; ++i) {
cout << assigned[i] << ' ';
}
cout << '\n';
return ;
}
// 为每袋饲料找到合适的猪
for (int feed = 0; feed < 5; ++feed) {
// 判断是否满足分饲料的条件
if (like[pig][feed] != 1 || assigned[feed] != -1) {
continue;
}
// 记录这袋饲料的分配情况
int nxt_assigned[5];
for (int i = 0; i < 5; ++i) {
nxt_assigned[i] = assigned[i];
}
nxt_assigned[feed] = pig;
// 为下一位猪猪找饲料
Try(pig + 1, nxt_assigned);
}
}
int main() {
int assigned[5]; // 为了不与Try函数的参数重名,在main函数内部定义
memset(assigned, -1, sizeof(assigned));
Try(0, assigned); // 从编号为0的猪开始寻找方案
return 0;
}
无关紧要
想必大家已经掌握“计算思维”的精髓啦,我们看看如何把这个思维用到dfs中~
知识概述
dfs里有一个经典问题,叫“迷宫问题”。在迷宫里,要做出很多操作。但无论在什么样的迷宫中,目前的位置都是必须要记录哒!
题目中的人物可以在迷宫里像很多方向走(最常见的就是上下左右)。我们要记录的就是目前的坐标。而所谓的方向数组就是位置改变的原则。
比如:
const int dir_arr[4][2] = {
{
0, 1}, {
0, -1}, {
-1, 0}, {
1, 0}}; // 上下左右
例题1:八皇后问题
在一个8*8的棋盘里,放置8个皇后,使得两两互不攻击。
#include
using namespace std;
#define f(n) for (q[(n) = 1]; q[(n)] <= 8; ++q[(n)])
bool is_safe(int q[]) {
......
}
int main() {
int q[9];
int sum = 0;
f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8)
if (is_safe(q)) {
cout << "第" << ++sum << "种方法:";
for (int i = 1; i <= 8; ++i) {
cout << q[i] << ' ';
}
cout << '\n';
}
return 0;
}
太暴力啦!!!
明显,这个算法的时间复杂度确实有点太高了。有没有其他方法呢?
#include
using namespace std;
const int N = 9, M = 17;
int sum; // 方案数
int Q[N]; // 8个皇后所占用的行号
bool S[N]; // 当前行是否安全
bool L[M]; // 右上到左下的对角线是否安全
bool R[M]; // 左上到右下的对角线是否安全
void dfs(int col) {
// 递归终止条件:所有列都有皇后了
if (col == N) {
cout << "第" << ++sum << "种方案:";
for (int i = 1; i <= 8; ++i) {
cout << Q[i] << ' ';
}
cout << '\n';
return ;
}
// 尝试当前列8行的位置
for (int row = 1; row < N; ++row) {
// 判断是否安全
if (!S[row] || !L[col - row + N] || !R[col + row]) {
continue;
}
// 记录当前行号
Q[col] = row;
// 修改是否安全的标记
S[row] = false;
L[col - row + N] = false;
R[col + row] = false;
// 继续尝试下一列
dfs(col + 1);
// 回溯
S[row] = true;
L[col - row + N] = true;
R[col + row] = true;
}
}
int main() {
for (int i = 0; i < N; ++i) {
S[i] = true;
}
for (int i = 0; i < M; ++i) {
L[i] = R[i] = true;
}
// 从第一行开始判断
dfs(1);
return 0;
}
struct state {
int Q[N]; // 8个皇后所占用的行号
bool S[N]; // 当前行是否安全
bool L[M]; // 右上到左下的对角线是否安全
bool R[M]; // 左上到右下的对角线是否安全
} s;
void dfs(int col, state S) {
// 递归终止条件:所有列都有皇后了
if (col == N) {
cout << "第" << ++sum << "种方案:";
for (int i = 1; i <= 8; ++i) {
cout << s.Q[i] << ' ';
}
cout << '\n';
return ;
}
// 尝试当前列8行的位置
for (int row = 1; row < N; ++row) {
// 判断是否安全
if (!s.S[row] || !s.L[col - row + N] || !s.R[col + row]) {
continue;
}
// 记录当前行号
state nxt = s;
nxt.Q[col] = row;
// 修改是否安全的标记
nxt.S[row] = false;
nxt.L[col - row + N] = false;
nxt.R[col + row] = false;
// 继续尝试下一列
dfs(col + 1, nxt);
}
}
例题2:过河卒
#include
using namespace std;
const int dir_zu[2][2] = {
{
0, 1}, {
1, 0}}; // 卒的方向数组,只能向右和向下走
const int dir_ma[2][9] = {
{
0, -2, -1, 1, 2, 2, 1, -1, -2},
{
0, 1, 2, 2, 1, -1, -2, -2, -1}}; // 马的方向数组
int n, m; // B点坐标
int x, y; // 马的坐标
int sum; // 总方案数
bool is_danger[30][30]; // (i, j)是否危险,false代表安全
inline bool judge_in(int x, int y) {
// 判断点(x, y)是否在棋盘里
return x >= 0 && x <= n && y >= 0 && y <= m;
}
void init() {
// 初始化
is_danger[x][y] = true; // 马现在的点肯定是马的控制点
for (int i = 0; i < 9; ++i) {
int pre_x = x + dir_ma[0][i]; // 目前点的x坐标
int pre_y = y + dir_ma[1][i];
if (judge_in(pre_x, pre_y)) {
// 判断是否在棋盘里
is_danger[pre_x][pre_y] = true; // 标记为不安全
}
}
}
void dfs(int x, int y) {
// 递归终止条件:卒已到达B点(坐标相等)
if (x == n && y == m) {
++sum;
return ;
}
// 尝试每一种方向
for (int i = 0; i < 2; ++i) {
int nxt_x = x + dir_zu[i][0]; // 下一个点的x坐标
int nxt_y = y + dir_zu[i][1]; // 下一个点的y坐标
if (!judge_in(nxt_x, nxt_y) || is_danger[nxt_x][nxt_y]) {
// 判断是否可以继续走
continue;
}
dfs(nxt_x, nxt_y); // 可以走就继续走
}
}
int main() {
cin >> n >> m;
cin >> x >> y;
init();
dfs(0, 0);
cout << sum << '\n';
return 0;
}
但是,在luogu上,这个代码的最后三个数据都TLE啦!
所以,我们要想一个新的思路…
这个思路在以后蒟蒻君给大家讲dp(动态规划)的时候会讲到。现在就是让大家熟悉以下方向数组的使用。
例题3:幻想迷宫
#include
using namespace std;
const int N = 1505;
const int dir_arr[4][2] = {
{
1, 0}, {
-1, 0}, {
0, 1}, {
0, -1}}; // 方向数组
int n, m;
int st_x, st_y; // 起点坐标
int vis[N][N][3];
bool flag, a[N][N]; // a[i][j]表示(i, j)是否可走,0表示可以
void dfs(int x, int y, int X, int Y) {
if (flag) {
return ;
}
// 具体含义见分析过程
if (vis[x][y][0] && (vis[x][y][1] != X || vis[x][y][2] != Y)) {
flag = true;
return;
}
vis[x][y][0] = 1;
vis[x][y][1] = X;
vis[x][y][2] = Y;
for (int i = 0; i < 4; ++i) {
int nxt_x = (x + dir_arr[i][0] + n) % n; // 不要忘记每轮的mod
int nxt_y = (y + dir_arr[i][1] + m) % m;
int nxt_X = X + dir_arr[i][0];
int nxt_Y = Y + dir_arr[i][1];
if (!a[nxt_x][nxt_y]) {
if (vis[nxt_x][nxt_y][1] != nxt_X ||
vis[nxt_x][nxt_y][2] != nxt_Y ||
!vis[nxt_x][nxt_y][0]) {
dfs(nxt_x, nxt_y, nxt_X, nxt_Y);
}
}
}
}
int main() {
while (cin >> n >> m) {
// 不要忘记每轮初始化
flag = false; // flag表示每轮答案
memset(a, false, sizeof(a));
memset(vis, false, sizeof(vis));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
char ch;
cin >> ch;
if (ch == '#') {
// 不能走到墙上
a[i][j] = 1;
}
if (ch == 'S') {
// 记录起点坐标
st_x = i;
st_y = j;
}
}
}
// 从起点开始尝试
dfs(st_x, st_y, st_x, st_y); // st_x和st_y取不取mod都是本身
if (flag == true) {
cout << "Yes\n";
} else {
cout << "No\n";
}
}
return 0;
}
今天的讲解就到这里,希望大家看完有所收获~~