YCKCOJ清明进阶专题题解

总的来说还是有难度的,这也能二分???
本套题需要大家尽量思考

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[L1],直接计算区间内有多少个不是 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]+(Ksum[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 NMK+1小,我们可以去check一下二分的答案在矩阵里面处于第几小。假设对于我们二分的答案 x x x它处于第 y y y小,而且 y < N ∗ M − K + 1 y<N*M-K+1 yNMK+1,说明我们二分的答案偏小,需要朝着更大的答案去寻找;假设对于我们二分的答案 x x x它处于第 y y y小,而且 y > N ∗ M − K + 1 y>N*M-K+1 yNMK+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 Ti秒,如果剩余时间不足以飞到指定位置,那么此答案为 f a l s e false false,需要扩大答案。无人机到位以后就是清理箱子的工作了,不难发现,对于 T − i T-i Ti秒内清理 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]/(Ti))架无人机,
如果当前需要的无人机数量 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 Ti秒内能清理 n e e d ∗ ( T − i ) need*(T-i) need(Ti)个箱子,实际上我们只有 A [ i ] A[i] A[i]箱子需要清理,如果 n e e d − ( T − i ) > A [ i ] need-(T-i)>A[i] need(Ti)>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;
}
  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值