刷题笔记-二分法

China☆狼群 提交于 2020-03-29 22:10:51

数的范围

给定一个按照升序排列的长度为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

思路:

1.找到左端点:
- 区间[0 , n-1]
- 判断条件:num[mid] >= tar
2.找到右端点:
- 区间[左端点, n-1]
- 判断条件:num[mid] <= tar

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,q;
int num[N];
int main(void)
{
    scanf("%d %d",&n,&q);
    for(int i = 0;i < n;i++)
        scanf("%d",&num[i]);
    while(q--)
    {
        int tar;
        scanf("%d",&tar);
        
        //找左端点
        int l = 0,r = n-1;
        while(l < r)
        {
            int mid = l + r >> 1;
            if(num[mid] >= tar) 
                r = mid;
            else
                l = mid + 1;
        }
        if(num[l] != tar)
            printf("-1 -1\n");
        else
        {
            printf("%d ",l);
            //找右端点
            r = n - 1;
            while(l < r)
            {
                int mid = l + r + 1 >> 1;
                if(num[mid] <= tar)
                    l = mid;
                else
                    r = mid - 1;
            }
            printf("%d\n",l);
        }
    }
	return 0;

}

机器人跳跃问题

机器人正在玩一个古老的基于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)≤10^5\)
\(1≤N,H(i)≤10^5\)

思路:

1.分析题目

  • 如果机器人具有的能量数是所有建筑高度的MAX,那么机器人一定可以通过所有建筑,因为对于建筑高度小于能量数的,能量数会增加,对于建筑高度等于能量数,能量数会增加0。
  • 且机器人具有的最小能量数是应该是1,因为建筑最低高度是1

2.二分法

  • 区间[1 , max]
  • 判断条件 cal(mid) >= 0,即对于一个能量值mid,定义一个函数cal(int x)进行判断,如果在通过所有建筑后还剩能量大于0,说明初始能量值可能还可以继续缩小。

代码

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

using namespace std;
const int N = 100010;
int n;
int num[N];
int cal(int x,int ma)             
{
    for(int i = 1; i <= n;i++)
    {
        if(x >= num[i])
            x += (x - num[i]);
        else
            x -= (num[i] - x);
        if(x < 0 || x >= ma)     //如果超过max,则一定可以通过;如果小于0,则一定不行
            break;
    }
    return x;
}

int main(void)
{
    scanf("%d",&n);
    int ma = 0;
    for(int i = 1 ;i <= n; i++ )
    {
        scanf("%d", &num[i]);
        ma = max(ma,num[i]);
    }
    //二分法    
    int l  = 1,r = ma;
    while(l < r)
    {
        int mid = (l + r) >> 1;
        if(cal(mid,ma) >= 0) 
            r = mid;
        else
            l = mid + 1;
    }
    printf("%d",r);
    return 0;
    
}

我遇到的坑

判断函数cal()中一开始是没有

if(x < 0 || x >= ma)     
     break;

数据会导致int x溢出,x累加后的范围确实不好想;我使用long long x结果也溢出了。最后才想到加上上面的判断条件就好了,既快也不会溢出。

分巧克力

儿童节那天有K位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。小明一共有N块巧克力,其中第i块是Hi x Wi的方格组成的长方形。

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

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

例如一块6x5的巧克力可以切出6块2x2的巧克力或者2块3x3的巧克力。

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

输入

第一行包含两个整数N和K。(1 <= N, K <= 100000)
以下N行每行包含两个整数Hi和Wi。(1 <= Hi, Wi <= 100000)
输入保证每位小朋友至少能获得一块1x1的巧克力。

输出

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

思路

和前面那道题很类似了,巧克力大小的区间范围是[1,max],对区间内巧克力的边长进行二分,如果
判断条件cal(mid) >= k,即可以分出边长为mid的巧克力大于k个小朋友,那么就可能可以把巧克力在分的更大一些。

代码

#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long LL;
const int N = 100010;
int num[N][2];
int n,k;
LL cal(int dat)
{
    LL sum = 0;
    for(int i = 0; i < n;i++)
    {
        int x = num[i][0]/dat;
        int y = num[i][1]/dat;
        sum +=  x * y;
        if(sum >= k)
            break;
    }
    return sum;
}
int main(void)
{
    scanf("%d %d", &n,&k);
    int ma = 0;               
    for(int i = 0; i < n;i++)
    {
        scanf("%d %d",&num[i][0],&num[i][1]);
        ma = max(ma,min(num[i][0], num[i][1]));//取所有巧克力中宽的最大值
    }
    int l = 1,r = ma;            
    while(l < r)
    {
        int mid = (l + r + 1) >> 1;
        if(cal(mid) >= k)
            l = mid;
        else
            r = mid - 1;
    }
    printf("%d",l);
    return 0;
}

遇到的坑

一开始边界区间没有找准,我认为是右边应该是所有巧克力边长中的最小值,因为如果要使用每一块巧克力,那么肯定要找所有边长的最小值,但是事实上,一些某一边长小的巧克力完全可以不要嘛。这个是提交后发现的,还是考虑不周全。

四平方和

四平方和定理,又称为拉格朗日定理:
每个正整数都可以表示为至多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 (N<5000000)
要求输出4个非负整数,按从小到大排序,中间用空格分开

思路:

我一开始就往二分去想,没有想出来,稍微想了一下枚举,但是感觉会超时,就看了题解,如下:
1.先计算枚举的时间复杂度:\(\sqrt{5*10^6} = 2240\)
估算三重for循环的时间复杂度是O(\(10^9\)),显然超时了(实际上,因为大部分的N的a,b都很小,大部分数据是不会超时的);

2.所以要改进枚举,一般的方法是 使用空间换取时间。在本题中:

  • 可以先枚举c和d,将 \(c^2+d^2\) 的值c,d保存下来(顺序存储、哈希)
  • 在枚举a,b,计算 \(t = n - a^2-b^2\) ,使用二分法或者哈希来判断t是否已经存储。
  • 如果t存在,那么直接输出。

方法1-哈希

使用\(c^2+d^2\)的值直接作为哈希函数,需要注意的是可能有多种方法可以构成某个值,但是只保留第一个,因为第一个字典序最小。

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

const int N = 5000010;
int num[N][2];
int n;
int main(void)
{
    scanf("%d",&n);
    int m = 0;
    for(int c = 0; c * c <= n;c++ )
        for(int d = c;c * c + d * d <= n;d++)
        {
            int tmp = c*c + d*d;
            if(num[tmp][0] == 0 && num[tmp][1] == 0) //在哈希数组还未赋值的情况下,才允许赋值
            {
                num[tmp][0] = c;
                num[tmp][1] = 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(t == 0 || (num[t][0] != 0 || num[t][1] != 0))   //判断t是否存在,存在则直接输出a b c d
            {
                printf("%d %d %d %d",a,b,num[t][0],num[t][1]);
                return 0;
            }
        }
    return 0;
}

方法2-二分
二分的话,在于第一步保存\(c^2+d^2\)的值时,如果有重复的,如何做到只保留字典序最小的,这个我能想到的只有遍历,看大佬的程序使用了我不知道的C++语法,贴出来,以供学习。

//作者:yxc
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 2500010;

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 n, m;

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!