[AcWing算法刷题]之二分(简单)

263 阅读9分钟

 

目录

关于(整数)二分的模板:

我的一些小经验总结:

二分的数字简单应用(剑指Offer)

不修改数组找出重复的数字

数字在排序数组中出现的次数

0到n-1中缺失的数字

数组中数值和下标相等的元素

旋转数组的最小数字(中等)

二分的综合应用

数的范围(lower_bound和upper_bound的原理)

         四平方和(重载比较)

         分巧克力(数学+枚举含义)

递增三元组(数学+构造二分)

我在哪?(字符串哈希)

中位数(大佬写法)

楼梯(数学+枚举含义)

奶牛棒球(数学+枚举的含义)

工资计算(数学+枚举含义)

愤怒的牛(枚举含义+前缀和)

数列分段 II(枚举含义+前缀和)

特别注意浮点数的二分

数的三次方根



关于(整数)二分的模板:

二分模板+高精度运算+前缀和与差分(C++)_lihua777的博客-CSDN博客

我的一些小经验总结:

(1)<判断题意>

首先是要判断该题目是否可以用二分,一般来说具有二段性的就可以用二分

何为二段性?

在一整个长串中,可被分为两个连续的段,且这两段的性质存在差异即可

典型的例子为:前一段满足,后一段不满足,那么就可以对该性质进行二分

(2)<判断类别>

对于二分,(y总)总结了两套方法:对于上面的二段性

<1>最后一个满足第一段性的位置:

更新区间的方式为:

int left = 0;
int right = nums.size() - 1;
while (left < right)
{
	int mid = (left + right + 1) >> 1;
	if (check(mid)) left = mid;
	else right = mid - 1;
}

<2>第一个满足第二段性的位置:

更新区间的方式为:

int left = 0;
int right = nums.size() - 1;
while (left < right)
{
	int mid = (left + right) >> 1;
	if (check(mid)) right = mid;
	else left = mid + 1;
}

(3)<弄懂枚举的意义>

这点是二分运用在综合题最难的地方,为什么要二分,因为要优化枚举,这时候就要知道枚举的这个是什么东西,才能写对正确的check函数,这点会在下面的题目中重点介绍

 


二分的数字简单应用(剑指Offer)


不修改数组找出重复的数字

抽屉原理:

AcWing 14. 不修改数组找出重复的数字 - AcWing

 

class Solution {
public:
    int duplicateInArray(vector<int>& nums) {
        int l = 1, r = nums.size() - 1;
        while (l < r)//寻找第一个满足的第二段性 
        {
            int mid = l + r >> 1; // 划分的区间:[l, mid], [mid + 1, r]
            int s = 0;
            for (auto x : nums) s += x >= l && x <= mid;//重点:如果有多个在这个区间
                                                    //那么就证明这个区间有重复元素
            if (s > mid - l + 1) r = mid;//在这个区间:符合check
            else l = mid + 1;
        }
        return r;
    }
};

数字在排序数组中出现的次数

 

class Solution {
public:
    int getNumberOfK(vector<int>& nums , int k) {
    
    auto l=lower_bound(nums.begin(),nums.end(),k);//第一个满足的
    auto r=upper_bound(nums.begin(),nums.end(),k);//最后一个满足的
    
    return r-l;//最后一个满足的-第一个满足的==满足的元素个数
    
    }
};

0到n-1中缺失的数字

 

class Solution {
public:
    int getMissingNumber(vector<int>& nums) {
        if (nums.empty()) return 0;

        int left = 0;//枚举的意义是:0~n的数字
        int right = nums.size();//预防处于边界的情况
        while (left < right)//二段性:不满足证明该数字小了,需要向右寻找
        {
            int mid = left + right >> 1;
            if (nums[mid] != mid) right = mid;
            else left = mid + 1;
        }
        return right;
    }
};

数组中数值和下标相等的元素

 

class Solution {
public:
    int getNumberSameAsIndex(vector<int>& nums) {
        int left=0;
        int right=nums.size()-1;
        while(left<right)
        {
            int mid=left+right>>1;
            if(nums[mid]>=mid) right=mid;//满足条件,缩小区间
            else left=mid+1;
        }
        if(nums[left]==left) return left;//特判
        else return -1;
    }
};

旋转数组的最小数字(中等)

class Solution {
public:
    int findMin(vector<int>& nums) {
        if(nums.empty()) return -1;
        int n=nums.size();
        while(nums[0]==nums[n-1]) n--;//防止重复元素影响单调性
        int left=0;
        int right=n-1;
        while(left<right)//二段性:单调性
        {
            int mid=left+right>>1;
            if(nums[mid]>nums[right]) left=mid+1;//证明还在左半区:‘=’不可删除:因为是严格单调
            else right=mid;
        }
        return nums[left];
    }
};

 


二分的综合应用


数的范围(lower_bound和upper_bound的原理)

 

 

#include <iostream>
using namespace std;
const int maxn = 100005;
int n, q, x, a[maxn];
int main() 
{
    scanf("%d%d", &n, &q);
    for (int i = 0; i < n; i++)    scanf("%d", &a[i]);
    while (q--) 
    {
        scanf("%d", &x);
        int l = 0, r = n - 1;
        while (l < r) 
        {
            int mid = l + r >> 1;
            if (a[mid] < x)  l = mid + 1;
            else    r = mid;
        }
        if (a[l] != x)
        {
            printf("-1 -1\n");
            continue;
        }
        int l1 = l, r1 = n;//l1为l,加快运行效率,因为lower_bound一定在upper_bound的位置或者左边
        while (l1 + 1 < r1) 
        {
            int mid = l1 + r1 >> 1;
            if (a[mid] <= x)  l1 = mid;
            else    r1 = mid;
        }
        printf("%d %d\n", l, l1);
    }
    return 0;
}

四平方和(重载比较)

 

 

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=2500010;
int n,m;
struct Sum{
    int s,c,d;
    bool operator< (const Sum&t) const
    {
        if(s!=t.s) return s<t.s;
        if(c!=t.c) return c<t.c;
        return d<t.d;
    }
}sum[N];

int main()
{
    cin>>n;
    for(int c=0;c*c<=n;c++)
        for(int d=c;c*c+d*d<=n;d++)
        {
            sum[m++]={c*c+d*d,c,d};
        }
    sort(sum,sum+m);
    for(int a=0;a*a<=n;a++)
        for(int b=a;a*a+b*b<=n;b++)
        {
            int t=n-a*a-b*b;
            int left=0;
            int right=m-1;
            while(left<right)
            {
                int mid=left+right>>1;
                if(sum[mid].s>=t) right=mid;
                else left=mid+1;
            }
            if(sum[left].s==t)
            {
                printf("%d %d %d %d\n",a,b,sum[left].c,sum[left].d);
                return 0;
            }
        }
    
    return 0;
}

分巧克力

 

 

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100010;
int h[N],w[N];
int n,k;

bool check(int mid)
{
    int res=0;
    for(int i=0;i<n;i++)
    {
        res+=(h[i]/mid)*(w[i]/mid);//分巧克力的公式
        if(res>=k) return true;
    }
    return false;
}

int main()
{
    scanf("%d%d",&n,&k);
    for(int i=0;i<n;i++) scanf("%d%d",&h[i],&w[i]);
    
    int left=1;
    int right=1e5;
    while(left<right)
    {
        int mid=left+right+1>>1;
        if(check(mid)) left=mid;//比较的是边长,大于这个边长的不满足,所以取的是最后一个满足的,也就是第一段的末尾
        else right=mid-1;
    }
    cout<<right<<endl;
}

递增三元组

 

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N], b[N], c[N];
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++)  scanf("%d", &a[i]);
    for (int i = 0; i < n; i++)  scanf("%d", &b[i]);
    for (int i = 0; i < n; i++)  scanf("%d", &c[i]);
    sort(a, a + n);  //二分需要满足单调性
    //sort(b, b + n);
    sort(c, c + n);
    LL res = 0;  //答案可能会很大,会爆int
    for (int i = 0; i < n; i++)
    {
        int l = 0, r = n - 1;  //二分查找a数组中最后一个小于b[i]的数的下标
        while (l < r)
        {
            int mid = (l + r + 1) / 2;
            if (a[mid] < b[i])   l = mid;
            else   r = mid - 1;
        }
        if (a[l] >= b[i])   //如果未找到小于b[i]的数,将x标记为-1,后续计算时 x+1==0
        {
            l = -1;
        }
        int x = l;
        l = 0, r = n - 1;
        while (l < r)
        {
            int mid = (l + r) / 2;
            if (c[mid] > b[i])   r = mid;
            else  l = mid + 1;
        }
        if (c[l] <= b[i])   //如果未找到大于b[i]的数,将y标记为n,后续计算时 n-y==0;
        {
            r = n;
        }
        int y = r;
        res += (LL)(x + 1) * (n - y);
    }
    printf("%lld\n", res);
    return 0;
}


我在哪?(字符串哈希)

 

整理题意:

要求的以枚举区间长度,遍历整个数组,是否存在相同的字符串即可

 

#include <iostream>
#include <unordered_set>
using namespace std;
int n;
string s;

int main()
{
    cin >> n >> s;
    for (int i = 1; i <= n; i++)//枚举区间长度
    {
        unordered_set<string> st;//自动去重
        bool flag = true;
        for (int j = 0; j + i <= n; j++)//枚举左端点
        {
            string t = s.substr(j, i);//截取片段
            if (st.count(t) != 0)//如果已经出现过了
            {
                flag = false;
                break;
            }
            st.insert(t);
        }
        if (flag == true)
        {
            cout << i << endl;
            break;
        }
    }
    return 0;
}

中位数

暴力思路:

合并后排序,输出中位数即可


大佬写法学习一下:

#include <iostream>
#include <vector>
using namespace std;
const int N = 2e5;
const int INF = 1e7;
vector<int> a(N), b(N);
int main()
{
    ios::sync_with_stdio(false);
    int n, m;
    cin >> n;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    cin >> m;
    for (int i = 0; i < m; i++)
        cin >> b[i];
    a[n] = INF, b[m] = INF; //避免数字序列长度不一致发生越界错误
    int median = n + m - 1 >> 1;
    int i = 0, j = 0, cnt = 0;
    while (cnt < median) //双指针寻找中位数,若a[i]<b[j],则a[i]排在前面,i++,反之b[i]排在前面,j++
    {
        a[i] < b[j] ? i++ : j++;
        cnt++;
    }
    cout << (a[i] < b[j] ? a[i] : b[j]) << endl;
    return 0;
}

楼梯

 

核心:相似三角形 的数学运用推导公式

大佬思路:

AcWing 1510. 楼梯 - AcWing

 

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
double x, y, c;
bool check(double mid)
{
    double ry = sqrt(y * y - mid * mid);
    double rx = sqrt(x * x - mid * mid);

    double h = rx * ry / (rx + ry);

    if (h < c) return true; //h<c,间距取大了,会让梯子下滑,这样交点高度就小了
    else return false;
}

int main()
{
    scanf("%lf%lf%lf", &x, &y, &c);

    double l, r;
    if (x < y) l = 0, r = x; //取小的边长,作为右边界,因为梯子不能趴地上吧
    else l = 0, r = y;

    while (r - l > 1e-5) //答案保留小数点后3位,这里多取2位即可  POJ原题数据1e-4也能过
    {
        double mid = (l + r) / 2;

        if (check(mid)) r = mid;
        else l = mid;
    }

    printf("%.3lf\n", l);

    return 0;
}

奶牛棒球

 

简单的数学推导关系:

枚举前两个数,根据范围二分出第三个数

#include <bits/stdc++.h>
using namespace std;

int main ()
{
    int n, ret = 0; cin >> n;
    vector<int> a(n); for (int & x : a) cin >> x;
    sort(a.begin(), a.end());
    for (int i = 0; i < n; i ++ )
        for (int j = i + 1; j < n; j ++ )
            ret += upper_bound(a.begin(), a.end(), 3 * a[j] - 2 * a[i]) - lower_bound(a.begin(), a.end(), 2 * a[j] - a[i]);
    cout << ret << endl;
    return 0;
}

工资计算

 

数学反推导

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

int T;

int get(int k){
    int a[] = {0, 1500, 4500, 9000, 35000, 55000, 80000, 1000000};
    double r[] = {0, 0.03, 0.1, 0.2, 0.25, 0.3, 0.35, 0.45};

    if(k <= 3500) return k;
    int tax = k - 3500;
    double sum = 0;
    for(int i = 1; i <= 7; i ++){
        if(tax >= a[i]){
            sum += (a[i] - a[i - 1]) * r[i];
        }

        else{
            sum += (tax - a[i - 1]) * r[i];
            break;
        }
    }
    return k - sum;
}

int main(){
    cin >> T;

    int l = T, r = T * 2;

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

    cout << l;

    return 0;
}

愤怒的牛

 

 

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1e5+10;
int x[N];
int n,m;

bool check(int mid)
{
    int cur = x[0];
    int res = 1;
    for(int i = 1; i < n; i++)
    {
        if(x[i] - cur >= mid)
        {
            res ++;
            cur = x[i];
        }
    }
    if(res >= m) return true;
    return false;
}
int main()
{
    cin>>n>>m;

    for(int i = 0;i < n;i++) cin>>x[i];
    sort(x,x+n);

    int l = 1,r = x[n-1];
    while(l < r)//枚举的是最大的最小距离:因此是二段性的第一段的末尾
    {
        int mid = (l + r + 1) / 2;
        if(check(mid)) l = mid;
        else r = mid - 1;
    }

    cout<<r<<endl;
    return 0;
}

数列分段 II

 

 

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 1e5+10;
int a[MAXN];
int N,M;
int l,r,mid;
bool check(int x){
    int tot=0;
    int sum=0;
    for(int i=1;i<=N;i++){
        if(sum+a[i]<=x) sum+=a[i];
        else{
            sum=a[i];
            tot++;
        }
    }
    return tot<M;
}
int main()
{
    cin>>N>>M;
    for (int i = 1; i <= N; i ++ ){
        scanf("%d", &a[i]);
        l=max(l,a[i]);
        r+=a[i];
    }
    while(l<r){
        mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<l<<endl;
    return 0;
}

特别注意浮点数的二分


数的三次方根

 

#include<iostream>
#include<stdio.h>
using namespace std;
int main()
{
    double x;
    cin >> x;
    double left = -10000;
    double right = 10000;
    while (right - left > 1e-8)
    {
        double mid = (right + left) / 2;
        if (mid * mid * mid > x) right = mid;
        else left = mid;
    }
    printf("%.6lf", left);

    return 0;
}