前面的碎碎念:
自考研复试之后,一年来没打过这种限时比赛了。这次疫情呆在家没事干,便想着报名来玩一玩。虽然心里已经做好准备,知道自己太久不打比赛可能会有所拉跨,不过结束后发现有几道题目其实在自己能力范围,属于应该要做出来的,这样还是比较令人难受的。
小白月赛时间是7点-10点,由于7-8点这段时间吃饭+有事,实际上我8点才开始做题,不过感觉时间就算再多,该卡住的题目还是很难能花时间想通的。题目整体难度官方解释是DIV2 A-C,大部分题目不是很难,个人觉得比赛时应该要做出7题,最终只做出了5题,还是太菜。
题解部分:
A-膜法记录
比赛时先看的第一题,脑子里冒出来的第一个想法就是枚举攻击哪些行,然后统计还有多少列需要攻击,需要攻击的列数能小于等于b就行了,但是这样做的时间复杂度是O(m2^n),肯定会超时。赛后发现,其实很多人都是写的这种做法然后莽过去了,出题人造数据不严谨啊...
咨询了大佬之后了解了正确做法:先进行预处理,把每一列变成一个01序列,其二进制形式对应着一个数字,用一个数组num[]统计这些数字个数,这部分时间复杂度为O(nm)。之后开始枚举每行,对于每行,枚举0-2^n-1的01序列,对每个01序列表示的集合,其个数要向其增集转移,这样就直接统计出了0-2^n-1种行攻击方式下,对应会消灭多少列。这部分有点类似于状压DP,时间复杂度为O(n2^n)。
之后再枚举0-2^n-1种行攻击方式,若满足行攻击次数小于等于a且m-对应会消灭多少列小于等于b,那么就可以全歼,否则不能全歼。
整体时间复杂度O(nm+n2^n)。
A题代码:
#include<bits/stdc++.h> using namespace std; int num[2000005]; char R[25][100005]; int main() { int i,j,k,n,m,a,b,T; scanf("%d",&T); while(T--) { scanf("%d%d%d%d",&n,&m,&a,&b); for(i=0;i<(1<<n);i++)num[i]=0; for(i=0;i<n;i++)scanf("%s",R[i]); for(j=0;j<m;j++) { for(k=i=0;i<n;i++)if(R[i][j]=='*')k|=(1<<i); num[k]++; } for(i=0;i<n;i++) for(j=0;j<(1<<n);j++)if((j&(1<<i))==0)num[j|(1<<i)]+=num[j]; for(i=0;i<(1<<n);i++)if(__builtin_popcount(i)<=a&&m-num[i]<=b)break; if(i<(1<<n))printf("yes\n"); else printf("no\n"); } return 0; }
B-阶乘
对于一个数P,我们可以把他分成若干个质数之积,也就是这种形式:P=p1^k1p2^k2...pn^kn,其中p1,p2,...,pn为互不相同的质数。那么我们对每个质数pi,找到能凑到指数ki的最小数字,比如pi=2,ki=2,那么2含有1个2因子,4含有2个2因子,加起来为3>=2,所以凑成2^2至少要4!。我们对每个质数都进行这样的一次寻找,ans保存最大的那个就行了。
比赛期间我一时没想清暴力寻找的复杂度其实最多也就log(P),想二分快速找到各质数对应需要的最小数字,但是一直没想出如何O(1)计算n!含有多少个质因子pi的方法,所以被这题坑了大概40多分钟。以后还是不能太谨慎,大力出奇迹...
时间复杂度O(Tlog(P))
B题代码:
#include<bits/stdc++.h> using namespace std; unordered_map<int,int>V; int main() { long long i,j,k,s,ans,T,p; scanf("%lld",&T); while(T--) { scanf("%lld",&p),j=p,ans=0; for(i=2;i*i<=p;i++) { if(p%i)continue; for(s=0;p!=1&&p%i==0;p/=i)s++; V.clear(); for(j=1,k=0;k<s;j++)V[j*i]=V[j]+1,k+=V[j*i]; ans=max(ans,(j-1)*i); } ans=max(ans,p); printf("%lld\n",ans); } return 0; }
C-完全图
首先,一个完全图本身就是一个联通分量。若我们把他变成两个连通分量,最少要把某点和其他点的所有边都去除掉,也就是要去掉n-1条边。若我们把他变成三个连通分量,在剩下n-1个结点的连通分量上,最少要把某点和其他点的所有边都去除掉,也就是要去掉n-2条边,以此类推...
我们发现规律后就很简单了,显然m越大,最后的答案也会越大,其具有单调性可以二分。我们设答案-1为ans,那么ans肯定要满足(n-1)+(n-2)+...+(n-ans)<=m,左边可以直接高斯求和,那么二分找到最大的ans即可。注意求和时可能炸long long,因此我们要使用__int128,比赛中此题通过人数不是很多,估计就是被高精度坑了。
时间复杂度O(log(n))
C题代码:
#include<bits/stdc++.h> using namespace std; int main() { long long ans,T,n,m; __int128 l,r,mid,t; scanf("%lld",&T); while(T--) { scanf("%lld%lld",&n,&m); for(l=0,r=n-1;l<=r;) { mid=(l+r)>>1,t=(n-1+n-mid)*mid/2; if(t<=m)ans=mid,l=mid+1; else r=mid-1; } printf("%lld\n",ans+1); } return 0; }
D-病毒传染
QAQ太菜了不会。
E-A+B问题
签到题,答案恒等于4294967296,也就是2^32,即int位数。
时间复杂度O(1)。
E题代码:
#include <bits/stdc++.h> using namespace std; int main() { int x; scanf("%d",&x); printf("4294967296\n"); return 0; }
F-美丽的序列I
比赛时看了一眼题没啥思路,遂放弃,以后有时间补上。
G-树上求和
最后10分钟看的题,一时没想出来,着实思想江化。
此题需要从算贡献的角度思考,对于每条边对答案的贡献,等于其左边子树结点个数右边子树结点个数边长。因此我们先随便从一点DFS,预处理出每个子树的结点个数。再遍历所有的边,把左边子树结点个数右边子树结点个数的积保存下来,从大到小排序,然后从前往后遍历一遍分别乘上1/2/3/.../n-1,加起来即可。
时间复杂度O(nlog(n))。
G题代码:
#include<bits/stdc++.h> using namespace std; struct node { int x,y; }E[100005]; bool cmp(long long a,long long b) { return a>b; } int n,S[100005]={0}; long long a,b,ans=0,B[100005]; vector<int>R[100005]; void DFS(int x,int fa) { int i,j; S[x]=1; for(i=0;i<R[x].size();i++) { j=R[x][i]; if(j!=fa)DFS(j,x),S[x]+=S[j]; } } int main() { int i,j; scanf("%d",&n); for(i=0;i<n-1;i++) { scanf("%d%d",&E[i].x,&E[i].y); R[E[i].x].push_back(E[i].y),R[E[i].y].push_back(E[i].x); } DFS(1,0); for(i=0;i<n-1;i++)a=min(S[E[i].x],S[E[i].y]),b=n-a,B[i]=a*b; sort(B,B+n-1,cmp); for(i=0;i<n-1;i++)ans+=(i+1)*B[i]; printf("%lld\n",ans); return 0; }
H-奇怪的背包问题增加了
首先我们要知道2^i+2^i=2^(i+1),想到了这点,那么本题基本上就能A掉了。
我们用V1和V2数组保存n个ki中,0-29分别有多少个。那么先从0遍历到29,每次把V1[i]/2加到V[i+1]上,因为根据上面的公式,两个V1[i]可以变成一个V1[i+1]。遍历完后检查V1[30]是否大于等于1,若是表示有解,否则无解。
若有解,我们反着利用V2数组从29遍历到0,用一个变量j表示当前数字期望需要多少个,一个need数组保存每个数字实际需要用到多少个。若期望个数大于实际能提供的个数,令j减去实际个数,j扩大两倍后继续遍历重复此过程,直到期望个数小于等于实际能提供的个数。最后我们根据need数组,遍历一遍k即可。
时间复杂度O(Tm)。
H题代码:
#include<bits/stdc++.h> using namespace std; int a[100005],V1[35],V2[35],need[35]; int main() { int T,i,j,n,k; scanf("%d",&T); while(T--) { scanf("%d",&n); memset(V1,0,sizeof(V1)),memset(V2,0,sizeof(V2)),memset(need,0,sizeof(need)); for(i=1;i<=n;i++)scanf("%d",&a[i]),V1[a[i]]++,V2[a[i]]++; for(i=0;i<30;i++)V1[i+1]+=V1[i]/2; if(!V1[30]){printf("impossible\n");continue;} for(j=2,i=29;i>=0;i--,j<<=1) { if(V2[i]>=j) { need[i]=j; break; } else need[i]=V2[i],j-=V2[i]; } for(i=1;i<=n;i++) { if(need[a[i]])need[a[i]]--,printf("1"); else printf("0"); } printf("\n"); } return 0; }
I-寻找子串
比赛时没看数据范围直接写了个O(n^3)算法居然过了,后面补上了正确做法2333
字典序最大的子串,一定是原串的后缀,所以我们枚举所有后缀串即可。
时间复杂度O(n^2)。
I题代码:
#include<bits/stdc++.h> using namespace std; int main() { int i,j,L; char R[1005],T[1005],ans[1005]; scanf("%s",R); ans[0]='a',ans[1]='\0'; for(i=0;R[i]!='\0';i++) { for(L=0,j=i;R[j]!='\0';j++)T[L++]=R[j]; T[L]='\0'; if(strcmp(T,ans)>0)strcpy(ans,T); } printf("%s\n",ans); return 0; }
J-最大的差
签到题,序列中的最大值-最小值就行了。
时间复杂度O(n)。
J题代码:
#include <bits/stdc++.h> using namespace std; int main() { int n,x,Max=-1e9,Min=1e9; scanf("%d",&n); while(n--) { scanf("%d",&x); Max=max(Max,x); Min=min(Min,x); } printf("%d\n",Max-Min); return 0; }
本人第一次写博客,写的比较啰嗦,大佬们轻喷啊,完结撒花~