总结
其实这场比赛也没啥好总结的,当天过生日,就没有打这场比赛,第二天刚好有空就找时间虚拟参与了一下。题目难度不大,都是代码量很小的思维题。做的时候还是遇到了很多卡住的问题。之后补题发现很多题自己的写法都不够优秀。这里写一篇题解记录一下这套题。如果有不对的地方,欢迎私信我来探讨。
A. Polycarp and Coins
这个A就是基本的签到,判断n%3的结果来确定是一样多还是1元多还是2元多。没有坑点
#include<bits/stdc++.h>
using namespace std;
int main()
{
int t,n,ans;
cin>>t;
while(t--)
{
int a=100;
cin>>n;
if(n%3==0)cout<<n/3<<" "<<n/3<<endl;
if(n%3==1)cout<<n/3+1<<" "<<n/3<<endl;
if(n%3==2)cout<<n/3<<" "<<n/3+1<<endl;
}
return 0;
}
B1,2 Wonderful Coloring -
B题两个都差不多,会了B2,B1就不用讲了,这里就说一下B2的做法。题目给一个数组和一些颜色,要求我们来染色,保证满足题目给的4个要求。那么我们只需要记录每个数的出现次数。超过k次就按k次算。剩下都都一起凑一凑。然后最后取个整就能得到可以染色的格数。具体染色方案的统计只需要按顺序填每种数字即可。用modk来维护当前的颜色。就能满足题目要求。这种构造问题只需要满足要求即可。不需要和它的答案一样。所以我们只需要保证满足条件就行。
#include<bits/stdc++.h>
using namespace std;
vector<int> cnt[200010];
int ans[200010];
int main()
{
int t,n,k ;
cin>>t;
while(t--)
{
cin>>n>>k;
for(int i=1,a;i<=n;i++)
{
scanf("%d",&a);
cnt[a].push_back(i);
}
int u=0;
for(int i=1;i<=n;i++)
{
u+=min(k,(int)cnt[i].size());
ans[i]=0;
}
u=u/k*k;
int color=1;
for(int i=1;i<=n;i++)
{
if(u==0)break;
for(int j=0;j<min(k,(int)cnt[i].size());j++)
{
if(u==0)break;
ans[cnt[i][j]]=color;//cnt里存储的数字i第j次出现的位置。
color=color%k+1;
u--;
}
}
for(int i=1;i<=n;i++)
cout<<ans[i]<<" ";
cout<<endl;
for(int i=1;i<=n;i++)
cnt[i].clear();
}
return 0;
}
C. Interesting Story
这道题刚看没啥好的思路。但是观察后发现我们可以吧问题转化一下。因为只有5个字母。我们可以分别枚举每个字母作为最多的字母的情况。然后对5种情况的结果取max。具体过程可以通过设计一个特征值函数。代表当前字符串的不等于某个字母的字符个数减除该字母字符个数的差值。然后拍个序。一直加,遇到第一个大于0的就说明不满足条件。复杂度只需要一个排序就够了。nlogn可以解决问题
#include<bits/stdc++.h>
using namespace std;
string s[200010];
int a[200010];
int get(string ss,char q)
{
int ans=0;
int len=ss.size();
for(int i=0;i<len;i++)
if(ss[i]!=q)ans++;
else ans--;
return ans;
}
int main()
{
int t,n;
cin>>t;
while(t--)
{
char oo[6];
oo[1]='a';oo[2]='b';oo[3]='c';oo[4]='d';oo[5]='e';
cin>>n;
int ans=0;
for(int i=1;i<=n;i++)
cin>>s[i];
for(int i=1;i<=5;i++)
{
for(int j=1;j<=n;j++)
{
a[j]=get(s[j],oo[i]);
}
sort(a+1,a+n+1);
int tmp=0;
for(int j=1;j<=n;j++)
{
tmp+=a[j];
if(tmp>=0){
ans=max(ans,j-1);
break;
}
}
if(tmp<0)ans=n;
}
cout<<ans<<endl;
}
return 0;
}
D1.Domino (easy version)
很经典的多米诺骨牌问题。给出n,m和要求水平放置的骨牌个数。我们首先很容易发现。如果n和m都是奇数。就不可能放满。所以说行和列最多只有一个奇数。我们先把可以构成2*2正方形的部分全部剥离出来,这部分内部可以2个横或2个竖。先不管。然后统计多出来的行可以放几个。判断下多出来的行可以放的骨牌个数和要求骨牌个数差值的奇偶性。和竖着放置的骨牌与应该竖着放置的骨牌个数差值的奇偶性。如果两部分均为偶数。说明一定可以放置。这里其实就是分了多出来的是行或列来进行判断。想清楚了代码很简单。
#include<bits/stdc++.h>
using namespace std;
int check(int x,int y)
{
return y>=x&&(y-x)%2==0;
}
int main()
{
int t,n,m,k;
cin>>t;
while(t--)
{
cin>>n>>m>>k;
int cnt=(n/2)*(m/2);
int h=0,v=0;
if(n%2==1)h=(m/2);
if(m%2==1)v=(n/2);
if(check(h,k)&&check(v,(n*m)/2-k))
cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
D2.Domino (hard version)
hard版本的题目前半部分一样。多了个要求是构造出这个骨牌的放置放置。要求字符不能引起歧义。我们可以有一个贪心的构造方式。按照d1的方式来确定骨牌的摆法。然后可以组成2*2的部分保证相邻横竖都按字典序来顺序构造。多出来的行或列放置在偶数部分的上面或左边。字符就用与他隔了2行或2列部分所用的字符。然后就构造完了。具体写法可以参考代码。
#include<bits/stdc++.h>
using namespace std;
int check(int x,int y)
{
return y>=x&&(y-x)%2==0;
}
char ans[500][500];
int main()
{
int t,n,m,k;
cin>>t;
while(t--)
{
memset(ans,0,sizeof(ans));
cin>>n>>m>>k;
int cnt=(n/2)*(m/2);
int h=0,v=0;
if(n%2==1)h=(m/2);
if(m%2==1)v=(n/2);
if(check(h,k)&&check(v,(n*m)/2-k))
{
cout<<"YES"<<endl;
int dx=0,dy=0;
if(n%2==1)
{
for(int i=0;i<m;i++)
ans[0][i]='a'+(i/2*2+2)%26;
dx=1;
}
if(m%2==1)
{
for(int i=0;i<n;i++)
ans[i][0]='a'+(i/2*2+2)%26;
dy=1;
}
int cnt=(k-h)/2;
for(int i=dx;i<n;i+=2)
for(int j=dy;j<m;j+=2){
int tmp=(i-dx+j-dy)%26;
char s1=tmp+'a',s2=tmp+1+'a';
if(cnt)
{
ans[i][j]=ans[i][j+1]=s1;
ans[i+1][j]=ans[i+1][j+1]=s2;
cnt--;
}
else
{
ans[i][j]=ans[i+1][j]=s1;
ans[i][j+1]=ans[i+1][j+1]=s2;
}
}
for(int i=0;i<n;i++)
cout<<ans[i]<<endl;
}
else cout<<"NO"<<endl;
}
return 0;
}
E. Fixed Points
这个e题要求删除a中的一部分数。满足删除后的数组中b[i]=i的个数为k个。问最少删几个可以达到。
其实这类型问题很容易想到动态规划。我们很难暴力枚举删除的过程。也没有什么好的贪心策略。所以很明显只能用dp来解决问题。那么我们先设计dp的状态转移方程。我们可以定义dp[i] [j]表示前i项中删除j项能满足b[i]=i的项的个数。
方程的转移也很简单dp[i] [j]可以从dp[i-1] [j-1]和dp[i-1] [j]来转移。如果不删除新的那么我们就要判断新加入的这个b[i]和(i-j)是否相等。这里的i-j代表的是新加入的数在b数组中位置。如果相等就+1;如果删除也就是从j-1转移。这一项已经删了。所以不会发送变化。
dp设计完了,然后从小到大枚举。遇到第一个大于k的dp[n] [i]直接输出即可
#include<bits/stdc++.h>
using namespace std;
int a[2010],dp[2010][2010];//dp代表前i个数删j个能满足的最大情况
int main()
{
int t,n,k;
cin>>t;
while(t--)
{
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
for(int j=0;j<=n;j++)
dp[i][j]=dp[i-1][j]+(a[i]==(i-j));//当前项不删的转移
for(int j=1;j<=n;j++)
dp[i][j]=max(dp[i][j],dp[i-1][j-1]);//删除当前项的转移
}
int ans=n+1;
for(int i=0;i<=n;i++)
if(dp[n][i]>=k)//判断至少删几个可以满足题目要求条件
{
ans=i;
break;
}
if(ans==n+1)cout<<-1<<endl;
else cout<<ans<<endl;
}
return 0;
}
F. Equidistant Vertices
这是一个树的题目问的是选k个点要求他们到根节点的距离都为一个相等的常数。问有多少种选法。要取模。
那么我们可以直接考虑如果k=2.那么任意两个点都行。这种情况直接特判。因为数据范围比较小。我们可以直接枚举所有点作为根节点的情况。然后统计出该节点作为根节点,实际上就是求距离根节点距离相等的以根节点LCA结点个数。然后枚举每个深度的点的个数。从中n个选择k个点。这个过程可以推式子用组合数来解决。不过这题数据范围不大。没必要推式子。直接放到dp里统计就行。这里的设计就是对根节点的直接相连的结点枚举每种深度的个数然后和dp[i-1] [j-1]相乘在加之前就选好的次数。直接统计出所有答案。不理解的可以看代码的注释。因为点不多。这里存图没写链式前向星,直接用邻接表存的。其实效率也差不多。这种题写邻接表好写很多。需要注意的是。这种方法其实多枚举了一层深度。我们其实可以直接先统计出所有大于u的深度个数。然后存起来,只枚举这一部分。大概能快10倍左右。不过实在是这题范围太小了。我这个枚举深度的代码跑了300ms。如果存起来的方法30ms就能跑完。有兴趣可以自己尝试写写看
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=105,mod=1e9+7;
ll ans,t,n,m,x,y,cnt,u[N][N],d[N],son[N],dp[N][N];
vector<int> G[N];
void dfs(int x,int fa,int col){
d[x]=d[fa]+1;
u[col][d[x]]++;
for(auto y:G[x])if(y!=fa)dfs(y,x,col);
}
int main(){
cin>>t;
while(t--){
cin>>n>>m,ans=0;
for(int i=1;i<=n;++i)G[i].clear();
for(int i=1;i<n;++i){
cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
if(m==2)//任意两点都满足,直接特判就行
{
cout<<(n*(n-1)/2)<<endl;
continue;
}
for(int i=1;i<=n;++i)//枚举根节点
{
memset(u,0,sizeof(u));
d[i]=cnt=0;
for(auto y:G[i])
{
dfs(y,i,y);
son[++cnt]=y;//记录当前根节点的子树
}
for(int D=1;D<=n;++D)//枚举深度
{
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int j=1;j<=cnt;++j){
dp[j][0]=dp[j-1][0];
for(int k=1;k<=j;++k)
dp[j][k]=(dp[j-1][k-1]*u[son[j]][D]%mod+dp[j-1][k])%mod;
}
(ans+=dp[cnt][m])%=mod;
}
}
cout<<ans<<endl;
}
return 0;
}