开学欢乐赛 & 蒟蒻 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 中取得好成绩!我们考场见!

posted @ 2025-04-25 19:05  蒟蒻OIer  阅读(35)  评论(0)    收藏  举报