26日 二分 总结

1、砍树
题目描述
伐木工人米尔科需要砍倒M米长的木材。这是一个对米尔科来说很容易的工作,因为他有一个漂亮的新伐木机,可以像野火一样砍倒森林。不过,米尔科只被允许砍倒单行树木。
米尔科的伐木机工作过程如下:米尔科设置一个高度参数H(米),伐木机升起一个巨大的锯片到高度H,并锯掉所有的树比H高的部分(当然,树木不高于H米的部分保持不变)。米尔科就行到树木被锯下的部分。
例如,如果一行树的高度分别为20,15,10和17,米尔科把锯片升到15米的高度,切割后树木剩下的高度将是15,15,10和15,而米尔科将从第1棵树得到5米,从第4棵树得到2米,共得到7米木材。
米尔科非常关注生态保护,所以他不会砍掉过多的木材。这正是他为什么尽可能高地设定伐木机锯片的原因。帮助米尔科找到伐木机锯片的最大的整数高度H,使得他能得到木材至少为M米。换句话说,如果再升高1米,则他将得不到M米木材。
输入格式
第1行:2个整数N和M,N表示树木的数量(1<=N<=1000000),M表示需要的木材总长度(1<=M<=2000000000)
第2行:N个整数表示每棵树的高度,值均不超过1000000000。所有木材长度之和大于M,因此必有解。
输出格式
第1行:1个整数,表示砍树的最高高度。
输入输出样例
输入 #1
5 20
4 42 40 26 46
输出 #1
36
这道题属于一个基本的二分,得到的数的长度随着高度的增加而减小,所以这是一个单调递减序列,可以进行二分。我们对高度进行二分,st定为1,ed定为最大高度,写一个判断函数,如果在二分得到的点砍的数比需要的多,那么就把mid赋给st(往上找),如果比需要的少,那么就把mid赋给ed(往下找)。

#include<cstdio>
#include<algorithm>
using namespace std;
int n,m;
int a[10000001] = { };
int main(){
    scanf("%d%d" ,&n,&m);
    for(int i = 1;i <= n; i++){
        scanf("%d" ,&a[i]);
    }
    sort(a + 1,a + 1 + n);
    int le = 1,ri = a[n];
    while(le < ri){
        int mid = (le + ri + 1) >> 1;
        long long int now = 0;
        for(int i = 1;i <= n; i++){
            if(a[i] - mid >= 0){
                now = now + a[i] - mid;
            }
        }
        if(now >= m){
            le = mid;//
        }
        else{
            ri = mid - 1;//
        }
    }
    printf("%d" ,le);
    return 0;
}

注意一下,在二分找中点和赋中点的时候,什么时候需要加一,什么时候不加,什么时候减一呢?看它所在的那种情况是否可以成为解,可以就不加,不可以就加。那么赋中点值的时候什么时候加呢?直接背住:如果下面减一了,赋的时候就加一。
2、数列分段
题目描述

对于给定的一个长度为N的正整数数列 A 1∼N ,现要将其分成
M(M≤N)段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 4 2 4 5 1 要分成 3 段。

将其如下分段:
[4 2][4 5][1]
第一段和为 6,
第 2 段和为 9,
第 3 段和为 1,
和最大值为 9。
将其如下分段:
[4][2 4][5 1]
第一段和为 4,
第 2 段和为 6,
第 3 段和为 6,
和最大值为 6。
并且无论如何分段,最大值不会小于 6。
所以可以得到要将数列 4 2 4 5 1 要分成
3 段,每段和的最大值最小为 6。
输入格式
第 1 行包含两个正整数 N,M。第 2 行包含 N 个空格隔开的非负整数 A i ,含义如题目所述。

输出格式
一个正整数,即每段和最大值最小为多少。
输入输出样例
输入
5 3
4 2 4 5 1
输出
6
对于这道题,我们分析可以得到,对于一个最优解x,肯定会有大于x的解,肯定不会有小于x的解(因为x已经是最小的最大值了),因此这就可以抽象的看作一个函数:解的左侧是0,右侧是1,这就又构成了一个不是不单调的函数,就可以进行二分了,判断的函数

int judge(int x){
    long long int tot2 = 0;
    int h = 1;
    for(int i = 1;i <= n; i++){
        tot2 = tot2 + a[i];
        if(tot2 > x){
            if(a[i] > x) return 0;
            h++;
            tot2 = a[i]; //这里要把最后算的那个值赋回去,不然接下来算就会落掉一个
            continue;
        }
        
    }
    return h <= m;//
}

如果最后分出来的组比给定的组要大,这就说明你最大值找小了,如果小于等于给定的组,就说明你找大了

#include<cstdio>
using namespace std;
int n,m,tot = 0;
int a[100001] = { };
int judge(int x){
    long long int tot2 = 0;
    int h = 1;
    for(int i = 1;i <= n; i++){
        tot2 = tot2 + a[i];
        if(tot2 > x){
            if(a[i] > x) return 0;
            h++;
            tot2 = a[i]; 
            continue;
        }
        
    }
    return h <= m;
}
int main(){
    scanf("%d %d" ,&n,&m);
    for(int i = 1;i <= n; i++){
        scanf("%d" ,&a[i]);
        tot = tot + a[i];
    }
    int le = 1;
    int ri = 1e9 + 1;
    while(le < ri){
        int mid = (le + ri) >> 1;//这里就不用加一了,根据刚才背住的规律
        if(judge(mid) == 1){
            ri = mid;
        }
        else{
            le = mid + 1;
        }
    }
    printf("%d" ,le);
    return 0;
}

3、路标设置
题目背景
B市和T市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。
题目描述
现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。
输入格式
第1行包括三个数L、N、K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。
第2行包括递增排列的N个整数,分别表示原有的N个路标的位置。路标的位置用距起点的距离表示,且一定位于区间[0,L]内。
输出格式
输出1行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。
输入输出样例
输入
101 2 1
0 101
输出
51
这道题跟刚才很类似,对于一个最优的空旷指数,一定没有比他更小的,也一定会有比他大的,所以我们挑选出一个最大的距离对其进行二分,分出一个点后,如果这个点可以的话,按照这个间距求一下路灯数,如果小于等于总数就说明找大了,否则就找小了

#include<cstdio>
using namespace std;
int l,n,k;
int a[100001] = { },x,b[100001] = { };
int jd = 0;
bool solve(int x){
    int num = 0;
    for(int i = 1;i <= n + 1; i++){
        num = num + (b[i] - 1) / x;
    }
    return num <= k;
}
int main(){
    a[0] = 1;
    scanf("%d %d %d" ,&l,&n,&k);
    for(int i = 1;i <= n; i++){
        scanf("%d" ,&x);
        a[i] = x;
        if(x > jd) jd = x;
        b[i] = a[i] - a[i - 1];
    }
    b[n + 1] = l - a[n];
    int st = 0;
    int ed = jd;
    while(st < ed){
        int mid = (st + ed) >> 1;
        if(solve(mid)) ed = mid;
        else st = mid + 1;
    }
    printf("%d" ,st);
    return 0;
}

4、木材加工
题目描述
木材厂有一些原木,现在想把这些木头切割成一些长度相同的小段木头(木头有可能有剩余),需要得到的小段的数目是给定的。当然,我们希望得到的小段木头越长越好,你的任务是计算能够得到的小段木头的最大长度。木头长度的单位是cm。原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为11和21,要求切割成到等长的6段,很明显能切割出来的小段木头长度最长为5.
输入格式
第一行是两个正整数N和K(1 ≤ N ≤ 100000,1 ≤ K ≤ 100000000),N是原木的数目,K是需要得到的小段的数目。
接下来的N行,每行有一个1到100000000之间的正整数,表示一根原木的长度。
输出格式
能够切割得到的小段的最大长度。如果连1cm长的小段都切不出来,输出”0”。
输入输出样例
输入
3 7
232
124
456
输出
114
这道题跟上两道很类似了,所以不再多说,二分木材总长度,如果该长度下得到的木材段数多了,就说明分小了,否则就是分大了

#include<cstdio>
using namespace std;
long long int n,k,tot = 0;
int a[1000001] = { };
int main(){
    scanf("%lld %lld" ,&n,&k);
    for(int i = 1;i <= n; i++){
        scanf("%d" ,&a[i]);
        tot = tot + a[i];
    }
    long long int st = 0;
    long long int ed = tot;
    int fin = 0;
    while(st < ed){
        fin = 0;
        long long int middle = (st + ed + 1) >> 1;
        for(int i = 1;i <= n; i++){
            fin = fin + a[i] / middle;
        }
        if(fin >= k){
            st = middle;
        }
        else{
            ed = middle - 1;
        }
    }
    printf("%lld" ,st);
 return 0;
}

5、三分
三分主要的应用就是求单峰函数极值。
题目描述

如题,给出一个 N 次函数,保证在范围 [l,r] 内存在一点 x,使得
[l,x] 上单调增,[x,r] 上单调减。试求出 x 的值。
输入格式
第一行一次包含一个正整数 N 和两个实数 l,r,含义如题目描述所示。第二行包含 N+1 个实数,从高到低依次表示该 N 次函数各项的系数。
输出格式
输出为一行,包含一个实数,即为 x 的值。若你的答案与标准答案的相对或绝对误差不超过 0.00001 则算正确。
输入输出样例
输入
3 -0.9981 0.5
1 -3 -3 1
输出
-0.41421
这道题就是一个基本的模版三分了,从中点依次分开去

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n;
double l,r;
struct function{
    int mi;
    double xi;
}a[1000001];
double f(double x){
    double ans = 0;
    for(int i = 1;i <= n + 1; i++){
        ans = ans + a[i].xi * pow(x,a[i].mi);
    }
    return ans;
}
int main(){
    scanf("%d" ,&n);
    scanf("%lf %lf" ,&l,&r);
    int k = n;
    for(int i = 1;i <= n + 1; i++){
        a[i].mi = k;
        k--;
    }
    for(int i = 1;i <= n + 1; i++){
        scanf("%lf" ,&a[i].xi);
    }
    double smid,emid;
    double st = l,ed = r;
    while(ed - st > 0.00001){
        double mid = (st + ed) / 2;
        smid = mid;
        emid = mid + 0.000001;
        if(f(smid) > f(emid)){
            ed = emid;
        }
        if(f(smid) < f(emid)){
            st = smid;
        }
        if(f(smid) == f(emid)){
            st = smid;
            ed = emid;
        }
    }
    printf("%.5lf" ,st);
    return 0;
}

6、秦腾与教学评估
题目描述
在秦腾进入北京大学学习的第一个学期,就不幸遇到了前所未有的教学评估。
在教学评估期间,同学们被要求八点起床,十一点回宿舍睡觉,不 准旷课,上课不准迟到,上课不准睡觉……甚至连著名的北大三角地也在教学评估期间被以影响校容的理由被拆除。这些“变态”规定令习惯了自由自在随性生活学习的北大同学叫苦不迭。
这一天又到了星期五,一大早就是秦腾最不喜欢的高等代数课。可是因为是教学评估时期,不能迟到,于是他在八点五分的 时候挣扎着爬出了宿舍,希望能赶快混进在八点钟已经上课了的教室。
可是,刚一出宿舍楼门他就傻眼了: 从宿舍到教学楼的路上已经站满了教学评估团的成员。他们的目的就是抓住像他这样迟到的学生,扣除学校的分数。
秦腾当然不能让评估团得逞。他经过观察发现,整个评估团分成了N个小组,每个小组的成员都分布在从宿舍楼到教学楼的路上的某一段,并且同一小组的成员间的距离是相等的。于是,我们可以用三个整数S, E, D来描述评估团的小组: 既该小组的成员在从宿舍到教学楼的路上的:S, S + D, S + 2D, …, S + KD (K ∈ Z, S + KD ≤ E, S + (K + 1)D > E)位置。
观 察到了教学评估团的这一特点,又经过了认真的思考,秦腾想出了对策: 如果在路上的某一位置有奇数个教学评估团成员,他就可以运用调虎离山,声东击西,隔山打牛,暗度陈仓……等方法,以这一地点为突破口到达教学楼。
但是由于 教学评估团的成员的十分狡猾,成员位置安排的设计极其精妙,导致在整条路上几乎没有这样的位置出现。即使由于安排不慎重出现了这样的位置,最多也仅有一个。
现在秦腾观察出了所有小组的安排,但是由于整个教学评估团的人数太多,他实在看不出这样的位置是否存在。
现在,你的任务是写一个程序,帮助他做出判断。
输入格式
输入文件的第一行为一个整数T。
接下来输入T组相互独立的测试数据。
每组测试数据的第一行包含一个整数,代表N接下来的N行,每行三个整数Si, Ei, Di, 代表第i个小组对应的三个参数。
输出格式
对于每个测试数据,如果题目中所求的位置不存在,既任意位置都有偶数个教学评估团的成员存在,在输出文件的中打印一行:Poor QIN Teng:( (不包含引号)否则打印两个整数Posi, Count,代表在唯一的位置Posi,有Count个教学评估团的成员。
根据题意,Count应为奇数。
输入输出样例
输入
3
2
1 10 1
2 10 1
2
1 10 1
1 10 1
4
1 10 1
4 4 1
1 5 1
6 10 1
输出
1 1
Poor QIN Teng:(
4 3
这道题,显然最无脑,最轻松的写法就是暴搜,用一个桶存人,然后从前到后扫一遍找到奇数,但是显然这样只能得20分。。。
所以我们另辟蹊径,对于唯一一个奇数的点,他前面任意一个数的前缀和都是偶数,他后面任意一个数的前缀和都是奇数,利用这个我们就又能发现一个不不单调的序列,前缀和的计算方法是长度/间隔+1

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int M=1e9;
long long n,t;
long long mx=0;
struct node{
    long long a,b,m;
}p[200500];
long long check(int x){
    long long tot=0;
    for(int i=1;i<=n;i++){
        if(p[i].b<=x){
            tot += (p[i].b-p[i].a)/p[i].m+1;
        }
        else if(p[i].a>x) continue;
        else{
            tot += (x-p[i].a)/p[i].m+1;
        }
    }
    return tot;
}
int main(){
    scanf("%lld",&t);
    for(int k=1;k<=t;k++){
        mx=0;
        scanf("%lld",&n);
        for(int i=1;i<=n;i++){
            scanf("%lld%lld%lld"
            ,&p[i].a,&p[i].b,&p[i].m);
            mx=max(mx,p[i].b);
        }
        long long st=0,ed=mx+1;
        while(st<ed){
            long long mid=(st+ed) >> 1;
            if(check(mid)%2==1) ed=mid;
            else st=mid+1;
        }
        if(st==mx+1){
            printf("Poor QIN Teng:( \n");
            continue;
        }
        printf("%lld %lld\n",st,check(st)-check(st-1));
    }
    return 0;
}

7、自动刷题机
题目背景
曾经发明了信号增幅仪的发明家 SHTSC 又公开了他的新发明:自动刷题机——一种可以自动 AC 题目的神秘装置。
题目描述
自动刷题机刷题的方式非常简单:首先会瞬间得出题目的正确做法,然后开始写程序。每秒,自动刷题机的代码生成模块会有两种可能的结果:
1.写了 x 行代码
2.心情不好,删掉了之前写的 y 行代码。(如果 y 大于当前代码长度则相当于全部删除。)对于一个 OJ,存在某个固定的正整数长度 n,一旦自动刷题机在某秒结束时积累了大于等于 n 行的代码,它就会自动提交并 AC 此题,然后新建一个文件(即弃置之前的所有代码)并开始写下一题。SHTSC 在某个 OJ 上跑了一天的自动刷题机,得到了很多条关于写代码的日志信息。他突然发现自己没有记录这个 OJ 的 n 究竟是多少。所幸他通过自己在 OJ 上的 Rank 知道了自动刷题机一共切了 k 道题,希望你计算 n 可能的最小值和最大值。
输入格式
第一行两个整数 l,k,表示刷题机的日志一共有 l 行,一共了切了
k 题。接下来 l 行,每行一个整数 x i ,依次表示每条日志。若
x i​ ≥0,则表示写了 x i行代码,若 x i <0,则表示删除了 −xi行代码。
输出格式
输出一行两个整数,分别表示
n 可能的最小值和最大值。
如果这样的 n 不存在,请输出一行一个整数 −1。
输入输出样例
输入
4 2
2
5
-3
9
输出
3 7
这道题,我们可以发现,对于给定的序列,n越大能过的题是越少的,所以可以二分来求刚好过k道题的左右边界。

若mid大于k,即做得太多了,就将l右移。

若mid小于k,即做得太少了,就将r左移。

求左边界,需要在mid等于k时将r左移,求右边界时则需将l右移。这个很好理解。

印象里二分写法极多,但现在普遍应用l<=r, l=mid+1, r=mid-1这个版本了,虽然要多记录一个ans,但是却在单调增单调减时都能工作,并且可以轻松应对无解的情况。

注意这种写法l应设为1,r设为无穷大即可。

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

int N, k, a[1000000];
long long int l, r, ans1 = -1, ans2 = -1;

long long int work(long long int im)
{
    int ans = 0;long long int sum = 0;
    for (int i = 1; i <= N; i++)
    {
        sum = max(sum + a[i], 0ll);
        if (sum >= im) sum = 0, ans++;
    }return ans;
}

int main()
{
    scanf("%d%d", &N, &k);
    for (int i = 1; i <= N; i++)
        scanf("%d", a + i);
    l = 1, r = 1e14; 
    while (l <= r)
    {
        long long int mid = (l + r) >> 1;
        if (work(mid) <= k)
        {
            r = mid - 1;
            if (work(mid) == k) ans1 = mid;
        }
        else l = mid + 1;
    }
    
    l = 1, r = 1e14; 
    while (l <= r)
    {
        long long int mid = (l + r) >> 1;
        if (work(mid) >= k)
        {
            l = mid + 1;
            if (work(mid) == k) ans2 = mid;
        }
        else r = mid - 1;
    }
    if (ans1 == -1) printf("-1");
    else printf("%lld %lld\n", ans1, ans2);
}

至此,二分的题我还差了一个三次方程求解,我也不知道怎么就wa了。写完这一篇博客可以发现,很多东西编完第一遍因为赶时间没有好好总结,有的忘了不少,就导致做题的价值大大削弱了。所以以后尽量要及时写博客,总结要写的详细一点,调题调的时间长的地方如果没时间立刻写博客至少也要加个注释简单标记一下,不然忘的太快了。
ps.其实二分是可以递归实现的,只是递归二分非主流,不好实现也不好调,知道就得了(其实没讲的时候我写的都是递归)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页