第一部分思考过程
一般写二分的思考顺序是这样的:首先通过题目背景和check(mid)函数的逻辑,判断答案落在左半区间还是右半区间。
左右半区间的划分方式一共有两种:
中点mid属于左半区间,则左半区间是[l, mid],右半区间是[mid+1, r],更新方式是r = mid;或者 l = mid + 1;,此时用第一个模板;
中点mid属于右半区间,则左半区间是[l, mid-1],右半区间是[mid, r],更新方式是r = mid - 1;或者 l = mid;,此时用第二个模板;
二分模板
二分浮点数
#include<bits/stdc++.h>
using namespace std;
int main()
{
double x;
cin>>x;
double l=0;
double r=x;
while(r-l>1e-8)
{
double mid=(l+r)/2;
if(mid*mid>=x)
r=mid;
else l=mid;
}
cout<<l<<endl;
}
有一个经验之谈就是,题目让你保留几位小数,我们比题目多保留两位小数
第三部分应用
二分的基础用法是在单调序列或单调函数中进行查找,因此当问题的答案具有单调性的时候,我们就可以通过二分把求解转化为判定,根据复杂度理论(判定的时间复杂度小于求解)
如果题目说序列单调不减,那么我们很容易就想到二分;
最方便的就是使用stl自带的二分函数
upper_bound和lower_bound
这两个函数的作用是二分查找一个数在数组中出现的位置,区别是upper_bound返回的是数组中第一个大于搜索数的位置,而lower_bound返回的是数组中第一个不小于搜素数的位置。
函数的用法lower_bound(a.begin(),a.end(),x)-a;
x为要查找的数;
注意在函数的后面要-a
也就是减去地址
在这一题中函数的写法lower_bound(a+1,a+1+n,x);
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
inline int read()
{
int x=0;
char c;
int f=1;
x=getchar();
while(c<'0'||c>'9')
{
if(c=='-')
f=-1;
c=getchar();
}
while(x>='0'&&x<='9')
{
x=x*10,x=x+c-'0';
c=getchar();
}
return x*f;
}
int a[maxn];
int main()
{
int n=read();
int m=read();
for(int i=1;i<=n;i++)
a[i]=read();
while(m--)
{
int x=read();
int ans=lower_bound(a+1,a+1+n,x);
if(x!=a[ans])
cout<<-1<<" ";
else cout<<ans<<" ";
}
return 0;
}
分析
这一天看了看二分,发现不是特别难。
在学习一个算法的时候我们要明白我们为什么要学习它
因为二分可以使复杂度降低到(log(n)),在大数据的情况下,比(O(n))朴素的暴力要好。
什么时候使用
在题目中说到最大值最小,或者说最小值最大,就可以使用二分进行求解
要想使用二分搜索法来解这种「最大值最小化」的题目,需要满足以下三个条件:
-
答案在一个固定区间内;
-
eg:
比如我们进行抄书,那就暗示着我们二分出来的答案所对应的人数要小于等于总人数,否则就是不合法的,即check函数return tot<=m(tot是当前二分出来的答案所对应的人数,m是总人数)
-
可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的;
-
可行解对于区间满足一定的单调性。换言之,如果(x) 是符合条件的,那么有 (x+1)或者 (x-1)也符合条件。(这样下来就满足了上面提到的单调性)
原理
二分法把一个寻找极值的问题转化成一个判定的问题(用二分搜索来找这个极值)。类比枚举法,我们
当时是枚举答案的可能情况,现在由于单调性,我们不再需要一个个枚举,利用二分的思路,就可以用
更优的方法解决「最大值最小」、「最小值最大」。这种解法也成为是「二分答案」,常见于解题报告
中。
心得
我觉得二分中比较重要的就是check函数
这个函数帮助我们不断逼近答案,
在check函数中进行模拟
判断当前$check(int k) $$ k$是否符合
在主函数中的(while)循环里面的条件,根据题面的不同进行修改,比如有的是浮点数,就需要让(r-l<=0.000001),记得初始化(l=0,r=1e9)(正无穷)
例题
题目背景
大多数人的错误原因:尽可能让前面的人少抄写,如果前几个人可以不写则不写,对应的人输出 0 0
。
不过,已经修改数据,保证每个人都有活可干。
题目描述
现在要把 m 本有顺序的书分给 k个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。
现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。
输入格式
第一行两个整数 m,k
第二行 m 个整数,第 i个整数表示第 i 本书的页数。
输出格式
共 k行,每行两个整数,第 i 行表示第 i个人抄写的书的起始编号和终止编号。 k 行的起始编号应该从小到大排列,如果有多解,则尽可能让前面的人少抄写。
输入输出样例
输入
9 3
1 2 3 4 5 6 7 8 9
输出
1 5
6 7
8 9
这道题典型的二分题,但是它要求尽可能让前面的人少抄写
这怎么办,我们可以倒着枚举,这样就可以使后面的人抄尽量多的书。
再注意一下细节,这道题就可以A掉了
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=600;
int m,k;
int a[maxn];
bool check(int fuzhi)
{
int tot=1;
int cnt=0;
for(int i=1;i<=m;i++)
{
if(a[i]>fuzhi)
return false;
if(tot>k)
return false;
if(cnt+a[i]<=fuzhi)
cnt+=a[i];
else cnt=a[i],tot++;
}
return tot<=k;
}
int main(){
cin>>m>>k;
for(int i=1;i<=m;i++)
cin>>a[i];
int l=0;
int r=1e9;
while(l<=r)
{
int mid=(l+r)/2;
if(check(mid))
r=mid-1;
else l=mid+1;
}
int tail[maxn];
int head[maxn];
int cnt=0;
int tot=k;
tail[tot]=m;
head[1]=1;
for(int i=m;i>=1;i--)
{
if(cnt+a[i]>l)
{
cnt=a[i];
head[tot]=i+1;
tail[--tot]=i;
}
else cnt=cnt+a[i];
}
for(int i=1;i<=k;i++)
cout<<head[i]<<" "<<tail[i]<<endl;
return 0;
}