二分算法
活动地址:CSDN21天学习挑战赛
二分的本质并不是单调性,当一个数列满足单调性时一定可以二分,但二分不一定需要满足单调性。
并且二分一定有答案,没有答案那只是没有符合题目要求的答案。
二分查找算法模板
二分模板一共有两个,分别适用于不同情况。
算法思路:假设目标值在闭区间 [l, r]
中, 每次将区间长度缩小一半,当 l = r
时,我们就找到了目标值。
版本1
当我们将区间 [l, r]
划分成 [l, mid]
和 [mid + 1, r]
时,其更新操作是 r = mid
或者 l = mid + 1;
,计算 mid
时不需要加 1。
C++代码模板:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
版本2
当我们将区间 [l, r]
划分成 [l, mid - 1]
和 [mid, r]
时,其更新操作是 r = mid - 1
或者 l = mid;
,此时为了防止死循环,计算 mid
时需要加 1。
C++代码模板:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
以上摘自 AcWing 中 y 总的分享:二分查找算法模板 - AcWing
简单来说就是:
可以将模板 1 中的 check[mid]
换成a[mid] >= x
,用来查找大于等于 x
的第一个元素;
将模板 2 中的 check[mid]
换成 a[mid] <= x
,用来查找小于等于 x
的最后一个元素。
一、二分查找
【例一】查找:
题目描述:输入 n (n <= 106 ) 个不超过 109 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a1, a2, …, an,然后进行 m (m <= 105 ) 次询问。对于每次询问,给出一个整数 q (q <= 109 ),要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 -1 。(数字从 1 开始编号)
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int a[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
while (m--)
{
int x;
cin >> x;
int l = 1, r = n;
while (l < r) //找到第一个等于x的数
{
int mid = l + r >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
//此时l和r都指向同一个数,任意输出一个即可
if (a[l] != x) cout << -1 << " ";
else cout << l << " ";
}
cout << endl;
return 0;
}
【例二】A-B 数对
原题链接:P1102 A-B 数对 - 洛谷
题目描述:给出一串数以及一个数字 C ,要求计算出所有 A - B = C 的数对的个数(不同位置的数字一样的数对算不同的数对)。
这里使用库函数二分的写法:
依次枚举 A ,将问题转变成统计数列中 B + C 出现了多少次。先对数列排序,那么 B + C 会对应这个数列的连续一段,只要找到这个连续段的左端点和右端点即可。(需使用头文件 algorithm
)
① lower_bound(begin, end, val)
可以在区间 [begin, end)
中找到 val
第一次出现的位置;
② upper_bound(begin, end, val)
可以在区间 [begin, end)
中找到 val
最后一次出现的位置的 后面一位 。
则这个数出现的次数就可以表示为 upper_bound() - lower_bound()
,时间复杂度为 O(nlogn).
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
const int N = 2e5 + 10;
int n, c;
ll a[N];
int main()
{
cin >> n >> c;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
ll tot = 0;
for (int i = 0; i < n; i++)
tot += upper_bound(a, a + n, a[i] + c) - lower_bound(a, a + n, a[i] + c);
cout << tot << endl;
return 0;
}
二、二分答案
一般来说,二分答案可以用来处理 “最大的最小” 或 “最小的最大” 的问题。
二分答案算法模板
定义区间为闭区间 [l, r]
,每次只需判断答案是否需要更新(是否记下ans)和(可能的)答案在哪一侧(改 L 还是 R )即可。
int ans;
int find(int l, int r)
{
while (l <= r)
{
int mid = l + r >> 1;
if (check(mid)){
ans = mid; //如果条件成立则记下答案
r = mid - 1; //判断可能的答案更新区间
}
else l = mid + 1;
}
return ans;
}
【例题】进击的奶牛
原题链接:P1824 进击的奶牛 - 洛谷
题目描述:一个牛棚有 n 个隔间,它们分布在一条直线上,,坐标是 x1, x2, …, xn 。现在需要把 c 头牛安置在某些隔间,使得所有牛中相邻两头的最近距离越大越好,求这个 最大的最近距离。
先构造判断 “条件”:可以把 c 头牛全部安置进这些隔间使相邻两头牛距离不超过 x 。x 越小,就越可能把所有牛合法安置;当 x 比较大时,牛棚就不够安置了。于是,存在一个分界线 ans,当 x 大于 ans 时就没有合法的安置方案,当 x 小于或等于 ans 时,则一定存在一个合法的安置方案。
可以得到,在合法的答案中,任意两个相邻安置点都不能小于 x 。那么只需要遍历所有点,从最左端开始,每隔超过 x 的距离,能安置则安置,最后判断能否全部安置完。若不能,则缩小 x ,重复上述遍历过程。
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
const int INF = 1e9;
int n, c;
int a[N];
bool check(int d)
{
int k = 0;
int last = -INF; //记录上一头牛的安置坐标
for (int i = 1; i <= n; i++)
if (a[i] - last >= d) //能安置就立刻安置
{
last = a[i];
k++;
}
return k >= c;
}
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + 1 + n);
int ans;
int l = 0, r = INF;
while (l <= r) //二分查找答案,最大的最小值
{
int mid = l + r >> 1;
if (check(mid)){
ans = mid;
l = mid + 1;
}
else r = mid - 1;
}
cout << ans << endl;
return 0;
}