高级算法基础-搜索
本文是一个备份,用于备份社团博客中我自己的文章,原文链接https://www.maxcell.com.cn/2020/10/24/%e9%ab%98%e7%ba%a7%e7%ae%97%e6%b3%95%e5%9f%ba%e7%a1%80-%e6%90%9c%e7%b4%a2/
从这篇文章开始,如无特殊情况,我应该会持续更新面向纯小白的算法方面的文章
嗯,我尽量不出现让人看得都头大的代码
什么是算法
算法,顾名思义,就是计算的方法,或者按照百度百科上的定义,就是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令
为什么要学算法
确实,让我们求解二元或者三元方程组,我们可以用各种奇技淫巧;找一个简单初等函数的零点或极值点,我们可以画图然后一眼看出来;求出100以内的所有素数,我们可以一个一个背出来
但是如果要求解更多未知数的方程组呢?要找复杂函数的零点极值点呢?要求出10^9以内的所有素数呢?
这些活,人是不会去干的,一定会交给计算机
但是计算机不智能,它学不会我们的奇技淫巧,没法直接看图获取足够的信息,也存不下所有素数。它只会机械运算。因此,需要我们先用算法解决问题,再告诉计算机怎么一步一步用算法求解这些问题,比如高斯消元,比如牛顿迭代,比如欧拉线性筛
算法简单吗
不简单
这是我的博客,也只是我的博客
国内比我强的大佬成千上万,我根本排不上号
正片开始
首先从一道题目讲起
一个m*n的网格,从左上角的格子[1,1]开始,每次只能向右或者向下走,走到右下角的格子[m,n],有多少种走法
这是一个高中问题,你可能会说很简单啊,答案就是$C^{n}_{m+n}$
但是如果其中几个格子不能走怎么办?
原来的方法不能用了,那就得另辟蹊径了
从0开始构建一个算法
假设你化身为一个小人,站在第一格,知道上下左右但不知道你在哪,也不知道哪几个格子不能走,你会怎么统计总路径数量?
也许这个还有点难,那就再降低点难度,不要求你统计总数了,只要找到一条路径就行
一个很自然的想法就是,向右一直走到底,然后向下;中间若碰到了死路就回头,在最近的一个能转向的地方转向
比如像这样
按照这个方法,你找到了第一条路径
那么接下来的路怎么找呢
回溯
让我们回顾一下,刚刚走到死胡同的时候,你是怎么干的?
后退,转向
而这种做法进一步抽象,就是算法界的一个名词,“回溯”
那就好办了。每一次到达终点,就执行一次回溯
就像这样
直到无法回溯
最终你到达了终点几次,就是有几条路径
深度优先搜索
问题解决了,你找到了一个能“完美”解决这个问题的算法
在算法界,这个算法的名字是深度优先搜索,英文简称DFS
顾名思义,深度优先,意思就是先一条路走到黑,不撞南墙不回头,然后通过回溯来搜索其他路径的搜索算法
什么?啥是搜索?
搜索么,最基础的搜索,就是通过枚举(列举),把所有可能的情况枚举出来,再进行统计,或者边枚举边统计
不过,深度优先。。。是不是意味着还有其他优先的搜索算法?
没错,而且还不少。不过这篇文章只介绍最基本的两个及其拓展,一个已经介绍过了,另一个,就是接下来要介绍的
宽度优先搜索
宽度优先搜索,又名广度优先搜索,英文简称BFS
你可以理解为DFS是优先纵向搜索,BFS是优先横向搜索
还是以一道问题为例,一张网格上有两个格子,规定每次移动只能上下左右移动,请算出两点间最短路径
当然,问题肯定没有那么简单。照例我们让其中几个格子不能走
这道题目,深搜显然效率太低了,那么宽搜是怎么做的呢
首先,把起点距离记为0并标记起点为“已到达”,然后把它能到达的所有点放入一个队列并让其距离为1,并标记它们为“已到达”
蓝底表示“已到达”,红加号表示队列中,左上角数字为距离
只要未到终点,那就重复以下步骤
取出队列中队首元素,把它能到达的所有未被标记的点距离设为自己+1,打上“已到达”标记并塞入队列
到达终点的时候,看一下终点的距离应该是几
那么最短路径长度就是几
好了,算法结束
深搜和宽搜的底层区别
仔细思考一下,是什么造成了深搜和宽搜的区别?
注意到深搜中我们需要不断地往前搜索,然后回溯
有一个底层数据结构很适合干这件事
那就是栈
而宽搜中,我们使用的是队列
就是这点区别,造成了深搜和宽搜的巨大不同
不信你可以试试,在宽搜算法中把队列换成栈,得到的算法表现出来就是一个深搜
时空复杂度
我们在介绍宽搜的时候,提到过一个词“效率”
为什么我会说深搜解决那道题目效率太低?我们用什么来衡量一个算法的效率?
在算法界,我们用时空复杂度来定量分析效率
时空复杂度分为两个部分,时间复杂度和空间复杂度,它们俩都是一个关于输入数据的量级的函数,用O表示
简单的来说,时间复杂度表示一个算法需要什么量级的时间来解决问题,而空间复杂度表示一个算法需要什么量级的内存空间来解决问题。一般来说,计算时空复杂度的时候,我们会忽略低次项,忽略常数,只保留最高次项
以时间复杂度为例,比如说,一个n个元素的数列,我们从1到n输出一遍,时间复杂度就是O(n)
循环logn次,每次把数列全部输出一遍,时间复杂度就是O(nlogn)
注意在算法界,logn一般以2为底数
这些都比较好理解,相对可能比较难以理解的来了
冒泡排序,令i从1到n-1循环,每次循环令j从n到i+1,将a[j]和a[j-1]比较大小,小的排前面大的排后面
按理说它比较了$\frac{n(n-1)}{2}$次
按照忽略常数忽略低次项的原则,冒泡排序的时间复杂度是O(n2)
这个例子足以说明,时间复杂度并不是精确的给出基础运算的次数,只是给出基础运算次数的量级
但有时候,时间复杂度没有那么好计算
快速排序,在当前排序区间中每次随机选定一个基准数,这个区间里所有比它小的数全部放左边,大的全部放右边,然后再对左右两个区间递归做快速排序
看起来很难计算,对吧
这里,我们要引入两个概念,期望(平均)复杂度,最坏复杂度
这个快速排序的复杂度就不给你们计算了,直接上结论,它的期望复杂度是O(nlogn),最坏复杂度是O(n2)
空间复杂度的计算同理
这里要注意一下,空间复杂度一般来说是一定会小于时间复杂度的。仔细想想就知道,如果时间复杂度比空间复杂度还小,这个算法根本不可能有时间把那么多内存空间全部遍历一遍
啥是遍历?遍历就是对数据或者节点进行访问,统计,运算
知道了时空复杂度了,请你自行计算一下宽搜的例题中深搜和宽搜的时间复杂度分别是多少,并说明为什么我说深搜效率太低
下面,我来计算一下第一个例题的时间复杂度
考虑任意一条从起点到终点的路径,其长度为m+n
通过排列组合,我们知道路径数最多为$C_{m+n}^{n}$
因此,复杂度应该为$O((m+n)C_{n+m}^{n})$
复杂度当中放着一个组合数令人感觉很不爽,所以用斯特林公式$n!\approx \sqrt{2\pi n}(\frac{n}{e})^n$对组合数进行近似,得到复杂度变为$O((m+n)\sqrt{\frac{m+n}{mn}}(1+\frac{n}{m})^m(1+\frac{m}{n})^n)$
一般我们会令n,m同阶,也就是量级相近,这样我们得到复杂度变为$O(4^n\sqrt{n})$
带入n发现当n取14的时候复杂度就超过了109,而现在的计算机一般每秒计算107到108次,可以看到这个复杂度还是很大的
那么,我们有没有办法降低复杂度提高效率呢
还真有
记忆化搜索
仍然考虑最开始的那道题,我们应该能发现,对于任意一个格子[x,y],它到终点[m,n]的路径数量应该是一定的,和起点到[x,y]的路径数无关
而原始的深搜效率就低在这,它会对一个格子进行反复多次的无意义的重复搜索
那么,如果我们在每次对一个格子进行搜索过后,将这个格子到终点的路径数量存下来,之后再从起点搜索到这个格子就可以直接返回其数量,效率是不是就会得到大幅提高?
这就是记忆化搜索
我们再对其求时间复杂度。注意到每个格子只能从上面和右边两个方向到达,因此每个格子最多只会到达2次。格子总数为n*m,认定n,m同阶后,复杂度就是O(n2)
这个复杂度是不是就大幅降低了?
不过要注意的是,记忆化搜索确实能大幅降低时间复杂度,但是很可能会让空间复杂度大幅提高,如何取舍得视具体情况而定
动态规划
刚刚记忆化搜索中,我们存储中间结果是先存终点再存起点,或者说,从后往前
那可以从前往后吗?
当然可以
可以证明,这种做法和上面那种是等价的,不过算法界将其称为动态规划,英文简称DP
剪枝
有时候,在最优解搜索的过程中,如果发现当前路径无解或者显然不是最优的,那么就可以立即回溯(深搜)或者跳过这个节点(宽搜),这就是剪枝
比如说在网格图中,每个点都有一个权值,一条路径的长度是这条路径上所有点权值之和,那么一旦我们已经找到了这么条从起点到终点的路径其长度为A,其他所有权值已经大于A的路径就可以全部舍弃了
搜索算法的更一般形式
上面提到的例子全是网格图
但是现实中的问题显然不能都化归到网格图上
所以我们要讲讲搜索算法更一般的形式
事实上,搜索,就是将目标问题拆分为多个层,每层有若干节点,每个节点都有一个唯一的状态
这个状态随着问题的不同而不同,在上面的问题中就是坐标[x,y],而在下面这个问题中则是不一样的东西
给定一个数n,将其拆分为m个不同的整数a1,a2,...,am,使得a1<a2<...<am且a1+a2+...+am=n
运用深搜,一共m-1层,第i层各个节点表示第i个数取哪个
这样,第i层每个节点的状态就是[a1,a2,...,ai],其中aj(j=1,2,...,i)表示第j层取了哪个数
深搜每次前进,都从第i层前进到第i+1层,每个节点的决策都基于它的状态,直到无解或者得到解
对于搜索算法,其中一个难点就是找到每个节点正确而又最少的状态,这个难点在之后可能应该大概或许会讲的DP里也会出现
结尾
以上差不多就是搜索算法的基础内容了
为什么讲算法要从搜索开始讲?
因为可以说DFS、BFS是几乎所有算法的基础,在之后的算法学习中,你会不断地用到搜索
学会搜索,你才能进一步学会更多算法