分治算法——二分查找法
分治算法在维基百科中的解释如下:
在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范式。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
这个技巧是很多高效算法的基础,如排序算法(归并排序、快速排序)、傅立叶变换(快速傅立叶变换)。
另一方面,理解及设计分治法算法的能力需要一定时间去掌握。正如以归纳法去证明一个理论,为了使递归能够推行,很多时候需要用一个较为概括或复杂的问题去取代原有问题。而且并没有一个系统性的方法去适当地概括问题。
分治法这个名称有时亦会用于将问题简化为只有一个细问题的算法,例如用于在已排序的列中查找其中一项的折半搜索算法(或是在数值分析中类似的勘根算法)。这些算法比一般的分治算法更能有效地运行。其中,假如算法使用尾部递归的话,便能转换成简单的循环。但在这广义之下,所有使用递归或循环的算法均被视作“分治算法”。因此,有些作者考虑“分治法”这个名称应只用于每个有最少两个子问题的算法。而只有一个子问题的曾被建议使用减治法这个名称。
分治算法通常以数学归纳法来验证。而它的计算成本则多数以解递归关系式来判定。
分治算法是一种很重要的算法,其字面上的意思是“分而治之”,就是将一个复杂的问题分解成两个或者更多个相同的问题或者相似的子问题,直到左后子问题可以简单的求解,,原来问题的解就是子问题的解的合并,分治的技巧是很多高效算法的基础,比如:快速排序,归并排序,快速傅里叶变换。
我们通过一道题目来理解分治算法中一个重要的算法——二分查找法(折半查找法,以下简称二分法)。
我们先看一下在维基百科中是怎样解释二分法的:
在计算机科学中,二分查找算法(英语:binary search algorithm),也称折半搜索算法(英语:half-interval search algorithm)、对数搜索算法(英语:logarithmic search algorithm)[2],是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
二分查找算法在情况下的复杂度是对数时间。二分查找算法使用常数空间,无论对任何大小的输入数据,算法使用的空间都是一样的。除非输入数据数量很少,否则二分查找算法比线性搜索更快,但数组必须事先被排序。尽管特定的、为了快速搜索而设计的数据结构更有效(比如哈希表),二分查找算法应用面更广。
二分查找算法有许多中变种。比如分散层叠可以提升在多个数组中对同一个数值的搜索。分散层叠有效的解决了计算几何学和其他领域的许多搜索问题。指数搜索将二分查找算法拓宽到无边界的列表。二叉搜索树和B树数据结构就是基于二分查找算法的。
二分法在之后很多的数据结构和算法都起到了很重要的作用。
我们来看一盗二分法的模板题:
输入数据
输入文件第一行是N,表示有N个元素,第二行是N个数,这N个数从小到大排序,第三行是M表示要查找的数(N <= 10000)。
输出数据
一个数,即如果找到该数,则输出位置,否则输出-1。
样例输入
3
2 4 6
4
样例输出
2
解决这道题目可以有两种思路,分别是采用递归的二分算法和采用非递归的二分算法:
递归的二分法
首先将输入的N个数字存入数组number中,之后我们在查找的时候就像查字典一样,首先先查找最中间的数据最中间的序号设为mid,这串数组的起始位置为head,终止位置为tail,其中mid = (head + tail) / 2,那么数组中间值和需要找到的值之间的关系有如下三种:
number[mid] == M,那么这个位置就是要求的数字,将mid + 1输出即可
number[mid] < M,那么这个数字的位置就稍微靠前,我们将mid = head,tail不变,继续进行二分法
number[mid] > M,那么这个数字的位置就稍微靠后,我们将mid = tail,head不变,继续进行二分法
#include<map>
#include<list>
#include<cmath>
#include<queue>
#include<stack>
#include<vector>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<iterator>
#include<iostream>
#include<algorithm>
#define R register
#define LL long long
#define pi 3.14159265358979323
using namespace std;
inline void search(int head, int tail, int M, int number[]){
if(tail > head){
int mid = (head + tail) / 2;
if(number[mid] == M){
printf("%d", mid + 1);
return ;
}
else{
if(number[mid] < M){
search(mid, tail, M, number);
}
else{
search(head, mid, M, number);
}
}
}
else{
printf("-1");
}
}
int main(){
int N, M, number[10100];
scanf("%d", &N);
for(R int i = 0; i < N; ++ i){
scanf("%d", &number[i]);
}
scanf("%d", &M);
search(0, N, M, number);
return 0;
}
非递归的二分法
非递归的二分法和之前递归形式的二分法处理这个问题思路相同,但是由于递归本身得问题使用递归的二分法效率低于采用递归的二分法。
#include<map>
#include<list>
#include<cmath>
#include<queue>
#include<stack>
#include<vector>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<iterator>
#include<iostream>
#include<algorithm>
#define R register
#define LL long long
#define pi 3.14159265358979323
using namespace std;
inline void search(int head, int tail, int M, int number[]){
while(head < tail){
int mid = (head + tail) / 2;
if(number[mid] == M){
printf("%d", mid + 1);
return;
}
else if(number[mid] < M){
head = mid;
}
else{
tail = mid;
}
}
printf("-1");
}
int main(){
int N, M, number[10100];
scanf("%d", &N);
for(R int i = 0; i < N; ++ i){
scanf("%d", &number[i]);
}
scanf("%d", &M);
search(0, N, M, number);
return 0;
}
我们再来看一道可以用二分法做的题目:
题目描述
找到最小的自然数N,使得N!在十进制下包含Q个0。
输入数据
一个数字Q(0 <= Q <= 10^8)。
输出数据
如果无解输出no solution,否则输出N
样例输入
2
样例输出n
10
观察Q的数据范围,也就是N!的末尾0已经超过一千万个,早已经超出long long的范围,所以如果采用将N!求出在取得末尾0数显然是不可行的,我们可以通过公式求得末尾0的数量。
f(N!) = N / 5 + N / (5^2) + N / (5^3) + … + N / (5^n)
通过这样的公式我么可以获得N!中末尾0的个数
具体求得末尾0数量的代码如下:
#include<map>
#include<list>
#include<cmath>
#include<queue>
#include<stack>
#include<vector>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<iterator>
#include<iostream>
#include<algorithm>
#define R register
#define LL long long
#define pi 3.14159265358979323
using namespace std;
int main(){
int N;
scanf("%d", &N);
int total = 0;
int number = 5;
while(number <= N){
total += N / number;
number *= 5;
}
printf("%d", total);
return 0;
}
通过这段代码我们尝试当N为109的情况下,N!末尾0的数量为:24999999,这样我们就确定了至少当N为一亿的情况下末尾0的数量已经超过了需要的范围,那么我们就可以进行二分了来查找在1到109中那个数字符合这个条件了。由于我们需要的是最小的N可以满足这个条件的数,那么这个数N一定是5的倍数,最后需要对求出的数进行判断,如果这个数不是5的倍数,那么就需要将这个数减到是5的倍数为止,代码如下:
#include<map>
#include<list>
#include<cmath>
#include<queue>
#include<stack>
#include<vector>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<iterator>
#include<iostream>
#include<algorithm>
#define R register
#define LL long long
#define pi 3.14159265358979323
using namespace std;
inline int count(int number){
int total = 0;
int number_1 = 5;
while(number_1 <= number){
total += number / number_1;
number_1 *= 5;
}
return total;
}
int main(){
int Q;
scanf("%d", &Q);
int head = 1, tail = 1e9;
while(head < tail){
int mid = (head + tail) / 2;
int ans = count(mid);
if(ans == Q){
if(ans % 5 != 0){
mid -= mid % 5;
}
printf("%d", mid);
return 0;
}
else if(ans < Q){
head = mid;
}
else if(ans > Q){
tail = mid;
}
}
printf("no solution");
return 0;
}