《算法笔记》——第八章 搜索 学习记录

DFS

使用递归可以很好地实现深度优先搜索。这个说法并不是说深度优先搜索就是递归,只能说递归是深度优先搜索的一种实现方式,因为使用非递归也是可以实现DFS的思想的,但是一般情况下会比递归麻烦。不过,使用递归时,系统会调用一个叫系统栈的东西来存放递归中每一层的状态,因此使用递归来实现DFS的本质其实还是栈。

接下来讲解一个例子,读者需要从中理解其中包含的DFS思想,并尝试学习写出本例的代码。

有n件物品,每件物品的重量为w[i],价值为c[i]。现在需要选出若干件物品放入一个容量为V的背包中,使得在选入背包的物品重量和不超过容量V的前提下,让背包中物品的价值之和最大,求最大价值。(1≤n≤20)

在这个问题中,需要从n件物品中选择若干件物品放入背包,使它们的价值之和最大。这样的话,对每件物品都有选或者不选两种选择,而这就是所谓的“岔道口”。

那么什么是“死胡同”呢?——题目要求选择的物品重量总和不能超过V,因此一旦选择的物品重量总和超过V,就会到达“死胡同”,需要返回最近的“岔道口”。

显然,每次都要对物品进行选择,因此DFS函数的参数中必须记录当前处理的物品编号index。

而题目中涉及了物品的重量与价值,因此也需要参数来记录在处理当前物品之前,已选物品的总重量sumW与总价值sumC。

于是,如果选择不放入index号物品,那么sumW与sumC就将不变,接下来处理index+1号物品;而如果选择放入index号物品,那么sumW将增加当前物品的重量w[index],sumC将增加当前物品的价值c[index],接着处理index+1号物品。

一旦index增长到了n,则说明已经把n件物品处理完毕(因为物品下标为从0到n-1),此时记录的sumW和sumC就是所选物品的总重量和总价值。如果sumW不超过V且sumC大于一个全局的记录最大总价值的变量maxValue,就说明当前的这种选择方案可以得到更大的价值,于是用sumC更新maxValue。

可以注意到,由于每件物品有两种选择,因此上面代码的复杂度为\(O(2^n)\),这看起来不是很优秀。但是可以通过对算法的优化,来使其在随机数据的表现上有更好的效率。在上述代码中,总是把n件物品的选择全部确定之后才去更新最大价值,但是事实上忽视了背包容量不超过V这个特点。也就是说,完全可以把对sumW的判断加入“岔道口”中,只有当sumW≤V时才进入岔道,这样效率会高很多。

这种通过题目条件的限制来节省DFS计算量的方法称作剪枝(前提是剪枝后算法仍然正确)。剪枝是一门艺术,学会灵活运用题目中给出的条件,可以使得代码的计算量大大降低,很多题目甚至可以使时间复杂度下降好几个等级。

事实上,上面的这个问题给出了一类常见DFS问题的解决方法,即给定一个序列,枚举这个序列的所有子序列(可以不连续)。例如对序列{1,2,3}来说,它的所有子序列为{1}、{2}、{3}、{1,2}、{1,3}、{2,3}、{1,2,3}。

枚举所有子序列的目的很明显——可以从中选择一个“最优”子序列,使它的某个特征是所有子序列中最优的;如果有需要,还可以把这个最优子序列保存下来。显然,这个问题也等价于枚举从N个整数中选择K个数的所有方案。

例如这样一个问题:给定N个整数(可能有负数),从中选择K个数,使得这K个数之和恰好等于一个给定的整数X;如果有多种方案,选择它们中元素平方和最大的一个。数据保证这样的方案唯一。例如,从4个整数{2,3,3, 4}中选择2个数,使它们的和为6,显然有两种方案{2,4}与{3,3},其中平方和最大的方案为{2, 4}。

与之前的问题类似,此处仍然需要记录当前处理的整数编号index;由于要求恰好选择K个数,因此需要一个参数nowK来记录当前已经选择的数的个数;另外,还需要参数sum和sumSqu分别记录当前已选整数之和与平方和。

此处主要讲解如何保存最优方案,即平方和最大的方案。首先,需要一个数组temp,用以存放当前已经选择的整数。这样,当试图进入“选index号数”这条分支时,就把A[index]加入temp中;而当这条分支结束时,就把它从temp中去除,使它不会影响“不选index号数”这条分支。

接着,如果在某个时候发现当前已经选择了K个数,且这K个数之和恰好为x时,就去判断平方和是否比已有的最大平方和maxSumSqu还要大:如果确实更大,那么说明找到了更优的方案,把temp赋给用以存放最优方案的数组ans。这样,当所有方案都枚举完毕后,ans存放的就是最优方案,maxSumSqu存放的就是对应的最优值。

上面这个问题中的每个数都只能选择一次,现在稍微修改题目:假设N个整数中的每一个都可以被选择多次,那么选择K个数,使得K个数之和恰好为X。例如有三个整数1、4、7,需要从中选择5个数,使得这5个数之和为17。显然,只需要选择3个1和2个7,即可得到17。

这个问题只需要对上面的代码进行少量的修改即可。由于每个整数都可以被选择多次,因此当选择了index号数时,不应当直接进入index+1号数的处理。

显然,应当能够继续选择index号数,直到某个时刻决定不再选择index号数,就会通过“不选index号数”这条分支进入index+1号数的处理。

BFS

前面介绍了深度优先搜索,可知DFS是以深度作为第一关键词的,即当碰到岔道口时总是先选择其中的一条岔路前进,而不管其他岔路,直到碰到死胡同时才返回岔道口并选择其他岔路。接下来将介绍的广度优先搜索(Breadth First Search,BFS)则是以广度为第一关键词,当碰到岔道口时,总是先依次访问从该岔道口能直接到达的所有结点,然后再按这些结点被访问的顺序去依次访问它们能直接到达的所有结点,以此类推,直到所有结点都被访问为止。这就跟平静的水面中投入一颗小石子一样,水花总是以石子落水处为中心,并以同心圆的方式向外扩散至整个水面,从这点来看和DFS那种沿着一条线前进的思路是完全不同的。

最后指出,当使用STL的queue时,元素入队的push操作只是制造了该元素的一个副本入队,因此在入队后对原元素的修改不会影响队列中的副本,而对队列中副本的修改也不会改变原元素,需要注意由此可能引入的bug。

这就是说,当需要对队列中的元素进行修改而不仅仅是访问时,队列中存放的元素最好不要是元素本身,而是它们的编号(如果是数组的话则是下标)。

posted @ 2021-02-22 21:22  Dazzling!  阅读(81)  评论(0编辑  收藏  举报