从零开始的二分法
第一关:递归
先来一个基本的二分法,用递归写出二分法:
int bsearch(vector<int> &nums, int l, int r, int target)
{
// 在区间[l,r)进行搜索
}
算法一:递归的左闭右开区间搜索
不错,我们继续,试试闭区间内搜索:
int bsearch(vector<int> &nums, int l, int r, int target)
{
// 在区间[l,r]进行搜索
}
算法二:递归的闭区间搜索
如果你完成了上面四个代码,说明你对二分的终止条件有了一定的认识,我们继续:
如果你像我这样,写了多的if,试试能不能简化他们,合并一些条件,比如将==合并到<里面
int bsearch(vector<int> &nums, int l, int r, int target)
{
// search in [l,r]
if (l == r && nums[l] != target)
{
return -1;
}
int mid = (l + r) / 2;
if (nums[mid] == target) // HOW TO merge == into < or >
{
return mid;
}
else if (nums[mid] < target)
{
return bsearch(nums, mid + 1, r, target);
}
else
{
return bsearch(nums, l, mid - 1, target);
}
}
算法三:递归的合并等于情况的搜索
如果你完成了上面的代码,说明你对终止区间的长度有了很好的理解,让我们再继续:
第二关:循环
递归需要调用栈空间,不太好,我们把递归改成循环,用while循环结构改写算法一和算法二:
此时你应该思考一个问题,While内的条件应该怎么写,是否随着区间的开闭性而改变?
如果你完成了上面的代码,说明你对二分已经有了基本的认识,我们继续,用循环结构改写算法三,试试看。
如果你都完成了上面的任务,说明你对二分法已经有了很好的理解。
二分法主要在于下面两个方面:
- 怎样缩小区间
- 终止时候的区间长度与while内条件的关系
在编写二分算法时要注意以上两个问题,才能保证二分法是正确的,各种关于二分法的变式题都是围绕着两个问题展开的。
第三关:进阶
- 如果一个升序列表内存在可重复元素,请写出二分法找到目标值(如果目标值有多个返回第一个元素的下标,例如1,2,3,3,4,5寻找3,应该返回2);变式:(如果目标值有多个返回最后个元素的下标,例如1,2,3,3,4,5寻找3,应该返回3)
- 33. 搜索旋转排序数组
- 。。。
不想考虑边界
我们如果不想考虑边界问题,可以使用倍增二分法,也是我极力推荐的一种方法,相比于上面的二分法,其优点为算法参数好确定,最坏答案错了也不会死循环。
其模板为:
int x = 0; // 当前值,初始值为边界左端点(左开,不包括)
// 如何确定mx的值,其x0+mx必须是边界的右端点(右闭,包括),x0为x的初始值
for (int b = mx; b > 0; b /= 2) // 步移
{
while (x + b <= mx && check(x + b)) // 倍增走
x += b;
}
例题:P2440
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out1.txt", "w", stdout)
int n, k;
int arr[100005];
bool check(int l)
{
int cnt = 0;
for (int i = 1; i <= n; i++)
{
cnt += arr[i] / l;
}
return cnt >= k;
}
int main()
{
scanf("%d %d", &n, &k);
int mx = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d", arr + i);
mx = max(mx, arr[i]);
}
int x = 0; // 当前值,初始值为边界左端点(左开,不包括)
// 如何确定mx的值,其x0+mx必须是边界的右端点(右闭,包括),x0为x的初始值
for (int b = mx; b > 0; b /= 2) // 步移
{
while (x + b <= mx && check(x + b)) // 倍增走
x += b;
}
printf("%d", x);
return 0;
}
未完待续