从今天开始将持续更新蓝桥杯的专项练习题
目录
7、技能升级(第十三届蓝桥杯 省赛 C++ C组 & JAVA 研究生组 & Python B组/研究生组)
1、借教室(NOIP2012提高组)
问题描述
在大学期间,经常需要租借教室。
大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。
教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。
面对海量租借教室的信息,我们自然希望编程解决这个问题。
我们需要处理接下来 n 天的借教室信息,其中第 i 天学校有 ri 个教室可供租借。
共有 m 份订单,每份订单用三个正整数描述,分别为 dj,sj,tj,表示某租借者需要从第 sj 天到第 tj 天租借教室(包括第 sj天和第 tj天),每天需要租借 dj 个教室。
我们假定,租借者对教室的大小、地点没有要求。
即对于每份订单,我们只需要每天提供 dj 个教室,而它们具体是哪些教室,每天是否是相同的教室则不用考虑。
借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。
如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。
这里的无法满足指从第 sj天到第 tj 天中有至少一天剩余的教室数量不足 dj 个。
现在我们需要知道,是否会有订单无法完全满足。
如果有,需要通知哪一个申请人修改订单。
输入格式
第一行包含两个正整数 n,m,表示天数和订单的数量。
第二行包含 n个正整数,其中第 i 个数为 ri,表示第 i 天可用于租借的教室数量。
接下来有 m行,每行包含三个正整数 dj,sj,tj,表示租借的数量,租借开始、结束分别在第几天。
每行相邻的两个数之间均用一个空格隔开。
天数与订单均用从 1 开始的整数编号。
输出格式
如果所有订单均可满足,则输出只有一行,包含一个整数 0。
否则(订单无法完全满足)输出两行,第一行输出一个负整数 −1,第二行输出需要修改订单的申请人编号。
数据范围
1≤n,m≤1e6
0≤ri,dj≤1e9
1≤sj≤tj≤n
输入样例:
4 3
2 5 4 3
2 1 3
3 2 4
4 2 4
输出样例:
-1
2
**思路:
如果有一天订单出现问题,那这一天之后的订单一定都有问题
所以可以划分为两部分:有问题和没有问题的
我们需要定位到最早有问题的一天,采用二分搜索(r=mid-1)
在这之前,先用差分处理数组
(因为都是对区间的加减相同的数,所以应该考虑差分来降低时间复杂度)
代码:
//想到一个区间想加减就该想到差分
//如果总有一天会迎来订单错误,那么这一天后的所有订单都会错误
//所以可以分为两段
//所以差分处理完数据后采用二分搜索
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m;//天数和订单数量
int rooms[N]; //每天可借教室的数量
//存储询问
int d[N],s[N],t[N];
//差分数组
long long b[N];
bool check(int mid)
{
memset(b,0,sizeof b);
//处理差分数组
for(int i=1;i<=mid;i++)
{
b[s[i]] +=d[i];
b[t[i]+1]-=d[i];//相当于 1到m个订单 ,每个订单的区间都减去 d【i】(当天要借的教室数)
}
//求前缀和
for(int i=1;i<=n;i++)
{
b[i]=b[i]+b[i-1];
//cout<<b[i]<<endl;
if(b[i]>rooms[i])return false;//如果某一天借的大于当天的教室数量返回false
}
//cout<<"yes"<<endl;
return true;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&rooms[i]);
}
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&d[i],&s[i],&t[i]);
}
int l=0,r=m;
while(l<r)
{
int mid=r+l+1>>1; //为什么要+1? 因为边界问题,+1非常重要
//cout<<mid<<endl;
if(check(mid))l=mid;//正确,那就继续往后借教室
else r=mid-1;//check返回fasle就是表示订单错误,需要查看前面的是否也错误
}
//如果没有错误,二分应该定格在r==m上,因为直到最后一天都没有错误
if(r==m)cout<<0<<endl;
//如果错误,则r==l==mid-1,定格在最早的错误的一天的前一天
else cout<<-1<<endl<<r+1<<endl;
return 0;
}
2、分巧克力(第八届蓝桥杯 c++/java :A/B组)
儿童节那天有 K 位小朋友到小明家做客。
小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi的方格组成的长方形。
为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。
切出的巧克力需要满足:
- 形状是正方形,边长是整数
- 大小相同
例如一块 6×56×5 的巧克力可以切出 66 块 2×22×2 的巧克力或者 22 块 3×33×3 的巧克力。
当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?
输入格式
第一行包含两个整数 N 和 K。
以下 N行每行包含两个整数 Hi 和 Wi。
输入保证每位小朋友至少能获得一块 1×11×1 的巧克力。
输出格式
输出切出的正方形巧克力最大可能的边长。
数据范围
1≤N,K≤1e5
1≤Hi,Wi≤1e5
输入样例:
2 10
6 5
5 6
输出样例:
2
**思路:
采用二分,把所有的巧克力遍数排序(天然就是排好的,1-1e5)
从第一块方案开始检索,尽量往后检索能满足的巧克力边数
由于小边数的巧克力肯定比大边数的多,所以小边数的巧克力如果不满足>=k,后满大边数的肯定更不满足,所以可以用二分在巧克力边长最长,并且满足的一点确定下来
在最后一次循环中,最后满足还会+1,所以最后输出l 或者 r 要-1
文中注释是测试用,与正文代码无关
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,k;
int a[N],b[N];
//int method[N];
//typedef long long LL;
bool check(int mid)
{
int x=0;
for(int i=0;i<n;i++)
{
x+=(a[i]/mid)*(b[i]/mid);
}
if(x>=k)return true;
else return false;
}
int main()
{
cin>>n>>k;
//读入巧克力的长宽
for(int i=0;i<n;i++)
{
scanf("%d%d",&a[i],&b[i]);
}
//6*5->6 * 2*2 -> 2* 3*3 ->1* 4*4
//找规律发现长和宽分别除以方块的边长,再相乘就能得到块数
/*
for(int i=0;i<n;i++)
{
for(int j=1;j<a[i] && j<b[i];j++)
{
int x=a[i]/j;
int y=b[i]/j;
m[j]+=x*y;//意思是j块方案的块数
}
}
*/
//sort(method,method+n);//二分选出最大的边长
int l=1,r=1e5+5;
while(l<r)
{
int mid = l+r>>1;
if(check(mid))l = mid+1;
else r = mid;
}
cout<<r-1<<endl;
return 0;
}
3、数的范围 (经典二分搜索)
给定一个按照升序排列的长度为 n 的整数数组,以及 q个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 00 开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n个整数(均在 1∼100001∼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
**思路:
由于给的数组具有单调性,通过二分确定左右边界即可
代码:
#include<iostream>
using namespace std;
int n,m;
const int N=1000010;
int q[N];
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)cin>>q[i];
while(m--)
{
int x;
cin>>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<<-1<<" "<<-1<<endl;
else
{
cout<<l<<" ";
int l=0,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;
}
}
return 0;
}
4、管道(第十四届蓝桥杯 省赛 python B组)
有一根长度为 len 的横向的管道,该管道按照单位长度分为 len 段,每一段的中央有一个可开关的阀门和一个检测水流的传感器。
一开始管道是空的,位于 Li 的阀门会在 Si时刻打开,并不断让水流入管道。
对于位于 Li 的阀门,它流入的水在 Ti(Ti≥Si)时刻会使得从第 Li−(Ti−Si)段到第 Li+(Ti−Si) 段的传感器检测到水流。
求管道中每一段中间的传感器都检测到有水流的最早时间。
输入格式
输入的第一行包含两个整数 n,len用一个空格分隔,分别表示会打开的阀门数和管道长度。
接下来 n行每行包含两个整数 Li,Si用一个空格分隔,表示位于第 Li 段管道中央的阀门会在 Si 时刻打开。
输出格式
输出一行包含一个整数表示答案。
数据范围
对于 30% 的评测用例,n≤200,Si,len≤3000;
对于 70% 的评测用例,n≤5000,Si,len≤1e5;
对于所有评测用例,1≤n<=1e5,1≤Si,len≤1e9;
输入样例:
3 10
1 1
6 5
10 2
输出样例:
5
**思路:
题意还是比较难懂的,大致意思就是每个开关会往两边放水(这就可以考虑到区间和并),看最早哪个时刻所有水管可以注满
先对每一个时刻进行分析,看是否能注满水,因为某一个时刻如果能注满水,那这个时刻之后都能注满水,具有单调性(这也是可以二分的原因),因此我们考虑二分搜索,我们可以二分出最早注满水的时刻
需要注意的是区间合并的时候条件需要修改一下,ed+1小于等于第二个区间的时候就可以合并了
注意ed要+1,因为管道不需要重合,合并后也是全是注满水的
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
typedef long long LL;
typedef pair<int,int> PII;
PII w[N],q[N];
int n,len;
bool check(int mid)//判断某一个时刻是否能注满
{
int cnt=0;//打开阀门的数量
for(int i=0;i<n;i++)
{
int pos=w[i].first;
if(mid<w[i].second)
{
continue;
}
else
{
int d=mid-w[i].second;//经过的距离
q[cnt].first=max(1,pos-d);
q[cnt].second=min((LL)pos+d,(LL)len);
cnt++;
}
}
//区间合并
sort(q,q+cnt);
int st=-2e9,ed=-2e9;
for(int i=0;i<cnt;i++)
{
if(ed+1<q[i].first)//不能合并的情况
{
st=q[i].first;
ed=q[i].second;
}
else ed=max(ed,q[i].second);
}
//cout<<cnt<<endl;
return st==1 && ed==len;
}
int main()
{
cin>>n>>len;
for(int i=0;i<n;i++)
{
int a,b;
scanf("%d%d",&a,&b);
w[i]={a,b};
}
int l=1,r=2e9;//最晚会在2e9覆盖
while(l<r)
{
int mid=(LL)l+r>>1;//用l=mid+1的模板
if(check(mid))r=mid;
else l=mid+1;
}
cout<<l<<endl;
return 0;
}
5、最佳牛围栏(算法进阶指南、浮点数二分)
农夫约翰的农场由 N 块田地组成,每块地里都有一定数量的牛,其数量不会少于 1 头,也不会超过 2000 头。
约翰希望用围栏将一部分连续的田地围起来,并使得围起来的区域内每块地包含的牛的数量的平均值达到最大。
围起区域内至少需要包含 F 块地,其中 F会在输入中给出。
在给定条件下,计算围起区域内每块地包含的牛的数量的平均值可能的最大值是多少。
输入格式
第一行输入整数 N 和 F,数据间用空格隔开。
接下来 N 行,每行输入一个整数,第 i+1 行输入的整数代表第 i 片区域内包含的牛的数目。
输出格式
输出一个整数,表示平均值的最大值乘以 1000再 向下取整 之后得到的结果。
数据范围
1≤N≤100000
1≤F≤N
输入样例:
10 6
6
4
2
10
3
8
5
9
4
1
输出样例:
6500
**思路:
一般平均值问题都能转化为二分问题,二分不一定要单调性,二分需要的是二分性质
我们只需要求出来有没有某种平均值能满足题意,尽量使他最大
那么二分性就来了,一部分是能满足要求的,一部分是不能满足要求的
我们的任务是在满足要求的平均值中找出最大平均值
先写一个简单的浮点数二分
再写check函数:check函数中要先对数组进行前缀和处理,并且全部减去当前要验证的平均数
然后至少要包含f个区域,我们先固定右端点k,那么左端点就是可移动的,我们的目的是让我们的s【k】尽量大,那就减去前面的前缀和(很可能是负数,最大也是0(当k==f时,s【0】==0)),所以我们维护一个 最小的要被减去的前缀和,然后用s【k】减去,同时保证我们的区间大于f,最后如果两数相减的结果大于0,那么说明有一个区间即使减去当前平均值也能大于0,说明有更好的选择,返回true
多说无益,上程序:
代码:
//一般平均值问题都可以转化为二分问题
#include<bits/stdc++.h>
using namespace std;
int n,f;//总共n块地,每块需要至少包含f块地
const int N=1e5+5;
double q[N];
//前缀和
double s[N];
bool check(double avg)
{
//求出前缀和全部减去avg
for(int i=1;i<=n;i++)s[i]=s[i-1]+q[i]-avg;
double mins=0;
//分为数个区间,每个区间求出最大值,如果大于0,就可以
for(int k=f;k<=n;k++)
{
mins=min(mins,s[k-f]);//s[k-f]使得区间至少长度为f,并且记录需要减去的前缀和最小值
if(s[k]-mins>=0)return true;
}
return false;
}
int main()
{
cin>>n>>f;
double l=0;
double r=0;
for(int i=1;i<=n;i++)
{
scanf("%lf",&q[i]);
r=max(r,q[i]);
}
while(r-l>1e-5)
{
double mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
cout<<(int)(r*1000);
return 0;
}
6、冶炼金属(第十四届蓝桥杯 省赛 C++ B组 )
小蓝有一个神奇的炉子用于将普通金属 O 冶炼成为一种特殊金属 X。
这个炉子有一个称作转换率的属性 V,V 是一个正整数,这意味着消耗 V 个普通金属 O 恰好可以冶炼出一个特殊金属 X,当普通金属 O 的数目不足 V 时,无法继续冶炼。
现在给出了 N条冶炼记录,每条记录中包含两个整数 A 和 B,这表示本次投入了 A 个普通金属 O,最终冶炼出了 B 个特殊金属 X。
每条记录都是独立的,这意味着上一次没消耗完的普通金属 O 不会累加到下一次的冶炼当中。
根据这 N 条冶炼记录,请你推测出转换率 V的最小值和最大值分别可能是多少,题目保证评测数据不存在无解的情况。
输入格式
第一行一个整数 N,表示冶炼记录的数目。
接下来输入 N 行,每行两个整数 A、B,含义如题目所述。
输出格式
输出两个整数,分别表示 V 可能的最小值和最大值,中间用空格分开。
数据范围
对于 30%30% 的评测用例,1≤N≤1e2。
对于 60%60% 的评测用例,1≤N≤1e3。
对于 100%100% 的评测用例,1≤N≤1e4,1≤B≤A≤1e9。
输入样例:
3
75 3
53 2
59 2
输出样例:
20 25
样例解释
当 V=20 时,有:⌊75/20⌋=3,⌊53/20⌋2,⌊59/20⌋2,可以看到符合所有冶炼记录。
当 V=25 时,有:⌊75/25⌋=3,⌊53/25⌋=2,⌊59/25⌋=2,可以看到符合所有冶炼记录。
且再也找不到比 20 更小或者比 25更大的符合条件的 V值了。
思路:
这里我选择写两个check函数,分别应对左右边界的情况,思路也比较简单,经典的二分搜索思路(和上面 第三题 数的范围 极其相似),看代码吧
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e4+5;
LL a[N],b[N];
int n;
bool check(LL mid)//冶炼值为mid
{
for(int i=0;i<n;i++)
{
//cout<<a[i]<<endl;
int t=a[i]/mid;
//cout<<b[i]<<endl;
//cout<<t<<" "<<b[i]<<endl;
if(t<b[i])return false;//不符合冶炼记录就输出false
}
return true;
}
bool checksp(LL mid)//冶炼值为mid
{
for(int i=0;i<n;i++)
{
//cout<<a[i]<<endl;
int t=a[i]/mid;
//cout<<b[i]<<endl;
//cout<<t<<" "<<b[i]<<endl;
if(t>b[i])return false;//不符合冶炼记录就输出false
}
return true;
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
scanf("%d%d",&a[i],&b[i]);
}
LL l=1,r=1e9+1;
LL mins=r;
while(l<r)
{
//cout<<r<<endl;
LL mid=(l+r)/2;
if(checksp(mid))r=mid;
else
{
l=mid+1;
}
}//确定最小值
cout<<l<<" ";
l=l-1,r=1e9+1;
while(l<r)
{
//cout<<l<<" "<<r<<endl;
LL mid=(l+r+1)/2;
if(check(mid))l=mid;
else r=mid-1;
}//确定最大值
cout<<l<<endl;
return 0;
}
7、技能升级(第十三届蓝桥杯 省赛 C++ C组 & JAVA 研究生组 & Python B组/研究生组)
小蓝最近正在玩一款 RPG 游戏。
他的角色一共有 N 个可以加攻击力的技能。
其中第 i 个技能首次升级可以提升 Ai 点攻击力,以后每次升级增加的点数都会减少 Bi。
⌈Ai/Bi⌉(上取整)次之后,再升级该技能将不会改变攻击力。
现在小蓝可以总计升级 M次技能,他可以任意选择升级的技能和次数。
请你计算小蓝最多可以提高多少点攻击力?
输入格式
输入第一行包含两个整数 N 和 M。
以下 N行每行包含两个整数 Ai和 Bi。
输出格式
输出一行包含一个整数表示答案。
数据范围
对于 40%40% 的评测用例,1≤N,M≤1e3;
对于 60%60% 的评测用例,1≤N≤1e4,1≤M≤1e7;
对于所有评测用例,1≤N≤1e5,1≤M≤2 * 1e9,1≤Ai,Bi≤1e6。
输入样例:
3 6
10 5
9 2
8 1
输出样例:
47
暴力思路:
每个技能每用一次都会减去一个固定的值,所以每个技能都可以看作一个序列,这个序列还是个等差序列,我们可以把每个技能序列中的每个元素全部放进一个数组里,排序(以便于贪心的取最大的攻击力),直接取前M个,显然这样在面对大量数据的时候会TLE
但是在考场上这不失为一个得分手段(在想出更好的解决方案之前)
注意:这个代码并不能成功AC,所以如果想要通过的代码请看本题的下个思路:二分思路
暴力代码:
//每个技能都是一个等差数列
//暴力做法就是把每个等差数列排序,然后把这些元素全部放到一个数组中
//然后排序,取前M个就是能得到的最大的攻击力(这一点也用到了贪心的思想)
#include<bits/stdc++.h>
typedef long long LL;
const int N=2e5+10;
int n,m;
using namespace std;
int a[N],b[N];
int q[N];
int hh=0;
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
scanf("%d%d",&a[i],&b[i]);
while(a[i]>=b[i])
{
q[hh++]=a[i];
a[i]-=b[i];
}
if(a[i]>0)q[hh++]=a[i];
}
sort(q,q+hh,greater<int>());
//至此排序完成
//开始二分,如果比赛时这里可以直接暴力贪心前m个,拿分走人
LL maxs=0;
for(int i=0;i<m;i++)
{
maxs+=q[i];
}
cout<<maxs<<endl;
return 0;
}
二分思路:
在暴力思路中我们提到每一个技能都相当于一个序列,并且这个技能最开始的加的攻击力相当于首项,在这里我们假设存在一个最大的x,使得所有项数中大于等于x的项数大于等于m个
我们不断的增大x的值,以期望求得最大的x,这样二分的性质就出来了,如果最大是x,那么大于x的就无法满足题意,小于等于x的肯定能满足题意,最终我们求出来的x表示:能满足加技能次数>=m,并且取的最大的数
每个等差数列中含多少个x我们能用(a[i]-x)/d[i]+1这个公式算出来
求出来x之后我们就可以求和了,遍历一遍每个等差数列的首项,再次利用上面的公式求出项数,然后利用等差数列公式求出该等差数列中大于等于x的元素的合,每个都这样累加,最终算出每个数组的结果累加上。
但现在还不是最终答案,因为x可能有多个,可能会多加一些x,所以说我们在累加的同时记数,最后的结果就是累加的结果减去(cnt-m)*r,就能把多加的r减掉
二分代码:
//每个技能都是一个等差数列
//暴力做法就是把每个等差数列排序,然后把这些元素全部放到一个数组中
//然后排序,取前M个就是能得到的最大的攻击力(这一点也用到了贪心的思想)
#include<bits/stdc++.h>
typedef long long LL;
const int N=2e5+10;
int n,m;
using namespace std;
int a[N],b[N];
bool check(LL mid)
{
LL res=0;
for(int i=0;i<n;i++)
{
if(a[i]>=mid)res+=(a[i]-mid)/b[i]+1;//加上第i项中大于等于mid的数的个数
}
return res>=m;
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
scanf("%d%d",&a[i],&b[i]);
}
int l=0,r=1e6;
while(l<r)
{
LL mid=l+r+1>>1;
if(check(mid))l=mid;//check(mid)表示检查大于等于mid的数是不是至少有m个
//为什么是大于等于?因为mid也有可能是答案
else r=mid-1;
}
LL res=0,cnt=0;
for(int i=0;i<n;i++)
{
if(a[i]>=r)
{
LL c=(a[i]-r)/b[i]+1;
LL end=a[i]-(c-1)*b[i];
cnt+=c;
res+=(a[i]+end)*c/2;
}
}
//cout<<r<<endl;
//cout<<res<<endl;
cout<<res-(cnt-m)*r<<endl;
return 0;
}