最近一直在补二分查找的题目,本来对二分的理解限制于二分查找的模板,以为补题补起来会特别容易 ,结果现实啪啪打脸。
二分专题大概陆陆续续地补了有三天了,从最普通的二分答案,到有特点的最大化最小值和最大值最小化,其实“ 二分 ”只是一种遍历的工具,“ 二分答案 ”只是一种思维方法。
记得看博客的时候,有一个大佬说二分答案其实就是在已知条件下又增加了一个条件,特别在理。在我看来二分答案特别像高中时的反证法,已知答案,然后判断对不对。例如,求n次操作后的最大高度h。如果利用二分答案的思路就是,找出可能的高度区间(这一步特别简单),判断当前高度值的操作次数是不是n,根据与n相比的大小改变二分区间。
二分答案的基本思路,对所求数值的所在范围进行二分查找,对特定数值进行判断是否符合要求。最难想的其实就是这个判断函数,即F(x)函数的写法,对特定要求的数求其次数,其变换个数…所以我认为二分答案所灵活的地方就在于那个判断函数的写法,那才是题目的灵魂。(因为掌握方法后人人皆可二分)
比较有特点的或者说能够直接间接提醒你这题使用二分答案这个方法的,也就是这里着重讲的----最大化最小值和最大值最小化。
1:最大化最小值
经典题目:愤怒的牛 poj2456
Farmer John has built a new long barn, with N (2 <= N <= 100,000) stalls. The stalls are located along a straight line at positions x1,…,xN (0 <= xi <= 1,000,000,000).
His C (2 <= C <= N) cows don’t like this barn layout and become aggressive towards each other once put into a stall. To prevent the cows from hurting each other, FJ want to assign the cows to the stalls, such that the minimum distance between any two of them is as large as possible. What is the largest minimum distance?
样例
input
5 3
1
2
8
4
9
output
3
这是一道最大化最小值的经典题目,N个房间C头牛。求最近距离的最大值。
根据上文的思路,很容易得出二分答案的区间,然后接下来就是判断这个距离x是不是能够满足C头牛都大于这个距离。
贪心的判断即可,设两个指针,找到最近的前面的不小于后面的位置坐标,M-1头牛全部找到即可。
贴代码:
#include
#include
using namespace std;
const int max_n = 100005;
int N,C;
int po[max_n];
bool check(int d)
{
int temp1=0;
for(int i=1;i<C;i++)
{
int temp2=temp1+1;
while(temp2 < N && po[temp2]-po[temp1]<d)//在有解范围内没找到解
{
temp2++;
}
if(temp2==N)
{
return false;
}
temp1=temp2;//找到的话进行位置的更新
}
return true;
}
int main()
{
scanf("%d%d",&N,&C);
for(int i=0;i<N;i++)
{
scanf("%d",&po[i]);
}
sort(po,po+N);
int ll=0;
int rr=po[N-1];
while(rr-ll>1)
{
int mid=(rr+ll)/2;
if(check(mid)) ll=mid;
else rr=mid;
}
printf("%d\n",ll);
return 0;
}
另外一道最大化最小值是最近做的修补木桶的题目,思路也跟愤怒的牛很类似,就是对当前高度,二分判定需要覆盖几次,当然这个判断需要覆盖几次是题目的难点。后来wa了t了好几发之后发现最难的是二分条件的判定的边解条件。
经典题目2: 修补木桶
一只木桶能盛多少水,并不取决于桶壁上最高的那块木板,而恰恰取决于桶壁上最短的那块。已知一个木桶的桶壁由N块木板组成,第i块木板的长度为Ai。现在小Hi有一个快捷修补工具,每次可以使用修补工具将连续的不超过L块木板提高至任意高度。已知修补工具一共可以使用M次(M*L
Sample Input
8 2 3
8 1 9 2 3 4 7 5
Sample Output
7
关于给定一个高度如何判定需要覆盖几次我想了很久。一开始觉得环形不好处理但是相加取模也就很容易的处理了。
关于覆盖几次,最后想到的是一种贪心的覆盖方法,即遍历整个木桶,找到低于这个高度的(就是需要覆盖的),直接向后覆盖L个,一直贪心的向后覆盖。因为每次的覆盖方法都是统一的向后覆盖而不是经过左右对比的覆盖,因此整个的覆盖次数取决于从哪个点开始覆盖,从不同点开始的覆盖次数是截然不同的。因为是一直往前的贪心策略每个起点对应一种情况,遍历整个木桶所有起点就可以找到最少即最优的覆盖策略。同样的,每一次覆盖一定会减少一个最小的情况,每一次覆盖都是有意义的,所以是一一对应的覆盖次数与最终答案关系。(即连续关系,划重点)
想在这里啰嗦的描述自己之前的一个错误思路。(如果对上述思路十分清晰请果断跳过)
之前我在想如何计算覆盖次数时想的是,找到一个低于目标高度的数据h时,因为可以连续覆盖L个,因此每个h会有L种覆盖方法。至于哪个是最优的,当然是覆盖低的板子最多的那一种。但是这个就很麻烦。因为既要考虑前效又要考虑后效。(然后我这个蒟蒻就把自己绕进去好久也没又出来QAQ)
之后换了换脑子就发现对于每一次覆盖,利用率最大的一定是把低的板子放在区间边界的那种(因为即使都集中区间中间也可以滑动到区间边界,这样多余的部分还可以更好地利用)。既然是放到了区间边界,就可以全部放到同侧的区间边界。(是不是很接近正确答案了?)
那么有了这样一个思路,贪心的覆盖方式也就呼之欲出,遇见低的板子即向后覆盖,这样也就没有后效性,一直贪心的遍历所有即可。正如上文所说不同起点对应不同答案,我们再遍历一遍所有起点即可。
做完这一题,经历了这些脑洞我觉得对区间覆盖次数这类二分问题我已经很熟练了。收获良多。
先贴代码:
#include
#include
using namespace std;
//最大化最小值,且是无空档期一一对应的类型
int N,M,L;
const int max_n = 1005;
int hi[max_n];
int mid;
int qaq;
int num;
int g;
int minn;
int time(int h)
{
minn = 1005;//注意初始化位置,已经出错一次
for(int i=0;i<N;i++)
{
num = 0;
for(int j=0;j<N;j++)
{
g = (i+j)%N;
if(hi[g]<h)
{
j+=(L-1);
num++;
}
}
minn = min(minn,num);
}
return minn;
}
int main()
{
scanf("%d%d%d",&N,&M,&L);
int maxx = 0;
int mi = 10000;
for(int i=0;i<N;i++)
{
scanf("%d",&hi[i]);
maxx = max(maxx,hi[i]);
mi = min(mi,hi[i]);
}
//printf("%d",time(8));
int lb = mi;
int ub = maxx;
while(ub>=lb)
{
mid = lb + (ub - lb) / 2;
qaq = time( mid );
if(qaq <= M)
{
lb = mid+1;
}
else
{
ub = mid-1;
}
}
printf("%d\n",ub);
return 0;
}
本来有了上述思路觉得此题已经毫无难点。后来发现二分的题目还有一个卡人的地方在于边界的判定。
后来经过一些尝试和黄大佬的指点,决定每一次二分都进行区间的收缩,即ub = mid-1;lb = mid + 1然后限制条件改成(lb <= ub)即lb和ub要在完全岔开的时候循环才结束,然后根据题目细节分析最终答案是取lb还是ub。
我也准备以后最大化最小值的题目都这么写。
以上就是我对最大化最小值的理解,下一篇博客主要讲二分答案中的最大值最小化。(我一定不会懒的)