算法学习 二分与前缀和

例题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

分析——二分

1.确定区间,确定目标在区间当中
2.找出一个性质,满足使得整个区间具有二段性
在这里插入图片描述

1.离散的情况
第一类

在这里插入图片描述
ans在右端点:M就归于右边

mid-1,求mid的时候就+1

第一类模板
while(L < R)
{
    M = (L + R + 1) / 2;
    if M 是 绿 
        L = M;  //根据这一步判断上一步需不需要  +  1   左边加一,右边不加
    else
        R = M - 1;
}
第二类

在这里插入图片描述
ans在左端点,M就归于左边
第二类模板

while(L < R)
{
    M  = (L + R) / 2;  //右边不用+1
    if M 是 蓝色
        R = M;
    else 
        L = M + 1;
}

整数二分分析

在这里插入图片描述

代码分析
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;

const int  N = 100010;
int n,m;  //n数组的长度  m  访问的个数
int q[N];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 0;i < n;i++) scanf("%d",&q[i]);
    //开始二分
    for(int i = 0;i < m;i++) 
    {
        int x;
        scanf("%d",&x);
        //二分的左端点
        int l = 0,r = n - 1;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(q[mid] >= x)
                r = mid;
            else
                l = mid + 1;
        }
        
        if(q[r] == x)
        {
            //先输出右边的值
            cout << r << ' ';
            //左边的边界不变,改变右边的边界
            r = n - 1;
            //二分的右端点
            while(l < r)
            {
                int mid = (l + r + 1) >> 1;
                if(q[mid] <= x)
                    l = mid;
                else
                    r = mid - 1;
            }
            cout << l << endl;
        }
        else 
            cout << "-1 -1" <<endl;
    }
    return 0;
}

例题2 数的三次方根

题目

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

输入格式

共一行,包含一个浮点数n。

输出格式

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

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

数据范围

−10000≤n≤10000

输入样例:

1000.00

输出样例:

10.000000

代码
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;

int main()
{
    double l = -10000,r = 10000;
    double x;
    cin >> x;
    while(r - l > 1e-8)  //注意这里把精度多调整两位
    {
        double mid = (l + r) / 2;
        if(mid * mid *mid <= x)
            l = mid;
        else 
            r = mid;
    }
    printf("%lf\n",r);
    return 0;
}

例题3 前缀和

题目

输入一个长度为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

分析——前缀和

前缀和:求某一个静态数组的某一段的和
原理:容斥原理
注意:前缀和输入的时候下标要从1开始,因为要保证前缀和数组S[0] 为0
公式
在这里插入图片描述

求子矩阵和的公式

在这里插入图片描述

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 100010;
int n,m;
int s[N],a[N];  //s[N]表示前缀和的值,a[N]表示读取的数组的值

int main()
{
    scanf("%d%d",&n,&m);  //n是整数数列有多少个,m是下面有多少组值
    for(int i = 1;i <= n;i++)  //读取整数的数列,注意这里的下标要从1开始,因为要保证s[0]为0
    {
        scanf("%d",&a[i]);
        s[i] = s[i - 1] + a[i];
    }
    
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d\n",s[r] - s[l - 1]);
    }
    return 0;
    
}

例题4 子矩阵的和

题目

输入一个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

代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;

const int N = 1010;

int n,m,q;
int a[N][N],s[N][N];
int main()
{
    scanf("%d%d%d",&n,&m,&q);
    
    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
        {
            scanf("%d",&a[i][j]);
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
        }
        while(q--)
        {
            int x1,y1,x2,y2;
            scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
            printf("%d\n",s[x2][y2] - s[x1 - 1][y2]  - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
        }
    return 0;
}

例5 机器人跳跃问题

问题

机器人正在玩一个古老的基于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:
问题分析:

有单调性必然可以用二分来做

代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>

using namespace std;

const int N = 100010;
int n;
int H[N];

bool check(int e)
{
    for(int i = 1;i <= n;i++)
    {
        e = e * 2 - H[i];
        if(e >= 1e5) return true;
        if(e < 0) return false;
    }
    return true;
}

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

例题6 四平方和

题目

四平方和定理,又称为拉格朗日定理:

每个正整数都可以表示为至多 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<N<5∗106

输入样例:

5

输出样例:

0 0 1 2

题目分析

最多枚举两个数 ,如果枚举三个数的话,三次方会超时。。
方法: 用空间换取时间
在这里插入图片描述

代码
二分写法
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>

using namespace std;

const int N = 2500010;

struct Sum
{
    int s,c,d;    //s存储  c * c + d * d  
    //实现一个字典序的排列 主要是对sort排序进行一个提前操作
    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 n,m;   //n 是输入的数  m是表示存储的多少的数

int main()
{
    cin >> n;
    //遍历存储所有的c和d的情况
    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);  //按照字典序进行排序   对c和d的情况
    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 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;
    
}
哈希写法
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<unordered_map>  //哈希表
#define x first
#define y second
using namespace std;
typedef pair<int,int> PII;

const int N = 2500010;

int n,m;
unordered_map<int,PII> S;

int main()
{
    cin >> n;
    for(int c = 0;c * c <= n;c++)
        for(int d = 0;c * c + d * d <= n;d++)
        {
            int t  = c * c + d * d;
            if(S.count(t) == 0) S[t] = {c,d};
        }
    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;
            if(S.count(t))
            {
                printf("%d %d %d %d\n",a,b,S[t].x,S[t].y);
                return 0;
            }
        }
    return 0;
}


例题7 分巧克力

题目

儿童节那天有 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≤105,
1≤Hi,Wi≤105

输入样例:

2 10
6 5
5 6

输出样例:

2

分析

二分法
在这里插入图片描述

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

const int N = 100010;
int h[N],w[N];
int n,k;  //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 l = 1,r = 1e5;
    while(l  < r)
    {
        int mid  = (l + r + 1) >> 1;
        if(check(mid)) l = mid;
        else r = mid - 1;
    }
    printf("%d\n",r);
    return 0;
}

例题8 激光炸弹

题目

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

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

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

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

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

输入格式

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

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

输出格式

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

数据范围

0≤R≤109
0<N≤10000,
0≤Xi,Yi≤5000
0≤Wi≤1000

输入样例:

2 1
0 0 1
1 1 1

输出样例:

1

分析

注意:
所有前缀和的问题,全部回归到 “1” 开始的局面,横纵坐标

初始化前缀和的操作:(二维的)
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];
        }
    }
求前缀和的操作
res = s[i][j] - s[i][j - R] - s[i - R][j] + s[i - R][j - R]
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>

using namespace std;

const int N  = 5010;

int n,m;  //n最大的x,m  最大的y
int s[N][N];  

int main()
{
    int cnt,R;  //cnt  数目  R  边长
    cin >> cnt >> R;
    R = min(R,5001);
    n = m = R;
    while(cnt--)
    {
        int x,y,w;
        cin >> x >> y >> w;
        x++,y++;
        s[x][y] = w;
        n = max(n,x),m = max(m,y);
    }
    
    //初始化前缀和
    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++)
        for(int j = R;j <= m;j++)
            res = max(res,s[i][j] - s[i][j - R] - s[i - R][j] + s[i - R][j - R]);
    cout << res << endl;
    return 0;
}

例题9 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

分析

用前缀和

初始想法
在这里插入图片描述
优化1.0
在这里插入图片描述
再次优化
在这里插入图片描述
最终优化
在这里插入图片描述

代码
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;

typedef long long LL;

const int N = 100010;
int n,k;
LL s[N],cnt[N];

int main()
{
    cin >> n >> k;
    for(int i = 1;i <=n;i++)
    {
        cin >> s[i];
        s[i] += s[i - 1];
    }
    LL res = 0;
    for(int i = 0; i <=n;i++)
    {
        res += cnt[s[i] % k];
        cnt[s[i] % k]++;
    }
    cout << res << endl;
    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值