dfs,bfs,深搜广搜的基本模式 ,bfs实例----noj1541.加1乘2平方【一文带你了解bfs,dfs】【算法养成】

关于普通深搜的优化的理解

之前分享过深搜的记忆化搜索,所谓记忆化搜索就是将自变量设置为维数,比如采药问题中的lefttime以及pos,就是两个自变量,这里的因变量是value,普通的递归那就会将value也放在递归体中,一般将不变的常量的作用域设置为包含main和dfs方法。

所谓记忆化就是将所有的位置的设置为初始值,之后每次得到value后放入,在边界条件或者值不是初始值都会结束dfs,这里的一个重要的实现就是依靠的是返回值得到答案,采药问题中就是依靠两个dfs(值)来记录两个分支的答案。

那普通的递归是如何优化实现呢,比如【奇怪的电梯】,之前提到多一个优化就是写一个visted[floor],访问之后设置为1,之后如果回溯之后就清除标记,这里如果递归dfs时又出现了这个楼层位置那就返回,不搜了;

这里的问题是 :这里只是简单排除到相同位置的不同路径,但是可能后面到达floor的位置的路径的步数更短,这里是可以优化的

所以这里的优化就是我们要记录到达每个楼层的步数cnt,注意,这里和记忆化不一样,这里还是将cnt这个因变量还是作为普通递归的表示在递归dfs里,这里和记忆化相似的地方就是也需要一个数组F[floor]来记录每个floor处的cnt,当然,这里由于特殊要求,数组的初始值要设置为inf而不是0,每次进入下一层递归,首先判断F【floor】与cnt的大小,如果cnt更小就记录F[floor] = cnt;否则就会返回,这里和之前的就是要更加符合实际。

dfs的一般模式()

假设自变量为x ,y,因变量为f

假设x的变化是x++,y的变化是y+m[x]; f的变化是f++

public static void dfs(int x,int y,int f)
{
    if(不满足边界条件)
    {
        return;
	}
    if(到达目标值)
    {
     	//相关记录操作
        return;
    }
    dfs(x++,y+m[x],f++); //第一种情况,这里是随便写的
    dfs(x++,y,f++);
}

还有就是分支过多,比如8皇后,有八种

for(int i = 0;i < 8;i++)
{
    if(满足递归条件)//不排除分支就会效率低
    {
        //进入递归
        //回溯条件:清除标记
	}
}

对于记忆化就不单独说了,就是将f给提取出来,用返回值来返回最优解

还有分治算法就是dfs的一个实际的应用,循环赛就是二分之后再分别搜索

dfs —> bfs

在图论中就讲过这两种搜索方式,dfs深度搜索,就按照一种情况一直搜下去,直到不满足条件,暴力搜索是解决很多问题最简单思考到的方法,就是题目的几种状态画出一颗搜索树,比如【过河卒】问题就是两种分支,右走或者下走,其余就是不满足条件时就不搜,还有很多迷宫,数独都是可以搜索的,只是题目看着复杂而已。

对于bfs,广搜就是一层一层搜,有的时候就会比dfs简单,这里的情况就是所求量与层数相关,比如最小步数之类的问题,这个是与层数t相关,比如【奇怪的电梯】,这是求最小步数,就是层数,那么直接深搜就没有广搜快了

对于【奇怪的电梯】,我们之前讲过,是一个二叉树,正常结束条件是到达目标层,我们深搜就是查找每一层的情况,但是我们可以发现不同的路的深度是不一样的,比如上的要走7步,下的可能就只有1步。所以不同的路径的深度是不一样的。

深搜就是将所有的方案都找出来,然后将最优解给记录下来,那么还有什么方法求最优解?

其实就是广度优先搜索,我们一层一层搜,对于奇怪的电梯这道题,比如考察第一层的两种情况,之后就考察第二层情况,一旦发现某一层有最优解,就不继续搜了,所以就是考察层数时,我们就要使用广度优先搜索更佳。我们就要设置一个层数变量。

这里我们每次将都是按层来取出数据,且先进先出,再想到打印杨辉三角时就使用了队列,那么我们这里就可以再bfs中使用队列来更加方便操作数据

下面举一个例子,就是加1乘2平方

题目来源NOJ

1541.加1乘2平方

时限:1000ms 内存限制:10000K 总时限:3000ms

描述

最简单的队列的使用
#include
#include
using namespace std;

queue q1;
int main()
{
int temp, x;
q1.push(5);//入队
q1.push(8);//入队
temp = q1.front();//访问队首元素
q1.pop();//出队
q1.empty();//判队列是否为空
q1.back();//返回队尾元素
q1.size();//返回队列长度
}

给定两个正整数m、n,问只能做加1、乘2和平方这三种变化,从m变化到n最少需要几次

输入

输入两个10000以内的正整数m和n,且m小于n

输出

输出从m变化到n的最少次数

输入样例

1 16

输出样例

3

这里就用c++写了,因为方便一点,语言都只是工具,这里我们来分析一下这个题目,这就是广搜bfs的最典型的例题。就像dfs中的斐波拉契一样

首先思考这个题的搜索树结构,应该有3个分支,分别是加1,乘2,平方;每个子支又具有3个分支,如果用深搜,就3中分支,写成一个for循环,广搜就是一层一层搜索,这里我们取出一个数(结点),首先先判断这个点,之后再标记这个点,之后给出最优解,让子支入队

在这里插入图片描述

这里可以发现每个结点的操作都是一样的,所以使用循环,不知道次数,使用while循环,因为子支最先搜索的之后进入操作的也是最先操作,符合队列先进先出的特性(操作一个结点时要出队),采用队列的结构来表示bfs非常常见

那这里需要注意的细节就是,要避免重复,也就是初步记忆化,和之前分享的简单递归优化相同,这里使用used[]来表示是否使用过,基本思想和普通递归优化相同,这里不存在说有更短的路径到达相同的数,如果时深搜dfs那么就要采用比较来判断,这里就使用step[]表示到达某个数所需要的步数,和记忆化或者dp相似,在判断一个结点时

  • 首先获得队首元素u,这里先进先出,所以子支的结点都在队尾,队首就是先搜索出的低层结点,这里操作结点就需要使用front获得,之后还要将其出队
  • 之后就是对u进行扩展,扩展就是将u的子节点表示出来,这里有三种情况,所以使用for循环表示,0代表加1,1代表*2,2代表平方 ,使用if判断边界条件,这里的边界条件应该要包含used,操作子节点就是让其入队,步骤都是标记----获得步数step----入队; 和提取 front----出队 ;形成一个闭环,当队列为空时就操作结束了,也就是最下层最后一个结点
  • 这里对u的扩展的代码较长且简单,所以就使用一个函数change来表示这个过程,边界条件也是如此,都使用函数来表示

完整代码如下(注释丰富)

#include<iostream>
#include<queue>

using namespace std;

queue<int> q; //这里使用c++中自带的queue,就不单独创建了
int start,target;//开始的数,和结束的数,求最短路径,设置为全局变量
int step[10001]; //记录最小步数
int used[10001];//记录是否使用过,这就是普通的递归优化

void init()
{
	used[start] = 1;//标记使用过
	step[start] = 0;//记录最优解
	q.push(start); //入队,最开始就是查找的头入队
}

int change(int u, int dire)//扩展子支
{
	int v;
	if (dire == 0)
	{
		v = u + 1;
	}
	else if (dire == 2)
	{
		v = u * 2;
	}
	else {
		v = u * u;
	}
	return v;
}

bool isExtend(int u, int dire)//边界
{
	int v;
	v= change(u, dire);//判断的是子支
	if (v <= target && used[v] == 0)
	{
		return true;
	}
	else
		return false;
}

//这里的状态就是每个数字都对应一个步数,所以数组为1维,和奇怪的电梯一样,就是step[floor]
int bfs()//这里变量就是层数或者说step,自变量就是三种处理方式,所以这里采用记忆化搜索的思路,不将结果放入
{
	int u, v;//头以及其扩展
	while (!q.empty())//队列不空
	{
		//1.取出队首元素u(front,pop)
		u = q.front();
		q.pop();
		//2.扩展u,(处理子支)
		for (int i = 0; i < 3; i++)//0代表+1,1代表*2,2代表平方
		{
			if (isExtend(u, i))//剪支,满足搜索条件
			{
				v = change(u, i); //进入子支
				if (v == target)
				{
					return (step[u] + 1);//相当于层数++,就是变成了数组而已
				}
				used[v] = 1;
				step[v] = step[u] + 1;
				q.push(v);//队尾插入,队首删除
			}
		}
	}
}

int main()
{
	cin >> start >> target;
	init();
	int num = bfs();
	cout << num << endl;
}

从这个题目就可以提取出bfs广搜的队列方法的基本模式

广搜的一般模式 (核心:状态) 这和记忆化一样,或者dp一样,判断每一个状态

这里首先应该就写成一重while循环,之后两个就是取出元素,并对其扩展

queue<int> q; //队列q

while(!q.empty())//不空
{
    //1.取出队首元素u
    //2.扩展u 
}

和深搜相比各有适用之处,但是dfs可用的范围更大,bfs更适合找出最短路径,之后还会继续讲解bfs,深搜广搜是算法中相当重要的一个模块,fighting~~

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值