二分答案基础

二分

为什么我为OI泪目?因为我菜得离谱…

引入

用来解决有序问题,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做 1,不满足看做 0,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值。

一言以概之:最大值最小化,最小值最大化
这些问题往往难以通过数据求出最优解,于是我们通过二分枚举问题最优解,再放入数据检查的方式,来得到答案。
发一下二分答案板子

int l=0,r=inf;
while(l<=r)
{
	int mid=(l+r)/2;
    if(check(mid)) l=mid+1;
    else r=mid-1;
}

其中check()表示你写的答案检查函数,l,r表示左右区间
这个板子可以说是万金油,l,r不用思考就这样写,但最后check()符合条件返回0还是1,以及答案是在l还是r就要自己思考了。
接下来有三道例题,读者可选读。

例题

木材加工 P2440黄

题目背景

要保护环境

题目描述

木材厂有 n n n 根原木,现在想把这些木头切割成 k k k 段长度 l l l 的小段木头(木头有可能有剩余)。

当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。

木头长度的单位是 cm \text{cm} cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。

例如有两根原木长度分别为 11 11 11 21 21 21,要求切割成等长的 6 6 6 段,很明显能切割出来的小段木头长度最长为 5 5 5

输入格式

第一行是两个正整数 n , k n,k n,k,分别表示原木的数量,需要得到的小段的数量。

接下来 n n n 行,每行一个正整数 L i L_i Li,表示一根原木的长度。

输出格式

仅一行,即 l l l 的最大值。

如果连 1cm \text{1cm} 1cm 长的小段都切不出来,输出 0

样例 #1
样例输入 #1
3 7
232
124
456
样例输出 #1
114
提示
数据规模与约定

对于 100 % 100\% 100% 的数据,有 1 ≤ n ≤ 1 0 5 1\le n\le 10^5 1n105 1 ≤ k ≤ 1 0 8 1\le k\le 10^8 1k108 1 ≤ L i ≤ 1 0 8 ( i ∈ [ 1 , n ] ) 1\le L_i\le 10^8(i\in[1,n]) 1Li108(i[1,n])

解析

读者可以很轻松得发现,我们没什么办法求出l的最优解,但如果我们得到了 l l l 的值,check()进行检查只需要 O ( n O(n O(n)的复杂度。
那我们能不能枚举答案呢?
如果直接枚举的话,复杂度高达 n × k n\times k n×k(10^13) ,考虑到答案具有单调性,如果l变大那么段数肯定变小(起码不会出现段数增大的情况)
想清楚这些我们就可以开始二分答案了
check()函数检查的是段数是否<=条件,符合条件说明段数太多,长度太短,于是长度应当缩小,应舍弃左区间的答案,应将左端点右移到mid。(注意这些判断性的语句,错了一个你的代码都有调好久)
再提一句:
二分右端点应当开大到可能区间的2倍,
最好在来一发#define int long long
以减少见祖宗的概率(保险起见,不然可能会调死你)

下面解析简略(^U^)ノ~YO

CODE1
#include<bits/stdc++.h>
#define mid ((l+r)>>1)
using namespace std;
#define int long long
const int tsn=1e5+5;
const int inf=1e9+5;
int a[tsn];
int n,k;
int check(int kis)
{
    if(kis==0) return 1;
    long long sum=0;
    for(int i=1;i<=n;i++) sum+=a[i]/kis;
    // cout<<kis<<" :sum: "<<sum<<endl;
    if(sum>=k) return 1;
    return 0;
}
signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    #ifndef ONLINE_JUDGE
        freopen("00in.txt","r",stdin);
        freopen("00out.txt","w",stdout);
    #endif
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>a[i];
    int l=0,r=inf;while(l<=r)
    {
        // cout<<"l,r:"<<l<<" "<<r<<endl;
        if(check(mid)) l=mid+1;
        else r=mid-1;
    }
    cout<<r;
    return 0;
}


[TJOI2007] 路标设置 P3583黄题

题目背景

B 市和 T 市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的“空旷指数”。

题目描述

现在政府决定在公路上增设一些路标,使得公路的“空旷指数”最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。

输入格式

1 1 1 行包括三个数 L , N , K L,N,K L,N,K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。

2 2 2 行包括递增排列的 N N N 个整数,分别表示原有的 N N N 个路标的位置。路标的位置用距起点的距离表示,且一定位于区间 [ 0 , L ] [0,L] [0,L] 内。

输出格式

输出 1 1 1 行,包含一个整数,表示增设路标后能达到的最小“空旷指数”值。

样例 #1
样例输入 #1
101 2 1
0 101
样例输出 #1
51
提示

公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点 50 50 50 51 51 51 个单位距离处,这样能达到最小的空旷指数 51 51 51

50 % 50\% 50% 的数据中, 2 ≤ N ≤ 100 2 \leq N \leq 100 2N100 0 ≤ K ≤ 100 0 \leq K \leq 100 0K100

100 % 100\% 100% 的数据中, 2 ≤ N ≤ 100000 2 \leq N \leq 100000 2N100000, 0 ≤ K ≤ 100000 0 \leq K \leq100000 0K100000

100 % 100\% 100% 的数据中, 0 < L ≤ 10000000 0 < L \leq 10000000 0<L10000000

解析

二分答案每个路标之间的间隔即可,不用担心中间间隔不等的情况,因为我们求的是最大间距。

CODE2
#include<bits/stdc++.h>
#define mid (L+R>>1)
using namespace std;
int l,n,k;
const int tsn=1e7+5;
const int inf=1e9+10;
int b[tsn];
int check(int key)
{
    if(key==0) return 0;
    long long sum=0;
    for(int i=1;i<=n;i++)
        if(b[i]-b[i-1]-key>0) sum+=(b[i]-b[i-1]-1)/key;
    if(sum<=k) return 1;
    return 0;
}
int main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    #ifndef ONLINE_JUDGE
        freopen("00in.txt","r",stdin);
        freopen("00out.txt","w",stdout);
    #endif
    cin>>l>>n>>k;
    for(int i=1;i<=n;i++) cin>>b[i];
    int L=0,R=inf;while(L<=R)//ans in R
    {
        if(check(mid)) R=mid-1;
        else L=mid+1;
    }
    cout<<L;
    return 0;
}


数列分段 Section II P1182黄题

题目描述

对于给定的一个长度为 N N N 的正整数数列 A 1 ∼ N A_{1\sim N} A1N,现要将其分成 M M M M ≤ N M\leq N MN)段,并要求每段连续,且每段和的最大值最小。

关于最大值最小:

例如一数列 4   2   4   5   1 4\ 2\ 4\ 5\ 1 4 2 4 5 1 要分成 3 3 3 段。

将其如下分段:

[ 4   2 ] [ 4   5 ] [ 1 ] [4\ 2][4\ 5][1] [4 2][4 5][1]

第一段和为 6 6 6,第 2 2 2 段和为 9 9 9,第 3 3 3 段和为 1 1 1,和最大值为 9 9 9

将其如下分段:

[ 4 ] [ 2   4 ] [ 5   1 ] [4][2\ 4][5\ 1] [4][2 4][5 1]

第一段和为 4 4 4,第 2 2 2 段和为 6 6 6,第 3 3 3 段和为 6 6 6,和最大值为 6 6 6

并且无论如何分段,最大值不会小于 6 6 6

所以可以得到要将数列 4   2   4   5   1 4\ 2\ 4\ 5\ 1 4 2 4 5 1 要分成 3 3 3 段,每段和的最大值最小为 6 6 6

输入格式

1 1 1 行包含两个正整数 N , M N,M N,M

2 2 2 行包含 N N N 个空格隔开的非负整数 A i A_i Ai,含义如题目所述。

输出格式

一个正整数,即每段和最大值最小为多少。

样例 #1
样例输入 #1
5 3
4 2 4 5 1
样例输出 #1
6
提示

对于 20 % 20\% 20% 的数据, N ≤ 10 N\leq 10 N10

对于 40 % 40\% 40% 的数据, N ≤ 1000 N\leq 1000 N1000

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 5 1\leq N\leq 10^5 1N105 M ≤ N M\leq N MN A i < 1 0 8 A_i < 10^8 Ai<108, 答案不超过 1 0 9 10^9 109

解析

二分答案每段和的最大和,再带入check()段数是否符合条件即可。

CODE3
#include<bits/stdc++.h>
#define mid (l+r>>1)
using namespace std;
const int tsn=1e5+5;
const int inf=1e9;
int n,m;
int a[tsn];
int check(int key)
{
    int sum=1;
    for(int i=1,tans=0;i<=n;i++)
    {
        // if(tans>key) sum++,tans=0; 
        if(a[i]>key) return 0;
        tans+=a[i];
        if(tans>key) sum++,tans=a[i];
    }
    if(sum<=m) return 1;
    return 0;
}
int main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    #ifndef ONLINE_JUDGE
        freopen("00in.txt","r",stdin);
        freopen("00out.txt","w",stdout);
    #endif
    cin>>n>>m;
    int maxn=0;
    for(int i=1;i<=n;i++) cin>>a[i];
    int l=1,r=2*inf;while(l<=r)
    {
        if(check(mid)) r=mid-1;
        else l=mid+1;
    }
    cout<<l;
    return 0;
}


完结撒花

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值