您的位置:首页 > 其它

由九度1502引出的对二分查找的一点总结v1.0

2015-11-24 14:29 375 查看
做了九度1502后,发现自己对二分查找越来越不理解了,特别是各种边界情况,left = mid还是left = mid + 1 ? right  = mid 还是 left = mid-1 ? return left ,return mid还是return right?仔细研究后发现这里面大有玄机。于是写下一点点心得体会,免得以后忘记了。

在刘汝佳大神的《算法竞赛入门经典》中的二分查找是这样的

int bsearch(int* A, int x, int y, int v)
{
while(x < y)
{
m = x + (y-x)/2;
if(A[m] == v)  //A[]是待查找数组, v是要查找的值
return m;
else if(A[m] > v)
y = m;
else
x = m+1;
}
return -1;
}


一开始并没有太深入探究为什么要这样写(每次如何更新x和y的值,最后返回的值等等),只是大概知道二分的工作方式就是每次缩减一般的搜索范围,直到最后查找的范围很小。

但是1502令我wa了差不多二十多三十次之后,我似乎理解了为什么要这样写(但貌似错的地方不是这一部分23333).

先贴上1502的题目描述:

题目1502:最大值最小化

时间限制:1 秒

内存限制:128 兆

特殊判题:否

提交:533

解决:197

题目描述:

在印刷术发明之前,复制一本书是一个很困难的工作,工作量很大,而且需要大家的积极配合来抄写一本书,团队合作能力很重要。

当时都是通过招募抄写员来进行书本的录入和复制工作的, 假设现在要抄写m本书,编号为1,2,3...m, 每本书有1<=x<=100000页, 把这些书分配给k个抄写员,要求分配给某个抄写员的那些书的编号必须是连续的。每个抄写员的速度是相同的,你的任务就是找到一个最佳的分配方案,使得所有书被抄完所用的时间最少。

输入:

输入可能包含多个测试样例。

第一行仅包含正整数 n,表示测试案例的个数。

对于每个测试案例,每个案例由两行组成,在第一行中,有两个整数m和 k, 1<=k<=m<=500。 在第二行中,有m个整数用空格分隔。 所有这些值都为正且小于100000。

输出:

对应每个测试案例,

输出一行数字,代表最佳的分配方案全部抄写完毕所需要的时间。

样例输入:
2
9 3
100 200 300 400 500 600 700 800 900
5 4
100 100 100 100 100


样例输出:
1700
200


题目的思路很明了,就是最大值最小化(我大一看过类似的题目,一直看不懂,还以为是dp一类的问题),先猜一个数字v(范围是最多的书的页码数max--总页码数sum。为什么不知0 -- sum? 我还没想到原因),然后从左到右统计书的页数,这里也用了一点贪心的思想,每个人都抄不超过v页的尽可能多的书,当超过了v后,就要给下一个人抄,直到全部书都抄完成。看需要的人数cnt, 再用cnt和输入的k比较,如果cnt < k,说明这种方法可取;如果cnt > k 说明 需要的人数超出了上限,不可取。(这就是judge()函数的内容),

如果judge(v) == 1,那么 每个人还可以抄更少的页数来达到最大值最小化的目的。这里用二分的方法提高效率。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <map>
#include <cmath>

using namespace std;
const int INF = 0x7fffffff;
int a[600];
int n, k;

bool judge(int mid)
{
int cnt = 1;
int sum = 0;
for(int i = 0; i < n; ++i)
{
if(sum + a[i] <= mid)
sum += a[i];
else
{
sum = a[i];
cnt++;
}
}

return (cnt <= k);
}

int main()
{
// freopen("1.txt", "r", stdin);
int t;
cin >> t;
while(t--)
{
cin >> n >> k;
int sum = 0;
int maxx = 0;
for(int i = 0; i < n; ++i)
{
cin >> a[i];
sum += a[i];
if(maxx < a[i])   //*********①*********
maxx = a[i];
}

int lb = maxx, ub = sum;   //**********⑤**********
int mid;
while(lb < ub)             <span style="font-family: Arial, Helvetica, sans-serif;">//**********②***********</span>

{
mid = lb+(ub-lb)/2;    <span style="font-family: Arial, Helvetica, sans-serif;">//**********③***********</span>

if(judge(mid))
ub = mid;        <span style="font-family: Arial, Helvetica, sans-serif;">//**********</span><span style="font-family: Arial, Helvetica, sans-serif;">②</span><span style="font-family: Arial, Helvetica, sans-serif;">***********</span><span style="font-family: Arial, Helvetica, sans-serif;">
</span>
else
lb = mid+1;      <span style="font-family: Arial, Helvetica, sans-serif;">//**********②************</span>

}
cout << lb << endl;      //*********④*********
}
return 0;
}


共有7处需要注意

① 这是最不应该犯的错误,我把他写成了if(maxx > a[i])。

② while(lb < ub) ,有的代码会写成while(lb <= ub) 或者 while(lb -1 < rb). 要注意三种不同的判断条件时,下面对于mid的更新也是不同的。比如 0 1 2,要找2,

第一次:mid = (2+0) / 2; mid == 1 < 2

假如 lb = mid的话。第二次查找时 lb = 1, ub = 2; lb < rb, mid = (1+2)/ 2 = 1; 第三次查找时 lb = 1, ub = 2; lb < rb, mid = (1+2)/ 2 = 1,会造成死循环。

由c和c++截尾取整的特性可知,当lb 刚好比ub小1时,如果lb = mid 的话,会永远处于同一种情况(lb + 1 = ub).

而为什么ub 可以直接取mid?因为取整是截尾取整,跟比他大的数无关。

③ mid = lb+(ub-lb)/2; 这样写是为了防止lb+ub造成的溢出。

④ cout << lb << endl; 其实写成 cout << ub << endl;也是可以的, 比如说 lb = 101 ub = 102 mid = 101, 那么下一步lb = 101 mid= 101 ub = 102,如果101ok那么 ub = 101.lb = 101. 如果101 不ok,那么 lb = 102 ub = 102. 就是说无论如何 最后的lb 和 ub都是相等的因为while(lb < ub)嘛,而且不可能出现lb > rb 的情况。

但是,千万不能写成 cout << mid << endl; 或者 cout << mid+1 << enl;

因为 当最后 lb mid rb 分别为 1700 1700 1701 ,而答案为1700 时,输出mid+1 = 1701 明显是错的

而当lb mid rb 分别为 198 199 200 ,答案为200 时,199 < 200 造成cnt > k, judge(199) == false, 下一步该是lb = mid +1 = 200 而此时 mid 的值依然是199,因为循环是while(lb < ub),此时lb = ub== 200 ,但是mid却还是上次循环中的199.所以输出199 显然是错的。

⑤ 一开始二分查找的下界应该是maxx ,而不是0,为什么会这样我还没弄清楚。

先写到这,想到什么后面再补充。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: