DFS的核心思想是循环嵌套,将一个大问题拆分成一个个相同小问题,然后将小问题(递归的每一轮)进行判断
模板
void dfs(int step) //步长
{
if(/*跳出循环的条件*/){
return; //return十分关键,否则循环将会无法跳出
}
/*函数主体
对功能进行实现*/
for(/*对现有条件进行罗列*/){
if(/*判断是否合理*/){
//将条件修改
dfs(/*新的step*/)
/*!重中之重,当跳出那层循环后将数据全部归位*/
}
}
}
矩阵模板
int f[4][2]={{0,1},{0,-1},{1,0},{-1,0}}; //用于判断下一步怎么走向几个方向走就是几个数据
void dfs(int x,int y){ //进入点的坐标
if(/*跳出循环的条件*/){
/*作出相应操作*/
return; //不要忘了return
}
for(int i=0;i</*f的长度*/;i++){
int x0=x+f[i][0];
/*此处是更新点的坐标,注意是直接让原来的点加上这个数据,不是直接等于*/
int y0=y+f[i][1];
if(/*用新坐标x0,y0判断是否符合条件*/){
dfs(x0,y0); //用新的坐标进行递归
}
}
}
注意事项
1.判断条件成功后一定要加return来跳出循环
2.当递归调用结束后一定记得将数据修改回来
可以应用的场景
1.数据的选择,比如从几个数据中选出其中一部分进行判断(CF6A Triangle、P1036 [NOIP2002 普及组] 选数、P1460 [USACO2.1]健康的荷斯坦奶牛 Healthy Holsteins)
2.给出一个矩阵,让你判断一些数据成立的条件(CF629A Far Relative’s Birthday Cake、CF445A DZY Loves Chessboard、P1596 [USACO10OCT]Lake Counting S)
3.两个元素之间插入指定要求的数据(P7200 [COCI2019-2020#1] Lutrija)
4.对数据进行排列(P1706 全排列问题)
常见的扩展应用
(一)特殊方方向判断的DFS(只向正方向判断,不向负方向判断)
例题:CF629A Far Relative’s Birthday Cake(洛谷)
#include<bits/stdc++.h>
using namespace std;
char a[105][105];
int n;
int sum=0;
int i,j;
int temp[105][105]={{0,0}};
bool check(int x,int y){ //递归并不会只是同一行或者同一列的数据,需要加条件进行判断
if(x==i&&y<n) //是不是同一行
return true;
if(y==j&&x<n) //是不是同一列
return true;
return false;
}
void dfs(int x,int y){
if(check(x,y)){
if(a[x][y]=='C'&&temp[x][y]==0){
sum++;
}
dfs(x,y+1); //只需要判断这个点正方向的点的情况,负方向情况不用记录
dfs(x+1,y);
}
}
int main(){
cin>>n;
for(i=0;i<n;i++){
for(j=0;j<n;j++){
char ch;
ch=getchar();
if(ch=='\n')
ch=getchar();
/*这么输入的本意是去除掉换行符,
但是有时候会出大问题,
导致输入格式出现错误,
可以直接用cin输入,cin会自动过滤掉换行符*/
a[i][j]=ch;
}
}
for(i=0;i<n;i++){
for(j=0;j<n;j++){
if(a[i][j]=='C'){
temp[i][j]=1;
dfs(i,j);
temp[i][j]=0;
}
}
}
cout<<sum;
}
本题注意事项:
1.输入时的代码其实不应该这么写,可能会导致输入错误?
2.本题只需要判断进入函数数据正方向的数据,负方向的不用判断
3.每次进入只需判断同一行和同一列的数据,但是递归不只是代表同一行和同一列的数据,所以应该加上判断
(二)从符合条件的点向四个方向扩展的DFS
例题:CF629A Far Relative’s Birthday Cake
#include<bits/stdc++.h>
using namespace std;
char a[105][105];
char b[105][105];
int toy[5]={1,0,-1,0};
int tox[5]={0,1,0,-1};
int n,m;
void dfs(int i,int j,int b){
if(i<0||j<0||i>n||j>m){
return;
}
if(b==1){ //判断本棋盘格应该是B还是W
a[i][j]='B';
}
else{
a[i][j]='W';
}
for(int k=0;k<4;k++){ //判断结束后向这个判断完的棋盘格的四个方向进行扩展搜索
int x0=i+tox[k];
/*因为已经知道这个棋盘格是B还是W,所以和他相邻的棋盘格一定是和他字母不一样的*/
int y0=j+toy[k];
if(a[x0][y0]=='.'){
if(b==0)
dfs(x0,y0,1);
else
dfs(x0,y0,0);
}
}
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
char ch;
ch=getchar();
if(ch=='\n')
ch=getchar();
a[i][j]=ch;
}
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(a[i][j]=='.'){
dfs(i,j,1); //当符合空棋盘的条件时进入深搜
}
}
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
cout<<a[i][j];
}
cout<<endl;
}
}
注意事项
1.本题核心思想:本题是在找到空白位置的基础上进入递归循环,当确定这个空白位置是黑还是白(W还是B),然后在对这个点的四个方向进行判断,如果这个点的四个方向还有空白棋盘的话,说明肯定和本棋盘格的颜色不同,所以递归时换个颜色。
(三)DFS的去重操作
例题:P1036 [NOIP2002 普及组] 选数(洛谷)
#include<bits/stdc++.h>
using namespace std;
int n,k;
int a[21]={0};
bool f[21];
int num=0;
long long sum=0;
void dfs(int start,int step){ //引入一个新的start变量来进行去重
if(step==k){
bool flag=true;
if(sum==0||sum==1){
return;
}
if(sum==2){
num++;
return;
}
for(long long i=2;i<=sqrt(sum);i++){
if(sum%i==0)
return;
}
num++;
return;
}
for(int i=start;i<n;i++){ //start用于for循环的判断
sum+=a[i];
step++;
dfs(i+1,step);
step--;
sum-=a[i];
f[i]=false;
}
}
int main(){
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
dfs(0,0);
cout<<num;
}
注意事项:
1.本题的去重思想是在递归中引入一个新的变量start用于函数内循环的判断
此处start的就是只能从没有比上一个数据大的地方进项筛选,类似于
for(int i=0;i<n;i++){
for(int j=i;j<n;j++){
for(int k=j;k<n;k++)
}
}
(四)对于不合适数据的跳过
例题:P1460 [USACO2.1]健康的荷斯坦奶牛 Healthy Holsteins
#include<bits/stdc++.h>
using namespace std;
int a[1005];
int b[1005][1005];
int c[1005]; //代表遍历每一步的结果
int ans[1005]; //代表最终结果,最小的那个
int m,n;
int mmin = 100000000;
bool Judge(int x){ //一共选中x种饲料
for(int i=1;i<=m;i++){
int sum=0;
for(int j=1;j<=x;j++){
sum+=b[c[j]][i];
}
if(a[i]>sum) return false;
}
return true;
}
void dfs(int t,int s){ //t表示第t类饲料 ,s表示所选饲料的总数
if(t>n){ //说明饲料总种类数到达边界
if(Judge(s)){ //判断是不是所有营养都能满足
if(s<mmin) { //判断s种饲料是不是最小的饲料
mmin=s;
for(int i=1;i<=mmin;i++){
ans[i]=c[i];
}
}
}
return;
}
/*未达到边界*/
c[s+1]=t;
dfs(t+1,s+1);
c[s+1]=0; //回溯
dfs(t+1,s); //不选第t种饲料的操作
}
int main(){
cin>>m;
for(int i=1;i<=m;i++){
cin>>a[i];
}
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>b[i][j];
}
}
dfs(1,0);
cout<<mmin<<" ";
for(int i=1;i<=mmin;i++){
cout<<ans[i]<<" ";
}
}
注意事项:
1.核心代码就是这几行
c[s+1]=t;
dfs(t+1,s+1);
c[s+1]=0; //回溯
dfs(t+1,s); //不选第t种饲料的操作
DFS核心就是回溯
漫水算法
例题:CF6B President's Office
#include<bits/stdc++.h>
using namespace std;
int m,n;
int f[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
char a[105][105];
char ch;
set<char> s;
void dfs(int x,int y){
/*核心就是通过DFS来对相连图形进行填充,然后输出需要填充几块*/
for(int i=0;i<4;i++){
int x0=x+f[i][0];
int y0=y+f[i][1];
if(x0<0||y0<0||x0>=m||y0>=n){
continue;
}
else if(a[x0][y0]!='.'){
if(a[x0][y0]!=ch){
s.insert(a[x0][y0]);
a[x0][y0]='.';
}
else{
a[x0][y0]='.';
dfs(x0,y0);
}
}
}
return;
}
int main(){
cin>>m>>n>>ch;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
cin>>a[i][j];
}
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(a[i][j]==ch){
dfs(i,j);
break;
}
}
}
cout<<s.size();
}
注意事项:
漫水的核心就是由一个点扩展到所有这一片所有的点进行覆盖
例题:P1506 拯救oibh总部
#include<bits/stdc++.h>
using namespace std;
int m,n;
char a[505][505];
int cx,cy;
int dx,dy;
int num;
int f[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
void dfs(int x,int y){
a[x][y]='1';
for(int i=0;i<4;i++){
int x0=x+f[i][0];
int y0=y+f[i][1];
if(x0>0&&x0<=m&&y0>0&&y0<=n&&a[x0][y0]=='0') dfs(x0,y0);
/*只要接触到边缘就说明没有被围住*/
}
return;
}
int main(){
cin>>m>>n;
int sum=0;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=m;i++){
if(a[i][1]=='0')
dfs(i,1);
if(a[i][n]=='0')
dfs(i,n);
}
for(int j=2;j<n;j++){
if(a[1][j]=='0')
dfs(1,j);
if(a[m][j]=='0')
dfs(m,j);
}
for(int k=1;k<=m;k++){
for(int l=1;l<=n;l++){
if(a[k][l]=='0'){
sum++;
}
}
}
cout<<sum;
}
注意事项:
本体的核心是判断被围起来的有几个点,漫水算法需要稍作改变,因为只要接触到边缘就说明没有被围住,
所以从四条边的周围向内漫水,所有能接触到的点说明都没有被围起来,最后在统计没有被着色的点就是题目中的答案
例题:P1331 海战
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
int c,r;
char map[1005][1005];
int po[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
int fx[4]={0,-1,1,0};
int fy[4]={-1,0,0,1};
void dfs(int x,int y){
map[x][y]='*';
for(int i=0;i<4;i++){
if(x+fx[i]>0&&x+fx[i]<=r&&y+fy[i]>0&&y+fy[i]<=c&&
map[x+fx[i]][y+fy[i]]=='#')dfs(x+fx[i],y+fy[i]);
}
}
bool Judge(int i,int j){
int c=0;
if(map[i][j]=='#')c++;
if(map[i+1][j]=='#')c++;
if(map[i][j+1]=='#')c++;
if(map[i+1][j+1]=='#')c++;
if(c==3)return 0;
return 1;
}
int main(){
int num=0;
cin>>r>>c;
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++){
cin>>map[i][j];
}
}
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++){
if(i<r&&j<c&&Judge(i,j)==0){
printf("Bad placement.");
return 0;
}
}
}
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++){
if(map[i][j]=='#'){
num++;
dfs(i,j);
}
}
}
printf("There are %d ships.",num);
}
/*#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int r,c;
char map[1010][1010];
int fx[4]={0,-1,1,0};
int fy[4]={-1,0,0,1};
int dfs(int x,int y){
map[x][y]='*';
for(int i=0;i<4;i++){
if(x+fx[i]>0&&x+fx[i]<=r&&y+fy[i]>0&&y+fy[i]<=c&&
map[x+fx[i]][y+fy[i]]=='#')dfs(x+fx[i],y+fy[i]);
}
}//把与#连通的所有点改成*因为它们是同一艘船
bool d(int i,int j){
int c=0;
if(map[i][j]=='#')c++;
if(map[i+1][j]=='#')c++;
if(map[i][j+1]=='#')c++;
if(map[i+1][j+1]=='#')c++;
if(c==3)return 0;
return 1;
}//判断是否合法
int main(){
scanf("%d%d",&r,&c);
register int i,j;
for(i=1;i<=r;i++){
for(j=1;j<=c;j++){
cin>>map[i][j];
}
}
int s=0;
for(i=1;i<=r;i++){
for(j=1;j<=c;j++){
if(i<r&&j<c&&d(i,j)==0){
printf("Bad placement.");
return 0;//不合法后面就没必要继续了
}
}
}
for(i=1;i<=r;i++){
for(j=1;j<=c;j++){
if(map[i][j]=='#'){
s++;
dfs(i,j);
}//因为前面已经确保了是合法的,现在只需统计船的数量
}
}
printf("There are %d ships.",s);
return 0;
}*/
注意事项:
本题在漫水算法的基础上加入了判断是否是船只,只需要将整个表遍历,判断它本身,它右边,它下面,它右下四个点,只要不是三个就说明合法,其他的都一样