7. 二分算法
- 基础算法中最难的
- 原理与模板简单
- 难点在细节
- 处理边界问题
- 解集中存在二段性
模板题 :
!leetcode
中等
给你一个按照非递减顺序排列的整数数组
nums,和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值
target,返回[-1, -1]。你必须设计并实现时间复杂度为
O(log n)的算法解决此问题。示例 1:
**输入:**nums = [
5,7,7,8,8,10], target = 8
输出:[3,4]示例 2:
**输入:**nums = [
5,7,7,8,8,10], target = 6
输出:[-1,-1]示例 3:
**输入:**nums = [], target = 0
输出:[-1,-1]提示:
0 <= nums.length <= 105-109 <= nums[i] <= 109nums是一个非递减数组-109 <= target <= 109
思路:
题目要求 -> 找到 起始位置 和 初始位置
-
方一 -> 暴力解法 -> 从前往后扫描数组 -> O(n)
暴力解法慢在没有利用数组的有序性
-
方二 -> 二分算法
-
查找起始位置
left---------------mid---------------right
a[mid] >= t ; right = mid ; [left, right];
a[mid] < t ; left = mid + 1 ; [left, right]细节问题:
-
while循环的判断条件
while ( left < right ); √
while ( left <= right );
不能
=, 可能会死循环 , eg:[2,2] -
求中点的方法
方一 :
( left + right ) / 2-> 奇数中点靠左 √方二 :
( left + right + 1 ) / 2-> 奇数中点靠右 -> 死循环 ->[2, 2] -
二分结束后, 相遇点的情况(容易忽略)
判断循环结束后的结果是否为我们所需
-
-
查找终止位置
left---------------mid---------------right
a[mid] <= t; left = mid ; [left, right]
a[mid] > t ; right = mid - 1; [left, right]细节问题:
-
while循环的判断条件
while ( left < right ); √
while ( left <= right );
不能
=, 可能会死循环 , eg:[2,2] -
求中点的方法
方一 :
( left + right ) / 2-> 奇数中点靠左 -> 死循环 ->[2, 2]方二 :
( left + right + 1 ) / 2-> 奇数中点靠右 √ -
二分结束后, 相遇点的情况(容易忽略)
判断循环结束后的结果是否为我们所需
-
-
代码:(二分模板)
cpp
//二分查找区间的左端点
int l = 1, r =n;
while(l < r)
{
int mid = (l + r) /2;
if(check(mid)) r = mid;
else l = mid + 1;
}
//二分结束后可能需要判断是否存在结果
cpp
//二分查找区间的右端点
int l = 1, r = n;
while(l < r)
{
int mid = ( l + r + 1 ) / 2;
if(check(mid)) l = mid;
else r = mid - 1;
}
//二分结束之后可能需要判断是否存在结果
-
有时会溢出( l+r > int的范围 ), 可采用
mid = l + ( r - l ) / 2 ;
mid = l + ( r - l + 1 ) / 2 ;或者用
long long定义 -
模板不要死记 , 具体题目具体分析
-
时间复杂度 ---> 二分的次数 ---> O ( l o g N ) O(log N) O(logN)
-
STL 中的二分查找
<algorithm>lower_bound: 大于等于 x 的最小元素, 返回迭代器 , O ( l o g N ) O(log N) O(logN)uooer_bound: 大于 x 的虽小元素, 返回迭代器, O ( l o g N ) O(log N) O(logN)
使用方法:
auto it = lower_bound(左端点 闭区间,右端点 开区间, 数字);
auto it = lower_bound(a+1 , a + n + 1 , target) ;
int a = *it-> 解引用均采用二分查找实现, 但STL中的这个仅仅只适用于"在有序的数组中查找" , 二分答案不能用
7.2 二分答案
7.2.1 木材加工(二分答案模板题目)
!洛谷
P2440 木材加工
题目背景
要保护环境。
题目描述
木材厂有 n n n 根原木,现在想把这些木头切割成 k k k 段长度均 为 l l l 的小段木头(木头有可能有剩余)。
当然,我们希望得到的小段木头越长越好,请求出 l l l 的最大值。
木头长度的单位是 cm \text{cm} cm,原木的长度都是正整数,我们要求切割得到的小段木头的长度也是正整数。
例如有两根原木长度分别为 11 11 11 和 21 21 21,要求切割成等长的 6 6 6 段,很明显能切割出来的小段木头长度最长为 5 5 5。
输入格式
第一行是两个正整数 n , k n,k n,k,分别表示原木的数量,需要得到的小段的数量。
接下来 n n n 行,每行一个正整数 L i L_i Li,表示一根原木的长度。
输出格式
仅一行,即 l l l 的最大值。
如果连 1cm \text{1cm} 1cm 长的小段都切不出来,输出
0。输入输出样例 #1
输入 #1
3 7 232 124 456输出 #1
114说明/提示
数据规模与约定
对于 100 % 100\% 100% 的数据,有 1 ≤ n ≤ 10 5 1\le n\le 10^5 1≤n≤105, 1 ≤ k ≤ 10 8 1\le k\le 10^8 1≤k≤108, 1 ≤ L i ≤ 10 8 ( i ∈ [ 1 , n ] ) 1\le L_i\le 10^8(i\in[1,n]) 1≤Li≤108(i∈[1,n])。
思路:
-
方一 : 暴力解法
-
枚举所有的切割长度 x
-
求 x 情况下 , 能切出来几段
-
-
方二 : 利用二分优化方一
-
x -> 切割出小段的长度 c -> 在 x 的基础下 , 最多能切出的段数 k -> 最终要切出的段数
-
设 ret 为最终结果(段数) c >= k;
±-------------±--------------+
left = 0 mid right = maxlen
-
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 +10;
typedef long long LL;
LL n, k;
LL l[N];
LL calc(LL x)
{
LL cnt = 0;
for(int i =1; i<= n; i++)
{
cnt += l[i] / x;
}
return cnt;
}
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> l[i];
int left = 0 , right = 1e8;
while(left < right)
{
LL mid = (left + right + 1) / 2;
if(calc(mid) >= k) left = mid;
else right = mid - 1;
}
cout << left;
return 0;
}