2022蓝桥杯学习——2.二分与前缀和

二分

关于二分

二分就是断确定边界的过程,二分一定有解所以当二分到无法再分时的那个元素就是解,即l=r指向的值。

二分不一定要有单调性,二分的本质是寻找某种性质的分界点。只要可以找到某种性质,使得区间的前半部分满足,后半部分不满足,那么就可以用二分把这个分界点找到。

二分模板

整数二分


bool check(int x){...};//检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用(取mid左边)
int bsearch(int l,int r){
    if(l==r) return a[l];
    while(l<r){
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用(取mid右边)
int bsearch(int l,int r){
    if(l==r) return a[l];
    while(l<r){
        int mid=l+r+1>>1;
        if(check(mid)) l=mid;
        else r=mid-1;
    }
}

浮点数二分

bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

例题

1.数的范围

题目描述
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1。

输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1。

数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

解题思路

暴力解法就是每次都遍历数组,找到第一次和最后一次某个数出现的位置,但很显然,加上外面的q次循环,O(Nq)是会超时的。优化只能是优化查找的过程,题目给出数组是有序的,那我们就可以用二分来优化,二分是O(logN)的,设要找的数是x,那么我们只需找出第一个小于等于x的数,再找到第一个大于等于x的数,对应的下标就是,如果第一次找到的数不等于x,输出-1即可

代码实现+详细注释C++

#include
#include
using namespace std;
const int N=100010;
int a[N];
int main()
{
    int n,q;
    cin>>n>>q;
    for(int i=0;i<n;i++) cin>>a[i];
    int x;
    while(q--){
        cin>>x;
        int l=0,r=n-1;
        while(l<r){//找到第一个小于等于x的数
            int mid=(l+r)>>1;
            if(a[mid]>=x) r=mid;//如果这个数大于等于x,我们就将区间缩小到左半边继续找
            else l=mid+1;
        }
        if(a[l]!=x){//如果没有找到x,说明这个数不存在,直接打印“-1 -1”即可
            cout<<"-1 -1"<<endl;
            
        }else{
            cout<<l<<" ";
            l=0,r=n-1;
            while(l<r){//找到第一个大于等于x的数
                int mid=(l+r+1)>>1;
                if(a[mid]<=x) l=mid;//如果这个数小于等于x,我们就将区间缩小到右半边继续找
                else r=mid-1;
            }
            cout<<l<<endl;
        }
    }
    return 0;
}

2.数的三次方根

题目描述
给定一个浮点数 n,求它的三次方根。

输入格式
共一行,包含一个浮点数 n。

输出格式
共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围
−10000≤n≤10000
输入样例:

1000.00

输出样例:

10.000000

解题思路

根据题目所给的范围,直接枚举加二分即可

代码实现+详细注释C++

#include
#include
#include
using namespace std;
int main()
{
    double n;
    cin>>n;
    double l=-10000,r=10000;//n的范围从l到r
    while(r-l>1e-8){//找到三个数相乘的结果是n
        double mid=(l+r)/2;
        if(mid*mid*mid>=n) r=mid;
        else l=mid;
    }
    printf("%.6lf\n",l);
}

3.机器人跳跃问题

题目描述
机器人正在玩一个古老的基于 DOS 的游戏。

游戏中有 N+1 座建筑——从 0 到 N 编号,从左到右排列。

编号为 0 的建筑高度为 0 个单位,编号为 i 的建筑高度为 H(i) 个单位。

起初,机器人在编号为 0 的建筑处。

每一步,它跳到下一个(右边)建筑。

假设机器人在第 k 个建筑,且它现在的能量值是 E,下一步它将跳到第 k+1 个建筑。

如果 H(k+1)>E,那么机器人就失去 H(k+1)−E 的能量值,否则它将得到 E−H(k+1) 的能量值。

游戏目标是到达第 N 个建筑,在这个过程中能量值不能为负数个单位。

现在的问题是机器人至少以多少能量值开始游戏,才可以保证成功完成游戏?

输入格式
第一行输入整数 N。

第二行是 N 个空格分隔的整数,H(1),H(2),…,H(N) 代表建筑物的高度。

输出格式
输出一个整数,表示所需的最少单位的初始能量值上取整后的结果。

数据范围
1≤N,H(i)≤105,

输入样例1:

5
3 4 3 2 4

输出样例1:

4

输入样例2:

3
4 4 4

输出样例2:

4

输入样例3:

3
1 6 4

输出样例3:

3

解题思路

如果 H(k+1)>E,那么机器人就失去 H(k+1)−E 的能量值,E=E-H(k+1)+E=2E-H(k+1),否则它将得到 E−H(k+1) 的能量值,E=E+E-H(k+1)=2E-H(k+1),所以两种情况的计算式都是一样的,为了找到一个合适的E,我们可以用枚举加上二分,在给定的范围内找到一个最小的可以满足条件的E,什么样的E是满足条件的?就是给定的E在对于数组里的每一个值都满足2*E-H(k+1)>=0,我们只需要枚举的E是否符合条件就行

代码实现+详细注释C++

#include
#include
using namespace std;
const int N=100010;
int a[N],n;
bool check(int e){//判断给定的e是否满足条件 满足条件就返回true 否则返回false
    for(int i=1;i<=n;i++){
        e=2*e-a[i];
        if(e>1e5) return true;//如果e>1e5,又1≤H(i)≤105 ,那么2*e-a[i]一定还是大于1e5的,所以不会存在e小于0的情况
        if(e<0) return false;
    }
    return true;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    int l=0,r=1e5;//将二分的区间设置为范围,这样就能对所有情况枚举
    while(l<r){
        int mid=l+r>>1;
        if(check(mid)) r=mid;//找最小的,当满足check时,则mid+1~r的数都是满足条件的,将范围缩小到l~mid,看看能不能找到更小的值
        else l=mid+1;
    }
    cout<<r<<endl;
    return 0;
}

蓝桥杯真题

四平方和

题目描述
四平方和定理,又称为拉格朗日定理:

每个正整数都可以表示为至多 4 个正整数的平方和。

如果把 0 包括进去,就正好可以表示为 4 个数的平方和。

比如:

5=02+02+12+22
7=12+12+12+22
对于一个给定的正整数,可能存在多种平方和的表示法。

要求你对 4 个数排序:

0≤a≤b≤c≤d
并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法。

输入格式
输入一个正整数 N。

输出格式
输出4个非负整数,按从小到大排序,中间用空格分开。

数据范围
0

输入样例:

5

输出样例:

0 0 1 2

解题思路

题目给定的范围是0≤a≤b≤c≤d,暴力解法就可以直接枚举,用四重循环看是否存在a*a+b*b+c*c+d*d=n,四个数都从从小到大枚举,这样找到的第一个符合条件的一定是最小的
优化+哈希表:将四重循环优化成两重循环,我们可以先枚举c*c+d*d,将结果(平方和与c,d)存到哈希表中,然后枚举a*a+b*b,当a*a+b*b+c*c+d*d=n即 在哈希表中找是否存在值为n-a*a-b*b的项,如果存在那就是找到了,为了保证找到的是最小的,往哈希表存值的时候,如果存在我们就不存了,这样就只保留了第一次的结果
优化+二分:和用哈希表不同的就是我们需要借助一个结构体来存结果,而且结构体数组按照c的大小来排序,存完结果,用二分就可找到第一个值为n-a*a-b*b的项

代码实现+详细注释C++
哈希

#include
#include
#include
#include
using namespace std;
const int N=2500010;
typedef pair<int,int > PII;
unordered_map<int,PII> mp;
int main()
{
    int n;
    cin>>n;
    for(int c=0;c*c<=n;c++)
        for(int d=c;d*d+c*c<=n;d++){
            int t=d*d+c*c;
            if(mp.count(t)==0) mp[t]={c,d};//只有当哈希表不存在这个值的时候我们才存c和d,这样就保证了结果是最小的
        }
    for(int a=0;a*a<=n;a++)
        for(int b=a;b*b+a*a<=n;b++){
            int t=n-a*a-b*b;
            if(mp.count(t)){
                printf("%d %d %d %d\n",a,b,mp[t].first,mp[t].second);
                return 0;
            }
        }
    return 0;
}

二分

#include
#include
#include
using namespace std;
const int N=2500010;
int m;
struct sum{
    int s,c,d;
    bool operator < (const sum & t)&{
        if(s!=t.s) return s<t.s;
        if(c!=t.c) return c<t.c;
        return d<t.d;
    }
}sum[N];
int main()
{
    int n;
    cin>>n;
    for(int c=0;c*c<=n;c++)
        for(int d=c;d*d+c*c<=n;d++){
            sum[m++]={d*d+c*c,c,d};
        }
    sort(sum,sum+m);//将所有结果排序,其中将c和d在s相同的情况下也按字典序排序了,这样在找到第一组的解一定是最小的
    for(int a=0;a*a<=n;a++)
        for(int b=a;b*b+a*a<=n;b++){
            int t=n-a*a-b*b;
            int l=0,r=m-1;
            while(l<r){
                int mid=(l+r)>>1;
                if(sum[mid].s>=t) r=mid;
                else l=mid+1;
            }
            if(sum[l].s==t){
                printf("%d %d %d %d\n",a,b,sum[l].c,sum[l].d);
                return 0;
            }
        }
    return 0;
}

分巧克力

题目描述
儿童节那天有 K 位小朋友到小明家做客。

小明拿出了珍藏的巧克力招待小朋友们。

小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi 的方格组成的长方形。

为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。

切出的巧克力需要满足:

形状是正方形,边长是整数
大小相同
例如一块 6×5 的巧克力可以切出 6 块 2×2 的巧克力或者 2 块 3×3 的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含两个整数 Hi 和 Wi。

输入保证每位小朋友至少能获得一块 1×1 的巧克力。

输出格式
输出切出的正方形巧克力最大可能的边长。

数据范围
1≤N,K≤10^5,
1≤Hi,Wi≤10^5

输入样例:

2 10
6 5
5 6

输出样例:

2

解题思路

将一块高为h,宽为w的巧克力切成边长为x的巧克力,可以切(h/x)*(w/x)块,求最大边长,能满足切k块,也就是枚举边长看最大的边长是多少可以满足切出来的块数大于等于k,1≤Hi,Wi≤10^5,所以边长枚举的范围就可以是1~1e5,枚举判断的过程通过二分来确定边界,找到最大的边长

代码实现+详细注释C++

#include
#include
using namespace std;
const int N=100010;
int w[N],h[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));//求所有巧克力切成边长为mid的正方形 可以切多少快
        if(res>=k) return true;
    }
    return false;
}
int main()
{
    cin>>n>>k;
    for(int i=0;i<n;i++){
        cin>>h[i]>>w[i];
    }
    int l=1,r=1e5;//从所有可能的边长中找一个满足可以且k快且最大的
    while(l<r){
        int mid=(l+r+1)>>1;
        if(check(mid)) l=mid;//当mid满足可以切k快之后,l~mid一定都满足,从mid~r继续找更大的边长
        else r=mid-1;
    }
    cout<<l<<endl;
    return 0;
}

前缀和

关于前缀和

前缀和

预处理求出s[1~n],然后就能快速求出任意数组任意区间里一段数的和,如求l~r区间的和,就是s[r]-s[l-1],数组要从1开始从,s[0]=0,这样比如算s[1]-s[9],就可以用s[9]-s[0],而s[0]其实就是0,没有元素,避免越界

S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]

子矩阵的和

把每一个点看成一个小方块!!!!

**预处理求出任意一点到(0,0)点的面积即任意一个矩阵的面积,即求前缀和,然后再求出子矩阵的和。**前缀和可以看成是第i,j个小矩阵加上剩余的面积,即

a[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1]

S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1](这里减一可以看做是去掉边即不将边减去,(x1,y1)~(x2,y2)是要加上(x1,y1)这一行和这一列的)

例题

1.前缀和

题目描述
输入一个长度为 n 的整数序列。

接下来再输入 m 个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。

输出格式
共 m 行,每行输出一个询问的结果。

数据范围
1≤l≤r≤n,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000

输入样例:

5 3
2 1 3 6 4
1 2
1 3
2 4

输出样例:

3
6
10

解题思路

只要看到求任意一段区间的和,都可联想到前缀和,预处理求出前缀和之和,直接套公式即可

代码实现+详细注释C++

#include
#include
#include
using namespace std;
int n,m;
const int N=100010;
int a[N];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) a[i]+=a[i-1];//预处理求前缀和,+完之后数组a就是前缀和数组了
    while(m--){//任意一段区间l到r的和就是a[r]-a[l-1]
        int l,r;
        cin>>l>>r;
        cout<<a[r]-a[l-1]<<endl;
    }
    return 0;
}

2.子矩阵的和

题目描述
输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。

对于每个询问输出子矩阵中所有数的和。

输入格式
第一行包含三个整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式
共 q 行,每行输出一个询问的结果。

数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000

输入样例:

3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4

输出样例:

17
27
21

解题思路

先根据给定的矩阵预处理求出前缀和,前缀和公式就是s[i][j]=a[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1],然后求给定的任意(x1,y1)到(x2,y2)的子矩阵的和就行,公式是sum=s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]

代码实现+详细注释C++

#include
#include
#include
using namespace std;
const int N=1010;
int a[N][N];
int n,m,q;
int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++){//预处理求出前缀和
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
            a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
        }
    }
    while(q--){//根据前缀和求出任意区间的和
        int x1,x2,y1,y2;
        cin>>x1>>y1>>x2>>y2;
        cout<<a[x2][y2]-a[x1-1][y2]-a[x2][y1-1]+a[x1-1][y1-1]<<endl;
    }
    return 0;
}

3.激光炸弹

题目描述
地图上有 N 个目标,用整数 Xi,Yi 表示目标在地图上的位置,每个目标都有一个价值 Wi。

注意:不同目标可能在同一位置。

现在有一种新型的激光炸弹,可以摧毁一个包含 R×R 个位置的正方形内的所有目标。

激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y 轴平行。

求一颗炸弹最多能炸掉地图上总价值为多少的目标。

输入格式
第一行输入正整数 N 和 R,分别代表地图上的目标数目和正方形的边长,数据用空格隔开。

接下来 N 行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,Wi,分别代表目标的 x 坐标,y 坐标和价值,数据用空格隔开。

输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。

数据范围
0≤R≤109
0 0≤Xi,Yi≤5000
0≤Wi≤1000
输入样例:

2 1
0 0 1
1 1 1

输出样例:

1

解题思路

题目可以简化成,给定一个矩形,矩形的不同的点的价值不同,让我们求框住一个边长为 r 的正方形,求这个子矩阵里的点的和的最大值是多少,所以还是前缀和,求任意一个边长为子矩阵的前缀和,我们枚举子矩阵的右下角坐标(i,j),则转化为求(i-r+1,j-r+1)~(i,j)的子矩阵的前缀和。但是因为 r 的值可能比这个矩阵的边长大,如果是这样,矩形所有的点的价值都应该加在一起,由于我们求得前缀和是矩形的前缀和,在求任意矩阵的区间和,我们枚举的区间是从r到m,r到n,如果r大于n或者是m,我们会枚举不到,所以n和m的最大值是r和所给的横坐标和纵坐标直接取最大,而且,n和m的最大值是5000,当r>=5001即可全部覆盖

代码实现+详细注释C++

#include
#include
#include
using namespace std;
const int N=5010;
int s[N][N];
int t,r;
int main()
{
    cin>>t>>r;
    r=min(r,5001);//防止r很大,导致最后枚举不到,当r大于等于5001时,可全部覆盖
    int n=r,m=r;//因此区域的边界初始最大值也取r,当r大于等于5001,或者比x,y大,也可枚举到
    while(t--){
        int x,y,w;
        cin>>x>>y>>w;
        n=max(x+1,n),m=max(y+1,m);//通过max来取得横坐标和纵坐标的最大值
        s[x+1][y+1]+=w;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
        }
    }
    int res=0;
    for(int i=r;i<=n;i++){//(i,j)是子矩阵的右下角,因此从r开始枚举,左上角是(i-r,j-r)
        for(int j=r;j<=m;j++){
            res=max(res,s[i][j]-s[i-r][j]-s[i][j-r]+s[i-r][j-r]);
        }
    }
    cout<<res<<endl;
    return 0;
}

蓝桥杯真题

K倍区间

题目描述
给定一个长度为 N 的数列,A1,A2,…AN,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是 K 倍区间。

你能求出数列中总共有多少个 K 倍区间吗?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含一个整数 Ai。

输出格式
输出一个整数,代表 K 倍区间的数目。

数据范围
1≤N,K≤100000,
1≤Ai≤100000
输入样例:

5 2
1
2
3
4
5

输出样例:

6

解题思路

题目很明显是求前缀和,但是如果想暴力枚举所有的前缀和,就需要O(N*N)的时间复杂度,是会TLE的,就要想办法优化,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是 K 倍区间,也即前缀和(s[j]-s[i-1])%k==0,可以转化为s[j]%k==s[i]%k;转化为求r确定时,在0~r-1之间有多少个l使得 a[l]%k==a[r]%k

代码实现+详细注释C++

#include
#include
using namespace std;
const int N=100010;
long long a[N],cnt[N];//cnt[i]表示余数为i的前缀和的个数
int n,k;
int main()//将求a[r]-a[l-1]模k等于0  转化为求r确定时,在0~r-1之间有多少个l使得 a[l]%k==a[r]%k
{
    cin>>n>>k;
    long long res=0;
    for(int i=1;i<=n;i++){//预处理求前缀和
        cin>>a[i];
        a[i]+=a[i-1];
    } 
    cnt[0]=1;//模的结果为0的已经有一个,就是l==0时
    for(int r=1;r<=n;r++){//枚举左端点l
        res+=cnt[a[r]%k];
        cnt[a[r]%k]++;
    }
    cout<<res<<endl;
    return 0;
}

小结

在二分的题中,很对都是通过对答案的范围进行二分,直到二分出一个我们想要的结果,所以有时候逆向思维也很重要。对于很多有序的序列,我们的第一反应也应该是二分,或者是对于一组数据,存在某个临界使左边和右边满足不同的条件,二分的两个模板可以背过一下
前缀和一般用来求任意一段子区间的和,如果正常求任意一段的和复杂度是O(N),但是前缀和的复杂度是O(1)的,但是公式可以背过一下,很多时候前缀和都是结合离散化和哈希表的,其实前面的题很多都隐含了离散化。

OK,终于又系统化的解决完一个小模块了,如果有什么没理解的地方可以问,如果有错误的地方也欢迎指出呀,我会继续更新的,一起加油吧!!!
学习网站:AcWing

你可能感兴趣的:(2022蓝桥杯冲刺,蓝桥杯,算法,数据结构,二分法)