最近团队训练二分专题,颇有体悟。
二分题解第一篇,主要讲述如何判断是否使用二分以及大部分二分的“模板”,以及一些附带的“私货”。
总的来说,如果一个题的答案以及计算的过程是单调的,就大概率可以用二分优化(个人理解),大体的模板便是一个check()函数加上一个while()循环。
先来看看 洛谷的P1083 [NOIP2012 提高组] 借教室这道题
看到题目时我就有了一个明确的想法,每一个订单出现后,将在这个订单中的出现的日期的限制中的每天的教室数都减去这个订单所要借的数目,当哪一个订单出现后,有某天的教室数小于0时就输出这一订单编号。
这时经过粗略计算,并根据题目所给的数据范围我可以很确定的说如果暴力(几个for循环嵌套)肯定超时,那么这时候发现订单的顺序是递增的,租借的数目是递减的,就可以用二分去缩减订单的范围,直到最终剩下的唯一解输出。
二分循环如下:
while(l<y){//二分,用来判断l到第y天的天数中是否有负数,最终缩减范围,输出答案
int mid=l+(y-l)/2;
if(check(mid)==false){
y=mid;
}else l=mid+1;
}
当然二分模板好写,但每个题的check()却不一样。这道题通过分析可以看出,每一次的订单都是对指定范围内的数同时减去相同的一个数,自然而然可以想到差分与前缀和。
为什么要用到差分和前缀和?(请看实例)
/*一组数据:2,5,4,8,5,6,7;
经过差分(就是一个数减去上一个数,第一个数不变):
变成的差分数组 :
2 3 -1 4 3 1 1
假设在2到4天都减去3;我们只需要给差分数组的第二天减3,并在第五天加三,在用前缀和还原,
过程如下: 差分数组变为
2 0 -1 4 0 1 1
通过前缀和(a[n]=a[n-1]+q[i]):
2 2 1 5 5 6 7
此时我们会发现通过以上变换我们成功达到了在2到4天都减去3的结果。
并且如果需要多次相减,我们只需要由原数组得到的差分数组不断在两个期限的地方减去数据要求的数即可,
最终通过前缀和相加来还原最终所得的数组
*/
如此一来就可以很轻松的实现我们的方法了,check()函数代码如下:
bool check(int x){
//差分,方便接下来在区间的天的租借教室的数量的加减
//第一个for循环是为了在每一次的二分后重置为原数组(也就是每一次最开始的差分数组)
for(int i=1; i<=n; i++) cf[i]=r[i]-r[i-1];
for(int i=1; i<=x; i++){
cf[s[i]]-=d[i];
cf[t[i]+1]+=d[i];
}
//前缀和,用来看是否有教室数为负的情况,就代表订单有问题
for(int i=1; i<=n; i++){
ans[i]=ans[i-1]+cf[i];
if(ans[i]<0) return false;
}
return true;
}
最终代码如下:
#include <bits/stdc++.h>
using namespace std;
const int M =1e6 + 7;
int n,m,l,y;
long long r[M],d[M],s[M], t[M];
long long c[M],ans[M],cf[M];
bool check(int x){
//差分,方便接下来在区间的天的租借教室的数量的加减
for(int i=1; i<=n; i++) cf[i]=r[i]-r[i-1];
for(int i=1; i<=x; i++){
cf[s[i]]-=d[i];
cf[t[i]+1]+=d[i];
}
//前缀和,用来看是否有教室数为负的情况,就代表订单有问题
for(int i=1; i<=n; i++){
ans[i]=ans[i-1]+cf[i];
if(ans[i]<0) return false;
}
return true;
}
int main(){
cin>>n>>m;
for(int i=1; i<=n; i++){
scanf("%lld", &r[i]);
}
for(int i=1; i<=m; i++){
scanf("%lld%lld%lld",&d[i],&s[i],&t[i]);
}
l=1;y=n;
if(check(m)){//如果所有订单都没问题的话就直接输出
cout<<0;
return 0;
}
while(l<y){//二分,用来判断l到第y天的天数中是否有负数,最终缩减范围,输出答案
int mid=l+(y-l)/2;
if(check(mid)==false){
y=mid;
}else l=mid+1;
}
cout<<"-1"<<endl<<l;
return 0;
}
还有下期!