综述: 答案有一个区间, 在区间中二分找到最优答案
适用于: 数据范围较大, 答案具有单调性
题目特点:
- 在一个区间内, 区间范围很大 暴力会超时
- 直接搜索不好搜, 但是可以判断某个答案行不行
- 单调性, 区间中的值越大, 题目中某个量对应会变大/小
- 求…最大值的最小, 最小值的最大
基本模版
while (l < r)
{
int mid = l + r >> 1;
if (check(mid))
r = mid;
else
l = mid + 1;
}
往左找答案, 求最小, mid 不用 + 1, r = mid, l + 1
往右找答案, 求最大, mid + 1, l = mid, r - 1
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid))
l = mid;
else
r = mid - 1;
}
浮点除法不会取整, 不用 +1 或-1
while (r - l > 1e - 5) // 需要一个精度保证
{
double mid = (l + r) / 2;
if (check(mid))
l = mid; // 或r=mid;
else
r = mid; // 或l=mid;
}
A-B 数对
给出一串正整数数列以及一个正整数
C
C
C,要求计算出所有满足
A
−
B
=
C
A - B = C
A−B=C 的数对的个数(不同位置的数字一样的数对算不同的数对)。
正整数中包含的满足
A
−
B
=
C
A - B = C
A−B=C 的数对的个数。
lower_bound / upper_bound
取得a数组中最小数的下标
lower_bound(a,a+n,x)-a //下标从0开始
lower_bound(a+1,a+n+1,x)-a //下标从1开始
upper_bound(a,a+n,x)-a //下标从0开始
upper_bound(a+1,a+n+1,x)-a //下标从1开始
#include <bits/stdc++.h>
using namespace std;
long a[200001];
long N, C, ans;
int main()
{
cin >> N >> C;
for (int i = 1; i <= N; i++)
{
cin >> a[i];
}
sort(a + 1, a + N + 1);
for (int i = 1; i <= N; i++)
{
ans += ((upper_bound(a + 1, a + N + 1, a[i] + C) - a) - (lower_bound(a + 1, a + N + 1, a[i] + C) - a));
}
cout << ans;
return 0;
}
二分
给出了C,我们要找出A和B。我们可以遍历数组,即让每一个值先变成B,然后二分找对应的A首次出现位置,看是否能找到。
如果找到A,那就二分找最后出现的位置,继而,求出A的个数,即数对的个数。
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
long long a[N], n, c, cnt, st;
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + 1 + n); // 先排序
for (int i = 1; i < n; i++) // 遍历每一个B
{
int l = i + 1, r = n; // 寻找A第一次出现的位置,使得A-B=C
while (l < r) // 因为是第一次出现,尽量往左,模板1
{
int mid = l + r >> 1;
if (a[mid] - a[i] >= c)
r = mid; // 判断:在目标值的右边,满足,往左来
else
l = mid + 1;
}
if (a[l] - a[i] == c)
st = l; // 能找到C就继续
else
continue;
l = st - 1, r = n; // 查找A最后出现的位置
while (l < r) // 因为是最后一次出现,尽量往右,模板2
{
int mid = l + r + 1 >> 1;
if (a[mid] <= a[st])
l = mid; // 判断:在目标值的左边,满足,往右去
else
r = mid - 1;
}
cnt += l - st + 1; // 最后出现的位置减首次出现的位置就是区间长度,即A的个数
}
cout << cnt;
return 0;
}
分巧克力
儿童节那天有 K K K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 N N N 块巧克力,其中第 i i i 块是 H i × W i H_i \times W_i Hi×Wi 的方格组成的长方形。
为了公平起见,小明需要从这 N N N 块巧克力中切出 K K K 块巧克力分给小朋友们。切出的巧克力需要满足:
-
形状是正方形,边长是整数。
-
大小相同。
例如一块 6 × 5 6 \times 5 6×5 的巧克力可以切出 6 6 6 块 2 × 2 2 \times 2 2×2 的巧克力或者 2 2 2 块 3 × 3 3 \times 3 3×3 的巧克力。
读题 => 每块巧克力可以切得块数为 (H / mid) * (w / mid)
从d = 1开始一个一个试, 会超时
采取二分, 从1, 100000, 开始试
(H[i]W[i]) / (xx) 会出现 1x8的巧克力裁不出2x2的巧克力,但(H[i]W[i]) / (xx)会认为这块巧克力可以裁成2块2x2的巧克力
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f
#define endl "\n"
#define MAX 100001
int h[MAX], w[MAX];
int n, k;
int Fun(int x)
{
int sum = 0;
for (int i = 1; i <= n; i++)
sum += (h[i] / x) * (w[i] / x);
if (sum >= k)
return 1;
return 0;
}
void solve()
{
int i;
cin >> n >> k;
for (i = 1; i <= n; i++)
cin >> h[i] >> w[i];
int left = 1, right = MAX, mid;
while (left < right)
{
mid = (left + right + 1) / 2;
if (Fun(mid))
left = mid;
else
right = mid - 1;
}
cout << left;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
子序列的平均值
给定一个长度为n的非负序列A,请你找出一个长度不小于L的子段(子段是序列A中一些连续的元素构成的集合),使得子段中数值的平均值最大。最终输出这个最大的平均值。
输入格式:
第一行两个整数n,L(1<=L<=n<=100,000)以下n行,每行一个非负整数,表示序列A中每个元素的值。
思路
答案一定位于最小值和最大值之间
check的标准: 序列和 / L 如果大于mid,说明平均值可以更大l = mid;
如果小于mid,说明平均值不够这么大r = mid;
代码
#include <iostream>
#include <cmath>
using namespace std;
int n = 0, L = 0;
double a[100001] = {0};
bool Check(double average)
{
bool flag = false;
double sum[100001] = {0};
for (int i = 1; i <= n; ++i)
{
sum[i] = sum[i - 1] + (a[i] - average);
}
double fronts = 1e9;
for (int i = L; i <= n; ++i)
{
fronts = fmin(fronts, sum[i - L]);
if (sum[i] - fronts > 0)
{
flag = true;
break;
}
}
return flag;
}
int main()
{
cin >> n >> L;
double max = 0, min = 1e9;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
max = fmax(a[i], max);
min = fmin(a[i], min);
}
double low = min, high = max, answer = (low + high) / 2;
while (high - low > 1e-5)
{
if (Check(answer))
low = answer;
else
high = answer;
answer = (low + high) / 2;
}
cout << (int)(high * 1000) << endl;
}
伐木工人
伐木工人 Mirko 需要砍 M 米长的木材。对 Mirko 来说这是很简单的工作,因为他有一个漂亮的新伐木机,可以如野火一般砍伐森林。不过,Mirko 只被允许砍伐一排树。
Mirko的伐木机工作流程如下:Mirko 设置一个高度参数 H(米),伐木机升起一个巨大的锯片到高度 H,并锯掉所有树比 H 高的部分(当然,树木不高于 H 米的部分保持不变)。Mirko 就得到树木被锯下的部分。例如,如果一排树的高度分别为 20,15,10 和17,Mirko 把锯片升到 15 米的高度,切割后树木剩下的高度将是 15,15,10 和 15,而 Mirko 将从第 1 棵树得到 5米,从第 4 棵树得到 2 米,共得到 7 米木材。
Mirko 非常关注生态保护,所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 H,使得他能得到的木材至少为 M 米。换句话说,如果再升高 1 米,他将得不到 M 米木材。【输入格式】:
第 1 行 2 个整数 N 和 M,N 表示树木的数量,M 表示需要的木材总长度。第 2 行 N 个整数表示每棵树的高度。
【输出格式】:
1 个整数,表示锯片的最高高度
思路
求一个最大高度,使得砍到的树的总长度满足条件
大于 ans 的高度,不满足条件,小于 ans 的高度,都满足条件.
- 确定答案所在的区间
- 判断 mid 是否满足条件,更新答案区间
- 得到符合题目要求的答案
①要求满足条件的最大高度,那么首先答案介于0-树的最大高度Heigt.
②用二分查找的框架去判断 mid符合条件,然后根据判断结果去更新答案区间
得到的总长度sum ≥ m说明高度低了->left变为mid + 1;
否则 right 变为 mid - 1;
int left = 0, right = height, mid, ans;
while (left <= right)
{
mid = (left + right) / 2;
long long sum = 0;
for (i = 1; i <= n; i++)
{
if (a[i] > mid)
sum += a[i] - mid;
}
if (sum >= m)
ans = mid, left = mid + 1;
else
right = mid - 1;
}
代码
#include <bits/stdc++.h>
using namespace std;
int n, m, a[1000005], height;
int Binary_answer(int h)
{
int left = 0, right = h, mid, ans;
while (left <= right)
{
mid = (left + right) / 2;
long long sum = 0;
for (int i = 1; i <= n; i++)
{
if (a[i] > mid)
sum += a[i] - mid;
}
if (sum >= m)
ans = mid, left = mid + 1;
else
right = mid - 1;
}
return ans;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
if (a[i] > height)
height = a[i];
}
cout << Binary_answer(height);
return 0;
}
满足条件的最大(小)值 - 木材加工
木材厂有
n 根原木,现在想把这些木头切割成 k 段长度均为 l 的小段木头(木头有可能有剩余)。当然,我们希望得到的小段木头越长越好,请求出 l 的最大值
例如有两根原木长度分别为 11 和 21要求切割成等长的 6 段,很明显能切割出来的小段木头长度最长为 5。
代码
#include <bits/stdc++.h>
using namespace std;
int n, m, a[100005], height;
long long sum;
int Binary_answer(int h)
{
int left = 0, right = h, mid, ans;
while (left <= right)
{
mid = (left + right) / 2;
int sum1 = 0;
for (int i = 1; i <= n; i++)
{
sum1 += a[i] / mid; // 计算长度为mid时能切割的段数
}
// 判断大小,更新区间
if (sum1 >= m)
ans = mid, left = mid + 1;
else
right = mid - 1;
}
return ans;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
sum += a[i]; // 求出木材的总和
height = max(height, a[i]); // 找到所有木材中的最大值
}
if (sum < m)
{ // 如果总和小于m,说明连长度切1厘米都无法切m段
cout << 0;
return 0;
}
// 二分答案
cout << Binary_answer(height);
return 0;
}
最大值最小化问题 - 数列分段
对于给定的一个长度为N的正整数数列 A 1 ∼ N A _{1∼N}A
1∼N.
现要将其分成 M(M≤N)段,并要求每段连续,且每段和的最大值最小。
关于最大值最小:
例如一数列 4 2 4 5 1 要分成 3 段。
将其如下分段: [4 2][4 5][1]
第一段和为 6,第 2 段和为 9,第 3 段和为 1,和最大值为 9。
将其如下分段: [4][2 4][5 1]
第一段和为 4,第 2 段和为 6,第 3 段和为 6,和最大值为 6。
并且无论如何分段,最大值不会小于 6。
所以可以得到要将数列 4 2 4 5 1 要分成 3 段,每段和的最大值最小为 6。
思路
- 确定最大值所在的区间,left 和 right
- 二分判断最大值为 mid 时数列是否可以分成 m 段
- 根据判断结果更新区间
- 得到符合题目要求的答案
①完成第一步,确定区间。要求最大值最小,则该值必定是大于数列中的最大值的(最大值单独为一段的时候),最大值则为所有数列之和。所以最大值的区间 left=数列中的最大值,right=数列的所有数相加之和,则:
int n,m,a[100005];
cin >> n >> m;
for(i = 1; i <= n; i++)
{
cin >> a[i];
sum += a[i];//sum为答案右边界
mx = max(mx,a[i]); //mx为答案左边界
}
②二分判断最大值为 mid 时,数列是否可以分成 m 段,采用贪心策略,分割的每一段的总和尽量接近但不超过 mid,则:
while (left <= right)
{
mid = (left + right) / 2;
// 计算最大值为mid时,能分割的段数
int sum1 = 0, cnt = 1; // 分别表示当前这一段的数字总和、数列目前段数
for (int i = 1; i <= n; i++)
{
// 判断a[i]是否可以连接到当前这段数字中
if (sum1 + a[i] <= mid)
sum1 += a[i]; // sum+a[i]不超过mid则可以继续连接
else
sum1 = a[i], cnt++; // 否则a[i]自起一段,数列总段数cnt+1
}
}
③根据判断结果更新区间,因为是求最大值的最小,所以区间上是向左找的。
当 cnt<=m 时,说明 mid 值偏大,此时去查看更小的,即 ans=mid,right=mid-1,如果分割的段数超过 m 段,说明 mid 要更大一点,即 left=mid+1。
// 判断结果,更新区间
if (cnt <= m)
ans = mid, right = mid - 1;
else
left = mid + 1;
④得到符合题目要求的答案:
printf("%d",ans);
代码
#include<bits / stdc++.h>
using namespace std;
int n, m, a[100005];
int sum, mx;
int Binary_answer(int l, int r)
{
int left = l, right = r, mid, ans;
while (left <= right)
{
mid = (left + right) / 2;
// 计算最大值为mid时,能分割的段数
int sum1 = 0, cnt = 1; // 分别表示当前这一段的数字总和、数列目前段数
for (int i = 1; i <= n; i++)
{
// 判断a[i]是否可以连接到当前这段数字中
if (sum1 + a[i] <= mid)
sum1 += a[i]; // sum+a[i]不超过mid则可以继续连接
else
sum1 = a[i], cnt++; // 否则a[i]自起一段,数列总段数cnt+1
}
// 判断结果,更新区间
if (cnt <= m)
ans = mid, right = mid - 1;
else
left = mid + 1;
}
return ans;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
sum += a[i]; // sum为答案右边界
mx = max(mx, a[i]); // mx为答案左边界
}
// 二分答案
cout << Binary_answer(mx, sum);
return 0;
}
最小值最大化问题 - 进击的奶牛
Farmer John 建造了一个有 N(2≤ N ≤100000)个隔间的牛棚,这些隔间分布在一条直线上,坐标是 x 1 , . . . , x N ( 0 ≤ x i ≤ 1000000000 ) x_1,…,x_N(0 ≤ x_i≤1000000000)x 1,…,x
他的 C(2≤ C≤ N)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John 想把这些牛安置在指定的隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢?
思路
- 确定最大值所在的区间,left 和 right
- 二分判断最小值为 mid 时数列是否可以安置 m 头奶牛
- 根据判断结果更新区间
- 得到符合题目要求的答案
代码
#include <bits/stdc++.h>
using namespace std;
int n, m, a[100005];
int mn, mx;
int Binary_answer(int l, int r)
{
int left = l, right = r, mid, ans;
while (left <= right)
{
mid = (left + right) / 2;
// 计算最小值为mid时可以放置的奶牛数量
int index = 1, cnt = 1; // index为当前坐标,cnt为奶牛数量
for (int i = 2; i <= n; i++)
{
// 判断a[i]和当前坐标之前的差值
if (a[i] - a[index] >= mid)
index = i, cnt++; // 条件成立,更新坐标,奶牛数量+1
}
// 判断结果,更新区间
if (cnt >= m)
ans = mid, left = mid + 1;
else
right = mid - 1;
}
return ans;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
// 将坐标从小到大排序,方便计算
sort(a + 1, a + n + 1);
// 确定区间最大值和最小值
mn = 1; // 默认最小值为1,就是紧挨着
mx = a[n] - a[1]; // 最大值为最后一个坐标减第一个坐标
// 二分答案
cout << Binary_answer(mn, mx);
return 0;
}