POJ3273 Monthly Expense (二分统计)+二分查找基础例子

来源:互联网 发布:运动香水知乎 编辑:程序博客网 时间:2024/05/21 10:35

二分边界小结:凡是浮点数二分则都是low=mid,high=mid;整数二分的时候视情况。求相等关键值最大下标时,那么相等的时候就是修改low,所以把相等的情况和大于的情况放在一起;求相等关键值最小下标的时候,那么相等的时候就是修改high,所以此时把相等的情况和小于的情况放在一起。

题意:
给定连续的n天(1<=n<=100000),每天都要花费一定的资金,需要将这连续的n天划分为m段(1<=m<=n),每一段都是连续的,且使得m段中各个段的资金总和的最大值最小,求出这个最小值。

这题让我联想到了曾经笔试中遇到的一道题,题意如下:

【说是有n个站点,每个站点有A[i]个网页(0<=i<=n-1)。有m个网络爬虫,每个爬虫只能对连续的站点进行信息提取,m个爬虫对一个网页的信息提取消耗的时间是相等的,一个时间单位。问设计一个算法,使得m个爬虫对所有站点信息提取的最少时间,并分析时间复杂度。】
这道笔试题和POJ3273本质上是一样的,POJ上类似这样的题还有POJ3258和POJ3122,有空的时候都可以做一下。
思路:
没想到这题可以用二分查找的思路来求解,一直在考虑用动态规划的思想。
当整个n天划分为1个段时,最大值就是n天所需资金的累加总和,我们记做sum,当它划分为n段时,最大值就是n天所需资金中的最大值,我们记做max。我们可以完全肯定的是,我们需要的答案一定在这个范围之内[max,sum],而且我们需要求的是最小值。这个问题其实就变成了在一个有序的区间[max,sum]内求满足条件的最小值,这个条件就是n天可以恰好划分为m段。这样,就可以采用二分查找的思路来求解了,只不过每次需要判定当前的值是否能够让n天恰好划分为m段。
当然,还有一点需要注意,传统的二分查找(我是指最基本的方式)并不能解决该问题,主要是边界条件的判定。这道题其实类似于在有多个相同元素同时存在的有序序列中查找下标最小的满足条件的解。
比如在1,2,2,3,3,3,4,4,4,4这一序列中查找下标最小的3,答案为3,如果用最一般的二分查找就不一定能保证答案是3了,可能是4或者5.
从这题我们可以看出二分查找的魅力,而且要非常灵活地运用二分查找的思想来求解问题并不是很容易的事情。难怪会出现这样的情况:《编程珠玑》(第二版)一书第四章中提及过100多名专业程序员使用两个小时的充足时间编写一个简单的二分查找程序,结果发现90%的人编出的代码都有BUG;Knuth也在他的TAOCP中的《Sorting and Searching》部分提过,第一个二分查找程序在1946年已经公布,但是到了1962年才出现第一个没有BUG的二分查找程序,期间经历了16年的时间。可见,对二分查找的确不可小觑啊。作为一名真正优秀顶尖的技术开发人员,高效地完成bugless的程序才是王道啊。

#include<iostream>
using namespace std;
const int N=100000;
int spends[N+1];
int n,m;
int work()
{
int i,low,high,mid,sum,cnts;
low=high=0;
for(i=0;i<n;i++)
{
high+=spends[i];
low=max(low,spends[i]);
}
if(m==1)return high;
else if(m==n) return low;
while(low<high)
{
mid=(low+high)/2;
sum=cnts=0;
for(i=0;i<n;i++)
{
sum+=spends[i];
if(sum>mid)
{
sum=spends[i];
cnts++;
}
}
cnts++;
if(cnts<=m) high=mid;
else low=mid+1;
}
return high;
}
int main()
{
int i;
scanf("%d%d",&n,&m);
for(i=0;i<n;i++)
scanf("%d",&spends[i]);
printf("%d\n",work());
return 0;
}

总结:划分m=n段时,low值之所以是n天中最大的一天,因为m要求连续!

转载出处:http://hi.baidu.com/poeticxuan/item/7ac0d2f292aa930bd6ff8c5d

另转一篇:

program2:http://www.mysjtu.com/page/M0/S574/574403.html

大致题意:

给出农夫在n天中每天的花费,要求把这n天分作m组,每组的天数必然是连续的,要求分得各组的花费之和应该尽可能地小,最后输出各组花费之和中的最大值

解题思路:

经典的二分穷举

详细的思路我写在程序注释中,这样会更容易懂

看完我的程序还是无法切入题目的同学,建议先用 朴素的穷举 去左这题,虽然很大机会会超时,但是只是为了辅助理解。本题的二分纯粹是一个优化穷举的工具。

 1 //Memory Time  2 //612K   297MS  3  4 #include<iostream> 5 using namespace std; 6  7 int n; //天数 8 int m; //规定的分组数 9 10 /*判断用当前的mid值能把天数n分成几组*/11 /*通过比较group与m的大小,对mid值进行优化*/12 13 bool judge_group(int mid,int money[])14 {15     int sum=0;16     int group=1;    //当前mid值能把n天分成的组数(初始把全部天数作为1组)17 18     for(int i=1;i<=n;i++)  //从第一天开始向下遍历每天的花费19         if(sum+money[i]<=mid)  //当前i天之和<=mid时,把他们归并到一组20             sum+=money[i];21         else               //若 前i-1天之和 加上第i天的花费 大于mid22         {23             sum=money[i];  //则把前i-1天作为一组,第i天作为下一组的第一天24             group++;    //此时划分的组数+125         }26 27     if(group>m)28         return false;   //若利用mid值划分的组数比规定的组数要多,则说明mid值偏小29     else30         return true;    //否则mid值偏大31 }32 33 int main(void)34 {35     while(cin>>n>>m)36     {37         int* money=new int[n+1];  //每天花费的金额38         int low=0;  //下界39         int high=0; //上界40 41         for(int i=1;i<=n;i++)42         {43             cin>>money[i];44 45             high+=money[i];   //把所有天数的总花费作为上界high(相当于把n天都分作1组)46             if(low<money[i])47                 low=money[i]; //把n天中花费最多的那一天的花费作为下界low(相当于把n天分为n组)48         }                     //那么要求的值必然在[low,high]范围内49 50         int mid=(low+high)/2;51 52         while(low<high)  //可能在low==high之前,分组数就已经=m,但是mid并不是最优,因此要继续二分53         {54             if(!judge_group(mid,money))55                 low=mid+1;     //mid值偏小,下界前移56             else57                 high=mid-1;    //mid值偏大,上界后移58 59             mid=(low+high)/2;60         }61 62         cout<<mid<<endl;  //二分搜索最后得到的mid值必然是使得分组符合要求的最优值63 64          money;65     }66     return 0;67 }

基础例子:

下面再附上三种版本的二分查找,分别是基本型,求相同值中下标最小的二分查找以及求相同值中下标最大的二分查找,注意这三者之间的微妙区别,如果能够彻底明白这些微妙的边界条件,那我想对于二分查找的精髓就算融会贯通了。

#include<iostream>
using namespace std;
//基本型的二分查找
int bsearch(int a[],int l,int r,int key)
{
int m;
while(l<=r)//while循环中需要两次比较
{
m=l+(r-l)/2; //就是m=(l+r)/2
if(a[m]==key)
return m;
else if(a[m]>key)
r=m-1;
else
l=m+1;
}
return -1;
}
//求相同值存在时下标值最小的二分查找
int bsearch1(int a[],int l,int r,int key)
{
int m;
while(l<r)//注意区别,while循环中只有一次比较
{
m=l+(r-l)/2;
if(a[m]>=key)//令右区间端点尽量等于关键值,相当于把下标大的挤往区间的右端
r=m;
else
l=m+1;
}
if(a[l]!=key)
return -1;
return l;
}
//求相同值存在时下标值最大的二分查找
int bsearch2(int a[],int l,int r,int key)
{
int m;
while(l<r)//注意区别,while循环中只有一次比较
{
m=l+(r-l)/2+1;//注意与bsearch1的区别,这是为了能够顺利退出while循环
if(a[m]<=key)//)//令左区间端点尽量等于关键值,相当于把下标小的挤往区间的左端
l=m;
else
r=m-1;
}
if(a[l]!=key)
return -1;
return l;
}
int main()
{
//简单的测试案例
int i,a[15]={1,2,2,3,3,3,4,4,4,4,5,5,5,5,5};
for(i=0;i<=6;i++)
{
printf("%d:%d\n",i,bsearch(a,0,14,i));
printf("%d:%d\n",i,bsearch1(a,0,14,i));
printf("%d:%d\n",i,bsearch2(a,0,14,i));
}
return 0;
}