本来想着每天更一篇,但是昨天由于各种原因断了…
二分答案这个知识陆陆续续看了好几天,也看了好多博主的文章,虽然明白基本二分的基本思路,但是那个check函数是真的难写,到现在还是有点迷。
看文章的时候我见到过好多二分答案模板的写法,不过还是觉得下面这两个比较舒服:
(l和r分别为初始时区间的下界和上界)
①当二分区间为[l,mid] [mid+1,r]时:
while(l<r)
{
int mid=(l+r)>>1;
if(check(mid))
{
r=mid;
}
else
{
l=mid+1;
}
}
②当二分区间为[l,mid-1] [mid,r]时:
while(l<r)
{
int mid=(l+r+1)>>1;
if(check(mid))
{
l=mid;
}
else
{
r=mid-1;
}
}
上面两个模板分别对应两种题型:最大值最小化问题和最小值最大化问题
具体为什么能解决这两类问题,这位大佬的证明和总结我觉得还是比较OK的:https://www.acwing.com/blog/content/91/
下面以两道最近做过的题为例:
1.计蒜客信息学题库 T1877 数列分段
(题目是从洛谷上截的)
题目很直观了,最大值最小化。我感觉这道题还有点模拟的意思在里面,模拟把一列数分成m段。由于要找的是最大值,并且数列的顺序确定了,那么根据贪心来说,加的数越多值越大。其实就是每次在比mid小的数中,找到最大的那个,然后利用check函数和上面的两个模板更新区间
直接上AC 代码了:(经过大佬指点,并且想了好久后的ac代码…不得不说数学真的太重要了)
#include
using namespace std;
int m[100005];
int N,M;
bool check(int x)
{
int sum=0;
int cnt=0;//把cnt模拟成分割线
for(int i=1;i<=N;i++)
{
sum+=m[i];//一直往右贪心
if(sum>x)//如果比mid大,那此时后面的数就不能加上了,前面数的和就是比mid小的数中最大的
{
cnt++;//画一条分割线
sum=m[i];//sum要重新开始
}
}
return cnt<=M-1;//这里就是当要分三段时,需要的分割线就是两条
//但是当cnt小于M-1时,说明此时的分法并不是最优解,还能按照这种分法继续往下分,所以返回true
//如果cnt>M-1 那就跟题意不一样了,直接返回false
}
int main()
{
cin>>N>>M;
int l=0;
int r=0;
for(int i=1;i<=N;i++)
{
cin>>m[i];
l=max(l,m[i]);//根据题意确定最初的区间 如果不能确定,那可以把l初始化成1,把r初始化成一个比较大的数
r+=m[i];
}
while(l<r)
{
int mid=(l+r)>>1;
if(check(mid))
{
r=mid;
}
else
{
l=mid+1;
}
}
cout<<l<<endl;
return 0;
}
2.NOIP 2015 跳石头
最短距离的最大值即最小值最大化,明确两个问题:移动的石头越多距离越大,相邻两个石头的距离一定是比中间隔着石头要短。同样需要把移动石头的过程模拟出来,每次在比mid大的数里找到最小的那个,然后实现区间的更新。
AC代码:
#include
using namespace std;
typedef long long LL;
const int N=50005;
int n,m;
LL d[N];
LL L;
bool check(int x)
{
int p=1;//起点的石头和终点的石头不能移 保留起点的石头
int cnt=0;//记录有几个石头被干掉
for(int i=1;i<=n;i++)
{
if(d[i]-p>=x)//每次和起点的石头相减 因为和第一个石头相减的值一定大于和前一个石头相减的值
{
p=d[i];//记下比mid大的最小那个数
}
else//如果不比mid大 就干掉一个石头
{
cnt++;
}
}
return cnt<=m;
}
int main()
{
scanf("%d%d%d",&L,&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&d[i]);
}
int l=0;
int r=L;
while(l<r)
{
int mid=(l+r+1)>>1;
if(check(mid))
{
l=mid;
}
else
{
r=mid-1;
}
}
printf("%d\n",l);
return 0;
}
不过还有个问题,第二个模板里求mid为啥还要加1
数学是真的有用,但是到大学依然很菜…