【算法1-6】二分查找与二分答案

本文探讨了整数二分在多种问题中的应用,如寻找数组中的特定值、计算立方根、解决牛围栏问题、一元三次方程求解等。通过案例分析,展示了如何利用二分法优化复杂度,实现更高效的搜索策略。同时,还涉及了其他搜索技巧,如双指针和前缀和的应用。
摘要由CSDN通过智能技术生成

789. 数的范围

整数二分 :

  • 在一个区间上定义某种性质,使得这个性质在右半区间是满足的,在左半区间是不满足的,且这两个区间没有交点(因为是整数二分);这样二分就既可以找右半部分也可以左半部分的边界,找这两个点分别是两个不同的模版
    在这里插入图片描述
    在这里插入图片描述
#include <iostream>
using namespace std;
const int N = 1e5 + 10;

int a[N];

int main() {
    int n, q;
    cin >> n >> q;
    for (int i = 0; i < n; ++ i)
        cin >> a[i];
    while (q -- ) {
        int x;
        cin >> x;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (a[mid] >= x) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        if (a[r] != x) {
            cout << "-1 -1" << endl;
        } else {
            cout << r << ' ';
            l = 0, r = n - 1;
            while (l < r) {
                int mid = (l + r + 1) >> 1;
                if (a[mid] <= x) {
                    l = mid;
                } else {
                    r = mid - 1;
                }
            }
            cout << r << endl;
        }
    }
}

790. 数的三次方根

#include <iostream>
using namespace std;

int main() {
    double n;
    cin >> n;
    double l = -10000, r = 10000;
    while (r - l > 1e-8) {
        double mid = (l + r) / 2;
        if (mid * mid * mid >= n) {
            r = mid;
        } else {
            l = mid;
        }
    }
    printf("%.6lf", l);
}

102. 最佳牛围栏

  • 对于二分,二分是二分性而不是单调性 只要满足可以找到一个值一半满足一半不满足即可 而不用满足单调性;本题没有出现二分的特征词(最大值最小、最小值最大),所给数列也没有单调性,并且不适于排序,可以先提出假设这道题可以用二分解出。
  • 假设最优解为x,如果当前需要判断是否存在一段的平均值大于等于mid,如果mid ≤ \leq x,必然存在一段平均值大于等于mid的(比如x);如果mid > > >x,必然不存在一段平均值大于等于mid的,因此证明了这道题的二分性,可以用二分解决问题
  • 我们要找一段 连续的且区间长度不小于m且区间的平均值大于等于mid的区间,优化为区间中每个数减去mid,利用前缀和得到的区间和大于等于0,这样可以省去求平均值的步骤
  • 由于区间长度只有下限的限制,长度并不固定,即使用前缀和优化,仍然需要 n 2 n^2 n2求和。我们使用双指针, i = 0 , j = m i=0,j=m i=0,j=m,每次i++,j++,这样i与j之间的差距固定为m+1,且 s [ j ] − s [ i ] s[j]-s[i] s[j]s[i]为i+1~j这一区间的和用一个变量minv维护i遍历过程中的前缀和最小值,这样比较的距离就一定是 ≥ \geq m的了,且为 O ( N ) O(N) O(N)
  • 因为我们找的极大值 所以要右端点*1000 否则可能会出错
#include <iostream>
using namespace std;
const int N = 1e5 + 10;

int n, m;
int a[N];
double s[N];

bool check(double x) {
    for (int i = 1; i <= n; ++ i) {
        s[i] = s[i - 1] + (a[i] - x);
    }
    double minv = 1e9;
    for (int i = 0, j = m; j <= n; ++ i, ++ j) {
        minv = min(minv, s[i]);
        if (s[j] - minv >= 0) {
            return true;
        }
    }
    return false;
}

int main() {
    cin >> n >> m;
    double l = 0, r = 0;
    for (int i = 1; i <= n; ++ i) {
        cin >> a[i];
        r = max(r, (double)a[i]);
    }
    while (r - l > 1e-5) {
        double mid = (l + r) / 2;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
    cout << (int)(r * 1000);
}

113. 特殊排序

  • 本题与一般排序有三个区别:
  • 1.这是交互题,不能用><,只能调用compare接口询问
  • 2.大小不具有传递性,a<b,b<c,不能推出a<c
  • 3.不能超过一万次询问,n为1000,nlogn略小于一万
  • 第二个性质仅仅导致结果不唯一,题目只要求输出其中一种,因此可以忽略
// Forward declaration of compare API.
// bool compare(int a, int b);
// return bool means whether a is less than b.

class Solution {
public:
    vector<int> specialSort(int N) {
        vector<int> res;
        res.push_back(1);
        for (int i = 2; i <= N; ++ i) {
            int l = -1, r = i - 1;
            while (l + 1 != r) {
                int mid = (l + r) >> 1;
                if (compare(res[mid], i)) {
                    l = mid;
                } else {
                    r = mid;
                }
                // [l] < i
            }
            res.push_back(i);
            for (int j = res.size() - 1; j >= l + 2; -- j) {
                swap(res[j], res[j - 1]);
            }
        }
        return res;
    }
};

P2249 【深基13.例1】查找

#include <iostream>
using namespace std;
const int N = 1e6 + 10;

int a[N];

int main() {
    int n, q;
    scanf("%d%d", &n, &q);
    for (int i = 1; i <= n; ++ i)
        scanf("%d", &a[i]);
    while (q -- ) {
        int x;
        scanf("%d", &x);
        int l = 1, r = n;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (a[mid] >= x) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        if (a[l] != x) cout << -1 << ' ';
        else cout << l << ' ';
    }
}

P1102 A-B 数对

  • 找等于一个数的数量:用大于的位置减去大于等于的位置
  • 找位置,既可以用库函数二分,也可以自己写二分,还可以用双指针,因为排序后遍历找大于每个数的位置必然是不变或者向右的
#include <iostream>
#include <map>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;

ll a[N];
map<ll, ll> ma;

int main() {
    int n;
    ll c;
    cin >> n >> c;
    for (int i = 1; i <= n; ++ i) {
        cin >> a[i];
        ma[a[i]] ++ ;
    }
    ll ans = 0;
    for (int i = 1; i <= n; ++ i) {
        ans += ma[a[i] - c];
    }
    cout << ans;
}

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;

int a[N];

int main() {
    int n;
    ll c;
    cin >> n >> c;
    for (int i = 1; i <= n; ++ i)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    ll ans = 0;
    for (int i = 1; i <= n; ++ i) {
        ans += (upper_bound(a + 1, a + n + 1, a[i] - c) - lower_bound(a + 1, a + n + 1, a[i] - c));
    }
    cout << ans;
}

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;

int n;
ll c;
int a[N];

int upper(ll x) {
    int l = 0, r = n + 1;
    while (l + 1 != r) {
        int mid = (l + r) >> 1;
        if (a[mid] <= x) {
            l = mid;
        } else {
            r = mid;
        }
    }
    return r;
}
int lower(ll x) {
    int l = 0, r = n + 1;
    while (l + 1 != r) {
        int mid = (l + r) >> 1;
        if (a[mid] < x) {
            l = mid;
        } else {
            r = mid;
        }
    }
    return r;
}

int main() {
    cin >> n >> c;
    for (int i = 1; i <= n; ++ i)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    ll ans = 0;
    for (int i = 1; i <= n; ++ i) {
        ans += (upper(a[i] - c) - lower(a[i] - c));
    }
    cout << ans;
}

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;

ll a[N];

int main() {
    int n;
    ll c;
    cin >> n >> c;
    for (int i = 1; i <= n; ++ i)
        cin >> a[i];
    sort(a + 1, a + n + 1);
    ll ans = 0;
    int r1 = 1, r2 = 1;
    for (int i = 1; i <= n; ++ i) {
        while (r1 <= n && a[r1] - a[i] < c) r1 ++ ;
        while (r2 <= n && a[r2] - a[i] <= c) r2 ++ ;
        if (a[r1] - a[i] == c && a[r2 - 1] - a[i] == c && r1 >= i + 1)
            ans += r2 - r1;
    }
    cout << ans;
}

P1873 [COCI 2011/2012 #5] EKO / 砍树

  • 答案h最高高度越大,木材总长度越小,满足二分,临界点就是木材总长度大于等于m(l)
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;

int n, m;
int a[N];

bool check(int x) {
    ll ans = 0;
    for (int i = 1; i <= n; ++ i) {
        ans += (a[i] - min(a[i], x));
    }
    if (ans >= m) return true;
    return false;
}

int main() {
    cin >> n >> m;
    int mx = 0;
    for (int i = 1; i <= n; ++ i) {
        cin >> a[i];
        mx = max(mx, a[i]);
    }
    int l = -1, r = mx + 1;
    while (l + 1 != r) {
        int mid = (l + r) >> 1;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
    cout << l;
}

P1024 [NOIP2001 提高组] 一元三次方程求解

  • 根据题意,每个根之间绝对值大于等于1,说明每一个长度为1的区间内最多有两个根
  • 在题目所给的范围内,以长度为1枚举每一个区间,如果两个端点之积小于等于0,说明之间有一个根,使用二分法找根;特别地,如果左端点的函数值等于0,说明这个左端点就是一个根
#include <iostream>
using namespace std;

double a, b, c, d;

double func(double x) {
    return a * x * x * x + b * x * x + c * x + d;
}

int main() {
    scanf("%lf%lf%lf%lf", &a, &b, &c, &d);
    int cnt = 0;
    for (int i = -100; i < 100; ++ i) {
        double l = i, r = i + 1;
        double y1 = func(l), y2 = func(r);
        if (!y1) {
            printf("%.2lf ", l);
            cnt ++ ;
        }
        if (y1 * y2 < 0) {
            while (r - l >= 1e-8) {
                double mid = (l + r) / 2;
                if (func(mid) * func(r) < 0) {
                    l = mid;
                } else {
                    r = mid;
                }
            }
            printf("%.2lf ", r);
            cnt ++ ;
        }
        if (cnt == 3) break;
    }
}

P1678 烦恼的高考志愿

  • 要注意没有小于它的数和没有大于等于它的数 的情况
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;

ll a[N];

int main() {
    int m, n;
    cin >> m >> n;
    for (int i = 0; i < m; ++ i)
        cin >> a[i];
    sort(a, a + m);
    ll ans = 0;
    while (n -- ) {
        ll x;
        cin >> x;
        int l = -1, r = m;
        while (l + 1 != r) {
            int mid = (l + r) >> 1;
            if (a[mid] >= x) {
                r = mid;
            } else {
                l = mid;
            }
        }
//        ans += min(abs(a[l] - x), abs(a[r] - x));
        if (l == -1) {
            ans += abs(a[0] - x);
        } else if (r == m) {
            ans += abs(a[m - 1] - x);
        } else {
            ans += min(abs(a[l] - x), abs(a[r] - x));
        }
    }
    cout << ans;
}

P2440 木材加工

  • 长度l越大,能得到的小段的数量越小,临界点为小段的数量大于等于k(l)
  • 注意这里边界l必须是0,不能是-1,否则会RE
#include <iostream>
using namespace std;
const int N = 1e5 + 10;

int n, k;
int a[N];

bool check(int x) {
    int ans = 0;
    for (int i = 0; i < n; ++ i) {
        ans += a[i] / x;
    }
    if (ans >= k) return true;
    return false;
}

int main() {
    cin >> n >> k;
    for (int i = 0; i < n; ++ i)
        cin >> a[i];
    int l = 0, r = 1e8 + 1;
    while (l + 1 != r) {
        int mid = (l + r) >> 1;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
//    if (l == -1) cout << 0;
//    else cout << l;
    cout << l;
}

P2678 [NOIP2015 提高组] 跳石头

  • 最短跳跃距离越大,需要移走的石头数量越大,临界点是移走小于等于m块石头
  • 二分最短跳跃距离,判断需要移走的石头数量
  • 最短跳跃距离左边界为输入的最小距离,右边界为之和
  • 判断当前这个最短距离是否可以满足移走石头数量小于等于m时,需要一个变量记录当前这一段的起点,然后动态更新起点和需要移走的石头数量
#include <iostream>
using namespace std;
const int N = 5e4 + 10;

int len, n, k;
int a[N];

bool check(int m) {
    int cnt = 0, last = 0;
    for (int i = 1; i <= n; ++ i) {
        if (a[i] - a[last] < m) {
            cnt ++ ;
        } else {
            last = i;
        }
    }
    if (len - a[last] < m) cnt ++ ;
    return (cnt <= k);
}

int main() {
    cin >> len >> n >> k;
    for (int i = 1; i <= n; ++ i)
        cin >> a[i];
    int l = 0, r = len + 1;
    while (l + 1 != r) {
        int mid = (l + r) >> 1;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
    cout << l;
}

P3853 [TJOI2007]路标设置

  • 最大距离越小,需要增加的路标数量越大,临界点为增加的数量小于等于k
  • 判断当前的最大距离是否满足增加路标数量小于等于k时,遍历每一个原先的路标,如果它与上一个之间的距离大于最大距离,说明要加这么多个路标在它们之间
#include <iostream>
using namespace std;
const int N = 1e5 + 10;

int len, n, k;
int a[N];

bool check(int m) {
    int cnt = 0;
    for (int i = 2; i <= n; ++ i) {
        if (a[i] - a[i - 1] > m) {
            cnt += (a[i] - a[i - 1]) / m;
            if ((a[i] - a[i - 1]) % m == 0) cnt -- ;
        }
    }
    return (cnt <= k);
}

int main() {
    cin >> len >> n >> k;
    for (int i = 1; i <= n; ++ i)
        cin >> a[i];
    int l = 0, r = len + 1;
    while (l + 1 != r) {
        int mid = (l + r) >> 1;
        if (check(mid)) {
            r = mid;
        } else {
            l = mid;
        }
    }
    cout << r;
}

P1182 数列分段 Section II

  • 每段之和最大值越小,要划分成的段数越大,临界点是划分段数小于等于m段
  • 判断时,首先判断加上是否会超出,然后直接加上
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
const ll inf = 1e9;

int n, m;
ll a[N];

bool check(ll x) {
    ll cnt = 1, now = 0;
    for (int i = 0; i < n; ++ i) {
        if (now + a[i] > x) {
            cnt ++ ;
            now = 0;
        }
        now += a[i];
    }
    return cnt <= m;
}

int main() {
    cin >> n >> m;
    ll mx = 0, sum = 0;
    for (int i = 0; i < n; ++ i) {
        cin >> a[i];
        mx = max(mx, a[i]);
        sum += a[i];
    }
    ll l = mx - 1, r = min(sum, inf) + 1;
    while (l + 1 != r) {
        ll mid = (l + r) >> 1;
        if (check(mid)) {
            r = mid;
        } else {
            l = mid;
        }
    }
    cout << r;
}

P1163 银行贷款

  • ∑ i = 1 k m ( 1 + p ) i = n \sum\limits_{i=1}^k{\frac{m}{(1+p)^i}}=n i=1k(1+p)im=n
  • 化简得 ∑ i = 1 k 1 ( 1 + p ) i = n m \sum\limits_{i=1}^k{\frac{1}{(1+p)^i}}=\frac{n}{m} i=1k(1+p)i1=mn,由于nmk为常数,因此左边的结果是一个一元单调函数,右边是固定值,因此可以二分;且p越大,左边的结果越小
  • 根据等比数列求和公式, 1 − ( 1 1 + p ) k p = n m \frac{1-(\frac{1}{1+p})^k}{p}=\frac{n}{m} p1(1+p1)k=mn
#include <iostream>
#include <cmath>
using namespace std;

int n, m, k;

bool check(double p) {
    double div = 1 - pow(1 / (1 + p), k);
    return (div / p >= n * 1.0 / m);
}

int main() {
    cin >> n >> m >> k;
    double l = 0, r = 100;
    while (r - l > 1e-5) {
        double mid = (l + r) / 2;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
    printf("%.1lf", l * 100);
}

P3743 kotori的设备

  • 精读不是越小越好,因为会TLE;这里1e-4的精读要求为1e-6即可满足(一般高两个数量级)
  • double 上界可以有 1 0 11 10^{11} 1011
  • 这里之所以将p和sum弄成double,是因为它们超出了int范围
  • 与其纠结于时间,将所有设备需要的总能量加起来,与总共能提供的能量做比较即可
#include <iostream>
using namespace std;
const int N = 1e5 + 10;

int n;
double p;
int a[N], b[N];

bool check(double x) {
    double sum = 0;
    for (int i = 1; i <= n; ++ i) {
        if (a[i] * x > b[i]) {
            sum += (a[i] * x - b[i]);
        }
    }
    return (sum <= p * x);
}

int main() {
    cin >> n >> p;
    double sum = 0;
    for (int i = 1; i <= n; ++ i) {
        cin >> a[i] >> b[i];
        sum += a[i];
    }
    if (sum <= p) {
        cout << -1.000000;
        return 0;
    }
    double l = 0, r = 1e11;
    while (r - l > 1e-6) {
        double mid = (l + r) / 2;
        if (check(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
    cout << l;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值