算法初探系列2 - 深度优先搜索之计算思维

最近有点忙,没来得及写博客,让大家久等啦。
上节课蒟蒻君给大家讲了dfs如何解决枚举类型的题,这节课咱们将会讲到它的另一种用途——图论上的用途(针对图和树的算法)。
大家看到标题肯定会想:什么叫计算思维?
计算思维就是将不好表示对东西用数来表示,比如以下几道题:
铺垫1

  • 题目

有四位同学其中对一位多次扶老奶奶过马路 (老奶奶们很善良,不会碰瓷) ,不留名,表扬信下来后,老师问是哪位同学做的好事。

  • A说:不是我。
  • B说:是C。
  • C说:是D。
  • D说:他胡说。
    经过老奶奶们的确认,知道了三个同学中有一个说的是假话,其余都是真话。现在请你根据以上信息,写出程序找到做好事的人。
  • 分析

我们如果不从数学对角度去考虑 (因为数学的太简单了 ) ,要完成本任务,我们首先要将这四个人说的自然语言改变成计算机能看懂的可计算的式子
在本题里,这个式子用的就是所谓的“布尔代数”。
我们先假设每个人是做好事的那位同学,并且带入,判断是否矛盾即可。

  • 关系表达式

先假设做好事的人为thisman。
算法初探系列2 - 深度优先搜索之计算思维_第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

  • 题目
    现在农场里有编号为0,1,2,3,4的五袋饲料,和A、B、C、D、E五只小猪猪。请你帮助农场主写一个程序,输出使得所有猪都能吃到喜欢的饲料的方案。
    假设五只猪喜欢吃的饲料如下表:
    算法初探系列2 - 深度优先搜索之计算思维_第2张图片
  • 解题思路
    step1:吃食兴趣用一个二维数组描述。
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));

step3:递归(与或图)。
算法初探系列2 - 深度优先搜索之计算思维_第3张图片
算法初探系列2 - 深度优先搜索之计算思维_第4张图片

  • 代码
#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个皇后,使得两两互不攻击。
算法初探系列2 - 深度优先搜索之计算思维_第5张图片

  • 思路1:for循环枚举
#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;
}

太暴力啦!!!
明显,这个算法的时间复杂度确实有点太高了。有没有其他方法呢?

  • 思路2:枚举+递归 = 深度优先搜索(与或图)
    算法初探系列2 - 深度优先搜索之计算思维_第6张图片
    算法初探系列2 - 深度优先搜索之计算思维_第7张图片
#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:过河卒

  • 题目
    算法初探系列2 - 深度优先搜索之计算思维_第8张图片
    算法初探系列2 - 深度优先搜索之计算思维_第9张图片
  • 思路1:暴力搜索
    先算出所有“马的控制点”,然后用深度优先搜索尝试每一条不途径这些点的路径。
    代码
#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啦!
算法初探系列2 - 深度优先搜索之计算思维_第10张图片
所以,我们要想一个新的思路…
这个思路在以后蒟蒻君给大家讲dp(动态规划)的时候会讲到。现在就是让大家熟悉以下方向数组的使用。
例题3:幻想迷宫

  • 题目
    算法初探系列2 - 深度优先搜索之计算思维_第11张图片
    算法初探系列2 - 深度优先搜索之计算思维_第12张图片
    在这里可怜喵星人一秒钟,一秒钟后让我们开始想题…
  • 分析过程
    这道题明显是用搜索的~~(因为蒟蒻君讲的是搜索)~~ ,题目里的迷宫很容易让人想到dfs。
    显然,我们可以把输入的一个迷宫变成九个迷宫,判断从起点(x, y)能否走到(x + n, y),(x - n, y),(x, y + m)或(x, y - m)。
    但是大家会发现,这样的空间复杂度是很大哒,明显会MLE。
    所以我们直接对迷宫去mod就好啦。
    但是,如果走到过一个点又走到了这个点,那是可以走∞远的。
    这里又出现了一大大大大大大堆问题。
    想到这蒟蒻君也懵(´・ω・`)了…
    参考网上大佬的题解后发现还有一个巧妙的方法…
    我们记录取模的横纵坐标x, y,同时记录没有取模的坐标X, Y
    当第一次走这个迷宫的时候,x, y和X, Y肯定是相等的。
    所以只要走到的一个点的x, y和X, Y不相等,那这个点一定是被走了第二遍。
  • 代码
#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;
}

今天的讲解就到这里,希望大家看完有所收获~~

你可能感兴趣的:(算法)