一、前言
时隔数月,我们终于有见面了!
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 年结束了。
无论在你过去的一年中是否生活的顺利,学习的顺利,这些往事都称为了过去。新的一年,有的是机会,有的是希望。
祝大家心有所悦、学有所成,万事皆可期!新年快乐!
每一次哭又笑着奔跑
一边失去一边在寻找
明天你好声音多渺小
却提醒我勇敢是什么
.------《明天你好》