1 月 29日算法练习-二分法

二分法是一种高效的查找方法,它通过将问题的搜索范围一分为二(两边具有明显的区别),迭代地缩小搜索范围,直到找到目标或确定目标不存在。
二分法适用于有序数据集合,并且每次迭代可以将搜索范围缩小一半。
二分法本质上也是枚举,但和暴力枚举不同,二分法利用数据结构的单调性减少了很多不必要的枚举,从而极大地提高了效率,一般可以将O(n)的枚举优化到O(logn)。
常见的二分类型有:
1)整数二分
2)浮点二分
3)二分答案(最常见)

1.研究并发现数据结构(或答案变量)的单调性。
2.确定最大区间1,,确保分界点一定在里面,具体有一些细节:若以r作为答案,那么答案区间在[1+1,r],若以1作为答案,那么答案区间在[1,r-1]。
3.确定check函数,一般为传入mid(区间中某个下标),返回mid所属区域或返回一个值,当check函数较简单时可以直接判断。
参数
4.计算中点mid=(1+r)/2,用check判断该移动l或r指针,具体移动哪个需要根据单调炒以及要求的答案来判断。
5.返回1或r,根据题意判断。

整数二分就是在一个已有的有序数组上,进行二分查找,一般为找出某个值的位置或者是找出分界点。
这个数组肯定是开的下的,其数组大小一般在1e6以内。区域划分如左图。
1 月 29日算法练习-二分法_第1张图片

二分答案是二分法中最常见也最重要的题型,考察的比较灵活,需要选手从题目中发现某个单调的函数,然后对其进行二分。
常见的模型是:
二分框架(时间复杂度O(logm)+ check函数(时间复杂度O(n)
一般情况下,我们会将答案进行二分,然后在枚举出某个可能解后判断其是否可以更优或者是否合法,从而不断逼近最优解。
二分答案的题的特征:如果已知某个答案,很容易判断其是否合法或更优。某些贪心问题可能可以转化成二分答案问题。

123

思路:用前缀和枚举,复杂度高,只能过 40%。这题需要利用数学解来降低复杂度。找到数学规律,利用二分求出第 l,和 r 位的组数,再求出位数,有了组数和位数就能通过数学规律或前缀和求出 l 和 r 位的值。1 月 29日算法练习-二分法_第2张图片

样例输入
3
1 1
1 3
5 8
样例输出
1
4
8

1 月 29日算法练习-二分法_第3张图片

#include
using namespace std;
using ll = long long;
//const int N = 2e6+10;
//int pre[N];
int T;


ll getPre(ll i){
    return i*(i+1)*(i+2)/6;
}

ll cula(ll x){
    ll i = 0,l = 0,r = 2e6;
    while(l<=r){
        ll mid = (l+r)>>1;
        if(mid*(mid+1)/2 > x)r = mid - 1,i = mid;
        else l = mid + 1;
    }
    ll j = x - (i-1)*(i-1+1)/2;
    
    return getPre(i-1) + j*(j+1)/2;
//    return pre[i-1]+j*(j+1)/2;
}

int main( ){
//    pre[1] =1;
//    for(int i=1;i<=2000000;i++)pre[i] = pre[i-1] + i*(i+1)/2;

    cin>>T;
    while(T--){
        ll l,r;cin>>l>>r;
        cout<<cula(r)-cula(l-1)<<'\n';
    }
    return 0;
}

跳石头

  • 思路:发现当“最短跳跃距离”越长时,需要移走的石头数量也越多。于是就产生了单调性,我们通过二分“最短跳跃距离”,在已知“最短跳跃距离”的情况下容易O(n)计算需要搬走的石头的数量,找到分界点即可(即在至多搬走M块石头的情况下的最远跳跃距离)。
    1 月 29日算法练习-二分法_第4张图片
#include
using namespace std;
const int N = 1e5;
using ll = long long;
ll l,n,m;
int a[N];

ll check(ll mid){
    int res = 0,last = 0;
    for(int i=1;i<=n;i++){
        if(a[i]-a[last]<mid){
            res++;
            continue;
        }
        last = i;
    }
    if(l-a[last]<mid)return m+1;
    return res;
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>l>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    
    ll left = 0,r = 1e9+9;
    while(left+1!=r){
        ll mid = (left+r)/2;
        
        if(check(mid)<=m)left=mid;
        else r=mid;
    }
    cout<<left<<'\n';
    return 0;
}

肖恩的苹果林

这题和上一题特征明显,要求的是“最大的最近距离”,当通过二分枚举出一个最近距离时,我们可以判断其合法性(即贪心地判断是否能够放得不m棵树苗),如果合法说明这个距离也许可以再变大,如果不合法就说明这个最近距离应该变小。
在这里“最近距离mid”和“种树的数量check(m是负相关的关系。

1 月 29日算法练习-二分法_第5张图片

#include
using namespace std;
const int N = 1e5+10;
using ll = long long ;
int n,m,a[N];
ll check(ll mid){
    int res=0;
    for(int i=1,lst=0;i<=n;i++){
        if(lst&&a[i]-a[lst]<mid)continue;
        res++,lst=i;
    }
    return res;
}

int main( ){
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    
    ll l = 0,r=1e9+10;
    while(l+1!=r){
        ll mid = (l+r)>>1;
        if(check(mid)>=m)l=mid;
        else r=mid;
    }
    cout<<l<<'\n';
    return 0;
}

肖恩的乘法表

思路:因为 k 和元素个数呈现单调性,所以可以利用二分枚举答案元素,通过用数学规律来计算出每行<k 的数得到全部<k 的数来判断是否答案满足,从而利用二分夹逼得到最终答案,注意边界r 是满足条件的最小值。

通过排名去计算元素是不好算的,但是如果已知一个元素可以利用矩阵每一行的规律来O(n)地计算排名。
所以我们枚举元素大小,设rnk(x)表示矩阵中小于等于x的元素的个数,那么对于第i行<=x的数字的个数就是min(m,x/ i)。
必然有rnk(l)=k,从而将整个整数域划分部分,最后返回r即可。

1 月 29日算法练习-二分法_第6张图片

#include
using namespace std;
using ll = long long;
ll n,m,k;

ll check(ll mid){
    int res=0;
    for(int i=1;i<=n;i++){
        res+=min(m,mid/i);
    }
    return res;
}

int main( ){
    cin>>n>>m>>k;
    ll l = 0,r = 1e12;
    while(l+1!=r){
        ll mid = (l+r)>>1;
        if(check(mid)>=k)r=mid;
        else l=mid;
    }
    cout<<r<<'\n';
    return 0;
}

你可能感兴趣的:(算法,算法)