做了九度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:最大值最小化题目的思路很明了,就是最大值最小化(我大一看过类似的题目,一直看不懂,还以为是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,为什么会这样我还没弄清楚。
先写到这,想到什么后面再补充。