CF 新年赛 Goodbye 2025 题解

一、前言

时隔数月,我们终于有见面了!

2025 年接近尾声,这是我今年的最后一篇题解了。祝大家 Happy new year!

二、题解

第 A 题 Yes or Yes

观察发现,Y 的数量不会减少而 N 一定会被相邻的 Y 所删除,如 YNNY 可以变化为 YY。 删除后只会剩下所有的 Y,如果此时 Y 多于两个,则只能操作 YY,非法。如果只有一个 Y 就结束了,合法。特殊的,如果没有 Y 也是合法的。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long 
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int t; cin>>t;
	while (t--){
		string s; cin>>s;
		int cnt=0;
		for (auto x:s) cnt+=(x=='Y');
		cout<<(cnt<=1?"YES\n":"NO\n");
	}
} 

第 B 题 Impost or Sus

如果字符串两段出现 u,那么一定不是可疑的(最近的两个 s 一定是最靠边的两个不同的 s,距离一定不同)。

如果中间出现相邻的两个 u,假设它们是可疑的,不妨设 u 的位置是 i , i + 1 i,i+1 i,i+1,距离分别为 x , y x,y x,y,则 i − x i-x i−x, i + x i+x i+x, i − y + 1 i-y+1 i−y+1, i + y + 1 i+y+1 i+y+1。那么存在不等式 x ≤ y − 1 x\le y-1 x≤y−1(不然 x x x 不是最小的)和 y ≤ x − 1 y\le x-1 y≤x−1(不然 y y y 不是最小的),那么容易发现 y + 1 ≤ x ≤ y − 1 y+1\le x\le y-1 y+1≤x≤y−1,矛盾。

如果没有相邻的 u 且两段都不是 u,则每个 u 的左右都是 s,显然是可疑的。

那么问题转化为如何用最少的操作使得不存在相邻的 u 且两段都不是 u

考虑 DP 求解,那么我们设 d p i , s / u dp_{i,s/u} dpi,s/u 为前 i i i 个以 s / u s/u s/u 结尾的最小操作。

那么我们只需要无脑合并即可,对于开头结尾的现在,开头加一个 u,那么答案是 d p n , s dp_{n,s} dpn,s。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long 
int dp[200010][2];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int t; cin>>t;
	while (t--){
		string s; cin>>s;
		s=" "+s; dp[0][0]=0; dp[0][1]=1000000000;
		for (int i=1; i<s.size(); i++){
			if (s[i]=='s') dp[i][0]=1000000000,dp[i][1]=min(dp[i-1][0],dp[i-1][1]);
			else dp[i][0]=dp[i-1][1],dp[i][1]=min(dp[i-1][0],dp[i-1][1])+1;
		}
		cout<<dp[s.size()-1][1]<<"\n";
	}
} 

第 C 题 First or Second

视角题,考虑枚举最后剩下的是第几个礼物。

那么我们发现这个礼物的后面只能使用 Second 操作。

对于前面的礼物,我们推测除了开头那个,都可以用任意操作(第一个显然只能 First)。

给出证明:

假设 F 为 First 操作,S 为 Second 操作。

那么操作串形如 FF...FSS...SFF...FSS...S...。那么对于 F F F 段,我们首先留下一个 F,其余操作掉,然后我们用这个 F 占位,操作掉下一个 S 段,然后把这个 F 操作掉即可。

对于答案,维护后缀和和前缀绝对值和即可。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long 
int sum[200010],sum2[200010],a[200010];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int t; cin>>t;
	while (t--){
		int n; cin>>n; sum2[n+1]=0;
		for (int i=1; i<=n; i++){
			cin>>a[i]; sum[i]=sum[i-1]+(i==1?a[i]:abs(a[i]));
		}
		int mx=sum[n-1];
		for (int i=n; i>=2; i--){
			sum2[i]=sum2[i+1]-a[i];
			mx=max(mx,sum2[i]+sum[i-2]);
		}
		cout<<mx<<"\n";
	}
} 

第 D 题 Xmas or Hysteria

吓人题目,差点绕晕了,其实很简单。

考虑分类讨论。

Case 1:要求剩下一些精灵。

那么将精灵攻击力排序,那么如果小的精灵攻击大的精灵(或反过来),小的精灵都会死。

那么我们考虑让最大的一些精灵活下来(或许者更容易)。那么首先它们之间不能自相残杀(攻击一定会有死亡),那么先让它们各自杀一只比自己弱的精灵,那么这一步的限制是 2 m ≤ n 2m\le n 2m≤n。

对于剩余的精灵,我们希望它们都死掉。考虑在上面的操作前让精灵从弱到强依次送死(小的攻击大的),最后前面的精灵都死了,构造完成。

Case 2:不要求剩下一些精灵。

那么我们考虑仍然排序并沿用上面送死的思路,考虑找出最大的 x x x,使得 a x + a x + 1 + . . . + a n − 1 ≥ a n a_x+a_{x+1}+...+a_{n-1}\ge a_n ax+ax+1+...+an−1≥an(这一步的条件是 ∑ a ≥ 2 a n \sum a\ge 2a_n ∑a≥2an)

那么我们将前面的精灵全部送死掉,最后让这些精灵同时向最大的精灵攻击,所有的精灵全部死亡。

注意编号发生了改变(排序),所以要记录一下编号。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long 
pair <int,int> pr[200010];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int t; cin>>t;
	while (t--){
		int n,m; cin>>n>>m;
		for (int i=1; i<=n; i++){
			cin>>pr[i].first; pr[i].second=i;
		}
		sort(pr+1,pr+n+1);
		vector <pair<int,int>> vc;
		if (m!=0){
			if (m*2>n){
				cout<<"-1\n"; continue;
			}
			for (int i=1; i<=n-2*m; i++){
				vc.push_back({pr[i].second,pr[i+1].second});
			}
			for (int i=n-2*m+1; i<=n-m; i++){
				vc.push_back({pr[i+m].second,pr[i].second});
			}
			cout<<vc.size()<<"\n";
			for (auto x:vc) cout<<x.first<<" "<<x.second<<"\n";
		}
		else{
			int sum=0;
			for (int i=1; i<n; i++) sum+=pr[i].first;
			if (sum<pr[n].first){
				cout<<"-1\n"; continue;
			}
			sum=0;
			for (int i=n-1; i>=1; i--){
				sum+=pr[i].first;
				if (sum>=pr[n].first){
				    for (int j=1; j<i; j++) vc.push_back({pr[j].second,pr[j+1].second});
				    for (int j=i; j<n; j++) vc.push_back({pr[j].second,pr[n].second});
				    break;
				}
			}
			cout<<vc.size()<<"\n";
			for (auto x:vc) cout<<x.first<<" "<<x.second<<"\n";
			
		}
	}
} 

第 E 题 Flatten or Concatenate

普通的交互题,思路还算简单。

考虑复制会给数组 a a a 的元素和 × 2 \times 2 ×2,但是拆解不会改变,那么对于我们拿到的那个 a a a,如果可以将这个 a a a 分成前后缀,使得它们和相等,那么我们认为 a a a 经历了一次连接操作。那么左右两边就是操作前的 a a a 和 b b b。

考虑这两个 a , b a,b a,b,如果可以平分,那么我们可以视为 a ′ , b ′ a',b' a′,b′ 拼接成了 a , b a,b a,b,然后 a , b a,b a,b 经历了拆解操作变成了如今的 a , b a,b a,b。

那么我们发现, a , b a,b a,b 的长度决定了它们的最大值,如果长度长,那么分解次数多,最大值较小。那么我们去长度长的一半递归计算。

最后无法分解时,我们只需要考虑它是由它的和这一个数拆解而成的,那么我们只需要模拟维护这个拆解过程即可。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long 
pair <int,int> pr[200010];
int ask(int l,int r){
	cout<<"? "<<l<<" "<<r<<endl;
	int x; cin>>x; return x;
}
int search(int l,int r,int sum){
	int s=l;
	while (l<r){
		int mid=(l+r>>1);
		if (ask(s,mid)>=sum) r=mid;
		else l=mid+1;
	}
	if (ask(s,l)==sum) return l;
	return -1;
}
int solve(int l,int r,int sum){
	int id=search(l,r,sum/2);
	if (id!=-1){
		if (id-l+1<r-id) return solve(l,id,sum/2);
		else return solve(id+1,r,sum/2);
	}
	else{
		int len=r-l+1;
		while (len) len/=2,sum/=2;
		return (sum==0?1:sum*2);
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int t; cin>>t;
	while (t--){
		int n; cin>>n;
		int sum=ask(1,n);
		int ans=solve(1,n,sum);
		cout<<"! "<<ans<<endl;
		
	}
} 

第 F 题 Conquer or of Forest

吓人题。

考虑我们把所有偶子树都拆开,这时可行的,因为拆下来偶子树不会改变任意子树的奇偶性。

我们接下来考虑拼接这些联通块,那么每个联通块是一颗只有根是偶子树,其他都是奇子树的树。画图分析可得:无论如何选取新根,上述条件依然成立。

那么我们观察题目里面的要求,其实就是把联通块串联起来。假设根所在的联通块的大小为 a 0 a_0 a0,其余为 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an。那么我们枚举一下那个连通块结尾。下面推导:

a n s = ∑ i = 1 n ( k − 1 ) ! × ∏ j = 0 n − 1 a p j × a p j + 1 ans=\sum_{i=1}^{n}(k-1)!\times\prod_{j=0}^{n-1}a_{p_j}\times a_{p_{j+1}} ans=i=1∑n(k−1)!×j=0∏n−1apj×apj+1
a n s = ∑ i = 1 n ( k − 1 ) ! × a 0 × ∏ j = 1 n a j 2 × 1 a i ans=\sum_{i=1}^{n}(k-1)!\times a_0\times \prod_{j=1}^{n} a_j^2\times \frac{1}{a_i} ans=i=1∑n(k−1)!×a0×j=1∏naj2×ai1

直接计算即可。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define M 998244353
vector <int> vc[300010],gro;
int sz[300010];
inline void dfs_sz(int id,int f){
	for (auto x:vc[id]){
		if (x==f) continue;
		dfs_sz(x,id); sz[id]+=sz[x]; 
	}
	sz[id]++;
}
inline int split(int id,int f){
	int ans=1;
	for (auto x:vc[id]){
		if (x==f) continue;
		if (sz[x]%2==0) gro.push_back(split(x,id));
		else ans+=split(x,id); 
	}
	return ans;
}
inline int Pow(int a,int n){
	int ans=1;
	while (n){
		if (n&1) ans=ans*a%M;
		a=a*a%M; n>>=1;
	}
	return ans;
}
signed main(){
	int t; cin>>t;
	while (t--){
		int n; cin>>n;
		gro.clear();
		for (int i=1; i<=n; i++){
			vc[i].clear(); sz[i]=0;
		}
		for (int i=1; i<n; i++){
			int u,v; cin>>u>>v;
			vc[u].push_back(v);
			vc[v].push_back(u);
		}
		dfs_sz(1,0); int ans=split(1,0);
		int len=gro.size(),tmp=0;
		if (len==0){cout<<"1\n"; continue ;}
		for (int i=1; i<len; i++) ans=ans*i%M;
		for (auto x:gro) ans=ans*x%M,ans=ans*x%M;
		for (auto x:gro) tmp=(tmp+Pow(x,M-2))%M;
		cout<<ans*tmp%M<<"\n";
	}
} 

第 G 题 deCH OR Dations

喵喵 Trick 大杂烩。

我们发现如果处理所有边所在链的数量,那么我们必须动态更新这个数量,不优秀。

那么我们考虑随机化一下,给每个边随机一个权值,那么所有链的边权值异或和的异或和为 0 0 0 就是合法的条件。

那么我们考虑使用弦的树状数组维护法,那么我们只需要将 l , r l,r l,r 和后缀异或,然后查询 l , r l,r l,r 之间的异或和即可。

对于这个权值异或和,我们把每个点结尾的链数量奇偶性和权值异或和求出即可。链奇偶性为前缀链奇偶性的异或和再异或 1 1 1,异或和为前缀链异或和,然后如果有奇数个结尾,那么再次异或一个 v a l val val(当前弦权值)即可。

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define M 998244353
#define N 1000010
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
struct tree{
	int C[N],n;
	void update(int id,int x){
		while (id<=n) C[id]^=x,id+=(id&-id);
	}
	int query(int id){
		int ans=0;
		while (id>0) ans^=C[id],id-=(id&-id);
		return ans;
	}
	void reset(int len){
		n=2*len;
		for (int i=1; i<=n; i++) C[i]=0;
	}
}tr1,tr2;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int t; cin>>t;
	while (t--){
		int n,tot=0; cin>>n;
		tr1.reset(n); tr2.reset(n);
		for (int i=1; i<=n; i++){
			int l,r; cin>>l>>r;
			int val=rnd();
			int x=tr1.query(l)^tr1.query(r);
			int y=tr2.query(l)^tr2.query(r);
			int nx=x^1,ny=y^((nx&1)*val);
			tr1.update(l,nx); tr1.update(r,nx);
			tr2.update(l,ny); tr2.update(r,ny);
			tot^=ny; cout<<(tot?0:1);
		}
		cout<<"\n";
	}
} 

三、后记

2025 年结束了。

无论在你过去的一年中是否生活的顺利,学习的顺利,这些往事都称为了过去。新的一年,有的是机会,有的是希望。

祝大家心有所悦、学有所成,万事皆可期!新年快乐!

每一次哭又笑着奔跑

一边失去一边在寻找

明天你好声音多渺小

却提醒我勇敢是什么

.------《明天你好》

相关推荐
闻缺陷则喜何志丹2 小时前
【二分查找】P10091 [ROIR 2022 Day 2] 分数排序|普及+
c++·算法·二分查找
only-qi2 小时前
leetcode2. 两数相加
算法·leetcode
鲨莎分不晴2 小时前
拯救暗淡图像:深度解析直方图均衡化(原理、公式与计算)
人工智能·算法·机器学习
DuHz3 小时前
242-267 GHz双基地超外差雷达系统:面向精密太赫兹传感与成像的65nm CMOS实现——论文阅读
论文阅读·物联网·算法·信息与通信·毫米波雷达
AI科技星3 小时前
时空的固有脉动:波动方程 ∇²L = (1/c²) ∂²L/∂t² 的第一性原理推导、诠释与验证
数据结构·人工智能·算法·机器学习·重构
2401_841495644 小时前
【LeetCode刷题】寻找重复数
数据结构·python·算法·leetcode·链表·数组·重复数
罗技1234 小时前
Easysearch 集群监控实战(下):线程池、索引、查询、段合并性能指标详解
前端·javascript·算法
一路往蓝-Anbo4 小时前
C语言从句柄到对象 (七) —— 给对象加把锁:RTOS 环境下的并发安全
java·c语言·开发语言·stm32·单片机·嵌入式硬件·算法
中國龍在廣州4 小时前
谈谈2025年人工智能现状及发展趋势分析
人工智能·深度学习·算法·自然语言处理·chatgpt·机器人·机器人学习