题解 | #牛客小白月赛 61 #
补完一套月赛,此图为证
说明
题目链接
赛中我就做出A,B两题,结果后来一直卡E。实际上这套题C、E并不是代码不好写,C就是bfs的板子,E就是推个式子,但是因为细节很多,很容易注意不到。
D、F算是压轴题,难度大概能对标cf div2的D甚至是E吧,远远不是出题人所说的对标cf div2的A~C难度。
D是一个模拟,根本想不到要用队列,就算想到了,也不会想到金币有可能爆long long而不能在这时接着加(和游戏里面差不多)。而且模拟的过程细节结果过多,只能说出题人想防小白AK那真是没办法~
F是双指针+差分,这个题主要是读完题之后很难联想到要用差分这个算法,因为你难以想到这m个约束对应于m个条件。
A 超市里扫货
直接按题意模拟。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int v[maxn];
int main()
{
int n,V;
cin>>n>>V;
for(int i=1;i<=n;i++)cin>>v[i];
long long ans=1,sum=0;
for(int i=1;i<=n;i++){
if(sum+v[i]<=V)sum+=v[i];
else sum=v[i],ans++;
}
cout<<ans<<endl;
return 0;
}
B 柜台结账
仍然是模拟,只是细节有点多。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
bool pd(string s){
bool flag=true;
for(int i=0;i<s.length();i++){
if(s[i]-'0'){
flag=false;
break;
}
}
return flag;
}
int pd5(string s){
int ans;
if(s[0]-'0'==5){
bool flag=true;
for(int i=1;i<s.length();i++){
if(s[i]-'0'){
flag=false;ans=1;break;
}
}
if(flag==true){
ans=3;
}
}
else if(s[0]-'0'>5){
ans=1;
}
else{
ans=2;
}
return ans;
}
int main()
{
string a1,a2;
cin>>a1>>a2;
if(pd5(a2)==1){
cout<<"Happy birthday to MFGG"<<endl;
}
else if(pd5(a2)==2){
if(!pd(a2))cout<<"Happy birthday to YXGG"<<endl;
else cout<<"PLMM"<<endl;
}
else {
if((a1[a1.length()-1]-'0')&1){
cout<<"Happy birthday to MFGG"<<endl;
}
else{
if(!pd(a2))cout<<"Happy birthday to YXGG"<<endl;
else cout<<"PLMM"<<endl;
}
}
return 0;
}
C 小喵觅食
这个题用不了dfs,因为dfs不能直接求出每个点的最短距离。但是bfs可以,因为可以在bfs的每一层利用dp的思想将层数状态转移。
先bfs求每个点以小猫为起点的单源最短路,再bfs求每个点以PLMM为起点的单源最短路,然后凡是满足条件的点都要算一遍最短路之和。注意,千万不能意淫,因为你不是计算机!每个满足约束的点都要算一遍其最短距离。
#include<bits/stdc++.h>
using namespace std;
int dir[4][2]={{0,1},{1,0},{-1,0},{0,-1}};
int n,m,r1,r2;
const int maxn=1e3+5;
char s[maxn][maxn];
struct node{
int x,y;
};
queue<node> q;
int cx,cy,px,py;
bool vis[maxn][maxn];
int disc[maxn][maxn],disp[maxn][maxn],dis[maxn][maxn];
void bfs(int sx, int sy){
node now;
now.x=sx;
now.y=sy;
q.push(now);
dis[sx][sy]=0;
vis[sx][sy]=1;
while(!q.empty()){
node next;
now = q.front();
q.pop();
for(int i=0;i<4;i++){
next.x=now.x+dir[i][0];
next.y=now.y+dir[i][1];
if(next.x>=1&&next.y>=1&&next.x<=n&&next.y<=m){
if(s[next.x][next.y]!='*'&&!vis[next.x][next.y]){
vis[next.x][next.y]=1;
dis[next.x][next.y]=dis[now.x][now.y]+1;
q.push(next);
}
}
}
}
}
int main(){
cin>>n>>m;
cin>>r1>>r2;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf(" %c",&s[i][j]);
if(s[i][j]=='P')px=i,py=j;
if(s[i][j]=='M')cx=i,cy=j;
}
}
memset(vis,0,sizeof(vis));
memset(dis,0,sizeof(dis));
bfs(cx,cy);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)disc[i][j]=dis[i][j];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(disc[i][j]==0)disc[i][j]=1e9;
}
}
disc[cx][cy]=0;
memset(vis,0,sizeof(vis));
memset(dis,0,sizeof(dis));
bfs(px,py);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)disp[i][j]=dis[i][j];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(disp[i][j]==0)disp[i][j]=1e9;
}
}
disp[px][py]=0;
int ans=1e9;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(abs(cx-i)+abs(cy-j)<=r2){
if(disp[i][j]<=r1){
ans=min(ans,disp[i][j]+disc[i][j]);
}
}
}
}
if(ans==1e9)ans=-1;
cout<<ans<<endl;
return 0;
}
D 石油大亨
这个代码以我现在一个cf绿名的码力写不出来,估摸着起码得蓝名才能写出这。这个代码是出题人的,但是注释是我自己加的,让我写我写不出来,等我上了蓝名再回来自己写一遍。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M = 1e5+5;
const ll linf = 0x3f3f3f3f3f3f3f3f;
//即4557430888798830600,4.557×10^18
//long long最大9223372036854775807,9.223×10^18
int n, ec, et, p; ll s;
int t[M];
void work()
{
scanf("%d %d %d %d %lld", &n, &ec, &et, &p, &s);
for (int i = 1; i <= n; ++i) scanf("%d", &t[i]);
if (s < ec) {printf("-1\n");return;}
//如果初始金额小于造一名工程师的金额,就不可能实现
//那么下面的就是一开始的钱至少够造一个工程师
queue<ll> q; //存储工程师占领油田的时间
ll cur_time = 0;//当前时间为0
for (int i = 1, oil_cnt = 0; i <= n; ++i) {//第i个油田,占领油田数一开始为0
ll next_time;//下一次可以开始训练工程师的最小时间点
if(i==1)next_time=0;//如果从第1个油田算起,那就是现在就可以开始训练
else next_time=cur_time+et;//如果不是第一个油田,那就是当前时间点+et秒训练时间
//当前工程师占领油田的时间点,在下一次可以开始训练工程师的最小时间点及以前
while (!q.empty() && q.front() <= next_time) { //计算从当前到next_time的金额收益
//如果金额没爆long long,才接着收钱,否则就不收了,不然爆了没法存
//虽然linf不是long long最大值,但大于这个值再存金币也没有意义
//之所以不设置long long最大值,还是为了大得适当,防止意外溢出
if(s < linf) s += oil_cnt * (q.front() - cur_time) * p;
//当前的油田数乘p金额是一秒的;
//经历的时间是下一名工程师占领油田的时间点-当前工程师占领油田的时间点
//因为在这一段时间油田数是固定的
cur_time = q.front();//更新当前工程师占领油田的时间点
q.pop();//因为已经用了,所以弹出队列
oil_cnt++;//占领的油田数加1,因为更新一次当前时间,就被占领一个油田
}
if (cur_time < next_time) {
//如果当前工程师占领油田的时间点,小于可以训练下一名工程师的最小时间点
if(s < linf) s += oil_cnt * (next_time - cur_time) * p;
//当前的油田数乘p金额是一秒的;
//经历的时间是可以训练下一名工程师的最小时间点-当前工程师占领油田的时间点
cur_time = next_time;//更新当前时间点为可以训练下一名工程师的最小时间点
}
if (s < ec) {
//如果当前金额不足以训练一名工程师
while (!q.empty() && s + oil_cnt * (q.front() - cur_time) * p < ec) {
//如果当前工程师占领油田时,金额不足以训练下一名工程师
s += oil_cnt * (q.front() - cur_time) * p;//先把这段时间的钱加上
cur_time = q.front();//先更新当前时间
q.pop();//当前工程师占领油田的时间点已经用过了,因此弹出队列
oil_cnt++;//占领的油田数加1
}
//金额不足,需要等待
ll wait_time = (ec - s + (ll)oil_cnt * p - 1) / (oil_cnt * p);
//需要等待wait_time的时间
//还需要金额ec-s,
s += oil_cnt * wait_time * p;//加上等待时间内生产的钱
cur_time += wait_time;//更新当前时间
}
s -= ec;//更新当前金额
q.push(cur_time + et + t[i]);//工程师占领油田的时间点等于
//当前时间点+et秒训练时间+工程师去油田的时间
//注意,每次循环结束后才加入队列
//因此最后一次循环的cur_time+et+t[n]就是答案
}
printf("%lld\n", cur_time + et + t[n]);
}
int main()
{
work();
return 0;
}
E 排队
就是分三类,正序数,负序数,零序数。有对称性知正序数和负序数数量相同,那么求出全排列,再减去零序数的数量,最后除2就是逆序数的数量。
零序数的数量用排列组合求。
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
const int N=1e5+10;
const ll mod=1e9+7;
ll cnt[N],a[N],j[N];
ll js(ll x){
if(x!=0){
return x*(x-1)/2;
}
return 0;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
ll n;cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
cnt[a[i]]++;
}
ll sum=n*(n-1)/2;
for(int i=0;i<N;i++)
{
sum-=js(cnt[i]);
}
sum%=mod;
for(int i=3;i<=n;i++)
{
sum*=i;
sum%=mod;
}
if(n==1)sum/=2;
cout<<sum<<endl;
return 0;
}
F 选座椅
由于区间连续,所以用双指针控制区间长度,不断尺取。如果长度区间[L,R]满足m个条件,则长度区间[L,R+1]也满足,以此类推。那么就可以看出可以用差分来对一段连续区间进行加操作。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=1e5+5;
const int mod=1e9+7;
int n,m;
vector<int> v[maxn];
int cnt[maxn],deta[maxn],fac[maxn];
void add(int id, int &now){
for(auto u:v[id]){
cnt[u]++;
if(cnt[u]==1)now++;
}
}
void del(int id, int &now){
for(auto u:v[id]){
cnt[u]--;
if(cnt[u]==0)now--;
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=3;i++){
for(int j=1;j<=m;j++){
int u;
cin>>u;
v[u].push_back(j);
}
}
int now=0;
memset(deta,0,sizeof(deta));
for(int i=1,j=1;i<=n;i++){
while(j<=n&&now<m)add(j,now),j++;
//j多加1是为了下一次add
if(now==m){
int L=(j-1)-i+1;
int R=n-i+1;
deta[L]++;
deta[R+1]--;
}
del(i,now);
if(i==j)j++;
}
fac[0]=fac[1]=1;
for(int i=2;i<=n;i++){
fac[i]=fac[i-1]*i%mod;
}
for(int i=1;i<=n;i++){
deta[i]=deta[i]+deta[i-1]%mod;
cout<<deta[i]*fac[i]%mod<<" ";
}
return 0;
}
小结
这套题难度比牛客小白月赛1难了不少,很多结论如果不熟练那就直接无了。但是多学多练,总有一天我终将成为你。