搜索优化。

一、剪枝

1.可行性剪枝

若当前情况已经不符合题意则可以直接返回,而不需要走到头。
例如,若搜索的第二个数已经不满足和第一个数匹配,那就没有必要继续搜索第三个数了。

2.最优性剪枝

若当前情况已经劣于已经找到的最优解时,直接返回。
例如,若当前搜到的路径已经比 ans 大了,就不用继续搜了。

3.搜索顺序剪枝

虽然从某种意义上说,这仅仅是“骗过了数据”。
但是不可否认的是大多情况下,改变搜索顺序可以优化搜索效率。

4.Alpha-Beta

为一种固定化的剪枝手段。
可自行了解。

5.人类智慧

以上剪枝策略只是形式化的剪枝策略。在具体实现中还需要利用人类智慧来进行剪枝:

• 极端法:考虑极端情况,如果最极端的情况都不满足,那一定不满足。
• 调整法:通过对子树的对比剪掉重复子树和明显不是最优的子树。
• 数学方法:比如在图论中借助连通分量,数论中借助模方程的分析,借助不等式的放缩来估计下界等等。

在实际应用中,往往需要选手自行根据题目发扬剪枝艺术。并没有一个固定的写法。

二、搜索状态的设计

虽然搜索是遍历所有的解空间。但对于不同的状态设计,会呈现出不同的解空间。
同时,在实际应用中,根据题目需要,还可以去掉一些无用的状态(剪枝)。
不同的状态定义所形成的搜索树也不尽相同。
通过巧妙的状态定义,也可更好地解决问题。

同时,虽然搜索是遍历所有解空间,但是时间复杂度并不是解空间大小。也不是状态数大小。因为在的搜索过程中,访问到搜索树上同一个节点时,会继续往下考虑,将搜过的内容再搜一遍。
仍以例 1 为例。看这样一组数据:
2 3 3 2 1 4
对于 (3, 5) 这样的状态,能被多少个状态转移而来:
(2, 2),(2, 3),(2, 1),(2, 4)。

而 (3, 5) 接下去的状态,(2, 2),(2, 3),(2, 1),(2, 4) 都会再考虑一遍。

三、记忆化搜索

记忆化搜索是一种通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。
与其说是记忆化搜索,笔者更愿称之为记忆化递归。通过递归将一个大的问题变成一个个小问题,然后拼接起来。
例 3
给定 10 个数,选出 3 个数的和小于 1000 且最大。
容易发现仅仅是三个数要满足的限制变了,所以按例 1 的DFS 写法把 check 函数变一变即可。但是这样无法记忆化的。因为它每一个状态维护的是到起点的结果,而不是到终点。既然是到终点,那么 sum 就不能直接维护,而是要通过函数递归返回值求得:
 

int dfs(int x)
{
	if (x > 3) return 0;
	int res = 0;
	for (int i = 1; i <= n; i++)
	{
		if (!vis[i])
		{
			vis[i] = 1;
			if (dfs(x + 1) + a[i] < 1000)
				res = max(res, dfs(x + 1) + a[i]);
			vis[i] = 0;
		}
	}
	return res;
}

        记忆化搜索因为使用以往搜到是结果,那么就要保证现在到这一步时确确实实能按以往的结果做选择。如果到这一步之前的选择的不同影响了后续的可选择性,那自然是不能用之前的结果的,也就是满足“子问题”。
        所以上述代码中仅仅用 (x) 描述状态并不是一个子问题。所以需要对状态设计进行修改,使得一个状态描述确确实实是一个子问题。
        状态可以修改为 (i, x) 最后选的一个数是第 i 个数,已经选了 x 个数。因为限制死了后续只能选择 i + 1, i + 2, ...n 中的数,那么就保证了遇到 (i, x) 这个状态时,后续的可选择性是一致的。

        因为只需要在一般搜索的基础上进行记录状态结果的行为,所以记忆化搜索算是一种优化思想。体现在代码上也并不复杂。
核心变为如何存储状态信息:
        一般而言,若状态信息并不复杂(主要是数值不大)那么便可以直接用数组进行存储,将状态信息记录为数组下标。

int dfs(int _i,int x)
{
	if (_i > n) return;
	if (x > 3) return 0;
	if (vis[_i][x]) return mem[_i][x];
	int res = 0;
	for (int i = _i + 1; i <= n; i++)
		if (dfs(i + 1, x + 1) + a[i] < 1000)
			res = max(res, dfs(i + 1, x + 1) + a[i]);
	vis[_i][x] = 1;//标记已使用
	mem[_i][x] = res;//储存结果
}

BFS 因为不搜到底,并不能利用已经到过的状态(就算状态是到过的,但是没有它到终点的结果)。所以 BFS 不存在记忆化搜索的写法。
事实上,问题的 sum 一般都会比较大,不能作为数组下标存储。但是我们可以使用 map 将状态和状态的结果视作一个映射,存起来。
因为记忆化搜索确保了每个状态只访问一次,它也是一种常见的动态规划实现方式

为什么要用例 3 记忆化而不是例 1?
        因为例 1 根本无法简单地记忆化,因为例 1 的后续的选择是和前面所有选的具体数字是有关的,而不是仅通过和就可以判断是否能选的(例 3 中的 check 只关心和的大小,不关心具体选
的数;例 1 中的 check 需要关心所有选择的数)。同一个状态描述对应的不同实际状态的后续操作不尽相同。比如例 1 中,选择2, 3 或 1, 4 都对应 (3, 5) 的状态描述,但在选择第三个数时,他
们的选择是不一样的。所以例 1 中的状态不足以支撑记忆化的过程。

        但是观察会发现,其实不是状态不行,而是状态描述不行。选择的数在例 1 中是要考虑到状态描述中去的,那么在状态描述中加上选择的数即可。但是这样产生的问题就是状态不易存储。我们需要更巧妙的状态描述。本文不再赘述。        

四、搜索方式

1.双向搜索

1.1 同时双向搜索

        同时从起点和终点开始搜索。往往采用 BFS 实现。
        把起点和终点都加入初始队列。
        在扩展过程中,把由起点扩展的一系列点标记为 1,把由终点扩展的一系列点标记为 2。当 1 能扩展到 2,或 2 能扩展到 1时,表示找到解。
        由于起点和终点交题前进,所以对于起点和终点,各自的深度只有原规模的一半,那么状态规模的增长也只有原来的一半。

1.2 meet in middle

        将原问题划分成两部分分别处理,后将两部分的结果合并。meet in middle 往往用于处理序列问题,把序列划分成左右两部分。

例 4:给定 40 个数,求选出若干个数的和不超过 109 的方案数。

        直接对 40 个数搜索所有解的时间复杂度为:O(2^{40})。
        若将序列划分成两个长度为 20 的序列,首先单独处理的时间复杂度为:O(2^{20})。

        之后,对于左半部分的某一个解,在右半部分中找到之和不超过 109 的方案数。即为在若干个数中求不超过某值的数的个数,可以使用二分解决(upper_bound)。

        总时间复杂度:O(20 × 2^{20})。

        一般而言,决定是否能双向搜索的均为两部分信息是否能合并。

        若原问题状态规模为 O(a^{k}),双向搜索状态规模降为 O(a^{\frac{k}{2}})

2.特殊DFS

2.1 迭代加深搜索

        每次限制递归层数的 DFS。
        每进行一次 DFS,递归深度限制增大 1。
        避免了 DFS 的递归层数过多;也避免了 BFS 在同一时刻状态数过多。
        但存在重复搜索前缀的缺点,不过由于搜索树的规模增长一般为指数,深度增大一层的数据规模可能会超过前面所有层的数据规模之和。所以影响并不是那么大。

2.2 IDA*

        A* 和迭代加深的结合。

3.特殊BFS

3.1  01 BFS

        在前文中,提到 BFS 主要用于解决边权为 1 的最短路径问题。但是注意到 BFS 过程中队列内是有两个距离值的,队尾比队首大 1,边权为 1 时每次扩展加在队尾。如果边权还存在 0 的话,则加在队首。其它部分与 BFS 完全相同。

3.2 优先队列 BFS

        在前文中,提到 BFS 如果处理边权不为 1 的问题,那便没有第一次出队即为最小值的性质了。究其本质,是因为队列内元素不呈现单调性了。但是可以“强行”让队列内元素呈现单调性。即:采用优先队列替换队列实现。
        但若是边权存在负值,那仍然不满足第一次出队时为最小值(不能使用 vis 数组)
        原因显然。

3.3 A*

        A* 是基于 BFS 的改进。
        定义从 x 到起点的距离为 g(x),从 x 到终点的距离为 h(x), x 的估价函数为 f (x)。初始起点入队,每次从队列中取出估价函数最小的一个元素。h(x) 称为启发函数,因为无法直接获取从 x
到终点的距离,所以对 h(x) 采用的往往是一个估价量,要求不超过真实距离,且越接近真实距离,越优。

3.4 Dancing Links

        解决精确覆盖问题和重复覆盖问题的高效搜索算法。
        采用十字链表加上优秀的剪枝实现。
        略。

五、启发式搜索

1.贪婪最佳优先搜索

        h(x) = 0 的 A*,也就是优先队列 BFS,也可以说是Dijkstra。

2.模拟灭火

        它是一种基于概率的搜索算法,模拟固体物质的退火过程。它以一定的概率接受比当前解更差的解,以避免陷入局部最优。它的估价函数是 f (n) = e− TE ,其中 E 表示新解和旧解的能量差,T 表示温度参数。它的优点是能够跳出局部最优,缺点是参数的选择比较难,收敛速度慢。
        算法竞赛中存在部分模拟退火的应用(相较其它启发式搜索而言),本身为一个算法框架,了解即可。

3.遗传算法

        略

4.蚁群算法

5.粒子群优化算法

六、番外

1.对抗搜索

        这里的对抗搜索是指双人进行对抗博弈。
博弈原理:
• 存在先手必胜策略,当且仅当存在一种策略使得不存在后手必胜策略。
• 存在后手必胜策略,当且仅当无论先手怎么操作,都有后手必胜策略。
        一般状态设计为 dfs(x, ...y) x = 1/0 表示先手/后手;
y = 1/0 表示必胜/必败;返回值为 1/0 表示是否存在。参数其它部分用于描述局面信息。
• dfs(x, ..., 1) =dfs(x ⊕ 1, ...0) 与之和。
• dfs(x, ..., 0) =dfs(x ⊕ 1, ...1) 或之和。

注意:

因为搜索其本身时间复杂度上界很高,以及剪枝没有严格时间复杂度证明,所以近年来的算法竞赛中搜索相关问题的出现次数大大减少,除非题目本身明确上界也能通过,否则不会是搜索问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值