二分(折半)笔记

二分(折半)查找

菜鸡今天又开始了一个新的算法,废话不多说,开始笔记。二分查找是一个十分常用的算法,适用于对带有单调性的数据进行处理或查找。大多数题目可能没那么显示,需要自己来找这个单调性在哪,所以这就是一个重点,具体的用法和代码很简单,就那么几行,但是对于折半的条件却需要有严格的要求。

统计一个大范围中具有某些特性的数据

[牛客]完全平方数
就如这个题目:多次查询[l,r]范围内的完全平方数个数。这样的题目通常需要经过预处理。因为数据量看起来比较大1e9,你也没法预处理哪个数是不是完全平方数。但是,完全平方数嘛,就必然是一个数的平方,那我们可以存一下,每一个数对应的完全平方数是谁。如:1->1,2->4,这样以此类推,正好我们的下标的平方就是我们存储的数据。而且我们也正好可以知道从1到这个数,已经有多少个完全平方数了,有点前缀和的感觉,这样,我们每遇到一个区间就可以直接二分查找两个端点的位置,然后做差,就正好是这个范围内所求性质的数的个数了。这里要注意一下,最好是要在二分查找的时候用左闭右开,这样你后面那个查到的永远是比你右端点大的位置,用这个直接减掉左端点就是其中数的个数了。

总结一下,我们的思想就是,大数据我们存不下,我们可以把某些具有该特性的数预处理先存在数组里面,借助数组下表当做前缀和,来相当于把一个大的区间进行了离散化,在进行二分查找。

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
long number[100010];

int main() {
    for(int i = 1; i <= 100001; i++)  number[i] = long(i)*i;
    int n, l, r;
    cin >> n;
    while(n--) {
        scanf("%d%d", &l, &r);
        int lPos = lower_bound(number, number+100001, l)-number;
        int rPos = upper_bound(number, number+100001, r)-number;
        printf("%d\n", rPos-lPos);
    }
    return 0;
}

代码很简单的,对吧~

01分数规划

01分数规划的题目可以借助二分进行近似求解。至于什么是01分数规划,在这里稍微简单做下笔记,当然可以问度娘。
0/1分数规划模型是指,有一些二元组(si,pi),从中选取一些二元组,使得∑si / ∑pi最大(最小)。
这种题一类通用的解法就是,我们假设x = ∑si / ∑pi的最大(小)值,那么就有x * ∑pi = ∑si ,即∑si - x * ∑pi= 0。也就是说,当某一个值x满足上述式子的时候,它就是要求的值。所以我们直接二分答案,当上述式子>0,说明答案小了,<0则说明答案大了,这样计算即可。
[牛客]wyh的物品

#include <cmath>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5+5;
const double eps = 0.001;
double v[maxn], w[maxn], x[maxn];

int main() {
    int T;
    cin >> T;
    while(T--) {
        int n, k;
        scanf("%d%d", &n, &k);
        double l = 0, r = 0;
        for(int i = 0; i < n; i++) {
            scanf("%d%d", &w[i], &v[i]);
            r = max(r, v[i]/w[i]);
        }
        double mid, sum = 0;
        while(fabs(r-l) > eps) {
            mid = (l+r)/2;
            for(int i = 0; i < n; i++)  x[i] = mid*w[i]-v[i];
            //这里用我们假设的值求出来与原价值做差,然后排序,取最小的k个,
            //如果和小于0,说明存在原来数据中的组合能够更优,需要往右区间找
            //与上面的说法不太一样,但原理都是一样的,理解就好。这里直接使用
            //sort函数就可以了,如果按照上述还需要写一个比较函数使排序从大到小
            sort(x, x+n);
            sum = 0.0;
            for(int i = 0; i < k; i++)  sum += x[i];
            if(sum <= 0)    l = mid;
            else    r = mid;
        }
        printf("%.2lf\n", mid);
    }
    return 0;
}

累加和

[牛客]andy的树被砍了
这种一般就比较简单吧,一般就是前缀和,毕竟是前缀和,也没有负数,单调性很明显,然后用二分的话可能代码就会简化一点,时间复杂度低一点。感觉可用可不用。我看好像都没用二分就过了。

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5+5;
long long c[maxn];
int h[maxn];

int main() {
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++) scanf("%d", &h[i]);
    for(int i = 1; i <= n; i++) scanf("%lld", &c[i]), c[i] += c[i-1];
    for(int i = 1; i <= n; i++) {
        int pos = lower_bound(c+1, c+n+1, h[i]+c[i-1])-c;
        printf("%d ", pos);
    }
    cout << endl;
    return 0;
}

emmm,比暴力的代码更简洁一点。
有些题目要特别巧妙的使用前缀和,可以大量的减少时间复杂度:[牛客]transform,而且确定边界的时候一定要好好想想,要不然就一直错。

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 5e5+5;
int n;
long long T;
int x[maxn];
long long a[maxn], _sum[maxn], _cost[maxn];

inline long long getRightCost(int l, int r) {
    return _cost[r]-_cost[l-1]-x[l]*(_sum[r]-_sum[l-1]);
}

inline long long getLeftCost(int l, int r) {
    return (_sum[r]-_sum[l-1])*(x[r]-x[l])-getRightCost(l, r);
}

bool check(long long _try) {
    int l, r, mid;
    long long _half_try = (_try>>1)+(_try%2);
    // @dev 先从左向右尝试,遍历左右边界,一点一点尝试
    l = r = mid = 1;
    while(true) {
        while(r <= n && _sum[r]-_sum[l-1] < _try)    r++;
        if(r > n)   break ; // @dev 如果r>n,说明全部取得都不够,直接返回
        while(mid <= n && _sum[mid]-_sum[l-1] < _half_try)  mid++;
        long long _surplus = (_sum[r]-_sum[l-1]-_try)*(x[r]-x[mid]);    //_sum[r]-_sum[l-1]-_try是整个区间全部取得多出来的,也会导致额外消耗,应该在后面减去
        if(getLeftCost(l, mid)+getRightCost(mid, r)-_surplus <= T)  return true;
        l++;
    }

    l = r = mid = n;
    while(true) {
        while(l >= 1 && _sum[r]-_sum[l-1] < _try)   l--;
        if(l < 1)   break ;
        while(mid >= 1 && _sum[r]-_sum[mid-1] < _half_try)  mid--;
        long long _surplus = (_sum[r]-_sum[l-1]-_try)*(x[mid]-x[l]);
        if(getLeftCost(l, mid)+getRightCost(mid, r)-_surplus <= T)  return true;
        r--;
    }
    return false;
}

int main() {
    long long l = 0, r;
    scanf("%d%lld", &n, &T);
    T >>= 1;
    for(int i = 1; i <= n; i++) scanf("%d", &x[i]);
    for(int i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        _sum[i] = _sum[i-1]+a[i];
        _cost[i] = _cost[i-1]+x[i]*a[i];
        l = max(a[i], l);
    }
    r = _sum[n];
    while(l <= r) {
        long long mid = (l+r)>>1;
        if(check(mid))  l = mid+1;
        else r = mid-1;
    }
    printf("%lld\n", r);
    return 0;
}

二分算法的用法特别灵活,应视情况而定

一般我们习惯性的对所求答案进行二分,比如:[牛客]接机
一般的都是一个模板:

	while(l < r) {
        mid = (l+r)>>1;
        if(check()) r = mid;
        else l = mid+1;
    }

重点在于根据题目怎么写这个check函数,还有就是对于边界的判断,要想好,哪一个是可行解,应该保留哪一个边界。

#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5+5;
int _time[maxn];

inline bool check(int n, int m, int c, int mid) {
	// @dev 如果返回true,说明这个解可行
    int pre = 0, cnt = 1;
    for(int i = 1; i < n; i++) {
        if(_time[i]-_time[pre] > mid || i-pre >= c) cnt++,  pre = i;
    }
    return cnt <= m;
}

int main() {
    int n, m, c;
    scanf("%d%d%d", &n, &m, &c);
    for(int i = 0; i < n; i++)  scanf("%d", &_time[i]);
    sort(_time, _time+n);
    int mid, l = 0, r = _time[n-1]-_time[0]+1;
    while(l < r) {
        mid = (l+r)>>1;
        if(check(n, m, c, mid)) r = mid;
        //此解是可行的的,那么我们可以令r=mid,这样边界r就是可行的,但是l是否可行并不知道。
        //这样,到最后循环结束的时候便是l==r,因为r一定是一个可行解,那么答案就是l或r。
        else l = mid+1;
    }
    printf("%d\n", r);
    return 0;
}

还有一种比较不容易出错的写法:

	while(l <= r) {
        mid = (l+r)>>1;
        if(check(n, m, c, mid)) r = mid-1, _result = mid;
        else l = mid+1;
    }

这样把最后求得的可行解保存在一个变量里面,就可以不用边界是否正确,最后的解一定是可行的。
刚开始,没做多少题目,日后再更~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值