二分
二分是个工具型算法,主要用来求单调性函数反函数值。
生活精致的人用上好的工具。
码风独特的人用不同的二分。
有一个这样的游戏:你能问 10 10 10 个问题,对方会回答是或否,怎样的方案才能猜到对方手中的 3 3 3 位自然数? 应该问形如“你的数比 x x x 大吗”这样的问题,最终 10 10 10 次恰到好处的询问便能求出对方手中的数。这就是二分思想。如下:
你手上的数比 500 500 500 大吗?——不
你手上的数比 250 250 250 大吗?——是
你手上的数比 375 375 375 大吗?——不
你手上的数比 313 313 313 大吗?——是
你手上的数比 344 344 344 大吗?——不
你手上的数比 328 328 328 大吗?——是
你手上的数比 336 336 336 大吗?——是
你手上的数比 340 340 340 大吗?——是
你手上的数比 342 342 342 大吗?——是
你手上的数比 343 343 343 大吗?——不
你手上的数就是 343 343 343!——送你棒棒糖!
每次问的 x x x 的取值是有讲究的,如果问不恰到好处,可能要问几百次才得到对方手中的数,那么有什么样的讲究呢?请看例题:
有一个 n n n 个数的有序序列 a a a,要查找 x x x 在序列中的位置。
解: 所以答案的范围是 [ 1 , n ] [1,n] [1,n],如果我们看 a [ i ] a[i] a[i] 是否比 x x x 大的话,接下来答案可能的区间是 [ 1 , i ] [1,i] [1,i] 或 [ i + 1 , n ] [i+1,n] [i+1,n],如果你运气差的话,答案接下来可能的区间大小是 max ( i , n − i ) \max(i,n-i) max(i,n−i),为了使运气不好的时候每次查询下来后答案可能的区间最小,应有 i = n 2 i=\frac{n}{2} i=2n。所以第一次询问就问“ a [ n 2 ] a[\frac{n}{2}] a[2n] 比 x x x 大吗? ”设询问完后答案可能的区间为 [ l , r ] [l,r] [l,r],就问“ a [ ( l + r ) 2 ] a[\frac{(l+r)}{2}] a[2(l+r)] 比 x x x 大吗?”。如果 l = r l=r l=r 了,这就是答案了。如果你懂了,蒟蒻就放代码了:
#include <bits/stdc++.h>
using namespace std;
int n,x,a[100010];
int main(){
scanf("%d%d",&n,&x);
for(int i=1;i<=n;i++) scanf("%d",a+i);
int l=1,r=n;
while(l<r){
int mid=(l+r)/2;
if(a[mid]>x) r=mid-1;
else l=mid;
}
printf("%d\n",l);
return 0;
}
可是你会发现一个问题,如果你输入:
5 6
1 3 5 6 8 9
那么就会输不出东西来,这其实是因为理论上的二分在一些特殊情况下会死循环,所以人们会把理论二分改造一下,变成下面两种二分:
第一种:一个开区间一个闭区间版二分
在这份代码中,区间为 [ l , r ) [l,r) [l,r),这样可以避免死循环。#include <bits/stdc++.h> using namespace std; int n,x,a[100010]; int main(){ scanf("%d%d",&n,&x); for(int i=1;i<=n;i++) scanf("%d",a+i); int l=0,r=n+1; while(l<r-1){ int mid=(l+r)/2; if(a[mid]>x) r=mid; else l=mid; } printf("%d\n",l); return 0; }
第二种:保持答案二分
两端都是闭区间,但是保证不会再查询已经查询过的东西。#include <bits/stdc++.h> using namespace std; int n,x,a[100010]; int main(){ scanf("%d%d",&n,&x); for(int i=1;i<=n;i++) scanf("%d",a+i); int l=1,r=n,ans; while(l<r){ int mid=(l+r)/2; if(a[mid]>x) r=mid-1; else ans=mid,l=mid+1; //记录答案 } printf("%d\n",ans); return 0; }
说了这么多,没说为什么要用二分,其实是因为二分的时间复杂度为 Θ ( log n ) \Theta(\log n) Θ(logn)。这与 Θ ( n ) \Theta(n) Θ(n) 的线性查询比较快好多,如下表:
n n n | n \sqrt n n | log n \log n logn |
---|---|---|
1 1 1 | 1 1 1 | 0 0 0 |
10 10 10 | 3 3 3 | 3 3 3 |
100 100 100 | 10 10 10 | 7 7 7 |
1000 1000 1000 | 32 32 32 | 10 10 10 |
10000 10000 10000 | 100 100 100 | 13 13 13 |
100000 100000 100000 | 316 316 316 | 16 16 16 |
1000000 1000000 1000000 | 1000 1000 1000 | 19 19 19 |
10000000 10000000 10000000 | 3162 3162 3162 | 23 23 23 |
可见二分算法时间复杂度的优越性。因此,二分是优化算法的重要工具,很多时候,如果你想到了用二分,就解决的题目的一半。
祝大家学习愉快!