经典基础算法的一些精髓和注意点总结

一.搜索遍历算法

        0.搜索概论

                搜索,某个维度(角度)上分为两种,分别是遍历逻辑结构和遍历状态空间结构,二者都是一张图,而一个是题目给定的,一个是由题目给定所确定的

                所谓状态空间,就是这个问题的询问,会分解成那些小问题,从上帝视角来看整体构成的结构

                初学算法的懵懂时期,我一直弄不明白为什么一个好端端的分苹果问题,为什么是对一个树的遍历,那时候感觉树要是能看得见摸得着的数据结构,然而并不是的。这棵树需要人为转化,然后把转化出来的具体的树交给搜索算法遍历。那么问题来了,人们如何构建这棵树交给计算机

                这里我们要明确一个点,一个数据结构的所有信息可知,则数据结构一定能够算出来(有点像拉普拉斯妖的思想),所以只要你告诉生成算法树是怎么分叉的,分多深。。。就可以给出一棵完美的树

                注意上面两个不起眼的词:怎么分叉的,分多深。。。

                这实际上就是我们说的单层派生逻辑和边界,但这个地方是构建树的时候,而真正的是一边建树,一边就搜索记录了,所以有两重意思:

                        到达边界,构建树的时候直接回溯,搜索的时候需要记录答案,但由于直接回溯操作能够被记录答案兼容包括,所以就放在一起也没问题

                而我们通常把生产和遍历的过程放在一起,就是一边建树,一边把建的树就遍历完了,所以经过这么多层包装和转化、简化(实际上把代码量简化了,但思维难度反而提升了),搜索的本质思想确实很难体会

                上面只是我的一念之词,对于搜索的理解,还是怎么适合怎么来

                当然最后做搜索题目的时候,不要老是想着本质是什么,这样这样在那样,只要知道自己曾经耗费过经理证明了搜索算法的本质,接着直接套用模板就行了(但下面我还是会按照本质来讲解模板和那些所谓的三要素,五步法云云是如何想到的,又是如何有本质转化而来的

                可能有些啰嗦,但是如果想从0开始一步一步从基本的思想堆砌其搜索算法本质的大楼,还是需要一些时间的

        1.DFS

                下面不管是状态空间还是数据结构,都直接叫树(因为实际上没有什么区别)

                好,开始

                DFS,深度优先搜索,派生子节点逻辑:每次找到所有能够成为解(或者一部分)的子节点,按顺序遍历就得了

                当然从遍历最终的顺序上,我们还是习惯于用“一条路走到黑,走不通就回头”的方式理解

                然后代码就很简短了

        2.BFS

                继续

                BFS,广度优先搜索,派生子节点逻辑:每次从旁边的一个集合中 找到所有能够走到的点,把这些点放在集合中,等待被拿出来,这也就保证了在这一层的兄弟们没有被遍历完之前,不会有下一代出现

                这种方法的好处就是,我们如果把他类比成一根横着的直线,一层层的向下扫描,扫到第一个叶子节点,就找到了答案

        小结、批注:以上两种都是对比较明显的“树型”状态空间进行搜索便利的,并且大部分时候会有重复搜索(因题而异),那这个时候时间复杂度就会飙升 解决的办法就是记忆化搜索,这样最终的时间复杂度就接近时最终状态空间的大小

        3.二分遍历

                这种遍历方法的状态空间是一个单调上升或者下降的线性空间,简而言之这个状态空间中有唯一的最优解,同时比这个最优解不优的:可行不最优;比这个最优解更优的:最优不可行;

这样的状态空间适合使用二分算法进行解决

        行,搜索算法的大体框架和算法精髓就这样,看不懂的(因为本人写这篇文章是建立在弄懂的基础上)可以在网上搜其他大佬的博文看,讲的很清晰,我就不写了(主要是写不懂)

        4.福利代码框架

                DFS

void dfs(参数){
	if(边界){
		记录答案 
		return ;
	}
	for(所有可能的子节点参数){
		派生子节点的单层逻辑 
		dfs(新的子节点)
		还原已经更改过的全局参数 
	}
} 

/*

这里接着注释写几个注意点

1.派生的逻辑在设计的时候,我们要假设派生出来的子节点们的答案是一定的,然后考虑该规模问题
否则会死循环、报系统栈。。。
2.vis数组的必要性:本质上这家伙也算是放在DFS后面的参数里,由于一直遗传下去太麻烦,反正大家都要一起用的吗,就开全局了
但要注意的是,如果把vis数组放在递归参数里面,本质上就不需要回溯,因为只要你没有引用,是形参和实参的传递是单向的
如果不用vis,呵呵,报系统栈等着你 

*/ 

        BFS

struct node{
	参数 
};
queue<node>q;
q.push(起点);//可能不止一个 
vis[起点状态]=1;
while(!q.empty()){
	node tmp=q.top();
	q.pop();
	if(tmp是边界){
		记录答案 
		return ;
	}
	for(所有可能的子节点){
		q.push(子节点的参数);
		vis[]置为1 
	}
}
return -1;//无解 

//需要注意的是,其中vis数组如果下标是string(luogu上有这种题目),就使用map映射 

二分

bool check(){
	//通常是贪心或者模拟 
}
int l=最小不可能达到的答案,r=最大不可能达到的答案,ans=-1;//ans==-1表示无解状态 
while(l<=r){
	int mid=(l+r)/2;
	if(check(mid))ans=mid,...
	else ...
}
printf(ans);
//...是什么?
//如果把答案向大了搜,l=mid+1
//如果把答案向小了搜,r=mid-1 

5.代码实现注意点(C党和C++党):

        1.二分答案

                二分答案的时候分成实数二分和证书二分,其中实数二分就要注意eps,不能写==(对于浮点数本来就是近似的没有精准的等于可言)而整数二分就要注意mid+1mid-1不要写反了,动不动就死循环

                怎么调试二分的死循环?

                        1.首先输出l,r,mid如果发现这些卡在一般不输出了,那就是对于某个数据check函数被卡了,然后输出check,如果有在某一个地方卡住了,那大部分可能就是地址访问越界,就把所有访问到的地址下标参数打印出来,如果有负数就找到了

                        2.如果l<=r之后不知道到第输出l,r,l-1还是r-1,那么就如上文所述,直接在if(check)后面ans=mid,那么保证每次一旦搜索到正确答案的,第一时间记录,最后cout<<ans<<endl就可以了

        2.对于DP、DFS、BFS的多组数据

                如果DP和BFS的辅助数组不是很大,那么直接在局部循环里面定义初始化就可以

                但是DFS递归函数必须在全局写,那么就要每次在DFS之前memset一下

        3.对于DP,BFS,DFS的初始化

                好吧这个问题估计卡了我百分之80以上的搜索DP代码,就是初始化!!!一是要清楚初始化什么,比如FS中从那几个节点开始,就要在搜索之前把走过的初始节点vis变成1;二是要记得初始化QWQ就是多组数据的是后每次vis数组都要初始化(BFS和DP需要,带回溯的DFS不需要,由于特殊性质不需要回溯的DFS如果回溯了就不需要,没有就需要)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值