这篇文章是上一篇文章的延续:
算法笔记#4:深搜(DFS)_captainfly_的博客-CSDN博客
写在最前:学深搜的时候确实遇到了一些问题,这段时间也是去看了很多参考的资料,受益匪浅,我会将这些一起整理到文章之中。解决回溯问题,实际上就是一个决策树的遍历过程。那么在这个过程中我们就需要思考三个问题:
1 路径:也就是已经做出的选择。
2 选择列表:也就是你当前可以做的选择。
3 结束条件:也就是达到决策树底层,无法再做选择的条件。
这三个问题也就是决策树遍历过程的核心。
题目均来自TZOJ。
1001 勘探油田
描述
某石油勘探公司正在按计划勘探地下油田资源。他们工作在一片长方形的地域中,首先将该地域划分为许多小正方形区域,然后使用探测设备分别探测每一块小正方形区域是否有油。若在一块小正方形区域中探测到有油,则标记为’@’,否则标记为’*’。如果两个相邻区域都为1,那么它们同属于一个石油带,一个石油带可能包含很多小正方形区域,而你的任务是要确定在一片长方形地域中有多少个石油带。所谓相邻,是指两个小正方形区域上下、左右、左上右下或左下右上同为’@’。
输入
输入数据将包含一些长方形地域数据,每个地域数据的第一行有两个正整数m和n,表示该地域为m*n个小正方形所组成,如果m为0,表示所有输入到此结束。否则,后面m(1≤m≤100)行数据,每行有n(1≤n≤100)个字符,每个字符为’@’或’*’,表示有油或无油。每个长方形地域中,’@’的个数不会超过100。
输出
每个长方形地域,输出油带的个数,每个油带值占独立的一行。油带值不会超过100。
样例输入
3 5
*@*@*
**@**
*@*@*
0 0
样例输出
1
这道题是上一篇文章中“Counting sheeps”一题的同类题,“玉树搜救行动”一题的加强版。我们从一个有油的地方开始,通过f数组,对四面八方进行遍历,符合条件就继续遍历,不符合条件就return。其中一些遍历过的地方,无论是有油还是没油,走过一遍之后都要做上标记。具体方法已经在代码之后的注释中写出,不过多赘述。
#include<bits/stdc++.h>
#define maxn 1001
using namespace std;
int visit1[maxn][maxn],sum=0,m,n;//如果用visit的话会出现不明确,原因是visit和保留字冲突了,后面加个1就可以了
int f[8][2]={{-1,0},{1,0},{0,-1},{0,1},{-1,-1},{1,-1},{-1,1},{1,1}};//下一步是四面八方
char maps[maxn][maxn];
void dfs(int i,int j)
{
if(maps[i][j]=='*') return ;//回到之前的起点,换一个方向,递归终止的条件
maps[i][j]='*';//如果说把这一行去掉,那么输出的结果就会变成5,想想时为什么
//上面这一句话,说明如果遇到了一个有油的地方,就把这个地方标记成*,代表已经来过
for(int k=0;k<8;k++)
{
int x=i+f[k][0];
int y=j+f[k][1];
if(x<0||x>=m||y<0||y>=n||visit1[x][y]==1||maps[x][y]=='*') continue;//回到上面那个for循环,改变k的值,即换个方向
visit1[x][y]=1;//如果说找到了一个有油的地方标记为1,表示来过
dfs(x,y);
visit1[x][y]=0;
}
}
int main()
{
while(cin>>m>>n)
{
memset(visit1,0,sizeof(visit1));
if(m==0) break;
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
cin>>maps[i][j];//输入地图
}
}
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(maps[i][j]!='*')//遇到了一个有油的地方,以这个地方为起点
{
sum++;
dfs(i,j);//遍历,这一个循环的过程代表,当找到了一个有油的地方时,以这个地方为起点
}
}
}
cout<<sum<<endl;
sum=0;//初始化,下一组数据
}
return 0;
}
我此时再把“counting sheeps”一题的代码放在这里,是否可以找到很多相似之处呢?
#include<bits/stdc++.h>
using namespace std;
int sum, n, m, dx[4]={-1,1,0,0}, dy[4]={0,0,-1,1},i,j,k;
char map[110][110];
void dfs(int x, int y)
{
int d;
map[x][y] = '.';
for(d = 0; d < 4; d++)
{
int nx=x+dx[d], ny=y+dy[d];
if(nx>=0&&nx<n&&ny>=0&&ny<m&&map[nx][ny]!='.')
{
map[nx][ny] = '.';
dfs(nx,ny);
}
}
}
int main()
{
int T;
scanf("%d", &T);
while(T--)
{
scanf("%d%d", &n, &m);
for(i = 0; i < n; i++)
scanf("%s", map[i]);
sum = 0;
for(i = 0; i < n; i++)
{
for(j = 0; j < m; j++)
{
if(map[i][j] == '#')
{
dfs(i,j);
sum++;
}
}
}
printf("%d\n", sum);
}
}
那么从这三道“地图搜索题”之后我们可以总结出一套做这种题的模版。在这个模版里包括几个因素:
用来输入地图都二维数组数组;
用来决定方向的二维数组(由0和1构成的二维数组,决定下一步往哪个方向走);
递归函数:
接下来我们来看两道有关全排列的问题。
全排列也是回溯算法的一种经典运用。我们高中的时候就做过排列组合的数学题,我们也知道n个不重复的全排列共有n!个。那么一般我们都是这样穷举全排列的:
比如说给你三个数1、2、3。
先固定第一位为1,然后第二位可以是2,那么第三位只能是3;然后可以把第二位变成3,那么第三位就只能是2了;现在就只能变化第一位,变成2,然后再穷举后两位。。。。
1002 排列型枚举
描述
把 1~n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。
输入
一个整数n(n<10)。
输出
按照从小到大的顺序输出所有方案,每行1个。
首先,同一行相邻两个数用一个空格隔开。
其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。
样例输入
3
样例输出
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
全排列问题,可以用一个放盒子的例子来描述过程。那么你站在下一个盒子面前就会出现选择,把选择过的标记好,没选择过的按照从小到大的顺序(题目要求字典序排序)放入。递归终止的条件就是当你站在第n+1个盒子面前,就说明你已经完成了所有盒子的放置。具体见代码。
#include <bits/stdc++.h>
using namespace std;
int a[101];//盒子,目前只能放最多100个盒子,你可以自己加,但是要同时修改b的个数
int b[101]={0};//标记数组,标记是否已经放入
void dfs(int step,int n)//当前在第几个盒子上,总共有多少数
{
int i;
if(step==n+1){//如果站在第n+1个盒子面前了,那说明,已经放好所有的了
for(i=1;i<=n;i++){//输出这个组合
if(i!=n)
printf("%d ",a[i]);
else
printf("%d",a[i]);
}
printf("\n");
return;//返回上一层,也就是调用它的地方 (回溯) ,然后才能输出更多的结果
}
for(i=1;i<=n;i++){//遵循最小原则 放入数字
//判断数字是否已经放入到之前的盒子
if(b[i]==0){//数字并未放入
a[step]=i;//放入
b[i]=1;//标记已经放入
//继续需要往下一个盒子走
dfs(step+1,n);
//走完这个盒子之后的所有操作,已经又回溯到这个盒子了,马上就要在此盒子放入下一个数了,所以我们需要将刚才的数字收回
b[i]=0; //等于就是说,在这个起始点的基础上再往后退一步,多出一个盒子,进行新一轮的搜索
}
}
return;
}
int main(){
int n,a,i;
scanf("%d",&n);
dfs(1,n);//step从1开始,内部会自动的进行递归,最后到n+1结束
return 0;
}
1003 组合型枚举
描述
从 1~n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案。n>0, 0<=m<=n, n+(n-m)<=25。
输入
两个整数n,m。
输出
按照从小到大的顺序输出所有方案,每行1个。
首先,同一行内的数升序排列,每个数字后面有一个空格。其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如1 3 9 12排在1 3 10 11前面)。
样例输入
5 3
样例输出
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
这道题是上一道题的加强版。
#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[30];
// u是枚举起点,即本轮枚举是枚举u, u + 1, ... ,n这些数,cnt是已经枚举了多少个数
void dfs(int u, int cnt) {
// 如果已经枚举足够了,则得到了一个组合,输出之
if (cnt == m) {
for (int i = 0; i < m; i++) cout << a[i] << ' ';
cout << endl;
return;
}
// 否则开始从u进行枚举。这里有个优化是,还能供枚举的数一共有n - i + 1个,
// 还需要枚举m - cnt个数,所以有n - i + 1 >= m - cnt,可以定下i的上界
for (int i = u; i <= n + 1 + cnt - m; i++) {
a[cnt] = i;
dfs(i + 1, cnt + 1);
}
}
int main() {
cin >> n >> m;
dfs(1, 0);
return 0;
}