二分专题 (一)

二分专题(一)

话说十个二分九个错,二分不写不知道,一写吓一跳。
解二分的重点还是思维比较重要,当然还有一个重要的就是,二分别写挂了。

CodeForces 165B Burning Midnight Oil

题目大意

给出数字n,k,求得最小的数字v使得:

vk+vk2+vk3+n

数据范围: 1n109,2k10

题意分析

对于式子中的下取整部分可以暴力求和,复杂度为 logv 。如何求解v呢?不难发现式子满足一致性:即如果v取小了,式子之和是小于等于n的;如果取大了,那么就会大于等于n。并且如果和值大了就应该减小v的取值,和值小了应该增大v的取值。 这恰恰和二分的思想是一样的,由此可以二分v来寻找答案。

时间复杂度: log2n

代码总览

#include 
using namespace std;
int n,k;
int cal(int mid){
    int tot = 0;
    while(mid){
        tot+=mid;
        mid/=k;
    }
    return tot;
}
int main(){
    while(scanf("%d %d",&n,&k) != EOF){
        int up = 1e9+5,down = 1,mid = 0,tot = 0;
        int last = mid,ans = mid;
        while(down<=up){
            mid = down + (up-down)/2; // 防溢出
            if(cal(mid) >= n){
                ans = mid;
                up = mid-1;
            }else down = mid + 1;
        }
        printf("%d\n",ans);
    }
    return 0;
}

CodeForces 192A Funky Numbers

题目大意

给出一个整数n,当n可以写成 a(a+1)2+b(b+1)2 时 (其中a,b均为整数,ab可以相等),输出YES,否则输出NO。

数据范围: 1n109

题意分析

题目并没有要求n的表达唯一,如果可以的话,只需要找出一组解即可。其实和式并没有什么规律。如果采用暴力算法,最坏情况内外层层循环 2n 次,这时近似有 2n=k2 ,则 k=2n 。想到这里,其实不难发现,对于特定的i,j其实也满足决策一致性,这样就可以采用二分的方法求解。

时间复杂度: nlogn

代码总览

#include
using namespace std;

int main(){
    int n;
    while(scanf("%d",&n) != EOF){
        bool isfind = false;
        for(int i = 1;i <= sqrt(2*n);++i){
            int fsum = i*(i+1)/2;
            int up = sqrt(2*n),down=i;
            int ssum = 0,mid = (up+down)/2;
            while(down<=up){
                mid = (up+down)/2;
                ssum = mid*(mid+1)/2;
                if(fsum+ssum1;
                if(fsum+ssum>n) up = mid-1;
                if(fsum+ssum==n){
                    isfind = true;
                    break;
                }
            }
        }
        if(isfind)printf("YES\n");
        else printf("NO\n");
    }
    return 0;
}

CodeForces 256D Multiplication Table

题目大意

给出一个n×m的格子,格子对应的值位其行列的乘积,求出整张表格中第k个数字。第k个数字的定义是,将整张表的数字按非递减写出,第k个数字即为所求。

数据范围: 1n,m5105,1knm

题意分析

首先读题不要读错,一开始我以为是求第k大的数字,发现样例2说不过去,仔细读了一下题,题目中有他自己的定义。

由于每一行,每一列的数字都是满足单调性的,所以二分一定是一个不错的选择,但是如何进行二分呢?其实可以想到,题目中所说的第k大数字有一个性质:小于第k大的数字的个数,一定小于k个。如对应样例二,1 2 2 3均小于4。如此一来,利用这个单调性,便可以二分枚举k,逐行统计小于k的个数,然后求出答案。

时间复杂度: nlogn

代码总览

#include
#define ll long long
using namespace std;
ll n,m,k;
ll check(ll mid){
    ll sum = 0;
    for(int i = 1;i<=n;++i)
        sum += min(m,(mid-1)/i);
    return sum;
}
int main(){
    while(scanf("%lld %lld %lld",&n,&m,&k) !=EOF){
        ll up = n*m,down = 1,mid;
        ll ans = 0;
        while(down<=up){
            mid = (up+down)/2;
            if(check(mid) >= k) up = mid -1;
            else if(tmp < k){
                ans = mid;
                down = mid+1;
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

CodeForces 672D Robin Hood

题目大意

给出n个数字,k次操作。每次操作可以将n个数字中的最大数减一,将最小数加一。经过k次操作后,n个数字中的最大值和最小值的差为多少。

数据范围: 1n500 000,0k109 , 数列元素 1ci109

题意分析

如果暴力算的话,肯定不行。其实想一下,如果进行无限次操作,最大值和最小值会在平均值上下浮动。进行二分的最关键的地方在于决策一致性。有了这个性质,就提供了一个思路。虽然无法直接暴力求出最后的最大值和最小值,但是可以用二分的方法来确定。
首先二分最小值,数列中比最小值小的元素一定是一次次+1变过来的,这样就可以统计出经过了多少次变换。然后跟题目中给出的k做比较,利用决策一致性改变二分上下界,从而求出最小值。
接着二分最大值,数列中比最大值大的元素一定是一次次-1变过来的,同理统计出经过多少次变换。如法炮制可以求出最大值。
最大值和最小值的差即为所求。

这里有一个坑,就是二分初始化上下界。
最小值一定小于等于floor(average),而最大值一定大于等于ceil(average)。如果上下界分别设定为数列中的最大元素和最小元素,这样的话会出现问题:有可能最后求出的最大值比最小值还小。

时间复杂度: nlogn

代码总览

#include
#define ll long long
using namespace std;
const int nmax = 5e5+5;
ll a[nmax];
ll n,k,minn = 0x3f3f3f3f ,maxn;
ll sum = 0;
bool findmin(int x){
    ll tmp = k;
    for(int i = 0;i < n;++i){
        if(a[i]abs(x-a[i]);
        if(tmp<0) return false;
    }
    return true;
}
bool findmax(int x){
    ll tmp = k;
    for (int i = 0;i< n ;++i){
        if(a[i]>x) tmp -= abs(a[i]-x);
        if(tmp<0) return false;
    }
    return true;
}
int main(){
    while(scanf("%lld %lld", &n,&k) != EOF){
        sum = 0;
        for(int i = 0;i < n;++i){
            scanf("%lld", &a[i]);
            sum += a[i];
            minn = min(minn,a[i]);
            maxn = max(maxn,a[i]);
        }
        ll down = minn,up = sum/n,mid = 0,l,r;
        while(down <= up){
            mid = down + (up - down) / 2;
            if(findmin(mid)){
                l = mid;
                down = mid + 1;
            }else up = mid - 1;
        }
        down = (ll)ceil(1.0*sum/n),up = maxn ,mid = 0;
        while(down <= up){
            mid = down + (up - down) / 2;
            if(findmax(mid)){
                r = mid;
                up = mid - 1;
            }else down = mid + 1;
        }
        printf("%lld\n",r-l);
    }
    return 0;
}

CodeForces 660C Hard Process

题目大意

给出n个元素的01序列,现在可以进行k次操作。每次操作可以将0变为1。请你求出操作后最长连续1的序列的长度,并且输出整个序列。

数据范围: 1n3×105,0kn

题意分析

这道题看起来和二分没啥关系。不过想想二分是对枚举的优化,暴力的做法是枚举起点和终点,统计里面0的个数,然后判断个数是否小于k,然后更新答案。对这个枚举思想,其实就可以加以改进。枚举一个起点(或终点)之后,对另外一个端点进行二分判断。如果区间内0的个数小于k,说明可以扩大这个区间,从而更新端点。直至不满足二分上下界为止。

时间复杂度: nlogn

代码总览

#include
#define nmax 1000000
using namespace std;
int a[nmax];
int sum[nmax];
int n,k;
int cal(int l,int r){
    return ((r-l+1) - (sum[r]-sum[l]+1)) + !a[l];
}
int main(){
    while(scanf("%d %d",&n,&k)!= EOF){
        for(int i = 1;i<=n;++i) scanf("%d",&a[i]);
        sum[1] = a[1];
        for(int i = 2;i<=n;++i) sum[i] = sum[i-1] + a[i];
        int adown = 0,aup = 0,up,down,ans = 0,mid,tmid = 0;
        for(int i = 1;i<=n;++i){
            down = 1,up = i;
            while(down<=up){
                mid = (down + up) / 2;
                int tmp = cal(mid,i);
                if(tmp <= k){
                    if(i-mid+1>ans){
                        ans = i-mid+1;
                        adown = mid;
                        aup = i;
                    }
                    up = mid - 1;
                }else if(tmp > k) down = mid + 1;
            }
        }
        bool isfirst = true;
        printf("%d\n",ans);
        for(int i = 1;i<=n;++i){
            if(i>=adown && i<=aup)
                if(isfirst) printf("1"),isfirst = false;
                else printf(" 1");
            else
                if(isfirst) printf("%d",a[i]),isfirst = false;
                else printf(" %d",a[i]);
        }
        printf("\n");
    }
    return 0;
}

CodeForces 535C Tavas and Karafs

题目大意

给出一个无限长序列,通项为 si=A+(i1)×B ,现在有n个询问。每个询问包括l,t,m三个参数。每次询问可以执行t次操作,每次操作可以将 [sl,sl+m] 区间内的元素值减一,若 sl 减至为0,l自增1。每次询问中,执行完操作后,全为0的序列部分的右端点是多少。若没有0,输出-1;。

数据范围: 1A,B106,1n105,1l,t,m106

题意分析

首先可以确定的是,如果 sl<t 那肯定没有0,输出-1即可。
否则的话,二分一个分界点r,这个问题可以变成 sl+sl+1+sl+2++sltm ,所需要求的也就是一个等差数列前n项和。

时间复杂度: nlog(l+t)

代码总览

#include
#define ll long long
using namespace std;
ll a,b,n,l,t,m;
ll cal(ll x){
    return a+(x-1)*b;
}
ll getsum(ll r){
    return (cal(l)+cal(r)) * (r-l+1) / 2 ;
}
int main(){
    while(scanf("%lld %lld %lld",&a,&b,&n) != EOF){
        while(n--){
            scanf("%lld %lld %lld",&l,&t,&m);
            if(cal(l) > t){
                printf("-1\n");
                continue;
            }else{
                ll up = (t-a)/b + 1,down = l,mid,ans;
                while(down <= up){
                    mid = (up+down) / 2;
                    if(getsum(mid)<=t*m){
                        ans = mid;
                        down = mid+1;
                    }else up = mid-1;
                }
                printf("%lld\n",ans);
            }
        }
    }
    return 0;
}

未完待续……

你可能感兴趣的:(算法---二分)