DFS算法学习笔记1

定义:

深度优先搜索属于图算法的一种,英文缩写为DFS即Depth First Search.其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次.
举例说明之:下图是一个无向图,如果我们从A点发起深度优先搜索(以下的访问次序并不是唯一的,第二个点既可以是B也可以是C,D),则我们可能得到如下的一个访问过程:A->B->E(没有路了!回溯到A)->C->F->H->G->D(没有路,最终回溯到A,A也没有未访问的相邻节点,本次搜索结束).简要说明深度优先搜索的特点:每次深度优先搜索的结果必然是图的一个连通分量。

摘自百度百科

例题:

1.在图上搜索
  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;
}
  1. 走方格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;
}
  1. 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;
}

当然,回溯并不是必须的,比如下面两题:

  1. 城堡问题

原文链接: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;
}

  1. 入门
    题目链接
    可以重复经过,自然无需回溯
#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;
}
  1. 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.不在(明确的)图上搜索
  1. 组合
#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;
}
  1. 自然数划分
    题目链接
    难度也不大
#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;
}
  1. 题目链接
    这是我给出的解法,仅能通过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;
}

  1. 砝码称重
    题目链接
    这题主要是考虑到左右两边都能放砝码,当然还可以不放
#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;
}

待补充……
有些题目,当时我做了很长时间,其实现在回过头来看,也不过是那么回事。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值