二分法简介

定义

对于区间[a,b]上连续不断且f(a)·f(b)<0的函数y=f(x),通过不断地把函数f(x)的零点所在的区间一分为二,使区间的两个端点逐步逼近零点,进而得到零点近似值的方法叫二分法。

例如对于一个连续的范围[0, 10],寻找x2+3x+4=22的一个解, 由于在取值范围内函数是随x单调增的,对于一个单调上升的x序列[0,10],我们要寻找的是最后一个值x使得x2+3x+4<22。一个显然的方法是,x = 1,1.000001,1.000002…,但这样费时费力,不好寻找,不妨使用二分法解决问题。(参见北师大版初三教材(O-O))

流程图

对于一个二分法,通常有两个要素:

  1. l,r的界
  2. check(x)函数,判断x是否还可行

通常利用下面的方法:

while (l <= r) {
    mid = (l+r)/2;
    if (check(mid)) l = mid + 1;
    r = mid - 1;
}

二分法代码很简单,主要是思路和check函数。下面用例题分析一下

例题

跳石头

描述

一年一度的“跳石头”比赛又要开始了!
这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终 点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。
为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳 跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能 移走起点和终点的岩石)。

格式

输入格式

输入第一行包含三个整数 L,N,M,分别表示起点到终点的距离,起点和终 点之间的岩石数,以及组委会至多移走的岩石数。接下来 N 行,每行一个整数,第 i 行的整数Di(0<Di<L)表示第 i 块岩石与 起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。

输出格式

输出只包含一个整数,即最短跳跃距离的最大值。

样例1

  • 样例输入1

    25 5 2
    2
    11
    14
    17
    21

  • 样例输出1

    4

限制

对于20%的数据,0MN10
对于50%的数据,0MN100
对于100%的数据,0MN500001L1000000000

提示

对于样例。将与起点距离为 2 和 14 的两个岩石移走后,最短的跳跃距离为 4(从与起点距离17 的岩石跳到距离 21 的岩石,或者从距离 21 的岩石跳到终点)。

分析

当时看到这道题很脑大,用贪心+堆维护,只搞到20分。其实二分思路很简单,因为答案是在0..L之间的,只需要找到最后一个l使得数据符合条件。因此可以二分答案,最后得出结果。

#include <iostream>
#include <cstdio>
using namespace std;

int a[50005];
int L, M, N;

int check(int x) {
    int num = 0, last = 0;
    for (int i = 1; i <= N; i++) {
        if (a[i]-last < x)
            num++;
        else
            last = a[i];
    }
    return num<=M?1:0;
}

int main() {
    scanf("%d%d%d", &L, &N, &M);
    for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
    a[++N] = L;
    int l = 0, r = L;
    while (l < r) {
        int mid = (l+r)>>1;
        if (check(mid)) l = mid+1;
        else r = mid-1;
    }
    if (!check(l)) l--;
    printf("%d\n", l);
    return 0;
}

借教室

描述

在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。我们需要处理接下来n天的借教室信息,其中第i天学校有ri个教室可供租借。共有m份订单,每份订单用三个正整数描述,分别为dj,sj,tj,表示某租借者需要从第sj天到第tj天租借教室(包括第sj天和第tj天),每天需要租借dj个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供dj个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第sj天到第tj天中有至少一天剩余的教室数量不足dj个。
现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。

格式

输入格式

第一行包含两个正整数n,m,表示天数和订单的数量。
第二行包含n个正整数,其中第i个数为ri,表示第i天可用于租借的教室数量。
接下来有m行,每行包含三个正整数dj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。天数与订单均用从1开始的整数编号。

输出格式

如果所有订单均可满足,则输出只有一行,包含一个整数0。否则(订单无法完全满足)输出两行,第一行输出一个负整数-1,第二行输出需要修改订单的申请人编号。

样例1

  • 样例输入1[复制]

    4 3
    2 5 4 3
    2 1 3
    3 2 4
    4 2 4

  • 样例输出1[复制]

    -1
    2

限制

每个测试点1s
对于10%的数据,有1≤ n,m≤ 10;
对于30%的数据,有1≤ n,m≤1000;
对于70%的数据,有1≤ n,m≤ 10^5;
对于100%的数据,有1≤n,m≤10^6,0≤ri,dj≤10^9,1≤sj≤tj≤n。

分析

第一反应是线段树,不过显然这数据线段树会被卡常数。考虑二分法。关键是check函数,把前l个请求用类似前缀和的方法叠加起来,如果需求总数超过房间数,就返回错误。

主要方法是:对于一个需求si(开始),ti(结束),di(数量),在si时需求加上di,ti时减去di,然后前缀累加,一旦大于总教室数就报false。

bool judge(int v) {
    int sum = 0;
    memset(temp, 0, sizeof temp);
    for (int i = 1; i <= v; i++) {
        temp[sj[i]] += dj[i];
        temp[tj[i]+1] -= dj[i];
    }
    for (int i = 1; i <= n; i++) {
        sum += temp[i];
        if (sum > a[i])
            return false;
    }
    return true;
}

剩余过程脑补(O-O)

posted @ 2016-05-01 20:47  ljt12138  阅读(702)  评论(0编辑  收藏  举报