收获:
不仅要熟练掌握基础算法,丰富的想象力亦是非常重要的。
二分查找的复杂度是O(logn)的,即便n变得很大,对数时间的算法依然非常快速。
1.6.3 难度增加的抽签问题
如果把最开始的问题中的n的限制条件改为1<=n<=1000,最初的四重循环O(n的4次方)复杂度的算法显然不够,必须改进算法。
for(int a=0;a<n;a++){
for(int b=0;b<n;b++){
for(int c=0;c<n;c++){
for(int d=0;d<n;d++){
if(k[a]+k[b]+k[c]+k[d]==m){
f=true;
}
}
}
}
}
上为最初程序的循环部分。最内侧关于d的循环所做的事情就是
检查是否有d使得k[a]+k[b]+k[c]+k[d]=m
通过对式子进行移项,就能得到另一种表达方式
检查是否有d使得k[d]=m-k[a]-k[b]-k[c]
即判断是否有m-k[a]-k[b]-k[c]
根据上述推论,让我们来考虑一下快速检查的方法。
1.二分搜索与O(n³logn)的算法
记所要查找的值m-k[a]-k[b]-k[c]为x。
预先把数组排好序,然后看k中央的数字:
如果它比x小,x只可能在它的后半段。
如果它比x大,x只可能在它的前半段。
如果再将上述方法运用到已经减半的x的存在区间上,x的存在区间就变成了初始的1/4.
反复操作,不断缩小x的存在区间,最终确定x存在与否。
二分搜索算法每次将候选区间大致缩小为原来的一半。
因此,要判断长度为n的有序数组k中是否包含x,只要反复执行约log₂n次即可。
二分查找的复杂度是O(logn)的,我们称称这种阶的运行时间为对数时间。
即便n变得很大,对数时间的算法依然非常快速。
n | 1 | 10 | 100 | 1000 | 1e6 | 1e9 |
log₂n | 0 | 3 | 7 | 10 | 20 | 30 |
将最内侧的循环替换成二分搜索算法之后
排序O(nlogn)
循环O(n³logn)
O(n³logn)比O(nlogn)大,所以这了合起来当作是O(n³logn)
于是我们得到了在O(n³logn)时间内解决的办法
程序:
#include<cstdio>
#include<algorithm>//sort函数头文件。sort(begin,end,cmp),cmp参数可以没有,如果没有默认非降序排序。
using namespace std;
const int MAX_N = 1000;
int n=3,m=10,k[MAX_N]={1,3,5};
//int n=3,m=9,k[MAX_N]={1,3,5};
bool binary_search(int x){
//x的对象范围是k[l],k[l+1],...,k[r-1]
int l=0,r=n;
//反复操作直到存在范围为空
while(r-l>=1){
int i=(l+r)/2;
if(k[i]==x) return true;//找到x
else if(k[i]<x) l=i+1;
else r=i;
}
//没找到x
return false;
}
void solve(){
//为了执行二分查找需要先排序
sort(k,k+n);
//是否找到和为m的组合的标记
bool f=false;
for(int a=0;a<n;a++){
for(int b=0;b<n;b++){
for(int c=0;c<n;c++){
if(binary_search(m-k[a]-k[b]-k[c])){
f=true;
}
}
}
}
//输出到标准输出
if(f) puts("Yes");
else puts("No");
}
int main(){
solve();
return 0;
}
其实像binary_search这样的函数,多是情况下无需自己实现,可以使用标准实现。
例如C++的STL库就含有提供基本同样功能的函数。
2.O(n²logn)的算法
但是,将n=1000带入n³logn,会发现这依然是远远不够的,必须进一步优化算法。
刚才我们只着眼于最内侧循环,接下来让我们着眼于内侧的两层循环。
内侧两个循环是在
检查是否有c和d使得k[c]+k[d]=m-k[a]-k[b]
这种情况不能直接使用二分查找。但是我们可以预先枚举出k[c]+k[d]所得的n²个数字并排好序,再使用二分搜索。
排序O(n²logn)
循环O(n²logn)
总时间O(n²logn),这样n=1000也可妥善应对。
程序:
#include<cstdio>
#include<algorithm>//sort函数头文件。sort(begin,end,cmp),cmp参数可以没有,如果没有默认非降序排序。
using namespace std;
const int MAX_N = 1000;
int n=3,m=10,k[MAX_N]={1,3,5};
//int n=3,m=9,k[MAX_N]={1,3,5};
//保存两个数的和序列
int kk[MAX_N*MAX_N];
bool binary_search(int x){
//x的对象范围是kk[l],kk[l+1],...,kk[r-1]
int l=0,r=n*n;
//反复操作直到存在范围为空
while(r-l>=1){
int i=(l+r)/2;
if(kk[i]==x) return true;//找到x
else if(k[i]<x) l=i+1;
else r=i;
}
//没找到x
return false;
}
void solve(){
//枚举k[c]+k[d]的和
for(int c=0;c<n;c++){
for(int d=0;d<n;d++){
kk[c*n+d]=k[c]+k[d];
}
}
//为了执行二分查找需要先排序
sort(kk,kk+n*n);
//是否找到和为m的组合的标记
bool f=false;
for(int a=0;a<n;a++){
for(int b=0;b<n;b++){
if(binary_search(m-k[a]-k[b])){
f=true;
}
}
}
//输出到标准输出
if(f) puts("Yes");
else puts("No");
}
int main(){
solve();
return 0;
}
本题既需要二分搜索的基础算法知识,也需要将四个数分成两两考虑的想象力.
像这样从复杂度较高的算法出发,不断降低复杂度直至满足问题要求的过程,也是设计算法时经常会经历的一个过程。