搜索优化
主要有:剪枝、双向搜索、启发式搜索、迭代加深。
【剪枝】
一道水题。基本想法:枚举每根木棍属于哪条边,\(O(\frac{4^m}{4!})\)。
但是这太慢了,我们剪枝:如果一条边目前的累计长度 \(>\frac{sum}{4}\),就返回 false。
于是我们就通过了。
先可以暴力枚举每个男匹配哪个女。
然后我们考虑优化:
如果对于两个匹配:\((i,k),(j,l)\),记其贡献为 \(S_1\);\((i,l),(j,k)\),记其贡献为 \(S_2\)。
若 \(S_1>S_2\),搜索时我们就不必搜索第二种情况了,因为我们交换一下可以更优。
若 \(S_1=S_2\),搜索时我们只需要搜一种即可。
【双端队列广搜】
想法:
对于每一个方格,我们把它的两组对角顶点连边。若一组顶点本来就被电线连接,其边权为 0;否则边权为 1。
发现此题边权只有两种,我们引入一种新的广搜:双端队列广搜。
考虑我们原本广搜求最短路的条件:无边权。(一种边权)
这是因为我们需要保证当第一次搜索到一个点的时候,当前距离一定刚好是最短路。
而现在我们可以用双端队列,保证我们一定是先搜完 0 的点、再搜完 1 的点。
具体而言,当我们遇到一个 0 的点,把它从队头入队;否则从队尾入队。
这样我们的搜索过程就像先把 0 的点给 floodfill 了一样。
0 的点在我们搜到的时候一定是最短路,而 1 的点一定在我们搜完所有 0 的点才会到,因此我们搜到的点第一次一定是最短的。
【双向广搜】
如果我们暴力枚举每一个元素取或不取,\(2^{40}\) 不可接受。
但是,我们可以考虑把序列分成两半:先求出前 \(n/2\) 的所有子序列和,然后在搜索后一半的时候和前面搜出来的结果加起来,然后求得对应的答案。
具体而言,我们先 \(srh1()\) 求出前 \(n/2\) 个数的所有子序列和,存在一个 map 里。然后在我们 \(srh2()\) 每次搜到底的时候,我们通过 \(lower\_bound\) 找出 map 里面和本次搜索得出的 \(sum\) 加起来最接近 \(k\) 的元素,最后用这两个数的和与 \(k\) 的距离更新 \(ans\) 即可。
注(法二):
我们也可以把两次搜索的和们分别存进两个数组里面,然后用双指针扫一遍求最小距离。
这是一道经典而简单的问题,但是我们要用双向广搜来写。
我们可以考虑建立两个队列,一个代表从起点搜,一个代表从终点搜。另外我们也建立两个 \(dis\) 数组,一个是从起点出发的距离,一个是从终点出发的距离。
当我们搜索的时候,每次都同时搜出起点和终点的下一层。并且当我们在搜索起点的时候,如果遇到一个终点搜过的格子,我们就可以把这个格子到起点、终点的最短路长度加起来求出从起点到终点的最短路长度。
正确性是显然的。因为无论是起点还是终点,我们都在做正常的广搜,所以每一个格子被搜的时候其最短路长度已经确定,并且我们的层数是从小到大搜的。
因此,当我们起点碰到终点的时候,我们可以确定这个格子以后不会出现更短的路径。
注意处理 起点=终点 的特殊情况。
双向广搜为什么能加快速度?
我们做正常广搜的时候,相当于在起点画圆,不断扩大半径,直到终点被包括进来。
双向广搜时,我们相当于同时在起点和终点画圆,同时扩大半径,直到两个圆有交点。
因为我们从起点和终点一起搜,所以圆的半径减半,那么 面积就会变成 \(\frac{1}{4}\)。
虽然题目中可能并没有画圆一样整齐的扩张,但是双向搜索仍能减少复杂度。
【启发式搜索】
启发式:任何不影响正确性的优化,都可以叫做启发式。
【启发式剪枝】
最初想法:
枚举原始木棍长度,全排列枚举,参数里面记录当前木棍的剩余长度。
优化:
-
原始木棍长一定是总长的因数;
-
避免同组内的木棍又排列一遍重复,令选择时必须比组内上一根木棍短;
-
组间重复,令每个组的第一根木棍长度递减;
-
重复数,如果选了当前木棍不行,就不要选和当前木棍同长度的木棍了,这可以用 cnt 数组完成,每次搜索循环所有长度,如果还有该长度木棍就尝试,不行直接循环下一种长度;
-
如果当前剩余的长度可以刚好和一根木棍匹配,一定要用这个木棍,如果用了这个木棍后返回不能拼成,一定是之前的木棍选错了,这是因为如果不选这个木棍,一定需要用几根更小的拼出来,不如直接用这个木棍;
-
如果当前木棍当第一根时不行,一定是之前选错了。
加上这些优化,就比较快了。
【A*】
对于一个人,我们在枚举答案的时候一定会想要从那些 “看起来更可能是答案” 的方向枚举。
但是计算机并不理解我们的想法,于是我们现在要尝试让计算机也理解我们的想法。
以找棋盘最短路为例。
我们可以建立一个估值函数 \(f(x)\),表示从 \(st\) 经过位置 \(x\) 走到 \(ed\) 的估计最短路长度。当这个 \(f(x)\) 越小,就代表我们越可能通过位置 \(x\) 走到最短路。然后我们在广搜的时候使用优先队列,每次取出 \(f(x)\) 最小的更新。
我们令 \(f(x)=g(x)+h(x)\),其中 \(g(x)\) 是我们已经搜出来的从 \(st\) 到 \(x\) 的长度,\(h(x)\) 是我们估计从 \(x\) 走到 \(ed\) 的最短路长度。
比如,如果我们把 \(h(x)\) 定义为 “\(x\) 到 \(ed\) 的曼哈顿距离”,那么我们就会优先朝 \(ed\) 所在的方向搜索。
上面我们说明了启发式搜索可以让我们的搜索更快,下面我们看看如何保证正确性。
正确性: \(h(x)\leq\) 实际 \(x\rightarrow ed\) 的距离 \(w(x)\)。
实际上,启发式已经破坏了广搜的正确性。
因为我们广搜的正确性是因为:当我们搜索 \(k\) 长度的时候,已经搜完了 \(1\sim k-1\) 的路径,所以如果在 \(k\) 长度第一次碰到终点,\(k\) 一定是最短长度。
但是,我们启发式可能朝靠近 \(ed\) 的方向搜到了长度 \(10\) 时碰到了终点,但是在反向可能只搜到了长度 \(5\),我们如何保证另一个方向不会有其他方法在长度小于 \(10\) 时碰到终点?
假如我们能保证 \(h(x)\leq w(x)\),那么我们就有 \(g(x)+h(x)\leq g(x)+w(x)\),即 \(f(x)\leq g(x)+w(x)\)(经过 x 走到终点的最短路估值 \(\leq\) 实际上经过 x 走到终点的最短路长度)
而我们会优先朝靠近 \(ed\) 的方向搜,肯定是因为那边的 \(f()\) 更小,所以 \(f(ed)\leq f(x)\),即:
从靠近 \(ed\) 的方向走到 \(ed\) 的最短路长度估值 \(\leq\) 从不靠近 \(ed\) 的方向走到 \(ed\) 的最短路长度估值。
而上面又有 \(f(x)\leq w(st)\),所以 \(f(ed)\leq g(x)+w(x)\),即从靠近 \(ed\) 的方向去 \(ed\) 比从不靠近 \(ed\) 的方向走 更短。
注意 \(f(ed)=g(ed)+h(ed)\),因为 \(ed\) 就是终点,所以 \(h(ed)=0\),所以 \(f(ed)=g(ed)\) 就是 从靠近 \(ed\) 的方向去 \(ed\) 的最短距离。
而这个最短距离已经被证实比其他不走这个方向的最短距离 更短,所以这个距离就是真正的最短距离。
综上所述,只要满足 \(h(x)\leq\) 实际 \(x\rightarrow ed\) 的距离,我们第一次搜到终点得到的距离一定就是最短距离。
还要注意一种特殊情况:
? | 5 | ... |
---|---|---|
3 | 4 | 5 |
... | ... | ... |
如图,我们现在搜到了 "3" 的格子,那么 “?” 的最短距离应当是 4,但是有可能因为 ? 的 \(h()\) 比其他方向更差,所以在加入优先队列后被其他几个邻格给挤到后面去了,没有第一时间搜到。而我们已经走其他方向把 ? 的距离更新为 6 了。而此时 ? 的最短距离就错误了。
但是我们不需要担心我们会用这个错误的距离更新终点导致答案错误。
-
如果 ? 就是终点,则 \(h(?)=0\),而我们上面没有第一时间取出 ?的原因是因为 \(h(?)\) 更差,矛盾;
-
如果 ? 用来更新终点,那么对于我们在 3 更新时加入的 ? 状态,它的 \(f(?)=g(?)+h(?)=4+h(?)\),而如果我们用了 \(f(?)=6+h(?)\) 的状态去更新终点,它比 \(4+h(?)\) 还要大,那么我们肯定在这之前就已经用了 \(4+h(?)\) 的状态更新过一遍了。
我们尝试推广第二种情况:
假如我们有两条去终点的路径,但是用了更长的那一条去更新了终点。
在更短的那一条路上取一个没有在另一条路径上的点 \(x\)。显然更长的那条路径长度 \(a\geq\) 实际最短路长度。
而 \(f(x)=g(x)+h(x)\leq\) 实际上的最短路长度。(因为 \(h(x)\leq\) 实际)
所以 \(f(x)\leq\) 最短路长度 \(\leq a\),那我们应该在更早的时候就已经用 \(x\) 更新了终点了。
这题也可以用 A* 做。我们定义 \(h(x)\) 为 \(x\) 和 \(ed\) 的曼哈顿距离,满足 \(h(x)\leq\) 实际 \(x\rightarrow ed\) 的距离。
【迭代加深】
众所周知,深搜的特点是一直往底走,走到底了再回头。
但是,如果我们遇到了一道没有底的题,也就是结束条件模糊的题,我们应该怎么用深搜做?
这个时候,我们就可以用迭代加深搜索。
我们可以人为限定一个最大深度,如果深搜的深度大于它,我们就认为在这个深度下,我们这么走是没有答案的,然后回溯。
每次将限定的深度增大一点,直到在限定深度内搜到答案。因为搜索树大小是指数级增长,所以之前搜索的时间可以当做一个较大的常数来看。
这题的特点在于我们不知道一共需要几个分数加起来,导致我们可能会搞出一大堆分母特别大的分数,然后无限往下搜。
于是我们循环需要的分数个数搜索。