复试机试算法总结#8:搜索

16 篇文章 0 订阅

1. 宽度优先搜索BFS

从搜索的起点开始,不断地优先访问当前节点的邻居。即首先访问起点,然后依次访问起点尚未访问的邻居结点,再按照访问起点邻居节点的先后顺序依次访问它们的邻居,直至找到解或遍历整个解空间。常用于搜索最优值的问题

步骤如下:

  1. 状态:需要确定所求解问题中的状态。通过状态的扩展,遍历所有状态,从中寻找所需要的答案。
  2. 状态扩展方式:再BFS中,需要尽可能扩展状态,并对先扩展得到的状态先进行下一次状态的扩展。
  3. 有效状态:对有些状态,无需再次扩展,而是直接舍弃。因为由分析问题可知,目标状态不可能由它们扩展得到。
  4. 队列:使用队列来实现先得到的状态优先扩展,所以每次取对头元素进行扩展。
  5. 标记:为了判断哪些状态有效而哪些无效。
  6. 有效状态数:问题中的有效状态数与时间复杂度同数量级,所以搜索之前要估算是否能接受。
  7. 最优:BFS常用于求解最优值,因为其搜索到的状态总是按照某个关键字递增。所以适用于“最少,最短,最优”等关键字的问题。

例题:农夫与🐄处于同一直线(可视为坐标轴)上,农夫的位置为N(0≤N≤100000),🐄的位置为K(0≤K≤100000)。农夫有两种移动方式:一分钟内从X走到X-1或X+1;一分钟内从X瞬移到2X。假设🐄不动,问农夫几分钟能追到🐄?

输入:每行两个数,分别为N和K

输出:时间

样例输入:5 17

样例输出:4

#include<iostream>
#include<cstring>
#include<queue>

using namespace std;

const int MAXN=100001;

struct Status//定义状态为(n,t)二元组 
{
	int n;//某个位置的坐标 
	int t;//到达这个位置需要的时间 
	Status(int n,int t):n(n),t(t) {} 
};

bool visit[MAXN];//访问队列 

int BFS(int n,int k)
{
	queue<Status> Q;
	Q.push(Status(n,0));//初始状态入队 
	visit[n]=true;//标记 
	while(!Q.empty())
	{
		Status current=Q.front();//访问对头元素 
		Q.pop();
		if(current.n==k)//查找成功 
			return current.t; 
		//查找失败 
		for(int i=0;i<3;++i)//有三种状态可供转入
		{
			Status next(current.n,current.t+1);//定义一个新状态 
			if(i==0)//右走 
				next.n+=1;
			else if(i==1)//左走 
				next.n-=1;
			else if(i==2)//瞬移 
				next.n*=2;
			//判断新状态是否合法 
			if(next.n<0||next.n>=MAXN||visit[next.n])
				continue;
			Q.push(next);//遍历过的状态入队 
			visit[next.n]=true;//标记 
		} 
	}
}

int main()
{
	int n,k;
	while(cin>>n>>k)
	{
		memset(visit,false,sizeof(visit));
		cout<<BFS(n,k)<<endl;
	}

	return 0;
}

 

 2. 深度优先搜索DFS

在搜索过程中,首先访问起点,然后访问它的邻居,再访问该节点的邻居(而非起点的其他邻居),重复直至找到解或当前结点已无未访问的邻居结点为止。之后回溯上一个结点,访问它的另一个邻居结点。

在DFS中,获得一个状态后,立即扩展这个状态,保证先得到的状态后扩展。思想类似堆栈,但是用递归来实现。常用DFS来判断问题是否有解

剪枝:DFS中通过放弃对某些不可能产生结果的子集的搜索,达到提高效率的目的。

例题1:pxq大小的长方形国际象棋棋盘(小于8x8)上有一个骑士,他想按照日字规则走遍每一个格子。骑士可以在任意格子上开始或结束旅行。请找一条符合的路径。

输入:第一行是样例个数,之后每一行是棋盘长宽p,q;

输出:路径

:国际象棋的棋盘行用数字表示,列用字母表示,日字走法如下

/*
样例输入:
3
1 1
2 3
4 3
样例输出:
Scenario #1:
A1

Scenario #2:
impossible

Scenario #3:
A1B3C1A2B4C2A3B1C3A4B2C4

*/

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>

using namespace std;

const int MAXN=30;
int p,q;
bool visit[MAXN][MAXN];
int direction[8][2]={//8个方向 
	{-1,-2},{1,-2},{-2,-1},{2,-1},{-2,1},{2,1},{-1,2},{1,2}
};

bool DFS(int x,int y,int step,string ans)//状态为(x,y,step)三元组,表示到(x,y)所需步数 
{
	if(step==p*q)//遍历成功 
	{
		cout<<ans<<endl;//输出路径结点字符串 
		return true;
	} 
	else
	{
		for(int i=0;i<8;++i)//按日字理论上有8个方向 
		{
			//遍历邻居结点,得到新状态坐标 
			int nx=x+direction[i][0]; 
			int ny=y+direction[i][1];
			char col=ny+'A';//用于ans字符串后缀更新 
			char row=nx+'1';
			//非法状态 
			if(nx>=p||nx<0||ny>=q||ny<0||visit[nx][ny])
				continue;
			//标记
			visit[nx][ny]=true; 
			//递归 
			if(DFS(nx,ny,step+1,ans+col+row))
				return true;
			visit[nx][ny]=false;//取消标记 
		}
	}
	return false;
}

int main()
{
	int n;
	cin>>n;
	int caseNumber=0;
	while(n--)
	{
		cin>>p>>q;
		memset(visit,false,sizeof(visit));//初始化 
		cout<<"Scenario #"<<++caseNumber<<" :"<<endl;
		visit[0][0]=true;
		if(!DFS(0,0,1,"A1"))//因为可以在任一格开始,所以可以默认从左下角开始
			cout<<"impossible"<<endl;//遍历失败 
	}
	
	return 0;
}

例题2棋盘游戏

#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>

using namespace std;

const int MAXN=6;
int a[MAXN][MAXN];//棋盘 
bool visit[MAXN][MAXN];//访问 
int sx,sy,ex,ey;//起点,终点
int answer=INT_MAX;//整体最小代价初始化为无穷大
int direction[4][2]={
{0,-1},{0,1},{-1,0},{1,0}//上下左右 
};

void DFS(int x,int y,int cost,int state)//cost为当前代价
{
	if(visit[x][y])
		return;
	
	if(x==ex&&y==ey)//找到终点
	{
		answer=min(answer,cost);//更新最小代价
		return;
	}
	
	if(cost>answer)
		return;
	
	visit[x][y]=true;
	
	for(int i=0;i<4;++i)//上下左右四个邻居
	{
		int nx=x+direction[i][0],ny=y+direction[i][1];
		if(x<0||y<0||x>=MAXN||y>=MAXN)
			continue;
		int add=a[nx][ny]*state;
		int nstate=add%4+1;
		DFS(nx,ny,cost+add,nstate);
	} 
	visit[x][y]=false;//回溯
	return; 
}

int main()
{
	memset(visit,false,sizeof(visit));
	for(int i=0;i<6;++i)
	{
		for(int j=0;j<6;++j)
		{
			cin>>a[i][j];
		}
	}
	
	cin>>sx>>sy>>ex>>ey; 
	DFS(sx,sy,0,1);
	cout<<answer<<endl;
	
	return 0;
}

例题3:输出1~n的全排列(n≤9)

#include<iostream>
#include<cstdio>

using namespace std;

const int MAXN=10;
int p[MAXN]={0};
bool visit[MAXN]={false};
int n;//对1~n全排列

void DFS(int x)//x为参与全排列的个数 
{
	if(x==n+1)//找到目标状态 
	{
		for(int i=1;i<=n;i++)//输出当前排列方式 
		{
			cout<<p[i]<<" ";
		}
		cout<<endl;
		return;
	}
	
	for(int i=1;i<=n;i++)//状态扩展 
	{
		if(visit[i]!=true)//状态合法 
		{
			p[x]=i;			//修改 
			visit[i]=true;	//标记 
			DFS(x+1);		//递归 
			visit[i]=false;	//还原标记(回溯) 
		}
	}
} 

int main()
{
	while(cin>>n)
	{
		DFS(1);
	}
	return 0;
 } 

例题4:组合数输出

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

const int MAXN=22;
int p[MAXN];
bool visit[MAXN]={false};
int n,r;

void DFS(int index)
{
	if(index==r+1)
	{
		for(int i=1;i<=r;i++)
			cout<<p[i]<<" ";
		cout<<endl;
		return;
	}
	for(int i=p[index-1];i<=n;i++)
	{
		if(visit[i]!=true)
		{
			p[index]=i;
			visit[i]=true;
			DFS(index+1);
			visit[i]=false;
		}
	}
	return ;
}

int main()
{
	while(cin>>n>>r)
	{
		memset(visit,false,sizeof(visit));
		p[0]=1;
		DFS(1);
	} 
	
	
	
	return 0;
} 

3.回溯法

回溯法(backtracking)是优先搜索的一种特殊情况,又称为试探法,常用于需要记录节点状态的深度优先搜索。通常来说,排列、组合、选择类问题使用回溯法比较方便。顾名思义,回溯法的核心是回溯。

原理:在搜索到某一节点的时候,如果我们发现目前的节点(及其子节点)并不是需求目标时,我们回退到原来的节点继续搜索,并且把在目前节点修改的状态还原。

优点:可以始终只对图的总状态进行修改,而非每次遍历时新建一个图来储存状态。

过程:与普通的深度优先搜索一样,都有[修改当前节点状态]→[递归子节点] 的步骤,只是多了回溯的步骤,变成了[修改当前节点状态]→[递归子节点]→[回改当前节点状态]
两个小诀窍:一是按引用传状态,二是所有的状态修改在递归完成后回改。回溯法修改一般有两种情况,一种是修改最后一位输出,比如排列组合;一种是修改访问标记,比如矩阵里搜字符串。

例题:

leetcode46全排列      

单词搜索:不同于排列组合问题,本题采用的并不是修改输出方式,而是修改访问标记。在我们对任意位置进行深度优先搜索时,我们先标记当前位置为已访问,以避免重复遍历(如防止向右搜索后又向左返回);在所有的可能都搜索完成后,再回改当前位置为未访问,防止干扰其它位置搜索到当前位置。使用回溯法,我们可以只对一个二维的访问矩阵进行修改,而不用把每次的搜索状态作为一个新对象传入递归函数中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值