总的来说还是有难度的,这也能二分???
本套题需要大家尽量思考
A题 DARLING in the FRANXX
实际上这是一部好看的日漫,本题的背景主要以
叫龙
叫龙
叫龙为主,它是一种生物。。。好了言归正传,抓住核心题意: 02只想一次性清理掉某一级别的叫龙,所以不难发现最终我们环的排列方式一定是所有的A B C分别连成一块,例如AAAABBBBBCCCCC这样子。
对于环的问题,我们要固定一个点,这样子思维才不会被环的特殊性所迷惑(因为是环所以无法区分首尾),假设我们以1位置作为环的开头。
实际上最终02消灭叫龙的时候无外乎消除顺序是ABC的几种排列:
A
B
C
ABC
ABC
A
C
B
ACB
ACB
B
A
C
BAC
BAC
B
C
A
BCA
BCA
C
A
B
CAB
CAB
C
B
A
CBA
CBA
我们就按六种方式依次枚举,当然需要借助到一个快速统计的工具
以最终排列
A
B
C
ABC
ABC为例子,先统计一开始分别有
A
,
B
,
C
A,B,C
A,B,C种类的叫龙个数,如果以
A
A
A开头,那么由于环的性质,我们可以在开头摆放若干个
A
A
A叫龙,结尾摆放剩余叫龙,假如说
A
A
A叫龙一共有
a
a
a只,那么我们可以摆放0~a只叫龙在首,剩余在尾,这个可以枚举。然后摆放所有的
B
B
B叫龙与
C
C
C叫龙。但是我们需要知道一个数据:
把某种叫龙全部填在某个区间需要的引导次数
把某种叫龙全部填在某个区间需要的引导次数
把某种叫龙全部填在某个区间需要的引导次数
这个东西可以用前缀和实现,定义前缀和
A
[
i
]
A[i]
A[i]为
1
1
1–
i
i
i位置内非
A
叫龙
A叫龙
A叫龙的个数,假如说我们现在要把
a
a
a只
A
A
A叫龙全部引导在一个长度为
a
a
a的区间
[
L
,
R
]
[L,R]
[L,R],那么求法就是
A
[
R
]
−
A
[
L
−
1
]
A[R]-A[L-1]
A[R]−A[L−1],直接计算区间内有多少个不是
A
A
A龙,然后引导即可,当然还要一种特殊的情况,那就是环,如果是环的话则是由开头一段+结尾一段来计算引导数,详情请看代码。
前缀和记录好以后,枚举第2个字母摆放位置,然后前缀和计算。
为什么枚举第2个字母的摆放位置呢?
实际上就是在考虑从首位置开始,摆放多少只第1个字母对应的叫龙,例如A叫龙一共a只,那么我可以在首位置开始摆放0~a只,那么第2个字母摆放的位置只能是1 ~ a+1
疑问 : 疑问: 疑问: 有同学觉得这样子的前缀和不合理,因为前缀和会超出能调动的某种龙的数量。。。这个不必担心因为我们每次使用前缀和的时候保证了只用“某种龙个数的区间长度” ,所以说这个区间一定能实现
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 50;
int A[maxn], B[maxn], C[maxn];
char s[maxn];
int num[3];
int main()
{
std::ios::sync_with_stdio(false);
int n;
cin >> n;
cin >> s + 1;
int a = 0, b = 0, c = 0;
for(int i = 1;i <= n;i++)
{
if(s[i] != 'A') A[i] = A[i - 1] + 1;
else A[i] = A[i - 1], a++;
if(s[i] != 'B') B[i] = B[i - 1] + 1;
else B[i] = B[i - 1], b++;
if(s[i] != 'C') C[i] = C[i - 1] + 1;
else C[i] = C[i - 1], c++;
}
int ans = 1e9;
for(int i = 1;i <= c + 1;i++)//AB
{
int cost = A[i + a - 1] - A[i - 1] + B[i + a + b - 1] - B[i + a - 1];
cost += C[i - 1] + C[n] - C[i + a + b - 1];
ans = min(cost, ans);
}
for(int i = 1;i <= c + 1;i++)//BA
{
int cost = B[i + b - 1] - B[i - 1] + A[i + a + b - 1] - A[i + b - 1];
cost += C[i - 1] + C[n] - C[i + a + b - 1];
ans = min(cost, ans);
}
for(int i = 1;i <= b + 1;i++)//AC
{
int cost = A[i + a - 1] - A[i - 1] + C[i + a + c - 1] - C[i + a - 1];
cost += B[i - 1] + B[n] - B[i + c + a - 1];
ans = min(cost, ans);
}
for(int i = 1;i <= b + 1;i++)//CA
{
int cost = C[i + c - 1] - C[i - 1] + A[i + c + a - 1] - A[i + c - 1];
cost += B[i - 1] + B[n] - B[i + c + a - 1];
ans = min(cost, ans);
}
for(int i = 1;i <= a + 1;i++)//BC
{
int cost = B[i + b - 1] - B[i - 1] + C[i + c + b - 1] - C[i + b - 1];
cost += A[i - 1] + A[n] - A[i + c + b - 1];
ans = min(cost, ans);
}
for(int i = 1;i <= a + 1;i++)//CB
{
int cost = C[i + c - 1] - C[i - 1] + B[i + c + b - 1] - B[i + c - 1];
cost += A[i - 1] + A[n] - A[i + c + b - 1];
ans = min(cost, ans);
}
cout << ans << endl;
return 0;
}
B题 工作安排
分组,我们也不知道能分多少组出来,但是这个组数一定是单调的。考虑二分答案。
假如说能分出mid组,那么如何去检查呢?
我们可以把mid组当成mid个盒子,N个工作部门看成N种不同颜色的小球,每个部门的人数看成当前种类小球的数量。我们可以把第
i
i
i种小球在所有盒子里面各放一个(前提是小球数量大于等于盒子数量),多余的肯定不能再分组了。如果第
i
i
i种小球不足以铺满所有的盒子一次,那么就记录一下当前这一层铺了的数量,然后再枚举下一种小球的数量去放入盒子。
大致情况就像,7种小球数量各异,5个盒子可以如此摆放,每个盒子限定5个球
整体来说就是一层一层地去铺,铺满一层需要M个,当铺满mid层就说明是合法解
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll a[222222];
ll k;
ll n;
bool check(ll x) {
ll last=0;
ll now=0;
for(int i=1; i<=n; i++) {
last=(last+min(a[i],x)); //剩下的
if(last>=x) {
last%=x;
now++;
}
if(now>=k)return true;
}
return false;
}
signed main() {
scanf("%lld%lld",&n,&k);
for(int i=1; i<=n; i++) {
scanf("%lld",&a[i]);
}
sort(a+1,a+1+n);
ll l=0,r=1e18;
ll ma=0;
while(l<r){
ll mid=(r+l+1)/2;
if(check(mid)){
l=mid;
}
else{
r=mid-1;
}
}
printf("%lld\n",l);
return 0;
}
C题 超长序列
先讲个普通解法,偏向于数学一点。计算出完全的A序列的和sum,
超越X不过是若干个sum+不完整的一段A序列>X
直接计算X对sum的余数,然后
O
(
N
)
O(N)
O(N)枚举A序列扫一遍即可
总共位置就是X/sum*N+余数超越位置
至于二分的话,我们可以记录一个A数组的前缀和数组 p r e [ i ] pre[i] pre[i],查找余数的时候二分查找即可
#include<bits/stdc++.h>
#include<stdlib.h>
#include<algorithm>
#include<stdio.h>
#include<string.h>
#include<queue>
#include<time.h>
#include <cstdio>
#include <iostream>
#include <vector>
#define ll long long
#define int long long
#define inf 0x3f3f3f3f
#define mods 1000000007
#define modd 998244353
#define PI acos(-1)
#define fi first
#define se second
#define lowbit(x) (x&(-x))
#define mp make_pair
#define pb push_back
#define si size()
#define E exp(1.0)
#define fixed cout.setf(ios::fixed)
#define fixeds(x) setprecision(x)
#define IOS ios::sync_with_stdio(false);cin.tie(0)
using namespace std;
ll gcd(ll a,ll b){if(a<0)a=-a;if(b<0)b=-b;return b==0?a:gcd(b,a%b);}
template<typename T>void read(T &res){bool flag=false;char ch;while(!isdigit(ch=getchar()))(ch=='-')&&(flag=true);
for(res=ch-48;isdigit(ch=getchar());res=(res<<1)+(res<<3)+ch - 48);flag&&(res=-res);}
ll lcm(ll a,ll b){return a*b/gcd(a,b);}
ll qp(ll a,ll b,ll mod){ll ans=1;if(b==0){return ans%mod;}while(b){if(b%2==1){b--;ans=ans*a%mod;}a=a*a%mod;b=b/2;}return ans%mod;}//快速幂%
ll qpn(ll a,ll b, ll p){ll ans = 1;a%=p;while(b){if(b&1){ans = (ans*a)%p;--b;}a =(a*a)%p;b >>= 1;}return ans%p;}//逆元 (分子*qp(分母,mod-2,mod))%mod;
ll a[111111];
ll s[111111];
signed main()
{
ll n;
read(n);
for(int i=1; i<=n; i++)
{
read(a[i]);
s[i]=s[i-1]+a[i];
}
ll x;
read(x);
if(x<s[n])
{
for(int i=1; i<=n; i++)
{
if(s[i]>x)
{
printf("%lld",i);
return 0;
}
}
}
ll op=x/s[n];
x=x-op*s[n];
ll ans=op*n;
for(int i=1; i<=n; i++)
{
if(s[i]>x)
{
printf("%lld",i+ans);
return 0;
}
}
return 0;
}
D题 消失的数
想找到不存在于A数组内的第K小的正整数。麻烦的就是如何越过
A
[
i
]
A[i]
A[i]
我们知道正整数序列就是
1
,
2
,
3
,
4....
A
[
1
]
.
.
.
.
A
[
2
]
.
.
.
.
A
[
N
]
1,2,3,4....A[1]....A[2]....A[N]
1,2,3,4....A[1]....A[2]....A[N],对于
A
[
i
]
A[i]
A[i]位置是没有答案贡献的
考虑计算一个差分,即统计相邻
A
[
i
]
A[i]
A[i]之间可以插多少数字,那么从左往右做一遍差分前缀和,就能得到对于第
i
i
i个位置而言,它前面能贡献前
s
u
m
[
i
]
sum[i]
sum[i]小的数字。我们二分
s
u
m
sum
sum数组,找到小于等于K并且最接近K的位置
p
o
s
pos
pos,即
s
u
m
[
p
o
s
]
小于等于
K
sum[pos]小于等于K
sum[pos]小于等于K,所以这个数字必定存在于
A
[
p
o
s
]
A
[
p
o
s
+
1
]
A[pos]~A[pos+1]
A[pos] A[pos+1]之间,具体值就是
A
[
p
o
s
]
+
(
K
−
s
u
m
[
p
o
s
]
)
A[pos]+(K-sum[pos])
A[pos]+(K−sum[pos])
注意一定要找到严格小于
K
K
K的前缀和
s
u
m
sum
sum的位置,如果找到的是小于等于的位置,实际上会影响最终答案的计算,如果
K
=
s
u
m
[
p
o
s
]
K=sum[pos]
K=sum[pos],那么就会直接落在
A
[
p
o
s
]
A[pos]
A[pos]上,这是非法的
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll n,q,A[100005],sum[100005],k;
bool check(int x){
return sum[x]<k;
}
signed main(){
scanf("%lld%lld",&n,&q);
A[0]=0;
for(int i=1;i<=n;i++){
scanf("%lld",&A[i]);
sum[i]=sum[i-1]+A[i]-A[i-1]-1;
}
while(q--){
scanf("%lld",&k);
int L=0,R=n;
while(L<R){
int mid=(L+R+1)/2;
if(check(mid)){
L=mid;
}
else{
R=mid-1;
}
}
printf("%lld\n",A[L]+k-sum[L]);
}
return 0;
}
E题 你真的会二分吗?
相信很多同学读完题目,都不知道怎么做,这也能二分?
其实这是二分答案的经典了。考虑二分答案,假设最终答案是
x
x
x
那么整个矩阵里面的第
K
K
K大,也就是第
N
∗
M
−
K
+
1
N*M-K+1
N∗M−K+1小,我们可以去check一下二分的答案在矩阵里面处于第几小。假设对于我们二分的答案
x
x
x它处于第
y
y
y小,而且
y
<
N
∗
M
−
K
+
1
y<N*M-K+1
y<N∗M−K+1,说明我们二分的答案偏小,需要朝着更大的答案去寻找;假设对于我们二分的答案
x
x
x它处于第
y
y
y小,而且
y
>
N
∗
M
−
K
+
1
y>N*M-K+1
y>N∗M−K+1,说明我们二分的答案偏大,需要朝着更小的答案去寻找。
当然check不是暴力check,check的时候,计算第 y y y小的过程也需要用到二分,因此我们需要把某个数组排序一遍(从小到大,假设是对B数组),check的时候枚举 A A A数组,二分查找 B B B数组里面存在多少个位置满足 A [ i ] ∗ B [ j ] < y A[i]*B[j]<y A[i]∗B[j]<y,注意一定要是严格小于。这个式子很明显是单调的,可以二分。
所以综上所示本题是二分套二分的经典题目,为什么要取严格小于呢?因为本题涉及到出现多少种相同数目的数字在矩阵中。
当然如果比赛遇到不会写的题目也不要空着,写个暴力也能拿点分数
#include<bits/stdc++.h>
using namespace std;
//n m 1e5 1e5
// ai bj 1e9
// 1~k~n*m 查找第K大
long long int A[100005];
long long int B[100005];
long long int k;
long long int n,m;
bool check(long long int ans){
long long int cnt=0;//
for(int i=1;i<=n;i++){
if(A[i]*B[1]>=ans)continue;
int L=1,R=m;
while(L<R){
int mid=(L+R+1)/2;
if(A[i]*B[mid]<ans){
L=mid;
}
else{
R=mid-1;
}
}
cnt=cnt+L;
}
if(cnt>=n*m-k+1)return false;
else return true;
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
cin>>A[i];
}
for(int i=1;i<=m;i++){
cin>>B[i];
}
sort(B+1,B+1+m);
long long int l=1,r=1e18;
while(l<r){
long long int mid=(l+r+1)/2;
if(check(mid)){
l=mid;
}
else{
r=mid-1;
}
}
cout<<l;
return 0;
}
F题 欺负萌新不会二分
其实,E题完全看懂并独立写了一遍的同学,本题简直就是切菜了
对于询问巨大矩阵里面的第K大或者第K小问题,如果计算公式是单调的,实际上可以二分答案
注意本题系数的一些影响,到底是单调递增还是单调递减
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,k;
ll a,b,c;
ll check(ll x) {
ll cnt=0;
if(c==0) {
for(int i=1; i<=n; i++) {
ll op=a*i*i+b*i;
if(op<x) {
cnt=cnt+m;
}
}
}
if(c>0) {
// 递增
for(int i=1; i<=n; i++) {
ll vel=a*i*i+b*i;
ll pos=(x-vel)/c;// >=
if(pos>0) {
if(pos>m) {
cnt+=m;
continue;
}
if((x-vel)%c==0) {
cnt=cnt+pos-1;
}
if((x-vel)%c!=0) {
cnt=cnt+pos;
}
}
}
}
if(c<0) {
for(int i=1; i<=n; i++) {
ll vel=a*i*i+b*i;
ll pos=(x-vel)/c;
if(pos<=0) {
cnt=cnt+m;
continue;
}
if(pos>0) {
if(pos>m) {
continue;
} else {
cnt=cnt+(m-pos);
}
}
}
}
return cnt;
}
signed main() {
scanf("%lld%lld%lld%lld%lld%lld",&n,&m,&k,&a,&b,&c);
ll l=1e18;
ll r=-1e18;
for(int i=1; i<=n; i++) {
if(c>0) {
l=min(l,a*i*i+b*i+c);
r=max(r,a*i*i+b*i+c*m);
} else {
l=min(l,a*i*i+b*i+c*m);
r=max(r,a*i*i+b*i+c);
}
}
ll ans=0;
// cnt 是严格小于 mid 的个数
ll opp=0;
while(l<=r) {
ll mid=(l+r)/2;
ll rnm=check(mid);
if(rnm<=(n*m-k)) {
l=mid+1;
ans=mid;
}
if(rnm>(n*m-k)) {
r=mid-1;
}
}
printf("%lld",ans);
}
G题 无人机运输
这个题目呢,非常明显的就是要二分答案,也就是时间,但是check是很难想到的
check函数其实是在验证如何高效率得规划时间,以及无人机去做完任务,这里涉及到贪心算法思维
一开始我们有
f
l
y
fly
fly架无人机
无人机要么向前飞1个单位,要么把现在位置的一个箱子移除,操作耗费1秒。
我们让无人机尽可能地在
T
T
T秒内都运转起来。
考虑倒着枚举箱子数组,对于第
i
i
i堆箱子,首先让若干架无人机直接飞过去,那么需要耗费
T
−
i
T-i
T−i秒,如果剩余时间不足以飞到指定位置,那么此答案为
f
a
l
s
e
false
false,需要扩大答案。无人机到位以后就是清理箱子的工作了,不难发现,对于
T
−
i
T-i
T−i秒内清理
A
[
i
]
A[i]
A[i]个箱子至少需要
n
e
e
d
need
need=
c
e
i
l
(
A
[
i
]
/
(
T
−
i
)
)
ceil(A[i]/(T-i))
ceil(A[i]/(T−i))架无人机,
如果当前需要的无人机数量
n
e
e
d
<
f
l
y
need<fly
need<fly,说明本答案无法完成任务,答案偏小,需要扩大范围寻找答案。
当然既然是
c
e
i
l
ceil
ceil也就是向上取整了,我们检验一下是否有无人机运载力溢出的情况,即
n
e
e
d
need
need架无人机在
T
−
i
T-i
T−i秒内能清理
n
e
e
d
∗
(
T
−
i
)
need*(T-i)
need∗(T−i)个箱子,实际上我们只有
A
[
i
]
A[i]
A[i]箱子需要清理,如果
n
e
e
d
−
(
T
−
i
)
>
A
[
i
]
need-(T-i)>A[i]
need−(T−i)>A[i]那么就运载力溢出,溢出的运载力我们也不要浪费,记录一下多余的运载力
l
e
le
le上面,以供后续处理箱子。
为什么这个多余运载力能供后续箱子处理呢?实际上我们是倒着枚举的,对于第 i i i位置运载力溢出的情况,我们可以让部分无人机在飞来之前稍微做一些事情,再飞到 i i i位置处理箱子,时间是肯定充足的
因此本题完全思路就浮现了
记录溢出运载力
l
e
le
le ,倒着枚举处理箱子,对于第
i
i
i堆箱子
A
[
i
]
A[i]
A[i],先判断能不能用溢出的运载力来解决一部分,如果能完全解决,那么第
i
i
i堆箱子就不需要专门派无人机,如果只能解决一部分,那么实际上处理的第
i
i
i堆箱子数量为
A
[
i
]
−
l
e
A[i]-le
A[i]−le,当然消耗了溢出运载力,需要减去。
#include<bits/stdc++.h>
#include<stdlib.h>
#include<algorithm>
#include<stdio.h>
#include<string.h>
#include<queue>
#include<time.h>
#include<set>
#include<map>
#include <cstdio>
#include <iostream>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define IOS ios::sync_with_stdio(false);cin.tie(0)
using namespace std;
ll gcd(ll a,ll b) {
if(a<0)a=-a;
if(b<0)b=-b;
return b==0?a:gcd(b,a%b);
}
template<typename T>void read(T &res) {
bool flag=false;
char ch;
while(!isdigit(ch=getchar()))(ch=='-')&&(flag=true);
for(res=ch-48; isdigit(ch=getchar()); res=(res<<1)+(res<<3)+ch - 48);
flag&&(res=-res);
}
const int maxn=1e5+9;
ll n,m;
ll a[maxn];
bool check(ll ti) {
ll re=0,fly=m;
for(int i=n; i>=1; i--) {
ll rnm=a[i]; //当前箱子
if(re>=rnm) {
re=re-rnm;
rnm=0;//空闲运力如果可以解决当前堆 那么不需要消耗时间 rnm为0
} else {
rnm=rnm-re;//消耗一部分运力 继续处理
re=0;
}
if(rnm==0) {
continue;
}
if(ti<=i) {
return false;
}
ll ne=ceil(rnm*1.0/(ti-i));//总时间下需要最少多少无人机来处理第i堆
if(ne>fly) {
return false;//不够
} else {
fly=fly-ne;//够的话 可以用的无人机减少
}
re=re+ne*(ti-i)-rnm;
// 无人机数量*总时间-rnm 表示还剩运力可能超出,理论上可以多运多少
}
return true;
}
signed main() {
ll cnm=0;
read(n);
read(m);
for(int i=1; i<=n; i++) {
read(a[i]);
cnm=cnm+a[i]+i;//移除+到达此位置的时间
}
ll l=0;
ll r=cnm*2;
ll ans;
while(l<=r) {
ll mid=(l+r)>>1;
if(check(mid)) {
ans=mid;
r=mid-1;
} else {
l=mid+1;
}
}
printf("%lld\n",ans);
return 0;
}
讨伐恶龙。。。。突然发现这个题跟B题一模一样
数列分段Ⅱ
直接二分答案,代入回去check
check的过程就是,从左到右依次求和,如果某个和>二分的答案,那么就另起一段,重新求和
记录一下这个mid需要多少段,如果段数<M,说明二分的答案比较大,尝试缩小。如果段数=M,尝试还有没有缩小的空间。如果段数>M,说明二分的答案偏小,尝试扩大答案
不过要注意本题的L,R范围,L起码要保证能让所有的数单独成段,也就是L要大于等于最大的A[i]
#include<bits/stdc++.h>
using namespace std;
int n;
int A[100005];
int m;
bool check(int x){
int cnt=1;
int s=0;
for(int i=1;i<=n;i++){
if(s+A[i]<=x){
s=s+A[i];
}
else{
s=A[i];
cnt++;
}
}
if(cnt<=m)return true;
else return false;
}
int main(){
cin>>n>>m;
int L=0,R=1e9;
for(int i=1;i<=n;i++){
cin>>A[i];
L=max(L,A[i]);
}
while(L<R){
int mid=(L+R)/2;
if(check(mid)){//假设划分段的和最大值是mid
R=mid;
}
else{
L=mid+1;
}
}
cout<<R;
return 0;
}