二分与前缀和

本文介绍了二分搜索和前缀和这两种算法在解决信息技术问题中的典型应用,包括数的范围查询、浮点数的三次方根求解、机器人跳跃问题、四平方和问题以及分巧克力问题等。通过实例解析解题思路,并提供了相应的解题代码,展示了如何利用二分搜索优化查找效率,以及如何运用前缀和快速计算区间和。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分

整数二分

 例题:数的范围

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

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

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

输入格式

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

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

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

输出格式

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

如果数组中不存在该元素,则返回 -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

解题思路:

 

解题代码: 

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

using namespace std;

const int N=100010;

int 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);
        //二分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<<' ';
            //二分x的右端点
            r=n-1;//右端点一定在[左端点,n-1]之间
            while(l<r)
            {
                int mid=l+r+1>>1;//因为写的是l=mid,所以需要补上1
                if(q[mid]<=x) l=mid;
                else r=mid-1;
            }
            cout <<l<<endl;
        }
        else cout<<"-1 -1"<<endl;
    }
    return 0;
}

实数二分

例题:数的三次方根

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

输入格式

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

输出格式

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

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

数据范围

−10000≤n≤10000

输入样例:

1000.00

输出样例:

10.000000

解题代码 

#include <iostream>
using namespace std;

int main()
{
    double x;
    cin >> x;
    
    double l=-10000,r=10000;
    while(r-l>1e-8)
    {
        double mid =(l+r)/2;
        if(mid*mid*mid>=x) r=mid;
        else l=mid;
    }
    printf("%lf\n",l);
    return 0;
}

拓展:三分法 

 习题1:机器人跳跃问题

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

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

编号为 0 的建筑高度为 0 个单位,编号为 ii 的建筑高度为 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)≤10^5

输入样例1:

5
3 4 3 2 4

输出样例1:

4

输入样例2:

3
4 4 4

输出样例2:

4

输入样例3:

3
1 6 4

输出样例3:

3

解题思路

 

 

解题代码 

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=100010;

int n;
int h[N];

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

int main()
{
    //读入个数
    scanf("%d",&n);
     //读入所有塔的高度
    for(int i=1;i<=n;i++) scanf("%d",&h[i]);
   
    int l=0,r=1e5;
    //二分
    while(l<r)
    {
        int mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    
    printf("%d",r);
    
    return 0;
}

 习题2:四平方和

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

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

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

比如:

5=0^2+0^2+1^2+2^2
7=1^2+1^2+1^2+2^2

对于一个给定的正整数,可能存在多种平方和的表示法。

要求你对 4 个数排序:

0≤a≤b≤c≤d

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

输入格式

输入一个正整数 N。

输出格式

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

数据范围

0<N<5∗10^6

输入样例:

5

输出样例:

0 0 1 2

解题思路

  

解题代码

三重循环暴力求解(超时):

#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <cstdio>

using namespace std;

const int N=2500010;

int n;

int main()
{
    cin>>n;
    for(int a=0;a*a<=n;a++)
        for(int b=a;a*a+b*b<=n;b++)
            for(int c=b;a*a+b*b+c*c<=n;c++)
            {
                int t=n-a*a-b*b-c*c;
                int d=sqrt(t);
                if(d*d==t)
                {
                    printf("%d %d %d %d\n",a,b,c,d);
                    return 0;
                }
            }
}

二重循环:

 O(N^2*logN) 2721ms

#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <cstdio>

using namespace std;

const int N=2500010;

int n,m;//m所有组合的个数

struct Sum
{
    int s,c,d;//s=c*c+d*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;
            //二分查找t
            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;
}

哈希表
O(N^2)超时 

#include <cstring>
#include <iostream>
#include <algorithm>
#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 = c; 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 = 0; 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;
}

 习题3:分巧克力

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

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

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

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

切出的巧克力需要满足:

  1. 形状是正方形,边长是整数
  2. 大小相同

例如一块 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

解题思路

 

解题代码

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

using namespace std;

const int N = 100010;

int n, k;
int h[N], w[N];//h巧克力高度,w长度

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;
}

前缀和

一维前缀和

例题:前缀和

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

 解题思路

解题代码

#include <iostream>

using namespace std;

const int N=100010;

int n,m;
int a[N],s[N];//a原数组,s前缀和数组

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        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;
}

二维前缀和

例题:子矩阵的和

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


const int N=1010;
int n,m,q;
int a[N][N],s[N][N];

int main()
{
    scanf("%d%d%d",&n,&m,&q);//n长宽q个数
    
    for(int i=1;i<=n;i++)//读入原矩阵
        for(int j=1;j<=m;j++)
            scanf("%d",&a[i][j]);
            
     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]+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;
}

习题1:激光炸弹

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

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

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

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

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

输入格式

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

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

输出格式

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

数据范围

0≤R≤10^9
0<N≤10000
0≤Xi,Yi≤5000
0≤Wi≤1000

输入样例:

2 1
0 0 1
1 1 1

输出样例:

1

解题思路

 

解题代码 

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 5010;

int n, m;//长,宽
int s[N][N];//前缀和数组

int main()
{
    int cnt, R;//目标个数,边长
    cin >> cnt >> R;
    R = min(5001, R);

    n = m = R;//n,m>r
    while (cnt -- )//读入
    {
        int x, y, w;
        cin >> x >> y >> w;
        x ++, y ++ ;
        n = max(n, x), m = max(m, y);
        s[x][y] += 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;

    // 枚举所有边长是R的矩形,枚举(i, j)为右下角
    for (int i = R; i <= n; i ++ )
        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;
}

习题2:k倍区间

给定一个长度为 NN 的数列,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

解题思路

 

解题代码

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

using namespace std;

typedef long long LL;

const int N = 100010;

int n, k;
LL s[N], cnt[N];

int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%lld", &s[i]);
        s[i] += s[i - 1];
    }

    LL res = 0;
    cnt[0] = 1;
    for (int i = 1; i <= n; i ++ )//枚举右端点
    {
        res += cnt[s[i] % k];
        cnt[s[i] % k] ++ ;
    }

    printf("%lld\n", res);

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值