折半搜索学习笔记

灰鹤在此

2021-09-06 16:31:44

Personal

折半搜索

1.前言

在计算机科学中,折半搜索,通常称之为 meet-in-the-middle,也称为二分搜索、对数搜索,是一种在有序数组中查找某一特定元素的搜索算法。其做法为将整个搜索的过程分为两部分,然后每部分分别进行搜索,最后将得到两个答案序列,再将答案序列进行合并,即可得到最终的答案。

折半搜索是效率较高的一种查找方法。假设有已经按照从小到大的顺序排列好的五个整数 a_0\sim a_4,要查找的数是 x,其基本思想是:设查找数据的范围下限为 l=0,上限为 r=4,求中点 mid=(l+r)/2,用 x 与中点元素 a_{mid} 比较,若 x 等于 a_{mid},即找到,停止查找;否则,若 x 大于 a_{mid},替换下限 l=mid+1,到下半段继续查找;若 x 小于 a_{mid},换上限 r=mid-1,到上半段继续查找;如此重复前面的过程直到找到或者 l>r 为止。如果 l>r,说明没有此数,输出找不到信息,程序结束。

该方法是查找的范围不断缩小一半,所以查找效率较高。

折半搜索的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。

我们知道,搜索的时间复杂度往往是指数级别的。

比如,在每一层搜索时,假如都有两种选择,那么其时间复杂度为 \mathcal{O}\left(2^n\right)。当 \large n 较大时,往往会导致超时。此时,如果使用折半搜索,其时间复杂度将缩小为 \mathcal{O}\left(2^{\frac{n}{2}}+\text{合并操作的时间复杂度}\right)

2.算法讲解

例题1

P4799 [CEOI2015 Day2]世界冰球锦标赛

题意描述

Bobek 有 m 元钱,总共有 n 场比赛,每场比赛的门票价格为 \large a_i

问 Bobek 有多少种买票方案。

限制条件:1\le n\le401\le m\le10^{18}\large a_i\le10^{16} 且:若方案 1 中观看了某场比赛,方案 2 中未观看该场,则认为两种方案不同。

因为 m 最大为 10^{18},背包就不行了,但是 n 最大为 40,于是答案的最大值就是 2^{40},普通的搜索会爆,但是折半搜索只用 22^{20} 就可以计算出所有的答案了(2^{20}=1,048,576)。

我们可以将所有比赛分成两部分,对这两部分分别进行搜索,然后使用 vector 来储存搜索过程中产生的可能的花费。最后再将两部分进行合并,得出最终的答案。

代码步骤:

$2.$ 然后将一个数组进行排序,让他成为有序数组(最好是升序,因为这样可以使用万能的 $STL:upper\_bound$) $3.$ 最后枚举另一个数组里的所有值,然后计算 $m-val\_b_i$ 在 $a$ 数组中的位置,因为如果其在 $x$ 的位置(或者不在但是第一个 $\le m-val\_b_i$ 的数的位置在 $x$),那么 $1\sim x$与 $val\_b_i$ 搭配的方案都是可以的。 ### 参考代码 ```cpp #include<bits/stdc++.h> using namespace std; const int MaxN=3e6+5; int n,cnt_front,cnt_back; long long m,ans,a[MaxN],val_front[MaxN],val_back[MaxN]; void dfs_front(int l,int r,long long sum){ if(sum>m){ return ; } if(l>r){ val_front[++cnt_front]=sum; return ; } dfs_front(l+1,r,sum); dfs_front(l+1,r,sum+a[l]); } void dfs_back(int l,int r,long long sum){ if(sum>m){ return ; } if(l>r){ val_back[++cnt_back]=sum; return ; } dfs_back(l+1,r,sum); dfs_back(l+1,r,sum+a[l]); } int main(){ scanf("%d%lld",&n,&m); for(register int i=1;i<=n;i++){ scanf("%lld",&a[i]); } dfs_front(1,n/2,0); dfs_back(n/2+1,n,0); sort(val_front+1,val_front+cnt_front+1); for(register int i=1;i<=cnt_back;i++){ ans+=upper_bound(val_front+1,val_front+cnt_front+1,m-val_back[i])-val_front-1; } printf("%lld",ans); return 0; } ``` 最终的时间复杂度为 $\LARGE\mathcal{O}\left(2^{\frac{n}{2}+1}+\frac{n+\log^{\frac{n}{2}}_2}{2}\right)$。前者为分别搜索的时间复杂度,后者为合并的时间复杂度。 做完这题我们发现,折半搜索适用于当 $n\le45$ 的搜索题。所以当 $n=20\sim45$ 时,我们可以用折半搜索来写。 ## 感谢[myee](https://www.luogu.com.cn/user/105050)提供的折半搜索题单[折半搜索题单](https://www.luogu.com.cn/training/104024#information)