c++深度优先搜索

1,剪枝优化

众所周知,搜索的算法时间复杂度大多是指数级的。即使是简单的不加优化的搜索,其时间效率往往也低得让人无法忍受,难以满足信息学竞赛对程序运行时间的严格限制的要求。

对深搜程序进行优化的一种基本方法就是——剪枝。

我们可以把搜索的过程看作是从树根出发,遍历一棵倒置的树(搜索树)的过程,其中每个结点就是一次递归调用。所谓剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象地说,就是剪去搜索树上的某些“枝条”,故称剪枝。

如下图是求Fibonacci 数列的递归搜索树,有很多不必要的重复遍历:

我们通过记忆化搜索的优化方式,即可对重复的部分进行剪枝:

剪枝的原则

1.正确性

必须保证不丢失正确的结果,这是剪枝优化的前提。

2.准确性

尽可能多地剪去不能通向正解的枝条。剪枝方法只有在具有了较高的准确性的时候,才能真正收到优化的效果。

3.高效性

为了加强优化的效果,必须提高剪枝判断的准确性,因此,常常不得不提高判断操作的复杂度,也就同时降低了剪枝判断的时间效率;但是,如果剪枝判断的时间消耗过多,就有可能降低、甚至完全抵消提高判断准确性所能带来的优化效果,这恐怕会得不偿失,过于复杂的判断操作无疑对效率提高会带来一定的副作用。

因此,能否较好地解决这个矛盾,往往成为搜索算法优化的关键。

综上所述,我们把剪枝优化的主要原则归结为六个字:正确、准确、高效

当然,我们在应用剪枝优化的时候,仅有上述原则是不够的,还需要具体研究一些设计剪枝判断方法的思路。

深度优先搜索的优化技巧

在一些搜索问题中,搜索树的各个层次、各个分支之间的顺序是不固定的。不同的搜索顺序会产生不同的搜索树形态,其规模大小也相差甚远。

在搜索过程中,如果我们能够判定从搜索树的当前结点上沿着某几条不同分支到达的子树是等效的,那么只需要对其中的一条分支执行搜索。

在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。这就好比我们在道路上行走时,远远看到前方是一个死胡同,就应该立即折返。

某些题目条件的范围限制是一个区间,此时可行性剪枝也被称为“上下界剪枝”。

在最优化问题的搜索过程中,如果当前花费的代价已经超过当前搜索到的最优解,那么后面也不可能得到更优解,此时可以停止对当前分支的搜索,执行回溯。

可以记录每个状态的搜索结果,在重复遍历一个状态时,直接检索并返回。就像我们对图进行深度优先遍历时,标记一个结点是否已经被访问过一样。

搜索过程中的剪枝,其实就是针对每个“维度”与该维度的边界条件,加以缩放、推导,得出一个相应的不等式,以减少搜索树分支的扩张。

为了进一步提高剪枝的效果,除了当前花费的“代价”之外,我们还可以对未来至少需要花费的代价进行预算,这样更容易接近每个维度的上下界。此外,在一般的剪枝不足以解决问题时,还可以结合各维度之间的联系得到更加精准的剪枝。

有时候,选择适当的搜索对象,在减少搜索量的同时,充分利用题目的约束条件构造一个有效的剪枝,可以使问题得到很好的解决。

  1. 优化搜索顺序
  2. 排除等效冗余
  3. 可行性剪枝
  4. 最优性剪枝
  5. 记忆化

迭代加深

为了形象地解释迭代加深搜索(IDDFS,Iterative Deepening Depth-first Search。)

从图中可以看出,从起点开始搜索。使用 DFS,搜到的第一个解便是3X3​。而使用 BFS,搜到的第一个解便是2X2​。但目前的最优解是1X1​。尽管两种方法最终都可以搜到解1X1​,但却无法保证最优的时间复杂度。

迭代加深搜索(IDDFS)就这样诞生了。

本质上它其实是给 DFS 加了一个限制。我们都知道 DFS 有层数这个概念。IDDFS 便规定了一个层数,让 DFS 只能在这一个规定的层数里进行。

比如我们规定 DFS 只能在上图的深度为 1 的地方进行,很明显它便可以轻松地搜索的了最优解。 于是便可以得出 IDDFS 基本的框架:

void iddfs(层数参数) {
	if (层数 > 规定最大层数) return ;
	进行搜索......
}

int main() {
	for (从 1 开始逐步增大最大层数限制) {
		iddfs(1);
	}
}

 信息学竞赛中搜索问题的常见优化技巧

摘要

结合例题分析归纳了信息学竞赛中解决搜索问题所常用的思考方法与解题方法,从深度优先搜索和广度优先搜索两个方面探讨了提高程序效率的适用技巧。

【关键词】

1信息学;2搜索顺序;3搜索对象;4Hash表 5剪枝。

在信息学竞赛中解决搜索问题通常采用两种方法进行,即:深度优先搜索和广度优先搜索。

深度优先搜索的优化技巧

我们在做题的时候,经常遇到这类题目——给出约束条件,求一种满足约束条件的方案,这类问题我们叫它“约束满足”问题。对于约束满足问题,我们通常可以从搜索的顺序和搜索的对象入手,进而提高程序的效率。

搜索的顺序及对象

在解决约束满足问题的时候,题目给出的约束条件越强,对于搜索中的剪枝就越有利。之所以深度优先搜索的效率在很大程度上优于穷举,就是因为它在搜索过程中很好的利用了题目中的约束条件进行剪枝,达到提高程序效率的目的。

显然,在同样的一棵搜索树中,越在接近根接点的位置利用约束条件剪枝效果就越好。如何在搜索中最大化的利用题目的约束条件为我们提供剪枝的依据,是提高深度优先搜索效率的一个很重要的地方。而不同的搜索顺序和搜索对象就直接影响到我们对于题目约束条件的运用。

下面,我们就从搜索的顺序和搜索的对象两方面来探讨一下不同的方法对程序效率的影响。

1. 搜索顺序的选择

我们先来看一道比较简单的题目:

题目描述

原题来自:ZOJ 1937 Addition Chains

已知一个数列0,1...a0​,a1​...am​(其中a0​=1,am​=n,a0​<a1​<a2​<...<am−1​<am​)。对于每个k,需要满足ak​=ai​+aj​(0≤i,j≤k−1,这里i 与j 可以相等)。

现给定n 的值,要求m 的最小值(并不要求输出),及这个数列每一项的值(可能存在多个数列,只输出任一个满足条件的就可以了)。

输入格式

多组数据,每行给定一个正整数 n 。

输入以 0 结束。

输出格式

对于每组数据,输出满足条件的长度最小的数列。

样例

输入数据 1

5
7
12
15
77
0

输出数据 1

1 2 4 5
1 2 4 6 7
1 2 4 8 12
1 2 4 5 10 15
1 2 4 8 9 17 34 68 77

数据范围与提示

1≤n≤100,1≤k≤m

分析

由于ak​=ai​+aj​(0≤i,j<k),所以我们在搜索的过程中可以采用由小到大搜索数列的每一项的搜索顺序进行试算。在一般搜索的时候我们习惯于从小到大依次搜索每个数的取值,但是在这到题目中按照这样的顺序搜索编程运算其结果(效率)十分不理想:

由于题目要求的是m 的最小值,也就是需要我们尽快得到数n,所以每次构造的数应当是尽可能大的数,根据题目的这个特性,我们将搜索顺序改为从大到小搜索每个数,新程序的效率如下:

显然,后一种搜索顺序得到的程序效率大大地优于第一种搜索顺序得到的程序。

 

当然,这道题还有很大的优化余地,但是搜索顺序这种思想在搜索的题目中是广泛运用的。也许大家会觉得这种单一的运用搜索顺序来优化程序的方法很普通,但是这种看似简单的方法在考试中出现得也不少,例如IOI2000 中的BLOCK,只要将木块从大到小经过旋转和反转后,依次放入进行搜索,对于比赛中的数据就可以得到满分。最近的一次出现是 NOI2005 中的智慧珠,同样的只是将珠子从大到小进行搜索,不加任何其他剪枝就可以在比赛中获得 90 分。

可见,选择合适的搜索顺序对于提高程序的效率是编程设计最有效的技巧之一,运用良好的搜索顺序来对搜索题目进行优化是一个性价比很高的算法。

2. 搜索对象的选择

让我们再来看看下面一道题:

题目描述

原题来自:USACO

已知原数列 1,2,⋯ ,a1​,a2​,⋯,an​ 中的前 11 项,前 22 项,前 33 项, ⋯⋯ ,前 n 项的和,以及后 11 项,后 22 项,后 33 项, ⋯⋯ ,后 n 项的和,但是所有的数都被打乱了顺序。此外,我们还知道数列中的数存在于集合 S 中。试求原数列。当存在多组可能的数列时,求字典序最小的数列。

输入格式

第 11 行,一个整数n 。
第 22 行, 2×n 个整数,注意:数据已被打乱。
第 33 行,一个整数m ,表示 S 集合的大小。
第 44 行, m 个整数,表示 S 集合中的元素。

输出格式

输出满足条件的最小数列。

样例

  输入数据 2

5
1 2 5 7 7 9 12 13 14 14
4
1 2 4 5

输出数据 2

1 1 5 2 5

数据范围与提示

数据范围

对于 100% 的数据, 1≤m≤500 ,且 S∈{1,2,⋯,500} 。

样例解释

从左往右求和从右往左求和
1=101=1+1+5+2+55=505=1+1+5+2+5
2=1+102=1+1+5+2+57=2+507=1+1+5+2+5
7=1+1+507=1+1+5+2+512=5+2+512=1+1+5+2+5
9=1+1+5+209=1+1+5+2+513=1+5+2+513=1+1+5+2+5
14=1+1+5+2+514=1+1+5+2+5

分析

因为题目中的 S∈{1…500},最坏的情况下每个数可以取到的值有 500 种,从数学方面很难找到有较好方法予以解决,而采用搜索方法却是一种很好的解决办法,根据数列从左往右依次搜索原数列每个数可能的值,然后与所知道的值进行比较。这样,我们得到了一个最简单的搜索方法A

但是搜索方法A的这个算法最坏的情况下扩展的节点为 500 ^ 1000,运算速度太慢了。

在这个算法中,我们对数列中的每个数分别进行了 500 次搜索,由此导致了搜索量如此之大。如何有效的减少搜索量是提高本题算法效率的关键。而前面提到的运用搜索顺序的方法在本题中由于规定了左边的数最小而无法运用。

让我们换个角度对这个问题进行思考:

搜索方法B:回过头来看看题目提供给我们的约束条件,我们用Si​ 表示前 i 项的和,用 Ti​ 表示后 i 项的和。

根据题目,我们得到的数据应该是数列中的 S1​,S2​,S3​…Sn​,以及T1​,T2​,T3​…Tn​。其中的任意 Si+1​−Si​ 和 Ti+1​−Ti​ 都属于集合S。

另一个比较容易发现的约束条件是对于任意的 i,有 Sn​=Tn​=Si​+Tn−i​。同样的,在搜索的过程中最大化这些约束条件是提高程序效率的关键。

那么当我们任意从已知的数据中取出两个数的时候,只会出现两种情况:

(1)、两个数同属于前缀或后缀

(2)、两数分别属于前缀和后缀

当两数同属于前缀或后缀时,当j=i+1 时,Sj​−Si​ 必然属于题目给出的集合S。由此,当每次得到一个数Si​ 或者Ti​ 时,如果我们已知 Si−1​ 或者Ti−1​,便能够判断出此时的 Si​ 或者 Ti​ 是否合法。所以我们在搜索中尽可能利用 Si−1​ 和 Ti−1​ 推得Si​ 和 Ti​ 的可能,便能尽可能利用题目的约束条件。

因为题目的约束条件集中在 Si​ 和 Ti​ 中,我们改变搜索的对象,不再搜索原数列中每个数的值,而是搜索给出的数中出现在Si​ 或者Ti​ 中的位置。又由于约束条件中得出的 Si+1​ 与Si​ 的约束关系,提示我们在搜索中按照Si​ 中 i 递增或者递减的顺序进行搜索。

例如,对于数据组:1 1 5 2 5,由它得到的值为

1 2 7 9 14 5 7 12 13 14

排序后为:

1 2 5 7 7 9 12 13 14 14

由于最大的两个数为所有数的和,在搜索中不用考虑它们,去掉 14

1 2 5 7 7 9 12 13

观察发现,数列中的最小数 1,只可能出现在所求数列的头部或者尾部。再假设 1 的位置已经得到了,去掉它以后,我们再观察剩下的数中最小的数 2,显然也只可能在当前状态的头部或者尾部加上一个数得到 2。这样,每搜索一个数,都只会将它放在头部和尾部,也就是放入Si​ 中或者 Ti​ 中。

推而广之,我们由小到大对排序的数进行搜索,判断每个数是出现在原数列头部还是尾部。此时我们由原数列的两头向中间搜索,而不是先前的从一头搜向另一头。由之前的分析已经知道,每个数只可能属于 Si​ 和 Ti​ 中。当我们已经搜索出原数列的 S1​,S2​…Si​ 和 T1​,T2​…Tj​,此时对于正在搜索的数 K,只可能有两种存在的可能:Si+1​ 或 Tj+1​,分别依次搜索这两个可能,即判断 K−Si​ 和 K−Tj​ 是否属于已知集合S。并且在每搜索出一个数 K 的时候,我们将排序后的数列中 Sn​−K 去掉。这样,当K−Si​(Ti​) 不属于集合S 或者Sn​−K 不在排序后的数列中时,就回溯。

这样得到的算法在最坏情况下扩展的节点为 2^1000(实际中远远小于这个数),并且由于在搜索过程中充分利用了题目约束条件。

在这道题目中,原始的搜索方法搜索量巨大,我们通过分析,选择适当的搜索对象,在搜索量减少的同时充分利用了题目的约束条件,成为了程序的一个有利的剪枝,使题目得到较好的解决。

广度优先搜索的优化技巧

相对于深度优先搜索的另外一类题目——给出起始和目标状态,以及状态转移的规则,要求找到一条到达目标状态的的路径或者方法。这类问题我们叫它路径寻找问题(例如走迷宫问题)。解决这类问题最有效的手段是选取合适的构造Hash表的方法。

Hash表的一般构造方法有:

  • 状态压缩-------运用2进制来记录状态。

  • 直接取余法-----选取一个素数M作为除数。

  • 平方取中法-----计算关键值平方,再取中间r位形成一个大小为2r的表。

  • 折叠法---------把所有字符的ASCII码加起来。

路径寻找问题中,经常会遇到走回头路的问题,所以在搜索的过程中都必须做一件事,就是判重。判重是决定程序效率的关键,而如何构造一个优秀的Hash表决定着这一切。一个好的Hash函数可以很大程度上提高程序的整体时间效率和空间效率。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值