目录
板块一:二分查找
第一题:cf706B
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n;
int bs(int x) {
int l = 0, r = n, mid;
while (l < r) {
mid = l + r >> 1;
if (a[mid] > x) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
sort(a, a + n);
int m;
cin >> m;
while (m--) {
int x;
cin >> x;
cout << bs(x) << '\n';
}
return 0;
}
复习二分查找模板:天下无二唯二模板
查找第一个合法的答案:
r一定是合法的,l是可能是合法的。l-1必然是非法的,而且不断中间偏左地取,因为除二会把奇数损失1,直到l是第一个合法的数,然后r逐渐逼近,直到相邻然后r取到l,出循环。
int bs(int x) {//binary search
int l = 0, r = n, mid;
while (l < r) {//小于
mid = l + r >> 1;
if (a[mid] > x) {//这里的if语句写的是正确的条件
r = mid;//可能直接踩到答案
} else {
l = mid + 1;//mid处肯定不是答案
}
}//最终一定可以相等的!!!
return l;
}
查找最后一个合法的答案:
这里是取中间偏右,这和最后的状态(相邻)有关。l一定是合法的,r可能是合法的,r+1一定是非法的,直到相邻,l是合法的,r是最后一个合法的,然后mid一下出循环,r = l;
while (l < r)
{
int mid = l + r + 1 >> 1; //(l+r+1)/2
if (check(mid)) l = mid;
else r = mid - 1;
}
上面那个模板用的比较多。两个都很重要。
两者的区别在于符合条件的数据再左还是在右,例如,如果说我们要找的是第一个大于xxx的数,而且是一个递增区间,那么答案区间在右侧,右侧才有大于xxx的数,check的时候r = mid,直到最后一个符合,一个不符合,例如l = 4, r = 5此时,r一定是符合的,mid = 4不符然后右移,出循环。
下面那个答案区间在左,移动l,直到l = 4, r = 5, mid = 5不符左移。这个求的是最后一个符合条件的值。
不过上面的好。
杂项知识点:getline函数就算是空行也是可以读取的,可以使用str == ""判断来跳过空行。
板块二:二分答案:
废板子:(不必要, 建议跳过)
while (l + 1 < r) {
mid = l + r >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid;
}
}
两个微妙的地方:一个是是l要来加一,另一个是mid不能加一。
两者都是为了避免超时而设计。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
开始是足够大的r和足够小的l,然后开始猜答案,答案有一个区间,存在且是唯一的(有某种单调性)。首先,l和r不是答案。第一次操作,区间折半,一直操作的过程中,r一定不是答案,而l是可能的答案。l从零开始,l = 合法,r = 非法, 如果 l + 1 = r 的话,说明l是最大的合法答案, 如果l + 1 < r 我们可以知道 l 合法 l + 1 ? …… r - 1 ? r 非法,中间未可知。mid肯定在l和r之间,判断合法和非法,区间减小,每次取中间偏左一些,直到长度为三的区间只需判断,中间一个是否合法就行了。建议对着上面的表格想象一下。
为什么l要加1呢?因为如果l不加一的话,区间缩小到相邻的时候,左边合法右边非法,会导致死循环。
其实也是因为mid没有加一的缘故,因为我们直到这个点是非法的话,下一个点就可能是非法的也可能是合法的,和上面的分析不同。
第一题:洛谷P2440
一样的题目:洛谷P1969
说了那么多其实并没有用。
突然明白其实二分模板只有两个,一个寻找第一个和寻找第最后一个合法的答案,其实用板子二就行了:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6 + 5;
ll n, k, a[N];
bool check(ll x) {
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans += a[i] / x;
}
return ans >= k;
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
ll l = 0, r = 1e8 + 1;
ll mid;
while (l < r) {
mid = l + r + 1 >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
cout << l << '\n';
return 0;
}
第二题:洛谷P1182
批注:模板确实只有两个,但是,但是,但是,注意你的check条件,上下两行的位置可能不同,不能盲目的套模板。
分析一下:check函数,这里的check函数是来求出给定的长度的最小值,当区间和不超过x的时候至少要几段。简单的看一下,第一段毫无争议,能放就放是当前的最优解,满出来之后我们不得不放到第二段,注意,对于当前的最小放法不是唯一的,我们可以将区间1的最后一个放到区间2的第一个,但是,这并没有改变区间当前的区间数目,所以根据贪心的想法,能放就放,不能放就新开区间可以求出每处的最小区间数。
二分答案就是去猜答案,答案区间找对,一半一半猜。题目给出最小区间数,要求将区间分割的最大值的最小值。首先,如果这个值很小,则区间数很多,不对,如果这个值很大,区间数为1,也不对,照道理两边找均可。不过可能要改模板,为了不改模板,我们把数很小区间数大于所给的区间数的当成非法的,数较大的区间数小于等于所给区间的看成是合法的。这样我们二分要求的是第一个合法的值(必然存在,必然相等,如果严格小于只需分割一些区间即可),用模板一。
下面是代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n, m;
int check(int x) {
int sum = 0, num = 1;
for (int i = 1; i <= n; i++) {
sum += a[i];
if (sum > x) {
sum = a[i];
num++;
}
}
return num <= m;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
int l = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
l = max(l, a[i]);
}
int r = 1e9 + 1, mid;
while (l < r) {
mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
cout << l << '\n';
return 0;
}
如果,把大于等于当成是合法的话,就麻烦了,必须改模板,这样边界条件又会让人头大。虽然左边都是合法的,但是你又不能求最后一个合法的,这样求出来的,不一定是最小的。只能说是能分出这个区间数里面最大的。
合法 | 合法 | 合法且区间数相符(答案) | 合法且区间数相符 | 合法且区间数相符(你的答案) | 非法 | 非法 | 非法 | 非法 | 非法 |
这是单增的。
求出来的是黑色加粗的,但是答案是红色的。
上面那个就不一样。
非法 | 非法 | 非法 | 非法 | 非法 | 合法且区间数相符(答案) | 合法且区间数相符 | 合法且区间数相符 | 合法 | 合法 |
这就刚刚好。
求最小的用模板一,设计check使得合法在右侧。
第三题:洛谷P1873
同样是构造二分答案,这里是取求最高的高度,选择模板二,求最后一个合法的的值,相应地我们要构造出,左边合法的样子,如上面的第一个表格。有时候两个都可以,合法的值只能有一个,但是很多情况是不行的,这里的话好像是可以的,因为锯子升高一定减少,这是严格单调的。实际上也是不行的,因为无法保证小于等于可以取到等号。
归纳:一般来说是单调增的答案区间,如果要求最最小值,也就是第一个合法的答案,用板子1,如果要求最大值,也就是最后一个合法的答案,用板子2。为此要相应地构造check条件,使得板子1从右开始合法,板子2从左开始合法。关于边界条件的记忆,在搜索的过程中,一端开始合法,最后也是合法,过程中一直合法,而一端开始不是合法的,那么就要加减一变得可能合法,而且,这一端停止的位置就是答案,mid求法的设置是偏向这个方位的,从而使得另一端可以与这端重合。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int a[N], n, m;
bool check(int x) {
long long sum = 0;
for (int i = 1; i <= n; i++) {
if (a[i] > x) {
sum += a[i] - x;
}
}
return sum >= m;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int l = 0, r = 2e9, mid;
while (l < r) {
mid = l + r + 1 >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
cout << l << '\n';
return 0;
}
第四题:洛谷P2678
跳石头这道题首先,我们需要求的是最大值,所以选择模板2,为此我们需要构造check函数,使得从左开始合法,找到最后一个合法的。首先,这里最短跳跃距离必然存在,而且是整数。单调性,当最短跳跃距离很小的时候,一个石头都不用搬,num = 0,照道理不应该取到这么小,因为这个根本不存在,但是不影响答案,如果有必要的话可以取两块石头间的最小距离。最大距离是起点到终点,取更大也不影响答案,在存在的情况下,最短距离越长要般的石头越多,所以num<=m,是合法的。如何判断num就是如果距离比最短的要小就说明一定要搬,否则就不用搬。有一个看起来不那么重要的问题,就是所有可能出现的距离的集合并不是连续的,一堆散点,如何能够保证最后取来的答案是存在的,或者说刚好有一个位置可以取到这个值。因为这是最后一个合法的值,它的下一个值必然是非法的,也就是无论这么搬都不可能实现,说明这个值刚刚好是搬动m个石头的极限值,通过搬走石头得到的最大的最小距离。
大功告成。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 50005;
int a[N], n, m;
bool check(int x) {
int num = 0, now = 0;
for (int i = 1; i <= n + 1; i++) {
if (a[i] - a[now] < x) {
num++;
} else {
now = i;
}
}
return num <= m;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int L;
cin >> L >> n >> m;
a[0] = 0;
a[n + 1] = L;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int l = 0, r = L + 1, mid;
while (l < r) {
mid = l + r + 1 >> 1;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
cout << l << '\n';
return 0;
}
第五题:洛谷P3853
题目分析:题目要求最小值用模板1,求第一个合法的答案,为此我们需要构造相应的check函数,使得在答案区间内部左侧为非法的,右侧为为合法的,右侧的数较小,空旷程度大,所需的补充的路标数目少,所以出口条件设置为num <= k。
核心代码:如何求出最小的路灯放置数,这里绝对不可能用n方的方法,既然已经用了二分想必这里肯定只需遍历一边,不然这个数量级可就没办法了。计算两者之间的距离,然后整除空旷指数,如果能够完全整除减一,如果不能完全整除就不减一。而且注意,无论是否放置都要改变i,这和跳石头有些不一样。
关于含义:这里求出的num是不得不放置的数目,如果说这小于等于k的话我们定义为合法,为了配合二分模板,下面是表格。
|非法| 非法 | 合法m也符合(答案) |合法,m也符合 |合法,m不符 | 合法,m不符|合法,m不符 |合法,m不符 |
我们要找的是第一个合法的即可。因为这刚好是这个摆放数的空旷指数的极限,但凡小一点就不够用。
等号是一定取到的,有兴趣可以看看。
坑点:l不能取0会下面的测试点会RE,除零错误(模零错误)可还行。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], n, k;
bool check(int x) {
int now = 1, num = 0;
for (int i = 2; i <= n; i++) {
int d = a[i] - a[now];
if (d > x) {
num += d / x - 1 + (d % x != 0);
}
now = i;
}
return num <= k;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int L;
cin >> L >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int l = 1, r = 1e7 + 5, mid;
while (l < r) {
mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
cout << l << '\n';
return 0;
}