定义:
深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.
举例说明之:下图是一个无向图,如果我们从A点发起深度优先搜索(以下的访问次序并不是唯一的,第二个点既可以是B也可以是C,D),则我们可能得到如下的一个访问过程:A->B->E(没有路了!回溯到A)->C->F->H->G->D(没有路,最终回溯到A,A也没有未访问的相邻节点,本次搜索结束).简要说明深度优先搜索的特点:每次深度优先搜索的结果必然是图的一个连通分量。
摘自百度百科
例题:
1.在图上搜索
- 走方格1
问题描述:有一个方格图形,假设从第a行第b列开始,每步只能往左、右、上三个方向走,只能走n步,问有多少种走法。
输入:三个整数a,b,n(1<=a,b<100,n<=30)
输出:走法总数。
#include <cstdio>
#include <algorithm>
#define M 100
using namespace std;
int vis[M][M]; //标记数组
long long int cnt=0;
void dfs(int i,int j,int n){
if(vis[i][j])
return;
if(n==0){
// printf("\n");
cnt++;
return;
}
vis[i][j]=1;
if(!vis[i][j-1]&&j>=2){ //判越界
dfs(i,j-1,n-1);
}
if(!vis[i][j+1]);{
dfs(i,j+1,n-1);
}
if(!vis[i-1][j]&&i>=2){
dfs(i-1,j,n-1);
}
vis[i][j]=0; //回溯,同一起点其他路径可能还要经过这个点
}
int main(){
int a,b,n; //a,b:起始行列号,n:允许走的步数
for(int i=1;i<=M;i++)
fill(vis[i],vis[i]+M,0);
scanf("%d%d%d",&a,&b,&n);
dfs(a,b,n);
printf("%I64d",cnt);
return 0;
}
- 走方格2
//效率较低
#include <cstdio>
#include <algorithm>
#include <cstring>
#define MAX 30
using namespace std;
bool vis[MAX][MAX];
int fx,fy;
long long int cnt=0;
bool iseven(int x) {
return (x%2==0)?true:false;
}
void solve(int x,int y) {
if(vis[x][y])
return;
if((iseven(x)&&iseven(y))||(x<1||y<1)||(x>fx||y>fy)) //越界判断
return;
if(x==fx&&y==fy) {
cnt++;
return;
}
vis[x][y]=true;
solve(x,y+1);
solve(x+1,y);
vis[x][y]=false;
}
int main() {
scanf("%d%d",&fx,&fy);
memset(vis,false,sizeof(vis));
if(iseven(fx)&&iseven(fy))
printf("0");
else {
solve(1,1);
printf("%I64d",cnt);
}
return 0;
}
- n皇后(回溯)
经典的DFS+回溯
//本代码仅输出三组解
#include <cstdio>
#include <algorithm>
#include <cstring>
#define MAX 20
using namespace std;
bool loc[MAX][MAX],row[MAX],col[MAX],zdjx[MAX*2+1],fdjx[MAX*2+1]; //这里不能只用一个数组,主、副对角线要开得足够大
short int n;
int cnt=0;
void setgraph(int x,int y,const bool s) { //标记
row[x]=col[y]=zdjx[y-x+n]=fdjx[y+x]=s; //为防止负数下标,主对角线要加上常数n
}
void dfs(int x) { //代表当前到第几个棋子
int i,j;
if(x>n) {
cnt++;
if(cnt<=3) {
for(i=1; i<=n; i++) {
for(j=1; j<=n; j++) {
if(loc[i][j])
printf("%d %d ",i,j);
}
}
printf("\n");
}
return;
}
for(i=1; i<=n; i++) {
if((!row[x])&&(!col[i])&&(!zdjx[i-x+n])&&(!fdjx[i+x])) {
setgraph(x,i,true);
loc[x][i]=true;
dfs(x+1);
loc[x][i]=false;
setgraph(x,i,false);
}
}
}
int main() {
scanf("%d",&n);
for(int j=1; j<=n; j++) { //在第一行的棋子的位置
setgraph(1,j,true);
loc[1][j]=true;
dfs(2);
loc[1][j]=false;
setgraph(1,j,false);
}
printf("%d",cnt);
return 0;
}
当然本题还有更优解,比如二进制优化(这个还真不会,有待学习)。
下面是升级版,2n皇后:
题目链接
其实也不难,就是把同一过程演绎两遍。我是比较两个解中是否有重复的元素,没有方案数就加1。
#include <cstdio>
#include <algorithm>
#define M 10
using namespace std;
bool row[M],col[M],zdjx[M*2+1],fdjx[M*2+1],vis[M][M];
int n,cnt=0,q[100][M];
short int g[M][M];
void setgraph(int x,int y,const bool st) {
row[x]=col[y]=zdjx[y-x+n]=fdjx[y+x]=st;
}
int comp(){
int m=0,j,k;
for(int i=0;i<cnt;i++){
for(j=i+1;j<cnt;j++){
for(k=1;k<=n;k++){ //这是k不是i,注意细节
if(q[i][k]==q[j][k])
break;
}
if(k>n){
m++;
printf("%d,%d\n",i,j);
}
}
}
return m*2; //黑白皇后倒过来也行
}
void dfs(int x) {
int i,j;
if(x>n) {
for(i=1; i<=n; i++) {
for(j=1; j<=n; j++) {
if(vis[i][j]){
q[cnt][i]=j; //某种方案的第i行为j
printf("%d %d\t",i,j);
}
}
}
printf("\n");
cnt++;
return;
}
for(i=1; i<=n; i++) {
if((!row[x])&&(!col[i])&&(!zdjx[i-x+n])&&(!fdjx[i+x])&&g[x][i]) {
setgraph(x,i,true);
vis[x][i]=true;
dfs(x+1);
vis[x][i]=false;
setgraph(x,i,false);
}
}
}
int main() {
scanf("%d",&n);
for(int i=1; i<=n; i++) {
for(int j=1; j<=n; j++)
scanf("%d",&g[i][j]);
}
for(int i=1; i<=n; i++) {
if(g[1][i]) {
setgraph(1,i,true);
vis[1][i]=true;
dfs(2);
vis[1][i]=false;
setgraph(1,i,false);
}
}
printf("%d",comp());
return 0;
}
当然,回溯并不是必须的,比如下面两题:
- 城堡问题
原文链接:https://blog.csdn.net/jack_jxnu/article/details/80958411
1 2 3 4 5 6 7
#############################
1 # | # | # | | #
#####—#####—#---#####—#
2 # # | # # # # #
#—#####—#####—#####—#
3 # | | # # # # #
#—#########—#####—#---#
4 # # | | | | # #
#############################
(图 1)
#= Wall
|=No wall
-=No wall
图1是一个城堡的地形图。(这里有排版问题)请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。城堡被分割成m*n(m≤50,n≤50)个方块,每个方块可以有0~4面墙。
Input程序从标准输入设备读入数据。第一行是两个整数,分别是南北向、东西向的方块数。在接下来的输入行里,每个方块用一个数字(0≤p≤50)描述。用一个数字表示方块周围的墙,1表示西墙,2表示北墙,4表示东墙,8表示南墙。每个方块用代表其周围墙的数字之和表示。城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。输入的数据保证城堡至少有两个房间。
Output城堡的房间数、城堡中最大房间所包括的方块数。
样例输入
4
7
11 6 11 6 3 10 6
7 9 6 13 5 15 5
1 10 12 7 13 7 5
13 11 10 8 10 12 13
样例输出:
5
9
思路:根据题意描述,可用按位与&判断四周是否有墙(死算也行),由题意不会有两个房间共用一堵墙的情况,所以无需回溯。
#include <cstdio>
#include <cstring>
#include <algorithm>
#define M 100
#define nor NUM_OF_ROOMS
//以下行为i,列为j
using namespace std;
bool vis[M][M]; //标记房间是否被走过
int nor=0; //房间数
int r,c,area; //r行c列,area为连通面积
void dfs(int room[][M],int i,int j){
if(vis[i][j])
return;
vis[i][j]=true;
area++;
//以下分别为左、上、右、下,进行按位与操作,判断是否有墙
//如某位置为9(1+8),则1(0001)&9(1001)为1(0001),说明右面有墙
//注意!的优先级高于&
if(!(room[i][j]&1))
dfs(room,i,j-1);
if(!(room[i][j]&2))
dfs(room,i-1,j);
if(!(room[i][j]&4))
dfs(room,i,j+1);
if(!(room[i][j]&8))
dfs(room,i+1,j);
}
int main(){
scanf("%d%d",&r,&c);
memset(vis,false,sizeof(vis)); //所有结点设为未走过
int room[M][M],maxarea=0; //room存放图的信息
for(int i=0;i<r;i++){
for(int j=0;j<c;j++)
scanf("%d",&room[i][j]);
}
for(int i=0;i<r;i++){
for(int j=0;j<c;j++){
if(!vis[i][j]){
//未走过表示遇到新的房间,走过就不要动了
nor++;
area=0;
dfs(room,i,j);
maxarea=max(area,maxarea);
}
}
}
printf("%d\n%d",nor,maxarea);
return 0;
}
- 入门
题目链接
可以重复经过,自然无需回溯
#include <cstdio>
#include <algorithm>
#include <cstring>
#define M 23
using namespace std;
char g[M][M];
bool vis[M][M];
int dx[4]= {-1,1,0,0},dy[4]= {0,0,-1,1},len=1,maxlen,row,col;
bool exced(int x,int y) {
if(x>row||y>col||x<1||y<1)
return true;
return false;
}
void solve(int x,int y) {
printf("%d %d\n",x,y);
for(int i=0; i<4; i++) {
int nx=x+dx[i],ny=y+dy[i];
if((g[nx][ny]=='.'||g[nx][ny]=='@')&&!exced(nx,ny)) {
if(!vis[nx][ny]) {
len++;
vis[nx][ny]=true;
solve(nx,ny);
}
//可以重复经过,不回溯即可,不要把递归调用放在这层if后面,防止反复在某两点间“横跳”。return后会回到此处。
}
}
return;
}
int main() {
int x,y;
scanf("%d%d",&col,&row);
getchar();
for(int i=1; i<=row; i++) {
scanf("%s",g[i]+1); //注意这里要+1
getchar(); //不要忘了这里getchar()
for(int j=1; j<=col; j++) {
if(g[i][j]=='@') {
x=i;
y=j;
}
}
}
vis[x][y]=true;
solve(x,y);
printf("%d",len);
return 0;
}
- Lake Counting(连通性判断)
题目链接
#include <cstdio>
#include <algorithm>
#define MAX 100
using namespace std;
bool vis[MAX][MAX]={false};
int n,m;
char g[MAX][MAX];
int ws=0,water=0;
int x_dir[8]={-1,1,0,0,-1,1,1,-1},y_dir[8]={0,0,-1,1,-1,1,-1,1};
int dfs(int i,int j){
if(vis[i][j])
return ws;
if(i<0||j<0||i>=n||j>=m)
return ws;
vis[i][j]=true;
if(g[i][j]=='W')
ws++;
else
return ws;
for(int k=0;k<8;k++)
dfs(i+x_dir[k],j+y_dir[k]);
return ws;
}
int main(){
scanf("%d%d",&n,&m);
getchar();
for(int i=0;i<n;i++){
scanf("%s",g[i]);
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
ws=0;
if(g[i][j]=='W'&&!vis[i][j]){
if(dfs(i,j)>=1)
water++;
}
vis[i][j]=true;
}
}
printf("%d",water);
return 0;
}
2.不在(明确的)图上搜索
- 组合
#include <cstdio>
#include <algorithm>
using namespace std;
int b[100],a[100],n,k; //从n个元素中选k个
void combin(int x,int st){ //下一个应从第st个开始
int i;
if(x>=k){
for(i=0;i<k;i++)
printf("%d ",b[i]);
printf("\n");
return;
}
for(i=st;i<n;i++){
b[x]=a[i];
combin(x+1,i+1);
}
}
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
combin(0,0);
return 0;
}
- 自然数划分
题目链接
难度也不大
#include <cstdio>
#include <algorithm>
using namespace std;
int a[10];
bool dizen(int *a,int cnt) { //由题意加数要递增。
//cnt为待比较个数
for(int i=0; i<cnt-1; i++) {
if(a[i]>a[i+1])
return false;
}
return true;
}
void div(int x,int num) { //x为递归深度,num为当前数
if(num<=0) {
// printf("%d\n",x);
if(dizen(a,x)&&x>1) {
for(int i=0; i<x; i++) {
printf("%d",a[i]);
if(i!=x-1)
printf("+"); //不要用\b \n否则多输出一个空格
}
printf("\n");
}
return;
}
for(int i=1; i<=num; i++) {
a[x]=i;
solve(x+1,num-i);
}
}
int main() {
int n;
scanf("%d",&n);
solve(0,n);
return 0;
}
- 题目链接
这是我给出的解法,仅能通过50%的数据
#include <algorithm>
#include <cstdio>
#define M 1001
using namespace std;
int a[M];
int cnt=0;
void dfs(int x,int num){ //num代表当前数是num
// for(int i=0;i<x;i++)
// printf("%d ",a[i]);
// printf("\n");
for(int i=1;i<abs(a[x-1]-a[x-2]);i++){
a[x]=i;
cnt=(cnt+1)%10000;
dfs(x+1,i);
}
return;
}
int main(){
int n;
scanf("%d",&n);
fill(a,a+M,9999);
a[0]=n;
for(int i=1;i<=n;i++){
a[1]=i;
cnt=(cnt+1)%10000;
dfs(2,1);
}
printf("%d",cnt);
return 0;
}
- 砝码称重
题目链接
这题主要是考虑到左右两边都能放砝码,当然还可以不放。
#include <cstdio>
#include <algorithm>
using namespace std;
int fm[25],thing[11],n,f[25];
bool solve(int x,int st,int tar) {
//当前搜索到第x个砝码,从fm[st]开始,目标重量为tar
if(tar==0)
return true;
if(x>n)
return false;
for(int i=st; i<n; i++) {
if(solve(x+1,i+1,tar-fm[i])||solve(x+1,i+1,tar+fm[i])||solve(x+1,i+1,tar)) //左右两边都能放砝码,也可以不放
return true;
}
return false;
}
int main() {
int m,i,sum=0;
scanf("%d%d",&n,&m);
for(i=0; i<n; i++)
scanf("%d",&fm[i]);
for(i=0; i<m; i++)
scanf("%d",&thing[i]);
for(i=0; i<m; i++) {
if(solve(1,0,thing[i]))
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
待补充……
有些题目,当时我做了很长时间,其实现在回过头来看,也不过是那么回事。