目录
- 引言
- 概念
- 一、借教室
- 二、分巧克力
- 三、管道
- 四、技能升级
- 五、冶炼金属
- 六、数的范围
- 七、最佳牛围栏
引言
这几天一直在做二分的题,都是上了难度的题目,本来以为自己的二分水平已经非常熟悉了,没想到还是糊涂了一两天才重新想清楚,想明白,还是得继续不断地刷题,把这些类型的题刷明白,刷透了,就可以了,加油!
概念
一般只有这两种情况,具体模板可以参考之前的博客二分,再特殊一点的就是本篇博客的第五、六题了,但看我的解析,都是一样的步骤。
一、借教室
标签:二分
思路:这道题如果用暴力来做的话就是遍历 m m m 个订单,每个订单然后去判断是否正确,去判断没天中的订单是否满足要求,就这样也是 1 0 12 10^{12} 1012 明显超时了。可以用二分来做,因为这个订单是具有单调性的,假设第 k k k 个订单是第一个不满足要求的,那么之后的订单也是不满足要求的,判断依据就是定义一个天数数组 b [ i ] b[i] b[i] ,每天的订单数是不能超过 w [ i ] w[i] w[i] 的,所以可以用二分检查条件从而判断出第一个不满足的,然后再 s j ∼ t j s_j\sim t_j sj∼tj天要用 d j d_j dj个教室可以用差分来做,这样就可以了。
这相当于一个模型:有多个订单,每天进行多个操作,问订单会不会满足条件.
题目描述:
在大学期间,经常需要租借教室。 大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。 教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。 面对海量租借教室的信息,我们自然希望编程解决这个问题。 我们需要处理接下来 n 天的借教室信息,其中第 i 天学校有 ri 个教室可供租借。 共有 m 份订单,每份订单用三个正整数描述,分别为 dj,sj,tj,表示某租借者需要从第 sj 天到第 tj 天租借教室(包括第 sj 天和 第 tj 天),每天需要租借 dj 个教室。 我们假定,租借者对教室的大小、地点没有要求。 即对于每份订单,我们只需要每天提供 dj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。 借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。 如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。 这里的无法满足指从第 sj 天到第 tj 天中有至少一天剩余的教室数量不足 dj 个。 现在我们需要知道,是否会有订单无法完全满足。 如果有,需要通知哪一个申请人修改订单。 输入格式 第一行包含两个正整数 n,m,表示天数和订单的数量。 第二行包含 n 个正整数,其中第 i 个数为 ri,表示第 i 天可用于租借的教室数量。 接下来有 m 行,每行包含三个正整数 dj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。 每行相邻的两个数之间均用一个空格隔开。 天数与订单均用从 1 开始的整数编号。 输出格式 如果所有订单均可满足,则输出只有一行,包含一个整数 0。 否则(订单无法完全满足)输出两行,第一行输出一个负整数 −1,第二行输出需要修改订单的申请人编号。 数据范围 1≤n,m≤106,0≤ri,dj≤109,1≤sj≤tj≤n 输入样例: 4 3 2 5 4 3 2 1 3 3 2 4 4 2 4 输出样例: -1 2
示例代码:
#include
using namespace std; typedef long long LL; const int N = 1e6+10; int n, m; int w[N]; int d[N], s[N], t[N]; LL b[N]; bool check(int mid) { memset(b, 0, sizeof b); for(int i = 1; i <= mid; ++i) { b[s[i]] += d[i]; b[t[i]+1] -= d[i]; } for(int i = 1; i <= n; ++i) { b[i] += b[i-1]; if(b[i] > w[i]) return true; } return false; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; ++i) cin >> w[i]; for(int i = 1; i <= m; ++i) cin >> d[i] >> s[i] >> t[i]; int l = 1, r = m; while(l < r) { int mid = l + r >> 1; if(check(mid)) r = mid; else l = mid + 1; } if(check(r)) { cout << -1 << endl; cout << r << endl; } else { cout << 0 << endl; } return 0; }
二、分巧克力
标签:二分
思路:这道题首先满足单调性,分的巧克力的大小在一定值后就不能切除 K K K 块巧克力了,所以可以用二分来求答案。
题目描述:
儿童节那天有 K 位小朋友到小明家做客。 小明拿出了珍藏的巧克力招待小朋友们。 小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi 的方格组成的长方形。 为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。 切出的巧克力需要满足: 形状是正方形,边长是整数大小相同例如一块 6×5 的巧克力可以切出 6 块 2×2 的巧克力或者 2 块 3×3 的巧克力。 当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么? 输入格式 第一行包含两个整数 N 和 K。 以下 N 行每行包含两个整数 Hi 和 Wi。 输入保证每位小朋友至少能获得一块 1×1 的巧克力。 输出格式 输出切出的正方形巧克力最大可能的边长。 数据范围 1≤N,K≤105,1≤Hi,Wi≤105 输入样例: 2 10 6 5 5 6 输出样例: 2
示例代码:
#include
using namespace std; typedef long long LL; const int N = 1e5+10; int n, k; int h[N], w[N]; bool check(int mid) { LL s = 0; for(int i = 1; i <= n; ++i) { s += ((LL)h[i] / mid) * (w[i] / mid); if(s >= k) return true; } return false; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> k; for(int i = 1; i <= n; ++i) cin >> h[i] >> w[i]; int l = 1, r = 1e5; while(l < r) { int mid = l + r + 1 >> 1; if(check(mid)) l = mid; else r = mid - 1; } cout << r << endl; return 0; }
三、管道
标签:二分
思路:这道题一看是求最早的时间,根据题意可以知道时间到了一定的值,那么后面的时间都可以满足条件,那么可以利用二分来枚举时间,然后根据判断条件来找到满足条件的第一个点。然后判断条件可以通过时间来算出来每个阀门在当前时间能够经过的区间,然后把这些区间合并,如果最后的区间包含了所有的区间那么就是满足条件的。而且这个区间可以是不重叠也可以合并的,因为 [ 1 , 2 ] [1,2] [1,2] 和 [ 3 , 4 ] [3,4] [3,4] 都能覆盖到,那么说明 [ 1 , 4 ] [1,4] [1,4] 都能检测道水流。
题目描述:
有一根长度为 len 的横向的管道,该管道按照单位长度分为 len 段,每一段的中央有一个可开关的阀门和一个检测水流的传感器。 一开始管道是空的,位于 Li 的阀门会在 Si 时刻打开,并不断让水流入管道。 对于位于 Li 的阀门,它流入的水在 Ti(Ti≥Si)时刻会使得从第 Li−(Ti−Si) 段到第 Li+(Ti−Si) 段的传感器检测到水流。 求管道中每一段中间的传感器都检测到有水流的最早时间。 输入格式 输入的第一行包含两个整数 n,len,用一个空格分隔,分别表示会打开的阀门数和管道长度。 接下来 n 行每行包含两个整数 Li,Si,用一个空格分隔,表示位于第 Li 段管道中央的阀门会在 Si 时刻打开。 输出格式 输出一行包含一个整数表示答案。 数据范围 对于 30% 的评测用例,n≤200,Si,len≤3000; 对于 70% 的评测用例,n≤5000,Si,len≤105; 对于所有评测用例,1≤n≤105,1≤Si,len≤109,1≤Li≤len,Li−1
-
示例代码:
#include
using namespace std; typedef long long LL; typedef pair PII; const int N = 1e5+10; int n, len; int L[N], S[N]; PII segs[N]; bool check(int mid) { int cnt = 0; for(int i = 1; i <= n; ++i) { if(mid >= S[i]) { int t = mid - S[i]; int l = max(1, L[i] - t), r = min((LL)len, (LL)L[i] + t); segs[cnt++] = {l,r}; } } sort(segs, segs+cnt); int l = -2e9, r = -2e9; for(int i = 0; i < cnt; ++i) { if(segs[i].first > r + 1) l = segs[i].first, r = segs[i].second; else r = max(r, segs[i].second); } return l == 1 && r == len; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> len; for(int i = 1; i <= n; ++i) cin >> L[i] >> S[i]; int l = 0, r = 2e9; // 等到1e9开阀门,再经过1e9从左边流到右边 while(l < r) { int mid = (LL)l + r >> 1; if(check(mid)) r = mid; else l = mid + 1; } cout << r << endl; return 0; }
四、技能升级
标签:二分
思路1:首先这道题先用了堆来做,过了7/12个数据,代码随后附上。
思路2:做题首先要先到怎么做出来然后再想时间复杂度的问题,首先这道题肯定是把所有的数加起来,然后从大到小排序选前 M M M 个数,就是最大的,那么实际上也是从这里面选前 M M M 个最大的数。那么我们假设第 M M M 个数为 x x x ,那么一定满足大于等于 x x x 的个数一定是大于等于 M M M 的,并且大于等于 x + 1 x+1 x+1的个数一定是小于 M M M 个的,因为x越大值就越少,越小值就越多,然后可以求出来 x x x ,然后最后的结果和个数可以通过数学公式来推出来,最后还要统计个数如果超 M M M 个了,这是因为可能有跟 x x x 一样的数,所以减去多余的即可。
模型:多路归并算法
题目描述:
小蓝最近正在玩一款 RPG 游戏。 他的角色一共有 N 个可以加攻击力的技能。 其中第 i 个技能首次升级可以提升 Ai 点攻击力,以后每次升级增加的点数都会减少 Bi。 ⌈AiBi⌉(上取整)次之后,再升级该技能将不会改变攻击力。 现在小蓝可以总计升级 M 次技能,他可以任意选择升级的技能和次数。 请你计算小蓝最多可以提高多少点攻击力? 输入格式 输入第一行包含两个整数 N 和 M。 以下 N 行每行包含两个整数 Ai 和 Bi。 输出格式 输出一行包含一个整数表示答案。 数据范围 对于 40% 的评测用例,1≤N,M≤1000; 对于 60% 的评测用例,1≤N≤104,1≤M≤107; 对于所有评测用例,1≤N≤105,1≤M≤2×109,1≤Ai,Bi≤106。 输入样例: 3 6 10 5 9 2 8 1 输出样例: 47
示例代码一: 堆 7/12
#include
using namespace std; typedef long long LL; typedef pair PII; #define x first #define y second const int N = 1e5+10; int n, m; int a[N], b[N]; priority_queue heap; int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; ++i) { cin >> a[i] >> b[i]; heap.push({a[i], b[i]}); } LL res = 0; while(m-- && heap.size()) { auto t = heap.top(); heap.pop(); res += t.x; if(t.x - t.y <= 0) continue; //为0的就不加进去了,加了最大值也不变还会增加时间复杂度 heap.push({t.x-t.y, t.y}); } cout << res << endl; return 0; } 示例代码二: 二分 12/12
#include
using namespace std; typedef long long LL; const int N = 1e5+10; int n, m; int a[N], b[N]; bool check(int mid) { LL s = 0; for(int i = 1; i <= n; ++i) { if(a[i] >= mid) { s += (a[i] - mid) / b[i] + 1; if(s >= m) return true; } } return false; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; ++i) cin >> a[i] >> b[i]; int l = 0, r = 1e6; // l可能取0,即全部包含进去 while(l < r) { int mid = l + r + 1 >> 1; if(check(mid)) l = mid; else r = mid - 1; } LL res = 0, cnt = 0; for(int i = 1; i <= n; ++i) { if(a[i] >= r) { int c = (a[i] - r) / b[i] + 1; int end = a[i] - (c - 1) * b[i]; cnt += c; res += (LL)(a[i] + end) * c / 2; } } cout << res - (cnt - m) * r << endl; return 0; }
五、冶炼金属
标签:二分
思路:这道题其实就是如下图所示的一种二分,在满足条件的情况下,找到 m i d mid mid 的最小值和最大值,然后就是一些细节问题,我标在注释里了。
题目描述:
小蓝有一个神奇的炉子用于将普通金属 O 冶炼成为一种特殊金属 X。 这个炉子有一个称作转换率的属性 V,V 是一个正整数,这意味着消耗 V 个普通金属 O 恰好可以冶炼出一个特殊金属 X ,当 普通金属 O 的数目不足 V 时,无法继续冶炼。 现在给出了 N 条冶炼记录,每条记录中包含两个整数 A 和 B,这表示本次投入了 A 个普通金属 O,最终冶炼出了 B 个特殊 金属 X。 每条记录都是独立的,这意味着上一次没消耗完的普通金属 O 不会累加到下一次的冶炼当中。 根据这 N 条冶炼记录,请你推测出转换率 V 的最小值和最大值分别可能是多少,题目保证评测数据不存在无解的情况。 输入格式 第一行一个整数 N,表示冶炼记录的数目。 接下来输入 N 行,每行两个整数 A、B,含义如题目所述。 输出格式 输出两个整数,分别表示 V 可能的最小值和最大值,中间用空格分开。 数据范围 对于 30% 的评测用例,1≤N≤102。 对于 60% 的评测用例,1≤N≤103。 对于 100% 的评测用例,1≤N≤104,1≤B≤A≤109。 输入样例: 3 75 3 53 2 59 2 输出样例: 20 25 样例解释 当 V=20 时,有:⌊7520⌋=3,⌊5320⌋=2,⌊5920⌋=2,可以看到符合所有冶炼记录。 当 V=25 时,有:⌊7525⌋=3,⌊5325⌋=2,⌊5925⌋=2,可以看到符合所有冶炼记录。 且再也找不到比 20 更小或者比 25 更大的符合条件的 V 值了。
示例代码:
#include
using namespace std; typedef long long LL; const int N = 1e4+10; int n; int a[N], b[N]; bool check1(int mid) // 这里的check判断的为是否在右半边 { for(int i = 1; i <= n; ++i) { // 说明在左半边 if(a[i] / mid > b[i]) return false; // 最小值,满足条件的是右半边,mid越大值越小,所以当大于b[i]时就不满足条件了 } return true; } bool check2(int mid) { for(int i = 1; i <= n; ++i) { if(a[i] / mid < b[i]) return false; // 同理,当小于b[i]时说明在右半边,而二分的正确的条件应该是左半边 } return true; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n; for(int i = 1; i <= n; ++i) cin >> a[i] >> b[i]; int r1, r2; int l = 1, r = 1e9; // 因为a[i], b[i]的值可能为1e9,1 while(l < r) { // 注意这里其实暴不了int因为就是是两个1e9也没用 int mid = (LL)l + r >> 1; // 这里的check判断的为是否在右半边 if(check1(mid)) r = mid; // 找最小值,发现右边是条件一样的,所以 r = mid else l = mid + 1; } r1 = r; l = 1, r = 1e9; while(l < r) { int mid = (LL)l + r + 1 >> 1; if(check2(mid)) l = mid; // 找最大值,发现左边和自己的条件是一样的,所以l = mid else r = mid - 1; } r2 = r; cout << r1 << " " << r2 << endl; return 0; }
六、数的范围
标签:二分
思路:这道题真的写了非常多遍了,但写的时候还是出错,而且还把我搞迷糊了一两天,导致其他的二分题都不会了,都不知道怎么做了,好在自己坚持,慢慢耗、磨出来了,知道了 c h e c k check check 函数和区间判断怎么写了,思路一下子清晰了,然后其余的二分题用这个方法也正确的做出来了。首先你要有个条件,比如说这道题是 a [ m i d ] = x a[mid] = x a[mid]=x,然后找满足这个条件的 m i d mid mid 的最小值和最大值 ,然后比如说你要找 r 1 r1 r1,那么和 r 1 r1 r1 满足相同条件的在 r 1 r1 r1 的右半边,所以 c h e c k check check 函数为 m i d mid mid 在右半部分满足什么条件,即 a [ m i d ] ≥ x a[mid] \geq x a[mid]≥x ,然后因为满足条件的在右半部分所以这个条件下的语句为 r = m i d r = mid r=mid ,然后 l = m i d + 1 l = mid + 1 l=mid+1 ,这个 r = m i d , l = m i d + 1 r = mid, l = mid + 1 r=mid,l=mid+1和 l = m i d , r = m i d − 1 l = mid, r = mid - 1 l=mid,r=mid−1都是一一对应的, c h e c k check check 条件下写前面的, e l s e else else 下写后面的,然后如果是 l = m i d l = mid l=mid 语句,那么在 i n t m i d = l + r > > 1 int\ mid = l + r >> 1 int mid=l+r>>1那,要改成 i n t m i d = l + r + 1 > > 1 int\ mid = l + r + 1 >> 1 int mid=l+r+1>>1,否则会有边界问题。然后说一下最大值,再梳理一遍,其最大值,满足相同条件的在左部分,虽然只有一部分但只要这么想就是对的, c h e c k check check 为 a [ m i d ] ≤ x a[mid] \leq x a[mid]≤x ,因为在满足条件的在左边所以 l = m i d l = mid l=mid ,然后就是 r = m i d − 1 r = mid - 1 r=mid−1 ,再在二分 m i d mid mid 那加一。
题目描述:
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。 对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。 如果数组中不存在该元素,则返回 -1 -1。 输入格式 第一行包含整数 n 和 q,表示数组长度和询问个数。 第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。 接下来 q 行,每行包含一个整数 k,表示一个询问元素。 输出格式 共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。 如果数组中不存在该元素,则返回 -1 -1。 数据范围 1≤n≤100000 1≤q≤10000 1≤k≤10000 输入样例: 6 3 1 2 2 3 3 4 3 4 5 输出样例: 3 4 5 5 -1 -1
示例代码:
#include
using namespace std; typedef long long LL; const int N = 1e5+10; int n, q; int a[N]; int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> q; for(int i = 0; i < n; ++i) cin >> a[i]; while(q--) { int x; cin >> x; int r1 = -1, r2 = -1; int l = 0, r = n - 1; // 注意看好起始位置从几开始的 while(l < r) { int mid = (LL)l + r >> 1; if(a[mid] >= x) r = mid; // 先看点,然后看相同条件的区间在哪来判断check,然后区间在右就是r = mid else l = mid + 1; // 在左就是l = mid,其余的else和+1跟着变就行 } if(a[r] == x) r1 = r; l = 0, r = n - 1; while(l < r) { int mid = (LL)l + r + 1 >> 1; if(a[mid] <= x) l = mid; else r = mid - 1; } if(a[r] == x) r2 = r; cout << r1 << " " << r2 << endl; } return 0; }
七、最佳牛围栏
标签:二分、前缀和
思路1:首先这道题用了双指针跟前缀和,再加上一些限制条件最大化的缩减时间,然后过了 10 / 15 10/15 10/15 个数据,还可以。
思路2:首先这道题想着求最大平均值,一般这个求最大最小的这种边界问题都可以想着用二分来做。首先是否有单调性,如果假设最大平均值为 m i d mid mid ,那么小于 m i d mid mid 的肯定有,大于 m i d mid mid 的肯定没有,所以可以通过 c h e c k check check 是否存在 m i d mid mid 来二分出来最大的 m i d mid mid 。条件就是是否存在一个长度大于等于 F F F 的连续子区间,使得这个区间的平均值为 m i d mid mid ,如果给每个点都减去 m i d mid mid ,那么问题就等于是否存在一个长度大于等于 F F F 的连续子区间使得这个区间的和非负,那么就可以用前缀和来求,即 s [ j ] − s [ i ] ≥ 0 s[j] - s[i] \geq 0 s[j]−s[i]≥0,即 s [ j ] ≥ s [ i ] s[j] \geq s[i] s[j]≥s[i],既然要找存在,那么这个 s [ i ] s[i] s[i] 当然越小越好了,就是找 i i i 之前的最小的 s [ i ] s[i] s[i] ,然后用双指针算法求,再处理好边界问题就行了。
模型:从一段区间中找数量大于等于F的连续子区间的最大平均值
题目描述:
农夫约翰的农场由 N 块田地组成,每块地里都有一定数量的牛,其数量不会少于 1 头,也不会超过 2000 头。 约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。 围起区域内至少需要包含 F 块地,其中 F 会在输入中给出。 在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。 输入格式 第一行输入整数 N 和 F,数据间用空格隔开。 接下来 N 行,每行输入一个整数,第 i+1 行输入的整数代表第 i 片区域内包含的牛的数目。 输出格式 输出一个整数,表示平均值的最大值乘以 1000 再 向下取整 之后得到的结果。 数据范围 1≤N≤100000 1≤F≤N 输入样例: 10 6 6 4 2 10 3 8 5 9 4 1 输出样例: 6500
示例代码一: 双指针、前缀和 10/15
#include
using namespace std; const int N = 1e5+10; int n, f; int a[N], s[N]; int main() { cin >> n >> f; for(int i = 1; i <= n; ++i) { cin >> a[i]; s[i] = s[i-1] + a[i]; } int res = -2e9; for(int i = 1; n - i + 1 >= f; ++i) { for(int j = i + f - 1; j <= n; ++j) { res = max((double)res, (double)(s[j] - s[i-1]) / (j - i + 1) * 1000); } } cout << res << endl; return 0; } 示例代码二:二分、前缀和 15/15
#include
using namespace std; typedef long long LL; const int N = 1e5+10; int n, m; int cow[N]; double sum[N]; bool check(double ave) { for(int i = 1; i <= n; ++i) sum[i] = sum[i-1] + cow[i] - ave; double minv = 0; for(int i = 0, j = m; j <= n; ++i, ++j) { minv = min(minv, sum[i]); if(sum[j] >= minv) return true; } return false; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m; for(int i = 1; i <= n; ++i) cin >> cow[i]; double l = 0, r = 2000; while(r - l > 1e-5) { double mid = (l + r) / 2; if(check(mid)) l = mid; else r = mid; } cout << (int)(r * 1000) << endl; return 0; }