P3853 [TJOI2007]路标设置 (二分答案)

什 么 最 大 值 最 小 , 最 小 值 最 大 — — 二 分 就 完 了 ! 什么最大值最小,最小值最大——二分就完了!


题目描述:

洛谷题目传送门戳他!!


解题思路:

一般遇见对答案有最优要求,且有各种奇奇怪怪(一看就想让人去模拟)的条件,都可以用复杂度为O(log n)的二分答案来高效查找答案。


分析:

看到这种题目,我们可以直接暴力枚举加剪枝枚举最优答案,然后愉快超时……
我们为什么对于这种题目不暴力呢?因为怕超时?错!是出于程序猿的尊严,暴力太没逼格了!

对于粗暴的枚举,我们更愿意接受优雅的二分答案

相信各位对于什么二分思路啊,二分查找什么的都不陌生,这里就不过多阐述了,直接上模板!

  • 如果真的想零基础学习二分,那么戳他!

二分模板

void search(int left,int right)
{
    
    
	int mid,ans;
	while(left<=right)  //边界条件很重要,主要看题目要求,我喜欢这么写
	  {
    
    
	  	mid=left+(right-left)/2;  //二分查找技巧,防止数据溢出
	  	//check函数根据题目来写内容,基本上就是模拟题目的某个步骤的实现过程,最后判断是否可行
	  	if(check(mid))
	  	  {
    
    
	  	  	ans=mid;  //考虑当前数为答案
	  	  	left=mid+1;  //基础二分改变左右边界
	  	  }
	  	else right=mid-1;
	  }
}

模板在手,二分我有( AC瞬间 )


没错,只要有模板,二分查找本身是人都会。但是,往往二分题目的难点并不仅仅在于边界条件的设定,而更在于二分算法中那个传说中的 check 函数!!
二分中的check ,顾名思义,就是通过各种玄学算法(搜索、模拟、贪心、DP等)来判断这个当前要试的mid是否合法,如果合法则根据这个题目的条件改变二分的左右界,最后找到最优的答案。
因此,check函数函数的重要性以及编写难度也就一目了然了。
那,到底难在什么地方呢?

  • 眼光要准(对check函数的作用要明确)

要准确的明白此时此刻,你在二分查找的量到底是什么,只有明白了二分两边的单调性才能知道你编写的check函数到底要用什么算法,要做什么。

  • 算法要好( check函数的效率一定要高,能直接判断是否可行就不要模拟!!)

解题思路(这才是正解!!!):

就拿上面的例题来说吧,题目要求我们要让路标之间的空旷指数最小,也就是说让相邻距离最大的两个路标之间的距离尽量小。

这时候,我们要二分枚举的量是什么呢?是当前可能的空旷指数啊!我们通过二分找出一个mid,把他设为当前可能的空旷指数,然后用check函数判断这个指数是否合法(就是说当前空旷指数是否可行),如果合法,那么就说明当前的mid 可能是本题的最优解,把他记下。然后将右边界向左移动,继续二分。

为什么合法就要像左移动右边界呢?因为如果合法的话,根据二分的单调两段性,我们可以知道在当前 mid 右边的所有数,哪怕合法,也只会是比当前 mid 要大的数。而根据题目我们要求的是最小的 mid ,所以答案当然不可能存在于 当前 mid 的右边),如果不合法则反之。

那么难点又来了,这个判断当前mid是否合法的check函数到底怎么写呢?其实这里可以用到一个二分答案check 函数中的一个通用算法——模拟大法!我们可以尝试从第一个路标往后,在每两个路标之间判断,如果这两个路标之间的距离大于了当前尝试的 mid ,那么我们就在这里塞路标,尽可能的让这里的距离小于 mid。如果最后插入的总路标数仍然小于规定的路标数,那么表示当前空旷指数是可行的。否则表示当前空旷指数太小了,不可行。

check函数 CODE

bool check(int x)  //X表示当前要尝试的空旷指数
{
    
    
	int k,pos;  //POS表示当前路标的位置,K表示还能设定的路标数目
	k=K;
	pos=a[0];  //当前路标设为起点
	for(int i=1;i<=N;)
	  {
    
    
	  	if(a[i]-pos<=x)  //如果当前路标与下一个路标之间的距离小于X,说明可行
	  	  {
    
    
	  	  	pos=a[i];   //改变当前路标
	  	  	i++;
	  	  	continue;
	  	  }
	  	pos=pos+x;  //如果当前路标与下一个路标的距离的空旷指数大于X,那么说明要设定一个路标在这里
	  	k--;  // 由于要新设定一个路标,所以路标数-1
	  	if(k<0) return false;  //如果可以新设的路标数为0了,那么直接退出,表示当前空旷指数不可行
	  }
	return true;  //如果一路顺风,那么可行
}

最后为水题小伙们附上CODE

#include <bits/stdc++.h>
using namespace std;
int L,N,K,a[100010],ans;
bool check(int x)
{
    
    
	int k,pos;
	k=K;
	pos=a[0];
	for(int i=1;i<=N;)
	  {
    
    
	  	if(a[i]-pos<=x)
	  	  {
    
    
	  	  	pos=a[i];
	  	  	i++;
	  	  	continue;
	  	  }
	  	pos=pos+x;
	  	k--;
	  	if(k<0) return false;
	  }
	return true;
}
int search(int l,int r)
{
    
    
	int mid;
	while(l<=r)
	  {
    
    
	  	mid=l+(r-l)/2;
	  	if(check(mid))
	  	  {
    
    
	  	  	ans=mid;
	  	  	r=mid-1;
	  	  }
	  	else l=mid+1;
	  }
	return ans;
}
int main()
{
    
    
	scanf("%d%d%d",&L,&N,&K);
	a[0]=0;
	a[N+1]=L;
	for(int i=1;i<=N;i++)
	  scanf("%d",&a[i]);
	search(1,L);
	printf("%d",ans);
	return 0;
}

总结:

所以说,二分答案的题目,困难的从来不是上下边界的设定,而是那个迷之 check 函数的编写。

由此可见,一个好的二分模板和好的check 思路,能让你诞生 AC瞬间

对于这题,洛谷上还有一个双倍经验,快去水!!

如果你喜欢我的内容,那么也请支持一下他吧

猜你喜欢

转载自blog.csdn.net/SAI_2021/article/details/119758878