首页 > 代码库 > 二分答案入门乱讲
二分答案入门乱讲
1.关于二分答案
如果reader没有学过二分,那么我建议您把这个网站关掉。不是我有偏见或者什么,看这篇文章对不了解二分的人来说没有好处。
对 于一些问题,它的解满足单调性,即如果x满足条件,则对于任意的 i ( 1<=i<=x) 或 (x <=i <=n) (假设1和n是答案的上下界)都会满足条件。一般遇上这种问题,我们就可以用二分答案来加快解决。这种问题常常有关键语句:使最大......最小。
对于上面的问题,在没学二分答案的时候,我们是这么写的:(假设答案是上界)
for(int i=1;i<=n;++i) if(!check(i)) { Ans=i-1; break; }
枚举答案,进行检查。出现第一个不合法的解,答案就是它前面那个值。但这样看来,未免太慢。我们花了很长时间来检查每一个答案。如果答案是10000,我们就会做很多无用功。或者说我们枚举1000是正确的,那么前999个都可以看成白枚举了,很浪费。
那么我们该如何尽可能的少做这些无用功呢?
我 们来设想一下。同样假设答案是上界,如果我们check了10000,发现它是满足解的,那么答案肯定不小于10000。如果我们又check了 20000,发现它是满足解的,那么10000~20000内的数我们都不用枚举。又或者20000是不满足解的,那么答案就在10000~20000的 左闭右开区间内。这个时候我们如果”恰当地“check 15000,答案的范围会进一步缩小。
看到这里我们大概都会想到二分了。一步一步地缩小答案范围最终出解。
身边的巴拉拉2016对我说:二分答案的板子大家都会啊。
int L=1,R=n; while(L<=R) { int mid=(L+R)>>1; if(check(mid))L=mid+1; else R=mid-1; } printf("%d",L);
是啊,zz的板子谁都会。重点就在check函数上面。我们要使用时间复杂度最优的check来写。怎么写出最好的方法呢?分析题目,优化算法,然后大胆猜想不用证明。多做题目,积累经验。然后因为我走歪了,check一向非主流,但莫名好用啊(其实check只要有不合法就可以立即弹出,复杂度可以降到玄学级)。
2.例题
下面分享一些题目,都是NOIP里面的水题,都是可以一遍AC的。
1.NOIP2015 河中跳房子
最大距离最小,一看就知道是二分答案。
那么该如何写check呢?
我们可以check当最大距离为mid时,所需要搬走的石头的个数。方法就是一个小小的贪心——能不拿走就不拿走,能少拿走就少拿走,然后一次check在O(n)的时间内跑过。总时间复杂度是O(nlogn)。
#include <iostream> #include <cstdio> #include <cstdlib> #include <algorithm> int L,M,N,pla[50100]; int gi() { int x=0;char ch=getchar(); while(ch>‘9‘ || ch<‘0‘)ch=getchar(); while(ch>‘/‘ && ch<‘:‘)x=(x<<1)+(x<<3)+ch-48,ch=getchar(); return x; } int check(int x) { int Ans=0,sta=0; for(int i=1;i<=N;++i) { while(pla[i]-sta<x && i<=N) { Ans++;i++; } sta=pla[i]; } return Ans; } using namespace std; int main() { L=gi();N=gi();M=gi();pla[0]=0; for(int i=1;i<=N;++i)pla[i]=gi(); pla[++N]=L; int l=1,r=L; while(l<=r) { int mid=(l+r)>>1,S=check(mid); if(S>M)r=mid-1; else l=mid+1; } printf("%d",r); }
我们check的返回值就是最少需要搬走的石头个数,可以证明Ans就是最优值。
2.NOIP2012 借教室
因为答案具有单调性——答案之前的肯定都能借到。所以我们来二分答案。
那么该如何写check函数呢?
对于这道题,我们需要干的事有:区间增加和单点求值。可能大犇都想到了线段树,但这种复杂度贼高的算法最高只能过90分。我们没必要用高级算法,可以用一种小技巧处理——差分+前缀和。
所 谓差分,就是把区间的操作转移到对区间端点的操作。比如我们对一个都是0的数组,要在5~13这段区间加上3,只要在5这个点上加上3,在14这个点上减 掉3,这样在算前缀和的时候,0~4都是0,5~13都是3,14及以后又都成了0。这种操作修改是O(1),查询是O(n),但你可以一遍查询求出所有 的点是否合法。所以查询的总复杂度就是O(n+m),题目的复杂度就是O((n+m)logm)。
#include <iostream> #include <cstdio> #include <cstdlib> using namespace std; int n,m,num[1010000],d[1010000],sta[1010000],end[1010000],Q[1010000],L,R; int gi() { int x=0;char ch=getchar(); while(ch>‘9‘ || ch<‘0‘)ch=getchar(); while(ch>‘/‘ && ch<‘:‘)x=(x<<1)+(x<<3)+ch-48,ch=getchar(); return x; } bool check(int x) { int total=0; for(int i=1;i<=n;++i)Q[i]=0; for(int i=1;i<=x;++i)Q[sta[i]]+=d[i],Q[end[i]+1]-=d[i]; for(int i=1;i<=n;++i) { total+=Q[i]; if(total>num[i])return false; } return true; } int main() { n=gi();m=gi(); for(int i=1;i<=n;++i)num[i]=gi(); for(int i=1;i<=m;++i)d[i]=gi(),sta[i]=gi(),end[i]=gi(); L=1;R=m; while(L<=R) { int mid=(L+R)>>1; if(check(mid))L=mid+1; else R=mid-1; } if(L>m)printf("0"); else printf("-1\n%d",L); return 0; }
我们check函数返回的是当前订单能否满足。
3.NOIP2011 聪明的质检员
一看就知道是二分答案:答案具有单调性,图像趋势类似于一个二次函数曲线,我们只要求出这个函数的顶点最近的整数就好了。二分答案,记录当前答案,比较一下就好了。
这里要解释一下那个式子的意思:L到R内所有满足Wj>W的j的个数乘以它们的体积和。这个可以用前缀和维护。开两个前缀和维护一下个数和体积和就好了。
#include <cstdio> #include <cstdlib> #include <iostream> #include <algorithm> #define LL long long int LL n,m,s,l[201000],r[201000],V[201000],W[201000],Qnum[200100],QY[200100],maxR; LL gi() { LL x=0;char ch=getchar(); while(ch>‘9‘ || ch<‘0‘)ch=getchar(); while(ch>‘/‘ && ch<‘:‘)x=x*10+ch-48,ch=getchar(); return x; } LL max(LL x,LL y){return x>y?x:y;} LL min(LL x,LL y){return x<y?x:y;} LL check(LL x) { LL Ans=0; for(int i=1;i<=n;++i) { Qnum[i]=Qnum[i-1]+(W[i]>=x); QY[i]=QY[i-1]+V[i]*(W[i]>=x); } for(int i=1;i<=m;++i){Ans+=(Qnum[r[i]]-Qnum[l[i]-1])*(QY[r[i]]-QY[l[i]-1]);} return Ans-s; } int main() { n=gi();m=gi();s=gi(); for(int i=1;i<=n;++i)W[i]=gi(),V[i]=gi(),maxR=max(maxR,W[i]); for(int i=1;i<=m;++i)l[i]=gi(),r[i]=gi(); { LL L=1,R=maxR,K=10000000000000; while(L<=R) { LL mid=(L+R)>>1,S=check(mid); if(S<0)R=mid-1,K=min(-S,K); else L=mid+1,K=min(S,K); } printf("%lld\n",K); } return 0; }
check函数返回的是当前答案下的W的原值(不能带abs())
如果小于0,证明这个点在原点右边,要往左边挪。否则往右边挪。
那就这么草率的结尾吧!
二分答案入门乱讲