二分搜索除了在有序数列查找值上非常有用外,在求最优解也有很大用处。
eg:“求满足某个条件C(x)的最小x”对于任意x满足C(x),那么x'>=x,也满足C(x')的话就可以用二分查找...详见《挑战程序设计竞赛》第三章
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
using namespace std;
double line[10005];
int n,k;
void cut()
{
double up=200005;
double down=0;
double mid;
for(int i=0;i<100;i++)//利用i来保证解的范围足够小,太强了吧
{
mid=(up+down)/2;
int s=0;
for(int i=0;i<n;i++)s+=(int)(line[i]/mid);
if(s<k)up=mid;
else if(s>=k) down=mid;
}
printf("%.2f\n",floor(up*100)/100);
}
int main()
{
while(scanf("%d%d",&n,&k)!=-1)
{
for(int i=0;i<n;i++)
scanf("%lf",&line[i]);
cut();
}
return 0;
}
注意上界要选择一个充分大的数字(这里是>绳子的长度)
POJ--2456
和上题同类型,感觉最重要的还是临界条件的选择以及从题目中提取出那个抽象的数学模型。这样以后就会很简单。
这道题是要把最大化的最小值求出来。牛与牛之间的最小距离是0,最大距离可以设置成1000,000,005。
运用贪心算法+二分查找
二分查找:不断减少所求值的范围,直到区间的上界和下界不可区分。
贪心算法:是在放牛的时候,不断向前试探,知道距离超过当前测试值,目的是判断能够放下所有的牛,如果能够放下,取右区间,如果放不下,取左区间。
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <iostream>
using namespace std;
//求最大的最短距离
long long int n,m;
long long int sta[100005];
bool judge(long long int dis)
{
long long int curr,pre=0;
for(int i=0;i<m-1;i++)
{
curr=pre+1;
while(curr<n&&sta[curr]-sta[pre]<dis)curr++;
if(curr==n)return false;//放不下所有的牛,那么向更小的方向尝试
pre=curr;
}
return true;//能把所有的牛都放下,那么向更大的方向尝试
}
void distance()
{
sort(sta,sta+n);//排序
long long int down=0;
long long int up = 1000000005;
while(up-down>1)//???
{
long long int mid=(up+down)/2;
//cout<<mid<<endl;
if(judge(mid))down=mid;
else up=mid;
}
printf("%d\n",down);
}
int main()
{
while(scanf("%lld%lld",&n,&m)!=-1)
{
for(int i=0;i<n;i++)scanf("%lld",&sta[i]);
distance();
}
}
例题三,寻找子序列 POJ--3061
一开始朴素的想一个一个元素试探,但是看了看限制时间,十有八九会TLE。反正自己也想不出来,不如参考一下书吧。
方法说不上多巧妙,但是也很有意思。
尺取法+二分
因为需要子元素之和都大于给定S,从第一个元素开始依次累加求出前N个元素的和。那么 sum[i]-sum[j]就是 元素i+1到元素j的和。
但是这里需要注意的是,因为可能只有整个序列之和才大于S,所以sum集合的长度为N+1。
此外,二分部分的话,也和前面两题用mid和up和down的常规方法不一样,这里用到了lower_bound函数。用处很简单,就是返回一个查找范围内,一个大于或者等于value的数的位置(iterator)。
详见:点击打开链接
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <algorithm>
using namespace std;
long long int k,n,s;
long long int num[100005];
long long int sum[100005]={0};
void solve(long long int s)
{
for(int i=0;i<n;i++)sum[i+1]=sum[i]+num[i] ;
if(sum[n]<s)printf("0\n");
else
{
int res=n;
for(int i=0;sum[i]+s<=sum[n];i++)
{
int tem=lower_bound(sum+i,sum+n,sum[i]+s)-sum;
//返回一个不小于sum[i]+s的值
res=min(res,tem-i);
}
printf("%d\n",res);
}
}
int main()
{
scanf("%d",&k);
for(int i=0;i<k;i++)
{
scanf("%ld %ld",&n,&s);
scanf("%d",&num[0]);
for(int i=1;i<n;i++)scanf("%d",&num[i]);
solve(s);
}
return 0;
}
例题4,POJ--3320
和前一道题无比相似,但还是没有独立思考出来..凄惨...太凄惨了...题解也看了半天才看懂,用到了尺取法和map以及set。
感觉map和set主要起到了辅助作用,如果掌握了核心方法应该还有其他的解决办法。
代码基本是copy的《挑战》书上的,但是加了一些注释。
总觉得尺取法的形式不止一种,不应该被定义所局限。关键是在于这种思考的办法。emmm,怎么描述呢?就是好像给了一个一维的坐标轴,然后用一个尺子在区间上卡范围,尺子长度不固定,然后,找答案就好像是把尺子移来移去来卡范围。有时候满足条件的子区间不止一种,所以可能还需要,选出最优的尺子。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <set>
#include <map>
using namespace std;
long long int page[1000005];
long long int p;
void solve();
int main()
{
while(scanf("%lld",&p)!=-1)
{
for(long long int i=0;i<p;i++)scanf("%lld",&page[i]);
solve();
}//读取页数上的知识点存入page中
}
void solve()
{
//计算全部知识点的总数
set<int>all;
for(int i=0;i<p;i++)all.insert(page[i]);
//set中所有元素都会通过键值自动排序,而且还不允许两个元素有相同键值
int size=all.size();
//利用尺取法求解
int s=0,t=0,num=0;//s是开始,t是结束
map<int ,int >count;//知识点映射到出现的次数
int res=p;
while(true)
{
while(t<p&&num<size)
{
if(count[page[t++]]++==0) num++; //出现新的知识点
}//首先找到涵盖所有知识点的区间
if(num<size)break;//如果子区间的下界已经到了页数最大,跳出
res=min(res,t-s);//每找出一种情况就判断是否比前一种情况短
if(--count[page[s++]]==0)num--;//某个知识点出现的次数变成0
}
printf("%d\n",res);
}
/*
* 让区间滑动
* 设一个区间的起点s,终点e,初始都为0
* 不断增加e,直到[s,e]中包含全部的知识点
* 此时记录下区间长度,然后s++,并且删除原先s所在的知识点
* 然后重复之前步骤
* 直到e == n && 当前知识点<总的知识点 || s == n为止
* 其实 s==n不写也可以,因为前者已经包含后者的情况。
*/
参考:《挑战程序设计竞赛》