二分算法
-
解集具"二段性"即可用:前一段不满足,后一段一定满足,或反之
-
两套模板
不用死记,根据题目思考一下就能知道用哪个
- 找左端点,满足条件的最小下标(≥x 的最小元素)
cpp
int l = 1, r = n;
while (l < r)
{
int mid = l + (r - l) / 2; // 防溢出
if (check(mid)) r = mid; // 条件真,答案≤mid
else l = mid + 1; // 条件假,答案>mid
}
// 结束时 l==r,再验证 a[l] 是否真满足
2.找右端点,满足条件的最大下标(≤y 的最大元素)
cpp
int l = 1, r = n;
while (l < r) {
int mid = l + (r - l + 1) / 2; // 向上取整
if (check(mid)) l = mid; // 条件真,答案≥mid
else r = mid - 1; // 条件假,答案<mid
}
- STL
cpp
#include <algorithm>
vector<int> a;
auto it1 = lower_bound(a.begin(), a.end(), x); // ≥x 最小元素
auto it2 = upper_bound(a.begin(), a.end(), x); // >x 最小元素
// 返回迭代器,解引用 *it,下标 it - a.begin()。
// 想拿 ≤x 的最大元素:--it2(先判 it2!=a.begin())
时间复杂度
O(log n)
二分答案
- 最大值最小/最小值最大
cpp
bool check(int mid) { /* 判定"答案≤mid"是否可行 */ }
int left = MINAns, right = MAXAns;
while (left < right) {
int mid = left + (right - left) / 2; // 左端点版
if (check(mid)) right = mid; // 可行,尝试更小
else left = mid + 1; // 不可行,需更大
}
// left 即为"最小化最大值"
时间复杂度
O(log n)
在解题时如何想到二分?
当想要枚举数据时发现时间太长,并且答案是区间某一个确定值;可以试着分析题目条件是否具有二段性
// 典型的问题描述:
// - 找到最大的x使得条件满足
// - 找到最小的x使得条件满足
// - 第一个不满足条件的点
// - 最后一个满足条件的点
从"过程思维"到"结果思维"
从贪心转换到二分
- 贪心:关注每一步该做什么(过程)
- 二分答案 :先假设知道答案,再验证(结果)
二分关注全局可行性(给定总时间,判断能否完成任务)
直接上例题(来源洛谷)
P1083 借教室
题目描述
在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来 nnn 天的借教室信息,其中第 iii 天学校有 rir_iri 个教室可供租借。共有 mmm 份订单,每份订单用三个正整数描述,分别为 dj,sj,tjd_j,s_j,t_jdj,sj,tj,表示某租借者需要从第 sjs_jsj 天到第 tjt_jtj 天租借教室(包括第 sjs_jsj 天和第 tjt_jtj 天),每天需要租借 djd_jdj 个教室。
我们假定,租借者对教室的大小、地点没有要求。即对于每份订单,我们只需要每天提供 djd_jdj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第 sjs_jsj 天到第 tjt_jtj 天中有至少一天剩余的教室数量不足 djd_jdj 个。
现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。
输入格式
第一行包含两个正整数 n,mn,mn,m,表示天数和订单的数量。
第二行包含 nnn 个正整数,其中第 iii 个数为 rir_iri,表示第 iii 天可用于租借的教室数量。
接下来有 mmm 行,每行包含三个正整数 dj,sj,tjd_j,s_j,t_jdj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。天数与订单均用从 111 开始的整数编号。
输出格式
如果所有订单均可满足,则输出只有一行,包含一个整数 000。
否则(订单无法完全满足)输出两行,第一行输出一个负整数 −1-1−1,第二行输出需要修改订单的申请人编号。
输入输出样例 #1
输入 #1
4 3
2 5 4 3
2 1 3
3 2 4
4 2 4
输出 #1
-1
2
说明/提示
【输入输出样例说明】
第 111 份订单满足后,444 天剩余的教室数分别为 0,3,2,30,3,2,30,3,2,3。第 222 份订单要求第 222 天到第 444 天每天提供 333 个教室,而第 333 天剩余的教室数为 222,因此无法满足。分配停止,通知第 222 个申请人修改订单。
【数据范围】
对于 10%10\%10% 的数据,有 1≤n,m≤101\le n,m\le 101≤n,m≤10;
对于 30%30\%30% 的数据,有 1≤n,m≤10001\le n,m\le 10001≤n,m≤1000;
对于 70%70\%70% 的数据,有 1≤n,m≤1051 \le n,m \le 10^51≤n,m≤105;
对于 100%100\%100% 的数据,有 1≤n,m≤1061 \le n,m \le 10^61≤n,m≤106,0≤ri,dj≤1090 \le r_i,d_j\le 10^90≤ri,dj≤109,1≤sj≤tj≤n1 \le s_j\le t_j\le n1≤sj≤tj≤n。
NOIP 2012 提高组 第二天 第二题
2022.2.20 新增一组 hack 数据
题解
⼒破万法:线段树。但是杀鸡焉⽤宰⽜⼑,可以⽤更简单的做法
设⽆法完成的订单的天数为 ret 。针对某⼀天 x ,根据题意可得:
- 当 x ≥ ret 时,处理完 [1, x] 天的订单,⼀定⽆法完成;
- 当 x < ret 时,处理完 [1, x] 天的订单,⼀定可以完成。
在解空间中,根据 的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。
接下来的问题就是给定⼀个天数 x ,如何判断能否完成 [1, x] 天内的所有订单:
利⽤「差分」数组
处理完 [1, x] 区间的修改,还原「原数组」之后判断是否「全部 ≥ 0 」即可。
时间复杂度: O(n log n) 。
cpp
#include <iostream>
using namespace std;
const int N = 1e6 + 10; // 定义最大数据范围
int n, m; // n-天数,m-订单数
int r[N]; // 每天可用的教室数量
int d[N], s[N], t[N]; // 订单信息:d-每天用量,s-开始日,t-结束日
int f[N]; // 差分数组
// 检查处理前x个订单是否可行
bool check(int x)
{
// 初始化差分数组,表示每天剩余的教室数量
for(int i = 1; i <= n; i++)
{
f[i] = r[i] - r[i - 1]; // 构建差分数组
}
// 处理前x个订单
for(int i = 1; i <= x; i++)
{
// 在差分数组上应用订单:从s[i]到t[i]每天减少d[i]个教室
f[s[i]] -= d[i]; // 开始位置减去需求量
f[t[i] + 1] += d[i]; // 结束位置的下一天加回需求量
}
// 通过前缀和还原实际的每天剩余教室数量
for(int i = 1; i <= n; i++)
{
f[i] = f[i - 1] + f[i]; // 计算前缀和,得到每天实际剩余教室
if(f[i] < 0) return false; // 如果某天教室不足,返回false
}
return true; // 所有订单都能满足,返回true
}
int main()
{
cin >> n >> m; // 输入天数和订单数
// 输入每天可用的教室数量
for(int i = 1; i <= n; i++) cin >> r[i];
// 输入订单信息
for(int i = 1; i <= m; i++) cin >> d[i] >> s[i] >> t[i];
// 二分查找:找到第一个无法满足的订单
int l = 1, r = m; // 搜索范围:从第1个订单到第m个订单
while(l < r)
{
int mid = (l + r) / 2; // 取中间点
if(check(mid))
l = mid + 1; // 前mid个订单可行,检查更大的范围
else
r = mid; // 前mid个订单不可行,检查更小的范围
}
// 输出结果
if(check(l))
cout << 0 << endl; // 所有订单都能满足
else
cout << -1 << endl << l << endl; // 输出-1和第一个无法满足的订单编号
return 0;
}
P1843 奶牛晒衣服
题目背景
熊大妈决定给每个牛宝宝都穿上可爱的婴儿装 。但是由于衣服很湿,为牛宝宝晒衣服就成了很不爽的事情。于是,熊大妈请你(奶牛)帮助她完成这个重任。
题目描述
一件衣服在自然条件下用一秒的时间可以晒干 aaa 点湿度。抠门的熊大妈只买了一台烘衣机 。使用用一秒烘衣机可以让一件衣服额外烘干 bbb 点湿度(一秒晒干 a+ba+ba+b 湿度),但在同一时间内只能烘一件衣服。现在有 nnn 件衣服,第 iii 衣服的湿度为 wiw_iwi(保证互不相同),要你求出弄干所有衣服的最少时间(湿度为 000 为干 )。
输入格式
第一行三个整数,分别为 n,a,bn,a,bn,a,b。
接下来 222 到 n+1n+1n+1 行,第 iii 行输入 wiw_iwi。
输出格式
一行,弄干所有衣服的最少时间。
输入输出样例 #1
输入 #1
3 2 1
1
2
3
输出 #1
1
说明/提示
样例解释
让机器烘第三件衣服即可一秒完成。
数据范围
1≤wi,a,b,n≤5×1051 \le w_i,a,b,n \le 5 \times 10^51≤wi,a,b,n≤5×105
题解
错误解
第一眼直接[[贪心]]
- 每次都烘最湿的衣服
- 优先处理湿度大的衣服
cpp
n=3, a=1, b=2
湿度:[3, 3, 3]
如果每次都烘最湿的,可能需要2秒。但最优解是1秒内烘任意一件,自然晾干其他的。
正解
思路
- 问题问的是"最少时间" → 可能二分
- 时间具有单调性 :
- 如果x分钟能烘干所有衣服,那么x+1分钟肯定也能
- 如果x分钟不能烘干,那么x-1分钟肯定也不能
设经过的时间是 x 。根据题意,我们可以发现如下性质:
- 经过的时间如果是 x 的话,烘⼲机的「使⽤次数」最多也是 x ;
- 当 x 在「增⼤」的时候,能弄⼲的⾐服在「增多」;
- 当 x 在「减⼩」的时候,能弄⼲的⾐服也在「减少」。那么在整个「解空间」⾥⾯,设弄⼲所有⾐服的最少时间是 ret ,于是有:
- 当 x ≥ ret 时,我们「能弄⼲」所有⾐服;
- 当 x < ret 时,我们「不能弄⼲」所有⾐服。
在解空间中,根据 ret的位置,可以将解集分成两部分,具有「⼆段性」,那么我们就可以「⼆分答案」。
接下来的重点就是,给定⼀个时间x ,判断「是否能把所有的⾐服全部弄⼲」。当时间为x时,所
有⾐服能够⾃然蒸发a* x 的湿度,于是:
- 如果 w[i] ≤ a ⋅ x :让它⾃然蒸发;
- 如果 w[i] > a ⋅ x:需要⽤烘⼲机烘⼲ t = w[i] − a ⋅ x的湿度,次数为 t / b + (t % b == 0 ? 0 : 1)
那么我们可以遍历所有的⾐服,计算烘⼲机的「使⽤次数」: - 如果使⽤次数「⼤于」给定的时间 x ,说明不能全部弄⼲;
- 如果使⽤次数「⼩于等于」给定的时间 x ,说明能全部弄⼲。
cpp
#include <iostream>
using namespace std;
typedef long long LL; // 定义长整型别名,防止数据溢出
const int N = 5e5 + 10; // 定义数组最大大小
LL n, a, b; // n-衣服数量,a-自然晾干速度,b-烘干机额外速度
LL w[N]; // 存储每件衣服的湿度
// 检查在x秒内是否能烘干所有衣服
bool check(LL x)
{
LL cnt = 0; // 需要烘干机工作的总时间(秒)
for(int i = 1; i <= n; i++)
{
// 如果自然晾干就能满足,跳过
if(w[i] <= a * x) continue;
// 计算需要烘干机补足的湿度
LL d = w[i] - a * x;
// 计算这件衣服需要烘干机工作的时间
// 向上取整:d/b 如果有余数就多需要1秒
cnt = cnt + d / b + (d % b == 0 ? 0 : 1);
// 注意:这里应该提前判断,如果cnt已经超过x,可以直接返回false优化性能
// if(cnt > x) return false;
}
// 总烘干机时间不超过x秒(因为只有一台机器)
return cnt <= x;
}
int main()
{
// 输入数据
cin >> n >> a >> b;
for(int i = 1; i <= n; i++) cin >> w[i];
// 二分查找最小时间
LL l = 1, r = 5e5; // 时间范围:1秒到最大可能时间
while(l < r)
{
LL mid = (l + r) / 2; // 取中间时间
if(check(mid))
r = mid; // mid秒可行,尝试更小的时间
else
l = mid + 1; // mid秒不可行,需要更多时间
}
// 输出最小可行时间
cout << l << endl;
return 0;
}
P3853 路标设置
题目背景
B 市和 T 市之间有一条长长的高速公路,这条公路的某些地方设有路标,但是大家都感觉路标设得太少了,相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题,我们把公路上相邻路标的最大距离定义为该公路的"空旷指数"。
题目描述
现在政府决定在公路上增设一些路标,使得公路的"空旷指数"最小。他们请求你设计一个程序计算能达到的最小值是多少。请注意,公路的起点和终点保证已设有路标,公路的长度为整数,并且原有路标和新设路标都必须距起点整数个单位距离。
输入格式
第 111 行包括三个数 L,N,KL,N,KL,N,K,分别表示公路的长度,原有路标的数量,以及最多可增设的路标数量。
第 222 行包括递增排列的 NNN 个整数,分别表示原有的 NNN 个路标的位置。路标的位置用距起点的距离表示,且一定位于区间 [0,L][0,L][0,L] 内。
输出格式
输出 111 行,包含一个整数,表示增设路标后能达到的最小"空旷指数"值。
输入 #1
101 2 1
0 101
输出 #1
51
说明/提示
公路原来只在起点和终点处有两个路标,现在允许新增一个路标,应该把新路标设在距起点 505050 或 515151 个单位距离处,这样能达到最小的空旷指数 515151。
50%50\%50% 的数据中,2≤N≤1002 \leq N \leq 1002≤N≤100,0≤K≤1000 \leq K \leq 1000≤K≤100。
100%100\%100% 的数据中,2≤N≤1000002 \leq N \leq 1000002≤N≤100000, 0≤K≤1000000 \leq K \leq1000000≤K≤100000。
100%100\%100% 的数据中,0<L≤100000000 < L \leq 100000000<L≤10000000。
题解
[[二分]]答案
⼆段性:设最优解为 ret :
- 如果 x >= ret ,需要设置路标的数量就⼩于等于 k ;
- 如果 x < ret ,需要设置路标的数量就⼤于 k 。
对于⼀个数 x ,如何判断设置路标的数量: - 设 d = a[i] - a[i - 1] ,那么需要路标的数量就是 d / x ;
- 如果 d % x == 0 ,此时就可以少⽤⼀个路标。
cpp
#include <iostream>
using namespace std;
const int N = 1e5 + 10; // 定义最大数组大小
int len, n, k; // len:公路长度, n:原有路标数量, k:最多可增设路标数量
int a[N]; // 存储原有路标位置
// 检查函数:判断当最大间隔为x时,需要增设的路标数量是否不超过k
bool check(int x)
{
int cnt = 0; // 记录需要增设的路标数量
// 遍历所有相邻的原有路标
for(int i = 2; i <= n; i++)
{
int d = a[i] - a[i - 1]; // 计算相邻路标间的距离
cnt += d / x; // 在距离d中,每x长度需要增设一个路标
// 如果距离正好能被x整除,最后一个间隔不需要增设路标
if(d % x == 0) cnt--;
}
// 判断需要增设的路标数量是否在允许范围内
return cnt <= k;
}
int main()
{
// 输入公路长度、原有路标数量、最多可增设路标数量
cin >> len >> n >> k;
// 输入原有路标的位置(按递增顺序排列)
for(int i = 1; i <= n; i++)
cin >> a[i];
// 二分查找最小空旷指数
int l = 1, r = len; // 最小可能间隔为1,最大可能间隔为公路全长
while(l < r)
{
int mid = (l + r) / 2; // 取中间值作为候选的最大间隔
if(check(mid))
r = mid; // 如果mid可行,尝试更小的间隔(往左搜索)
else
l = mid + 1; // 如果mid不可行,需要更大的间隔(往右搜索)
}
// 输出最小的可能空旷指数
cout << l << endl;
return 0;
}