二分是个非常神奇的算法,可以把时间复杂度优化到 Logn ,所以二分应用还是很广的,下面总结一下二分的几种题型与例题:

 


一、最大化最小值、最小化最大值问题

 

1.基本题意:

 

①最大化最小值问题:n个东西分给m个人,每人至少拿x个,问x最大值。

②最小化最大值问题:n个东西分给m个人,每人至多拿x个,问x最小值。

分析:首先这种问题需要一个限制条件,而以上题目的限制条件就是要分给m个人:

 

 

所以

①:每人至少拿x个,我们假设现在x的最大值就是 p,所以当前每人至少拿p个,因为我们要考虑在最坏的情况下能不能够分m个人,所以每个人都拿p个,看此时可以分的人数是否>=m,如果>=m true,否则false。
②:每人至多拿x个,我们假设现在的x最小值就是p,所以当前每人至多拿p个,此时的最坏情况应该是都拿p个,判断一下是否<=m个人,如果小于的话则满足要求,否则则不满足要求(因为已经都尽可能的拿了还多着)


 

2.例题

 

①POJ3273&&Monthly Expense

 

题目链接

 

中文题意:给你n个数,让分成m组,每组必须是连续的一些数,求每组的和的最大值最小。

 

分析:

One:最小化最大值问题,首先分析制约因素-m,然后思考判断条件:假设当前的和是最大,以此和分组可以分多少组呢?如果当前组数>m,则返回false,否则返回true。

 

Two:为什么可以这么想呢?在这里解释一下单调性:如果我们以当前和为依据分组,如果大于m组,说明当前的和太小,我们需要增大当前最大和,也就是当前的最大和不满足分m组的条件。否则当前最大和满足条件但是不一定是最小,所以我们向小寻找发现有没有更优解(即一个为可行解,一个为最优解)。

 

所以代码也就写出来了:

int n,m;
int num[maxn];
bool judge(int x)
{
    for(int i=1;i<=n;i++)
        if(num[i]>x) return false;//既然假设了当前是最大和,那么如果有一个数大于当前的话,这个最大和绝对不成立,因为无论怎么分,这个数也大于当前最大和
    int tot=1,sum=0;//初始tot=1!!!
    for(int i=1;i<=n;i++)//求最多可以连续分多少段
    {
        sum+=num[i];
        if(sum>x)//sum>x就不可以了
        {
            tot++;
            sum=num[i];//看看与后面是否可以
        }
    }
    if(tot>m) return false;
    return true;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&num[i]);
    int l=0,r=INF;
    int ans;
    while(l-r<=0)//二分最小值即可
    {
        int mid=(l+r)/2;
        if(judge(mid)==true)
        {
            r=mid-1;
            ans=mid;
        }
        else
            l=mid+1;
    }
    printf("%d\n",ans);
    return 0;
}

 

②NOPI提高组跳石头  之前写过:here 跳石头

 

题意:一群组委会想要为难一只青蛙,想要把青蛙从起点跳到终点过程中的 石头移走 ,以致于导致青蛙每次跳跃的距离变大,那么请问青蛙最短跳跃距离的最大值是多少(组委会想让青蛙多跳),但组委会 最多可以移走 m块石头

 

分析:首先考虑制约条件:移走的石头总数,然后我们进行二分,假设当前二分的跳跃距离是最短跳跃距离,那么青蛙在跳跃期间不可以 出现 比这个距离还短的距离,所以一旦出现我们就需要移走一块石头,最后判断一下 该距离下 移走的石头数 与 m的关系,如果移走的石头>m返回false ,缩小范围,否则扩大范围 寻找最大值。

 

代码之前博客中有写过,这里不再写了。


二、实数域上的二分问题

 

基本题型:与上述 第一种 题型类似,有一个制约条件+范围,不同的是在实数上进行二分需要注意精度问题。

 

①POJ2018 &&Best Cow Fences

 

题目链接

 

 

题目大意:抽象一下,给你n个数,把这n个数分成长度最少为m的区间,问平均值最大是多少,输出平均值*1000的结果

分析:制约条件,m个区间,所以我们二分平均值,判断在该平均值下长度大于m的序列中是否有大于该平均值的,如果有则说明当前平均值还不够大,继续右移。如果没有说明平均值太小。

 

 

细节问题需要处理:

第一点:如何判断平均值是否可以分成m个区间?我们把每个数都减去当前二分的平均值,那么如果之前一段连续子序列的平均值要大于当前平均值的话 ,现在子序列的和应该大于0,所以问题转换成了 求 序列是否存在长度大于等于m的非负子段和。

第二点:对于 寻找 长度大于等于m的非负子段和的处理,首先我们记录当前所有数的前缀和,利用a1-an的和等于 sum[n]-sum[0]的方法。当n=m时,我们只能求出当前m项的和,n=m+1时,我们可以求出 1-m的和与1-m+1的和,这两个和长度均大于等于m,哪个大呢?肯定是 减去sum[0],与sum[1]的最小值。不懂看例子

1 2 3 4 5假设找出长度不小于2的 最大的和,首先1 2是一种可能,其次1 2 3是2 3,此时比较 sum[3]-sum[1-1]与sum[3]-sum[2-1]的大小,所以说我当我们移动到某一点时,我们可以算出当前点对应的 长度大于等于m的子段的最大值,之后把所有 大于等于m的点遍历一下就好了。 (可以想象成一个区间在移动,每次都增加1,之前区间的最小值发生改变)

第三点:关于精度问题,实数上的二分通常精度卡的严,为了提高精度我们,改变区间时令 l=mid,r=mid,最大值时输出r,最小值时输出l。

 

 

代码:

bool judge(double x)
{
    for(int i=1;i<=n;i++)
        sum[i]=sum[i-1]+(num[i]-x);
    double maxl=-INF,minl=INF;
    for(int i=m;i<=n;i++)
    {
        minl=min(minl,sum[i-m]);
        maxl=max(maxl,sum[i]-minl);
    }
    if(maxl>=0) return true;
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%lf",&num[i]);
    double l=-INF,r=INF;
    double ans;
    double eps=1e-5;
    while(r-l>eps)
    {
        double mid=(l+r)/2.0;
        if(judge(mid)==true) l=mid;
        else r=mid;
    }
    printf("%d",(int)(r*1000));
    return 0;
}

②Pie&&POJ3122

 

题目链接

 

题目大意:有n个pie,想要分成m个人,每个人得到的pie必须来自不同的pie且体积大小相同。非常好的实数二分入门题

考虑限制条件即可:分成m个人,二分枚举当前的体积,看当前体积是否可以 从n个pie中拿到 m个pie即可。

 

细节问题:每人拿的都是来自不同P的,而且圆周率要到 十位 ,以后建议 只要 圆周率 的都卡到十位,不然会wa

 

具体代码:

int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);m++;
        double maxl=-INF;
        for(int i=1;i<=n;i++)
        {
            scanf("%lf",&num[i]);
            num[i]*=num[i];
            maxl=max(num[i],maxl);
        }
        double l=0,r=maxl;
        double eps=1e-7;//精度以后最好1e-7
        while(r-l>eps)
        {
            double mid=(l+r)/2.0;
            int cnt=0;
            for(int i=1;i<=n;i++)
                cnt+=(int)(num[i]/mid);
            if(cnt>=m) l=mid;
            else r=mid;
        }
        printf("%.4lf\n",r*PI);//最后*PI是为了确保精度,PI等于3.1415926至十位以上
    }
    return 0;
}

三、二分与其他简单问题的联系

 

基本题意:

题目会根据一些数学问题,即寻找中位数,寻找对数等。

此类题目有一个特征就是外要套循环,卡二分时间度,比如说寻找中位数,排序加遍历就可以找到,但是如果加一个循环T=50000,就不那么容易了。

 

例题 ①pair 链接好像找不到了..

 

基本题意:给你一些数,问你这些数中,输入n,x1,x2,xn,k,问|xb-xa|<=k的有多少对。

 

分析:这个题好像普通的遍历也会很麻烦,那么如何二分呢?我们首先对这组数进行排序,排序完之后就非常容易了,我们把上述 数学式化简:排序后 后面绝对比前面大 所以 直接去掉绝对值 xa-xb<=k,所以xa<=k+xb,所以我们现在只需要找到以xa为起点到第一个大于 xb+k之间有几个数,就是xa的对数,遍历一遍 序列 都找一遍,查找过程中套用二分,非常奈斯出解:

/*Accepted
Time
202ms
Memory
2172kB
Length
504
Lang
G++
*/

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e6+5;
ll num[maxn];

ll n,m;

int main()
{
    int T;scanf("%d",&T);
    while(T--)
    {
        scanf("%lld%lld",&n,&m);
        for(int i=1;i<=n;i++)
            scanf("%lld",&num[i]);
        sort(num+1,num+1+n);
        ll cnt=0;
        for(int i=1;i<=n;i++)
        {
            int e=upper_bound(num+i,num+1+n,num[i]+m)-num;//找到大于它的第一个位置
            cnt+=e-i-1;//e-i-1即为中间的数
        }
        printf("%lld\n",cnt);
    }
    return 0;
}

时间复杂度 O(nlgn)


那么到这里,关于二分的 简单问题 的类型就说完了,二分通常用于优化复杂度,所以 也很普遍的用于复杂问题中,根源就是如何进行二分,所以说单单掌握基本题型还是不够的,还是需要善 变。

总而言之,二分的应用很广,以后有新的题型还会继续总结!加油!