开学欢乐赛 & 蒟蒻 OIerのCSP - J 膜你赛 2024 官方题解
题解
#1. 纳西妲
我们注意到这个题和平时 OI 中对中位数的定义不太一样。这个题中偶数个数的中位数是排序后中间两个里较大的那一个。然后我们就发现一个长度为 \(2\) 的区间操作后较小的值会变成较大的值。由于可以无限次操作,我们可以将整个序列都变成其最大值,这样答案就是整个序列的最大值。
ll n,a,mx;
int main(){
ios::sync_with_stdio(0);
cin>>n;
while(n--){
cin>>a;
mx=max(mx,a);
}
cout<<mx;
return 0;
}
#2. 琳妮特
考虑 \(k=10\) 的情况。
一般来说小学应该会讲到如何判定一个数能否被 \(9\) 整除:将各数位上的数加起来看这个和能不能被 \(9\) 整除。
注意到数的位数非常大(就像在题面中说的一样)所以盲猜一把和数的位置没啥关系,联系刚才 \(k=10\) 的结论,我们大胆猜测直接把 \(k\) 进制下各数位加起来看能不能被 \((k-1)\) 整除就行。证明:
- 由于 \(k\equiv 1\pmod {(k-1)}\),\(k^2\equiv k(k-1)+k\equiv k \equiv 1\pmod{(k-1)}\),所以对于任意的 \(n \in \mathbb{N}\) 我们有 \(k^n\equiv\pmod{(k-1)}\)。
- 因此,如果第 \(p\in\mathbb{N^+}\) 位上是 \(a\),那么该位对整个数模 \((k-1)\) 的值的贡献就是 \(ak^{p-1}\equiv a\pmod{(k-1)}\)。
- 于是刚才的结论成立。
ll t,n,s,a,u,v,w,k;
int main(){
cin>>t;
while(t--){
cin>>n>>k;
s=0;
while(n--){
cin>>a>>u>>v>>w;
s+=a;
s%=k-1;
}
if(s)cout<<"No";
else cout<<"Yes";
cout<<endl;
}
return 0;
}
#3. 阿贝多
大模拟不讲。
注意由于修改次数太多,我们需要给所有的化学式开 set 然后二分查找。计算化学式量时对于括号可以直接递归。其他问题请参考代码。
struct node{
//化学式、化学式量
string s;
ll m;
};
bool operator<(node x,node y){
return x.s<y.s;
}
string st[]={"H","He","Li","Be","B","C","N","O","F","Ne","Na","Mg","Al","Si","P","S","Cl","Ar","K","Ca","Sc","Ti","V","Cr","Mn","Fe","Co","Ni","Cu","Zn","Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y","Zr","Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn","Sb","Te","I","Xe","Cs","Ba","La","Ce","Pr","Nd","Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb","Lu","Hf","Ta","W","Re","Os","Ir","Pt","Au","Hg","Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th","Pa","U","Np","Pu","Am","Cm","Bk","Cf","Es","Fm","Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds","Rg","Cn","Uut","Fl","Uup","Lv","Uus","Uuo"};
ll m[]={10,40,69,90,108,120,140,160,190,202,230,243,270,281,310,321,355,400,391,401,450,479,509,520,549,559,589,587,636,654,697,726,749,790,799,838,855,876,889,912,929,960,980,1011,1029,1064,1079,1124,1148,1187,1218,1276,1269,1313,1329,1373,1389,1401,1409,1442,1450,1504,1520,1573,1589,1625,1649,1673,1689,1730,1750,1785,1809,1838,1862,1902,1922,1951,1970,2006,2044,2072,2090,2090,2100,2220,2230,2260,2270,2320,2310,2380,2370,2440,2430,2470,2470,2510,2520,2570,2580,2590,2620,2650,2680,2710,2720,2700,2760,2810,2800,2850,2840,2890,2880,2930,2920,2940};
ll op,p,pp,b[200];
set<node> a;
node TMP[10010];
ll calc(string s,ll l,ll r){//计算化学式量
if(r<l)return 0;
ll sum=0;
for(int i=l;i<=r;i++){
if(s[i]=='('){//有括号就递归
ll tmp=i+1,cnt=1;
while(tmp<=r&&cnt>0){//找到括号右端点
if(s[tmp]=='(')cnt++;
if(s[tmp]==')')cnt--;
tmp++;
}
if(cnt!=0){
return -1;
}
ll ttmp=calc(s,i+1,tmp-2);
if(ttmp==-1){
return -1;
}
while(s[tmp]>='0'&&s[tmp]<='9'){//处理下标
cnt*=10;
cnt+=s[tmp]-'0';
tmp++;
}
sum+=ttmp*max(cnt,1ll);
i=tmp-1;
continue;
}
if(s[i]>='A'&&s[i]<='Z'){//判非大写开头
ll tmp=i+1,cnt=0;
string ttmp="";
ttmp+=s[i];
while(tmp<=r&&s[tmp]>='a'&&s[tmp]<='z'){//根据大小写找到完整的元素符号
ttmp+=s[tmp];
tmp++;
}
ll flg=0;
for(int j=0;j<118;j++){//暴力判断该符号是否存在
if(ttmp==st[j]){
flg=j+1;
break;
}
}
if(!flg){
return -1;
}
flg--;
while(s[tmp]>='0'&&s[tmp]<='9'){//处理下标
cnt*=10;
cnt+=s[tmp]-'0';
tmp++;
}
sum+=m[flg]*max(cnt,1ll);
i=tmp-1;
}
else{
return -1;
}
}
return sum;
}
bool check(string s,string t){//暴力查找 s 中是否含有元素 t
for(int i=0;i<=s.length()-t.length();i++){
if(s[i]!=t[0])continue;
bool flg=1;
for(int j=0;j<t.length();i++,j++){
if(s[i]!=t[j]){
flg=0;
break;
}
}
if(flg&&(s[i]<'a'||s[i]>'z'))return 1;
i--;
}
return 0;
}
void add(string s,ll op){//加入统计信息
bool flg[120]={};
for(int i=0;i<s.length();i++){
if(s[i]<'A'||s[i]>'Z')continue;
string ss="";
ss+=s[i++];
while(s[i]>='a'&&s[i]<='z')ss+=s[i++];
for(int j=0;j<118;j++){
if(st[j]==ss){
if(!flg[j]){
flg[j]=1;
b[j]+=op;
}
break;
}
}
i--;
}
}
void query(){
ll mm;
pp=0;
string s[20];
cin>>mm;
for(int i=0;i<mm;i++){//判断元素是否存在
cin>>s[i];
bool flg=0;
for(int j=0;j<118;j++){
if(st[j]==s[i]){
flg=1;break;
}
}
if(!flg){
cout<<"Invalid.\n";
pp=-1;
return ;
}
}
for(set<node>::iterator i=a.begin();i!=a.end();i++){//遍历 set 中的每个位置
bool flg=0;
for(int j=0;j<mm;j++){
if(!check((*i).s,s[j])){
flg=1;
continue;
}
}
if(!flg){
TMP[pp++]=(*i);
}
}
cout<<pp<<" formula(s) are founded.\n";
pp=min(50ll,pp);
for(int i=0;i<pp;i++){
cout<<i+1<<" - "<<TMP[i].s<<'('<<TMP[i].m/10.0<<')'<<endl;
}
}
int main(){
while(1){
cin>>op;
if(op==0)return 0;//退出
if(op==1){//插入
string s;
cin>>s;
bool flg=0;
for(set<node>::iterator i=a.begin();i!=a.end();i++){//判重
if((*i).s==s){
flg=1;
break;
}
}
if(flg){
cout<<"Repeat.\n";
continue;
}
ll tmp=calc(s,0,s.length()-1);//计算与判定合法
if(tmp==-1){
cout<<"Invalid.\n";
continue;
}
a.insert({s,tmp});
add(s,1);
cout<<'='<<tmp/10.0<<".\n";
continue;
}
if(op==2){//删除
cin>>op;
if(op){//调用查找模块
query();
if(pp==-1)continue;
ll tmp;
cout<<"Type a number:";
cin>>tmp;
if(tmp>0&&tmp<=pp){
add(TMP[tmp-1].s,-1);
a.erase(a.lower_bound(TMP[tmp-1]));
cout<<"Successfully.\n";
}
else cout<<"Invalid.\n";
continue;
}
string s;
cin>>s;
set<node>::iterator tmp=a.lower_bound({s,0});//在 set 中找到字符串
if(tmp==a.end()||(*tmp).s!=s)cout<<"Not found.\n";
else{
add((*tmp).s,-1);
a.erase(tmp);
cout<<"Successfully.\n";
}
continue;
}
if(op==3){//查找
query();
continue;
}
if(op==4){//统计
cout<<"In general:"<<a.size()<<endl;
for(int i=0;i<118;i++){
if(b[i]){
cout<<st[i]<<':'<<b[i]<<endl;
}
}
continue;
}
}
return 0;
}
#4. 温 迪
本题最大的难度是发现它是图论。
我们看到每个限制条件是两首歌在相邻的节目,这种条件很大可能是建边来转化成图论问题。于是我们给每一个条件建双向边。
然后先判定是否有解。注意到有边相连的节点由于是相邻的节目,所以场次奇偶性一定不同,然后进行黑白染色看能否成功即可。
接着求最多的节目数量。由于路径上相邻两点的节目编号之差的绝对值为 \(1\),所以我们只需要找到一对点,使其间最短路径最长,这个路径所包含的点数就是答案。
另外原题保证了图连通,而本题并没有保证。注意到不同连通块之间完全不影响,我们将各连通块分别计算然后把答案加起来就行了。如果直接 copy 原题代码,只能得到至多 \(20\) 分,因为我往每个包(除了连通的包)里都塞了一组 hack。
ll n,vis[2010],v[2010],dis[2010],ans,flg,mx;
ll u;
vector<ll> a[2010];
void work(ll now){
mx=0;//当前连通块内的答案
memset(v,0,sizeof(v));//记录本连通块内的点集
vis[now]=1;//1/2染色方案 0表示没染
v[now]=1;
queue<ll> q;
q.push(now);
while(!q.empty()){//BFS 进行染色
ll tmp=q.front();
q.pop();
for(int i=0;i<a[tmp].size();i++){
if(!vis[a[tmp][i]]){
vis[a[tmp][i]]=3-vis[tmp];
v[a[tmp][i]]=1;
q.push(a[tmp][i]);
}
else if(vis[a[tmp][i]]==vis[tmp]){
flg=1;
return ;
}
}
}
for(int i=1;i<=n;i++){//枚举每个点
if(!v[i])continue;
memset(dis,0,sizeof(dis));
dis[i]=1;
q.push(i);
while(!q.empty()){//BFS 求到各点的距离
ll tmp=q.front();
q.pop();
for(int i=0;i<a[tmp].size();i++){
if(!dis[a[tmp][i]]){
dis[a[tmp][i]]=dis[tmp]+1;
q.push(a[tmp][i]);
}
}
}
for(int j=1;j<=n;j++){//求最大值
mx=max(mx,dis[j]);
}
}
ans+=mx;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
ll tmp;
cin>>tmp;
while(tmp--){
cin>>u;
a[i].push_back(u);
a[u].push_back(i);
}
}
for(int i=1;i<=n;i++){
if(!vis[i])work(i);
if(flg){
cout<<-1;
return 0;
}
}
cout<<ans;
return 0;
}
反思
- T1 需要你在审题时发现题目中中位数的定义与平时所看到的略有不同。和去年一样,提醒大家,只要看到异常的东西就应该引起注意。一些比较经典的:
- 某变量的范围小于 \(25\)。
- 序列每一项的值域不是 \(10^9,10^{18},2^{31}-1,2^{63}-1\) 中的某一个,且不是 \(10^9\) 或 \(10^{18}\) 除以长度。
- 操作次数等的范围在 \(10^8\) 以上。
- 数据范围中出现了一些奇怪的式子,例如P8814 [CSP-J 2022] 解密。
- 多种操作的题中对某类操作的次数做出单独限制,例如P7910 [CSP-J 2021] 插入排序。
- 一些东西的定义和常用的不一样。
- T2 需要你注意到 \(k=10\) 的部分分并进行思考。类似于数学压轴题,一道好的题目的部分分,尤其是特殊性质,通常能够将思路向正解引领。同时也提醒大家不要放弃思考部分分,万一写到一半发现可以推广成正解呢?
- T3 是大模拟题,考场上遇到此类题目建议先读题并看数据范围,找比较好写的部分分(一般会有十几二十几分),防止写不完。
- T4 主要是提醒大家不要想当然地以为一些东西。使用条件前先确定其是否存在。
- 最后提醒大家不要过于相信大样例,之前有人跑过了所有大样例但是最终 \(0\) 分。为此,本场的大样例都比较弱。
- T1 的大样例最大值超过了一半。
- T3 的大样例刻意放过了
2 0操作暴力查找的算法。 - T4 的样例中的图全部连通。有解的大样例是链。无解的大样例是长为奇数的环。但正式数据每个包里都塞了一组不连通的,其中连通的部分是链,不连通的部分是三元环。(采纳了 lhx 大佬的建议)
希望大家都能在 CSP J/S 2024 和 NOIP 2024 中取得好成绩!我们考场见!

浙公网安备 33010602011771号