DFS(深度优先算法)是常见的搜索算法,早期常用于需要搜索的地方,并且还拓展出很多其它算法。
深度优先算法(DFS)
DFS(深度优先算法)是早期开发爬虫时常用的算法,它的搜索思路是从树根开始一直找直到找到树型数据结构的叶节点。以搜索一个节点数据为例,DFS算法会从树根开始,沿着一条路一直找到某个叶节点,如果还是找不到,它会回溯到上一个分支,沿着上一个分支节点又向下,直到找到下一个叶节点,如此往复,直至从最后一个叶节点返回到根节点。
由于DFS算法是一种灵活的算法,所以还是举个常用的例子来说明这个算法。
八皇后问题
八皇后问题指的是将八个皇后棋子放到一个棋盘中,使得任意两个棋子不在同一横线、同一竖线、同一对角线和同一反对角线上。
下面我们来看一下如何用代码求解这个问题:
我们不妨将这个问题抽象出来,由于满足条件的排法所有的棋子一定会不同行,所以我们将这个问题看成是在8行里面每一行中任意一个格子放置一个棋子,然后再在其中选出排列出合适的情况。那么我们排列的过程就如下图所示:
那么我们可以利用一个树型的结构来描述这个问题,假设我们有一个二维数组,二维数组和棋盘位置对应,那么我们每次往棋盘里面填棋子的过程可以看成是往该二维数组里面特定位置填充元素的过程。填充的过程可以理解为从树根到树叶的一次搜索过程:
要想实现这个过程,我们首先需要一个二维数组,我们先创建该二维数组,然后在里面填入字符'o'代表初始时为空,并且设置一个输入n代表要开辟的棋盘的格数,这里的N表示上限:
#include<iostream>
using namespace std;
const int N = 20;
int n;
char chessboard[N][N];
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
chessboard[i][j] = 'o';
}
return 0;
}
因为我们需要判断是否有处于同一列或者同一反对角线、同一对角线的元素,所以我们需要开辟三个数组:
bool col[N], diagonal[N], back_diagonal[N];
这些数组每个元素类型都是bool类型,数组的下标和对应的对角线如下:
之后我们就可以着手写DFS搜索算法了:
我们的DFS是一个使用递归的算法。它会自动帮我们从根节点开始往下创建节点这也是在逐步填棋子。
我们首先来看for循环,这个循环就是往下创建节点的核心,for循环每一层循环是在第u行从左到右移动格子的情况,里面的DFS递归是从上一行到下一行,所以递归每次u=u+1就代表向下移动一行。
void DFS(int u)
{
for (int i = 0; i < n; i++)
{
chessboard[u][i] = 'Q';
DFS(u + 1);
}
}
为了判断当前的棋子是否和之前的棋子位置冲突了,我们就需要对三个布尔数组进行设置:当一个棋子放到某个位置时,这个位置对应的一列、对角线、反对角线的位置都被视作为true,表示以后棋子不能放到这个位置。
如果中间一个棋子放到这个位置,那么这个位置下面的情况都将不会被考虑,这种情况不符合if条件,代码会进行回溯,也就是说这条路就不会找到叶节点的位置,而会回溯到上一个节点的位置,并且重新向下找,这个过程就是“剪枝”
void DFS(int u)
{
for (int i = 0; i < n; i++)
{
if (!col[i] && !diagonal[u + i] && !back_diagonal[n - u + i-1])
{
chessboard[u][i] = 'Q';
col[i] = diagonal[u + i] = back_diagonal[n - u + i-1] = true;
DFS(u + 1);
col[i] = diagonal[u + i] = back_diagonal[n - u + i-1] = false;
chessboard[u][i] = 'o';
}
}
}
因为该程序设置的是如果中途不符合条件的路径会被剪掉,所以走到最后(u==n)的情况都是符合条件的。故我们将其打印出来输出。下图是代码执行过程的一部分:
所以总的代码如下:
#include<iostream>
using namespace std;
const int N = 20;
int n;
char chessboard[N][N];
bool col[N], diagonal[N], back_diagonal[N];
void DFS(int u)
{
if (u == n)
{
for (int i = 0; i < n; i++) cout << chessboard[i]<<endl;
cout <<endl;
return;
}
for (int i = 0; i < n; i++)
{
if (!col[i] && !diagonal[u + i] && !back_diagonal[n - u + i-1])
{
chessboard[u][i] = 'Q';
col[i] = diagonal[u + i] = back_diagonal[n - u + i-1] = true;
DFS(u + 1);
col[i] = diagonal[u + i] = back_diagonal[n - u + i-1] = false;
chessboard[u][i] = 'o';
}
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
chessboard[i][j] = 'o';
}
DFS(0);
return 0;
}
排列组合问题
提及DFS算法那必然需要提一下排列组合问题,这个问题和DFS算法的原理有密切的关系。我们知道,DFS算法是从树根开始向叶节点寻找,再从一个叶节点回溯到上一个节点再去找下一个节点的叶节点的动作。这种操作和我们求数字的排列原理很相似。如果我们将每搜索一个节点就理解为放一个数进去排列,那么一遍遍地搜索就是一次次地排列组合,下面是实现的代码。
#include<iostream>
using namespace std;
const int N = 10;
int n;
int arr[N];
bool flag[N];
void DFS(int u)
{
if (u == n)
{
for (int i = 0; i < n; i++)
cout << arr[i];
cout << endl;
}
for (int i = 0; i < n; i++)
{
if (!flag[i])
{
arr[u] = i;
flag[i] = true;
DFS(u + 1);
flag[i] = false;
}
}
}
int main()
{
cin >> n;
DFS(0);
return 0;
}