二分答案问题总结

二分答案

对于给定的求解问题,该问题具有单调性。也就是说在所有可行域中,对于题目要求条件的判定在某个数的一侧一定是可行的,而另一侧是不可行的。于是我们可以把求解最优值的问题,转化为对值域的二分找到临界数的问题。

常见的二分写法:

//整数域二分
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(1n1e5) 个设备,每个设备有一个初始电量 b ( 1 ≤ a ≤ 1 e 5 ) b(1 \leq a \leq 1e5) b(1a1e5) 和耗电速率 a ( 1 ≤ a ≤ 1 e 5 ) a ( 1 \leq a \leq 1e5) a(1a1e5),现在有一个充电速率为 p ( 1 ≤ p ≤ 1 e 5 ) p(1 \leq p \leq 1e5) p(1p1e5) 的充电器,当有设备电量降为 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=aix,若 n e e d > b i need > b_i need>bi,那么需要额外充电 n e e d − b i p \frac{need - b_i}{p} pneedbi时间,累加所有额外时间判断是否小于等于 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+kd(s+kde) 每个数加一。题目保证只有一个数最后为奇数,现在需要找到这个位置。

解题思路

这题的单调性很难发现。对于奇数和偶数要经常考虑他们求和的性质,于是可以发现问题的单调性在于,若前缀和为偶数,那么要找的位置一定在当前 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;
}

猜你喜欢

转载自blog.csdn.net/qq_44691917/article/details/119481696