二分算法入门
一、基本知识
- 本质:找区间边界,通过满足特定的条件最终找到相应区间,也可以用来找边界(找左右边界时有区别)
- 优点:相比于遍历每个数据的时间复杂度 O ( n ) O(n) O(n),二分算法可以达到 O ( l o g n ) O(logn) O(logn)的时间复杂度
二、模板
- 以HDU 2578 为例
题意: 给定n个正整数和一个整数k。计算方程x+y=k有多少个不同的解。x和y必须在给定的n个整数中。两种解决方案是不同的。
输入: 第一行包含一个整数T,然后是T行。每种情况以两个整数n(2<=n<=100000)和k(0<=k<2^31)开始。下一行包含n个整数。
输出: 对于每种情况,输出方程的解的数目。
#include<algorithm>
#include<iostream>
#include<stdio.h>
using namespace std;
const int maxn=1e6+9;
int a[maxn];
int n,k,ans,T;
bool check(int x)//模板
{
int l=1,r=n;
while(l<r) //但有时候条件会变 eg.l+1<r(避免死循环) r-l<1e-3(精度类问题)
{ //具体问题具体分析
int mid=(l+r)/2;
if(a[mid]==x) return 1; //找到方程的解
else if(a[mid]>x) r=mid; //mid过大 缩小右边界
else l=mid+1; //mid过小 缩小左边界
}
return 0;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&k);
ans=0;
memset(a,0,sizeof a); //初始化数组
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+n+1);
for(int i=1;i<=n;i++)
{
if(check(k-a[i]))
{
ans++;
if(a[i]==a[i-1]&&i!=1) //避免重复的解
ans--;
}
}
printf("%d\n",ans);
}
}
- 注:不同题目模板的区别:
l = m i d l=mid l=mid的时候, m i d = ( r + l + 1 ) / 2 mid=(r+l+1)/2 mid=(r+l+1)/2 ; r = m i d − 1 r=mid-1 r=mid−1;
r = m i d r=mid r=mid的时候, m i d = ( r + l ) / 2 mid=(r+l)/2 mid=(r+l)/2 ; l = m i d + 1 l=mid+1 l=mid+1;
在 l l l取到 m i d mid mid的时候,要避免整数除法向下取整而造成死循环,所以要+1。
三、函数
头文件中有个叫binary_search 的函数,是通过二分查找的方式来寻找某区间内特定的值。
eg. binary_search(a+first,a+end,value);
如果在a数组中下标[first,end)范围内 存在任一元素和value值相等,则返回true,否则返回false .
优点:代码量少,时间复杂度低(log级别).
这个函数是基于二分查找的,可以用于判断查找值是否存在 。而且也不一定要是数组,容器的迭代器也可以。像这样基于二分查找的函数还有很多。
科普传送门stl中基于二分查找的相关函数
四、例题
1、HDU 2199
- 题意: 给定方程8x^ 4+7x^3+2x ^2+3x+6==Y,找到它在0到100之间的解;
- 输入: 输入的第一行包含一个整数T(1<=T<=100),表示测试用例的数量。接着是T行,每行有一个实数Y(fabs(Y)<=1e10
- 输出: 对于每个测试用例,有解则输出(精确到小数点后4位),如果0到100之间的方程没有解,则输出“No solution!”
- 思路: 这题因为是实数需要控制精度,一开始没什么想法,思维有些局限了,后来过了一会儿才想起来可以用两个y值的差值的绝对值来控制精度,不断逼近答案。
Sample Input
2
100
-4
Sample Output
1.6152
No solution!
#include <cstdio>
#include <cmath>
double f (double x)
{
return 8 * pow(x, 4) + 7 * pow(x, 3) + 2 * pow(x, 2) + 3 * x + 6;
}
int main()
{
int T;
double xl, xr, xmid, y, yl, yr, ymid;
scanf("%d", &T);
while(T--)
{
xl = 0;
xr = 100;
scanf("%lf", &y);
yl = f(xl) - y;
yr = f(xr) - y;
if (yl > 0 || yr < 0)
printf("No solution!\n");
else
{
while (fabs(yl - yr) >= 0.00001)
{
xmid = (xl + xr) / 2;
ymid = f(xmid) - y;
if (ymid >= 0)
xr = xmid;
else
xl = xmid;
yl = f(xl) - y;
yr = f(xr) - y;
}
printf("%.4lf\n", xmid);
}
}
return 0;
}
2、POJ 2456
- 题意: 有N个牛舍于X1、…、Xn(0 <<Xi<= 1000000000)的直线上
- 输入: 第1行:两个空格分隔的整数:N和C;接下来N行:每行包含一个整数表示牛舍位置
- 输出: 一个整数:最小距离的最大值
- 思路: 把牛舍之间的距离作为二分的对象,写个check函数试验该距离,把牛依次放进去看看能不能放下,直到出现一个值作为临界值刚好可以放得下所有牛,那这就是最小距离的最大值了
Sample Input
5 3
1
2
8
4
9
Sample Output
3
#include<algorithm>
#include<iostream>
#include<stdio.h>
using namespace std;
const int maxn=1e9;
int n,c;
long long a[100010];
bool check(int s)
{
int p=0;
for(int i=1;i<c;i++)
{
int q=p+1;
while(q<n&&a[q]-a[p]<s) //需要满足右边界不越界并且两牛舍距离<s
q++; //寻找最大值
if(q==n) return 0; //右边界越界了 即牛放不下 则返回0
p=q; //两牛舍距离>=s 则左区间更新 缩小范围来逼近临界点
}
return 1;
}
int main()
{
while(~scanf("%d%d",&n,&c))
{
for(int i=0;i<n;i++)
scanf("%lld",&a[i]);
sort(a,a+n);
int l=0,r=maxn;
while(l+1<r) //用二分法不断寻找合适的距离
{
int mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
printf("%d\n",l);
}
}
3、POJ 3258
- 题意: 奶牛们在河里从一块石头跳到另一块石头。在一条长而直的河流上,起点有一块石头,终点有另一块石头,距离起点有L个单位(1≤L≤1e9)。在河流的起点和终点岩石之间,出现了N(0≤N≤50000)多个岩石,每个岩石与起点之间的积分距离为Di(0<Di<L)。每头母牛依次从起点的岩石开始,并试图到达终点岩石,只从一块岩石跳到另一块岩石。现在农夫计划移走几块石头,以增加母牛必须跳到终点的最短距离。他知道不能移除开始和结束的岩石,但他计算出他有足够的资源移除多达M个岩石(0≤M≤N)。试确定在移除最佳的M块岩石后,奶牛必须跳跃的最短距离的最大值。
- 输入: 第1行:三个空格分隔的整数:L、N和M;接下来N行:每行包含一个整数,表示某个岩石离起始岩石有多远。没有两块石头的位置相同。
- 输出: 第1行:一个整数,是牛在移除M块石头后必须跳跃的最短距离的最大值
- 思路: 一开始看到的时候,就觉得跟上一题差不多,换汤不换药,就是从牛舍变成石头而已
Sample Input
25 5 2
2
14
11
21
17
Sample Output
4
#include<algorithm>
#include<iostream>
#include<stdio.h>
using namespace std;
const int maxn=1e9;
int L,n,m;
long long a[50010];
bool check(int s)
{
int ll=0,rr=0;
for(int i=0;i<=n;i++)
{
if(a[i]-ll<s) //距离是否小于s
rr++; //如果还小说明距离可以再大点试试
else
ll=a[i]; //如果过大了就更新左边界缩小范围
}
return rr<=m?1:0; //避免越界
}
int main()
{
while(~scanf("%d%d%d",&L,&n,&m))
{
for(int i=0;i<n;i++)
scanf("%lld",&a[i]);
sort(a,a+n);
int l=0,r=maxn;
a[n]=L;
while(l+1<r) //将距离作为二分法的对象,并利用函数尝试该种最小距离是否可行,直到找到最大的最小距离
{
int mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
printf("%d\n",l);
}
}
4、POJ 2785
- 题意: 求和问题可以表示为:给定四个整数值列表 A , B , C , D A,B,C,D A,B,C,D,计算出有多少个四元 ( A , B , C , D ) ∈ A × B × C × D (A,B,C,D)∈A×B×C×D (A,B,C,D)∈A×B×C×D,使得 A + B + C + D = 0 A+B+C+D=0 A+B+C+D=0。在下面,我们假设所有列表都具有相同的大小n。
- 输入: 输入文件的第一行包含列表n的大小(该值可以高达4000)。然后我们有n行包含四个整数值(绝对值为2^28 ),分别属于A、B、C和D。
- 输出: 对于每个输入列表,输出和为零的数字四元组组数。
- 思路: 这题一开始看翻译没读懂题目,以为是所有数里面抽取四个,后来是csdn上看了别人的博客才发现,它的意思是总共四列数字,每列n个,从每列里面抽取一个数字,如果四个数和为0则组数++。然后又开始琢磨这个有四组,要怎么用二分法,当然菜鸡琢磨不出来还是搜了题解,看完直呼nb。就是总共abcd四组,把a和b分为同一组,枚举出所有可能的和值,并存入数组sum1,把c和d分为同一组,也枚举出所有可能的和值并存入数组sum2,最终对sum1和sum2这两个对象用二分算法来做,就又回到了模板流程。
#include<algorithm>
#include<iostream>
#include<stdio.h>
using namespace std;
const int maxn=16000005;
int n,a[4010],b[4010],c[4010],d[4010],sum1[maxn],sum2[maxn],l,r,len;
int main()
{
while(scanf("%d",&n)!=EOF)
{
for(int i=0;i<n;i++)
scanf("%d%d%d%d",&a[i],&b[i],&c[i],&d[i]);
len=0;
for(int i=0;i<n;i++) //将a、b归为同一组计算所有和值存入sum1
{
for(int j=0;j<n;j++)
{
sum1[len]=a[i]+b[j];
len++;
}
}
len=0;
for(int i=0;i<n;i++) //将c、d归为同一组计算所有和值存入sum2
{
for(int j=0;j<n;j++)
{
sum2[len]=c[i]+d[j];
len++;
}
}
int ans=0;
sort(sum2,sum2+len);
for(int i=0;i<len;i++)
{
l=0,r=len-1;
while(l<r) //二分模板
{
int mid=(l+r)/2;
if(sum1[i]+sum2[mid]<0) l=mid+1;
else r=mid;
}
while(sum1[i]==-sum2[l]&&l<len)
{
ans++;
l++;
}
}
printf("%d\n",ans);
}
return 0;
}
ps:对于模板中while循环的条件以及l和r的初始值一般不同题目都是+1、-1的区别,具体条件是什么可以拿临界值自己去模拟一遍