算法dfs+剪枝(见代码中)+回溯详解与例题提升

前言:深入学习了两天dfs算法相关的知识,根据自己的学习认识总结一下:

常见的dfs算法题有如下几类:

1:不需要回溯的dfs

      A:特点:

               a:判断 “是否存在” 的问题,遍历所有的点直接判断;

               b:要求能到达的 “数量” 问题;

     

       B:例题

          典型的例题1——数量问题:红与黑_牛客题霸_牛客网 (nowcoder.com)

 

#include<stdio.h>
int m,n,i,j;
char a[30][30];
int dfs(int x,int y)
{
    if(x>=m||x<0||y>=n||y<0)//边界
    {
        return 0;
    }
    else if(a[x][y]=='#')//限制条件
    {
        return 0;
    }
     else//正常搜索条件
    {
        a[x][y]='#';//标记不能回头
        return 1+dfs(x-1,y)+dfs(x+1,y)+dfs(x,y-1)+dfs(x,y+1);//递归公式
    }
}
int main()
{
    while(scanf("%d%d",&m,&n)!=EOF)
    {
        for(i=0;i<m;i++)
        {
            scanf("%s",a[i]);
        }
        for(i=0;i<m;i++)
        {
            for(j=0;j<n;j++)
            {
                if(a[i][j]=='@')
                {
                    printf("%d\n",dfs(i,j));
                    break;
                }
            }
        }
    }
} 

          例题2——是否存在的问题:

原题目描述
一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n∗n 的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。

同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。

如果起点或者终点有一个不能通行(为#),则看成无法办到。

注意:A、B不一定是两个不同的点。

输入格式
第1行是测试数据的组数 k,后面跟着 k 组输入。

每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n∗n 的。

接下来是一个 n∗n 的矩阵,矩阵中的元素为.或者#。

再接下来一行是 4 个整数 ha,la,hb,lb,描述 A 处在第 ha 行, 第 la 列,B 处在第 hb 行, 第 lb 列。

注意到 ha,la,hb,lb全部是从 0 开始计数的。

输出格式
k行,每行输出对应一个输入。

能办到则输出“YES”,否则输出“NO”。

数据范围
1≤n≤100

输入样例:
2
3
.##
..#
#..
0 0 2 2
5
.....
###.#
..#..
###..
...#.
0 0 4 0
1
2
3
4
5
6
7
8
9
10
11
12
13
输出样例:
YES
NO
 

#include<stdio.h>
#include<string.h>
int k,n,e,b,c,d,ans;
char a[105][105]={0};
int st[105][105]={0};
int p[4]={-1,0,1,0},q[4]={0,1,0,-1};//偏移矩阵 
void dfs(int x1,int y1,int x2,int y2)
{
	if(x1==x2&&y1==y2)//出口 
	{
		ans=1;
		return;
	}
	for(int i=0;i<4;i++)//递归 
	{
		int j=x1+p[i];
		int k=y1+q[i];
		if(j>=0&&j<n&&k>=0&&k<n&&a[j][k]=='.'&&st[j][k]==0)//这里其实就是所谓的剪枝,即事先判断去除不必要的路径
		{
			st[j][k]=1;//标记不能回头
			dfs(j,k,x2,y2);
		}
	}
	
}

int main()
{
	scanf("%d",&k);
	for(int i=0;i<k;i++)
	{
		ans=0;
		memset(a,0,sizeof(a));//重置矩阵
		scanf("%d",&n);
		for(int j=0;j<n;j++)
		{
			scanf("%s",a[j]);
		}
		scanf("%d%d%d%d",&e,&b,&c,&d);
	    if(a[e][b]=='#'||a[c][d]=='#')
	    {
			printf("NO");
		}
		else
		{
			st[e][b]=1;
			dfs(e,b,c,d);
			if(ans==1)
			{
				printf("YES\n");
			}
			else
			{
				printf("NO\n");
			}
		}
		
	}
	
}

注意:这个里面提到的偏移矩阵,实际上两两组合起来就是确定从当前位置下一步的走法(上下左右、东南西北),根据实际情况会有所变化,也有的人习惯直接用二维矩阵来表示,原理是一样的

2:需要回溯的dfs:

    A:特点:

              a:往往是求最优路径/方法;

              b:基础的就是求方案数量。

   B:例题

            典型例题1——方案数量类型: 

题目描述
马在中国象棋以日字形规则移动。

请编写一段程序,给定 n∗m 大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。

输入格式
第一行为整数 T,表示测试数据组数。

每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。

输出格式
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。

数据范围
1≤T≤9,
1≤m,n≤9,
0≤x≤n−1,
0≤y≤m−1

输入样例:
1
5 4 0 0
1
2
输出样例:
32
 

#include<stdio.h>
#include<string.h>
int T,n,m,index;
char a[10][10];
int p[8]={-2,-1,2,1,-2,-1,2,1},q[8]={1,2,1,2,-1,-2,-1,-2};//偏移量 
void dfs(int x,int y,int sum)
{
	if(sum==n*m)
	{
		index++;
		return;
	}
	a[x][y]='F';
	for(int i=0;i<8;i++)
	{
		int j=x+p[i];
		int k=y+q[i];
		if(j>=0&&j<m&&k>=0&&k<n&&a[j][k]=='T')//剪枝
		{
			//printf("%d %d %d\n",j,k,sum);//测试语句,忽略
			dfs(j,k,sum+1);
		}
	}
	a[x][y]='T'; //回溯
}
int main()
{
	scanf("%d",&T);
	for(int i=0;i<T;i++)
	{
		index=0;
		int x,y;
		scanf("%d%d%d%d",&n,&m,&x,&y);
		memset(a,'T',sizeof(a));
		dfs(x,y,1);//起始时占有一个点位 
		printf("%d\n",index); 
	}
}

结合这些代码来总结一下回溯和剪枝就十分好理解了:

1:回溯用人话来说,就是在标记之后,又要消除标记,消除标记的递归过程,就是回溯。

2:剪枝用人话来讲,就是根据题目中或者隐藏的限制条件,来避免不必要的搜索,电脑性能好的甚至可以不用,但是做题由时间限制的话必须得考虑。

以上就是入门dfs的入门课,吃透了之后就基本能理解了,不然有些题连代码都看不懂,下面进行更复杂的应用

3:提升应用

例题1:蓝桥杯2013年第四届真题-剪格子 - C语言网

#include <stdio.h>
int a[10][10];//存值
int b[10][10];//标记数组
int min;
int m,n,sum=0,index=0;
int dx[4]={0,1,0,-1};//偏移矩阵
int dy[4]={1,0,-1,0};
void f(int s,int i,int j,int bs)
{  
     if(s==sum/2&&min>bs)
	      min=bs;//判断是否更新答案
      b[i][j]=0;  //此时a[0][0] 只作连接用 所以格子和值不增加
       for(int k=0;k<4;k++)
	   {
           int x=dx[k]+i;
           int y=dy[k]+j;//边界判断和剪枝
           if(b[x][y]==1&&x>=0&&x<n&&x>=0&&x<m){
                     if(x==0&&y==0) f(s,x,y,bs);
                      else f(s+a[x][y],x,y,bs+1);
             }  
             }
             b[i][j]=1;//消除标记
 }
int main()
{
int i,j;
scanf("%d%d",&m,&n);//输入宽、高 
min=n*m;
for(i=0;i<n;i++)//注意坑点  先输入纵坐标的m 再输入的横坐标
for(j=0;j<m;j++)
{scanf("%d",&a[i][j]);//值
sum+=a[i][j]; b[i][j]=1;//初始化标记数组
} 
if(sum%2==1)printf("0");//预判是否可以分两部分
else{ f(a[0][0],0,0,1);
   if(index!=0) printf("%d\n",min);
   else printf("0\n");
}    
return 0;
}

   

    

例题二:蓝桥杯2013年第四届真题-危险系数 - C语言网

#include<stdio.h>
int a[1001][1001]={0};//邻接矩阵 
int c[1001]={0};//为0可以访问 
int b[1001]={0};//站点访问 次数 
int n,m,index=0;// 记录可行路径条数 

void dfs(int x,int y)
{
	if(x==y)
	{
		index++;//得到可行路径的条数
		for(int i=1;i<=n;i++)//记录可行路径访问点的次数
		{
			if(c[i]==1)
			{
				b[i]++;
			}
		}
	}
	c[x]=1;
	for(int i=1;i<=n;i++)
	{
		if(a[x][i]==1&&c[i]==0)
		{
			dfs(i,y);
			
		}
	}
	c[x]=0;
}

int main()
{
	int p,q,flog=0;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)//得到目标邻接矩阵 
	{
		int x,y;
		scanf("%d%d",&x,&y);
		a[x][y]=1;
		a[y][x]=1;
	}
	scanf("%d%d",&p,&q);
	dfs(p,q);
	if(index==0)
	{
		printf("-1\n");
	}
	else
	{
		for(int i=1;i<=n;i++)
		{
			if(b[i]==index)//可行路径的条数和访问的次数相等,则表明该点位必需点
			{
				flog++;
			}
		}
		printf("%d\n",flog-1);
	}
}

       总结:

       在写dfs题的时候,往往是在矩阵的基础上进行的,有些矩阵里的值有实际作用的,就不能轻易的在原矩阵上做修改,而是需要通过标记矩阵来进行标记,根据实际情况,标记矩阵的形式和数量也会变化,至于偏移矩阵,根据移动的方式来看是否需要设置。(宏的工作)

         而为了减少dfs的复杂度,根据题意往往是由很明显的不符合项,可以在dfs之前就完成判断(主函数里的工作)

        dfs与剪枝和和回溯往往会被同时涉及到,如果不明白递归原理,则可以记一记这个回溯模板(dfs函数里的工作)

       同时,dfs参数的模式定义比较个性化,主要包含:起点、终点、随递归变化的终止元素、固定的参考值等。(dfs函数的设置)

下面小结一下dfs基本应用模板:

#include<>
设置原矩阵;
设置标记矩阵,....;
设置偏移矩阵;
设置一些全局变量;
dfs()
{
   if()//终止条件
   {
      终止时会进行到的操作
   }
   标记
   for()
   {
      移动到下一步的操作;
      if()//剪枝,根据限制条件设置
      {
          dfs();
      }
   }
   消除标记//回溯
}


    

个人见解,希望有所帮助,不喜勿喷

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值