二分答案
对于给定的求解问题,该问题具有单调性。也就是说在所有可行域中,对于题目要求条件的判定在某个数的一侧一定是可行的,而另一侧是不可行的。于是我们可以把求解最优值的问题,转化为对值域的二分找到临界数的问题。
常见的二分写法:
//整数域二分
while (l <= r) {
mid = (l + r) >> 1;
if (check(mid)) {
ans = mid;
l = mid + 1;
}else r = mid - 1;
}
while (l < r) {
mid = (l + r) >> 1;
if (check(mid))
l = mid + 1;
else r = mid;
}
//最后的答案就是l(r = l)
//实数域二分
while (l + eps < r) {
double mid = (l + r) / 2;
if (check(...)) {
ans = mid;
l = mid;
}else r = mid;
}
for(int i = 1; i <= 100; i++) {
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid - 1;
}
常见的二分题目有:
- “最大的最小”、"最小的最大"型问题。
- 求满足条件的最值问题
当然还有很多题目很难分析出单调的性质,需要从一个点切入进行问题转化,这个就需要有丰富的做题经验。
例题分析
下列题目源于洛谷二分题单和《算法竞赛进阶指南》。
洛谷 P3743 kotori的设备
题目大意
给出 n ( 1 ≤ n ≤ 1 e 5 ) n(1 \leq n \leq 1e5) n(1≤n≤1e5) 个设备,每个设备有一个初始电量 b ( 1 ≤ a ≤ 1 e 5 ) b(1 \leq a \leq 1e5) b(1≤a≤1e5) 和耗电速率 a ( 1 ≤ a ≤ 1 e 5 ) a ( 1 \leq a \leq 1e5) a(1≤a≤1e5),现在有一个充电速率为 p ( 1 ≤ p ≤ 1 e 5 ) p(1 \leq p \leq 1e5) p(1≤p≤1e5) 的充电器,当有设备电量降为 0 0 0 时视为结束,充电速率的意思是每秒可以给接通的设备充能 p p p 个单位,充能是连续的。你可以在任意时间给任意一个设备充能,从一个设备切换到另一个设备的时间忽略不计。给定 n , p n, p n,p 求出最多能支撑的时间。
解题思路
题目一开始可能会想到贪心,也就是说肯定优先给掉电速度快且初始电量少的设备充电,但是仔细想想这个过程极为复杂很难去维护,这样的题目就要去考虑DP或者二分的做法了。
DP显然无法维护,然后二分呢,分析之后发现若设备不能无限使用,每次肯定是先给电量即将到 0 0 0 的设备充一部分电然后重复这个过程,这个过程具体如何维护我们不管,但是如果在一个较大的时间 t i t_i ti 可以支撑,那么对于一个较小的时间 t j t_j tj 肯定也可以支撑,那么肯定会有一个最大的使用时间。显然答案是单峰函数,因此可以使用二分答案。
- 若所有充电设备的掉电速度之和小于充电器的充电速度,那么能无限使用。
- 二分时的判定函数我是这样写的:假设能支撑的时间为 x x x,对于设备 i i i,先求出 x x x时间内耗费的电量 n e e d = a i ∗ x need = a_i * x need=ai∗x,若 n e e d > b i need > b_i need>bi,那么需要额外充电 n e e d − b i p \frac{need - b_i}{p} pneed−bi时间,累加所有额外时间判断是否小于等于 x x x。
对于这种问题的最优解正常贪心很难维护的情况下,如果答案满足单调性且问题的判定很好写,那么就肯定是二分答案了。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
typedef long long ll;
typedef pair<int, int> pii;
const int inf = 0x3f3f3f3f;
const double eps = 1e-6;
const int Mod = 1e9 + 7;
const int maxn = 1e5 + 10;
struct node {
int a, b;
} p[maxn];
int n, m;
int dcmp(double d) {
if (fabs(d) < eps) return 0;
return d < 0 ? -1 : 1;
}
bool check(double x) {
double sum = 0;
for (int i = 1; i <= n; i++) {
double need = p[i].a * x;
if (dcmp(p[i].b - need) < 0) {
need -= p[i].b;
sum += need / m;
}
}
return dcmp(sum - x) <= 0;
}
int main() {
// freopen("in.txt", "r", stdin);
// freopen("out.txt", "w", stdout);
// ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
scanf("%d%d", &n, &m);
ll sum = 0;
for (int i = 1; i <= n; i++) {
scanf("%d%d", &p[i].a, &p[i].b);
sum += p[i].a;
}
if (sum <= m) {
puts("-1");
return 0;
}
double l = 0, r = 1e10, mid;
while (l + eps < r) {
mid = (l + r) / 2;
if (check(mid))
l = mid;
else
r = mid;
}
printf("%.6lf\n", l);
return 0;
}
AcWing102. 最佳牛围栏
题目大意
给定一个正整数数列 a a a,找到一个长度不小于 F F F,且平均数最大的连续子段。
解题思路
这题没有解题经验很难想的,首先我们可以二分平均数,转化为问题的判定,但是如何找到一段长度不小于 F F F 的区间使得该数的平均数不小于当前的平均数 m i d mid mid 呢?
查看了题解,发现可以对于该序列的所有数都减去该平均数,判定问题变成了找到一段长度不小于 F F F 的区间使得区间的和非负。于是我们可以维护最大子段和以及最大子段和的长度。
这题需要注意精度。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
typedef long long ll;
typedef pair<int, int> pii;
const double eps = 1e-6;
const int Mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 2e5 + 10;
int n, f;
int a[maxn];
double b[maxn];
bool check(double x) {
for (int i = 1; i <= n; i++) b[i] = b[i - 1] + a[i] - x;
double sum = inf, ans = -inf;
for (int i = f; i <= n; i++) {
sum = min(sum, b[i - f]);
ans = max(ans, b[i] - sum);
}
return ans >= 0;
}
int main() {
// freopen("in.txt","r",stdin);
// freopen("out.txt", "w", stdout);
// ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> f;
for (int i = 1; i <= n; i++) cin >> a[i];
double l = 1e-6, r = 1e9;
while (r - l > eps) {
double mid = (l + r) / 2.0;
if (check(mid))
l = mid;
else
r = mid;
}
int ans = (l + eps) * 1000;
printf("%d\n", ans);
return 0;
}
AcWing120. 防线
题目大意
初始无穷的整数序列均为0。给出若干个三元组 { s , e , d } \{s,e,d\} {s,e,d},代表在区间 [ s , e ] [s,e] [s,e] 内,每隔 d d d个数加 1,即 s , s + d , . . . , s + k ∗ d ( s + k ∗ d ≤ e ) s,s+d,...,s+k*d(s + k * d \leq e) s,s+d,...,s+k∗d(s+k∗d≤e) 每个数加一。题目保证只有一个数最后为奇数,现在需要找到这个位置。
解题思路
这题的单调性很难发现。对于奇数和偶数要经常考虑他们求和的性质,于是可以发现问题的单调性在于,若前缀和为偶数,那么要找的位置一定在当前 m i d mid mid 的右边;否则就在左边。问题的判定枚举所有的三元组求前缀和即可。
#include <bits/stdc++.h>
using namespace std;
#define ENDL "\n"
typedef long long ll;
typedef pair<int, int> pii;
const double eps = 1e-6;
const int Mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const int maxn = 2e5 + 10;
struct node {
int s, e, d;
} a[maxn];
int n;
ll cal(ll x) {
ll ans = 0;
for (int i = 1; i <= n; i++) {
if (x < a[i].s) continue;
ll y = min(x, (ll)a[i].e);
ans += (y - a[i].s) / a[i].d + 1;
}
return ans;
}
int main() {
// freopen("in.txt","r",stdin);
// freopen("out.txt", "w", stdout);
ios_base::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].s >> a[i].e >> a[i].d;
ll l = 1, r = 1e10, ans = -1;
while (l <= r) {
ll mid = (l + r) >> 1;
// cout << mid << " " << cal(mid) << endl;
if (cal(mid) & 1) {
ans = mid;
r = mid - 1;
} else
l = mid + 1;
}
if (ans > 0)
cout << ans << " " << cal(ans) - cal(ans - 1) << endl;
else
cout << "There's no weakness." << endl;
}
return 0;
}