[题解]CSP-S 2025 T1~T3 题解
T1. P14361 [CSP-S 2025] 社团招新 / club
Tag:贪心、排序。
因为要求每个社团不超过 \(\dfrac{n}{2}\) 个人,所以无论怎么分配,最多只会有一个社团超出限制。
因此,我们先让每个人选最满意的社团。若存在超出限制的社团,则从中调出一些人,使其恰好剩余 \(\dfrac{n}{2}\) 个人。此时一定没有社团超出限制。
为了让答案尽可能优,调出的人的最大满意度 \(-\) 次大满意度应尽可能小。
时间复杂度 \(O(n\log n)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define eb emplace_back
using namespace std;
const int N=1e5+5,inf=1e15;
int t,n,a[5],c[5];
vector<int> v[5];
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>t;
c[0]=-inf;
while(t--){
v[1].clear(),v[2].clear(),v[3].clear();
cin>>n;
int ans=0;
for(int i=1;i<=n;i++){
int mxp=0,_mxp=0;
for(int j=1;j<=3;j++){
cin>>a[j];
if(a[j]>=a[mxp]) _mxp=mxp,mxp=j;
else if(a[j]>a[_mxp]) _mxp=j;
}
ans+=a[mxp];
v[mxp].eb(a[mxp]-a[_mxp]);
}
for(int i=1;i<=3;i++){
if((int)v[i].size()>n/2){
sort(v[i].begin(),v[i].end());
for(int j=0;j<(int)v[i].size()-n/2;j++) ans-=v[i][j];
}
}
cout<<ans<<"\n";
}
return 0;
}
T2. P14362 [CSP-S 2025] 道路修复 / road
Tag:图论、生成树。
我们可以直接 \(O(2^k)\) 地枚举对哪些乡村进行改造,然后将它们与城市的边连上,跑 MST。时间复杂度 \(O(2^k m(\log m+\alpha(n+k)))\),期望得分 \(60\text{ pts}\)。
排序可以在枚举前进行,而且城市之间的连边可以只保留 MST 上的。时间复杂度 \(O(m\log m+(nk)\log(nk)+2^k(nk+\alpha(n+k)))\)。
进一步考虑,额外改造一个乡村后的 MST,只可能包含原 MST 和新加的 \(n\) 条边。可以通过归并在 \(O((n+k)\alpha(n+k))\) 内求得。时间复杂度 \(O(m\log m+(nk)\log(nk)+2^k(n+k)\alpha(n+k))\)。
洛谷上的民间数据,后两种做法都能通过。
#1 - 8.27s
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M=1e6+2,N=1e4+2,K=12,inf=1e18;
int n,m,k,fa[N+K],idx,c[K],ans=inf;
inline int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
struct Ed{int u,v,w;}_e[M],e[N+N*K];
inline bool cmp(Ed a,Ed b){return a.w<b.w;}
bitset<K> vis;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>k;
for(int i=1,u,v,w;i<=m;i++){
cin>>u>>v>>w;
_e[i]={u,v,w};
}
sort(_e+1,_e+1+m,cmp);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++){
int u=find(_e[i].u),v=find(_e[i].v);
if(u^v) fa[u]=v,e[++idx]=_e[i];
}
for(int i=1,x;i<=k;i++){
cin>>c[i];
for(int j=1;j<=n;j++){
cin>>x;
e[++idx]={n+i,j,x};
}
}
sort(e+1,e+1+idx,cmp);
for(int sta=(1<<k)-1;~sta;sta--){
int s=0;
for(int i=1;i<=k;i++){
if((sta>>(i-1))&1) vis[i]=1,s+=c[i];
else vis[i]=0;
}
for(int i=1;i<=n+k;i++) fa[i]=i;
for(int i=1;i<=idx;i++){
int u=e[i].u,v=e[i].v,w=e[i].w;
if(u>n&&!vis[u-n]) continue;
u=find(u),v=find(v);
if(u^v) fa[u]=v,s+=w;
}
ans=min(ans,s);
}
cout<<ans<<"\n";
return 0;
}
#2 - 5.96s
#include<bits/stdc++.h>
#define int long long
#define eb emplace_back
using namespace std;
const int M=1e6+2,N=1e4+2,K=11,inf=1e18;
int n,m,k,fa[N+K],c[K],ans=inf;
inline int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
struct Ed{int u,v,w;};
vector<Ed> _e,a[K],e[K];
inline bool cmp(Ed a,Ed b){return a.w<b.w;}
inline int merge(vector<Ed>& a,vector<Ed>& b,vector<Ed>& res){
auto it=b.begin();
_e.clear(),res.clear();
int s=0;
for(int i=1;i<=n+k;i++) fa[i]=i;
for(Ed i:a){
for(;it!=b.end()&&it->w<=i.w;it++) _e.eb(*it);
_e.eb(i);
}
for(;it!=b.end();it++) _e.eb(*it);
for(Ed i:_e){
int u=find(i.u),v=find(i.v);
if(u^v) fa[u]=v,res.eb(i),s+=i.w;
}
return s;
}
inline void dfs(int p,int s,int sc){//s为修复边的花费,sc为改造的花费
if(p>k){
return ans=min(ans,s+sc),void();
}
e[p]=e[p-1],dfs(p+1,s,sc);//不改造
dfs(p+1,merge(e[p-1],a[p],e[p]),sc+c[p]);//改造
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>k;
for(int i=1,u,v,w;i<=m;i++){
cin>>u>>v>>w;
_e.eb(Ed{u,v,w});
}
sort(_e.begin(),_e.end(),cmp);
for(int i=1;i<=n;i++) fa[i]=i;
int s=0;
for(Ed i:_e){
int u=find(i.u),v=find(i.v);
if(u^v) fa[u]=v,e[0].eb(i),s+=i.w;
}
for(int i=1,x;i<=k;i++){
cin>>c[i];
for(int j=1;j<=n;j++){
cin>>x;
a[i].eb(Ed{n+i,j,x});
}
sort(a[i].begin(),a[i].end(),cmp);
}
dfs(1,s,0);
cout<<ans<<"\n";
return 0;
}
T3. P14363 [CSP-S 2025] 谐音替换 / replace
Tag:字符串、Trie 树、数据结构。
我们可以直接对询问串进行字符串哈希,然后逐位枚举。时间是 \(O(L_1+nL_2)\),期望得分 \(25\text{ pts}\)。
特殊性质 B 蛮有启发性的。由于所有串串都由 a 和 b 构成,且 b 恰出现一次,所以所有模式串对 / 询问串对都可以用一个点对 \((x,y,p)\) 来描述。
其中,\(x,y\) 分别表示前串中 b 从左往右数、从右往左数的位置;\(p\) 表示前串到后串 b 位置的变化量。
那么模式串 \((x',y',p')\) 能对询问 \((x,y,p)\) 产生贡献,当且仅当:
- \(x'\le x\)
- \(y'\le y\)
- \(p'=p\)
我们按 \(p\) 分组,每一组内做二维数点即可。时间复杂度 \(O(L_1+L_2+(n+q)(\log L_1+\log (n+q)))\)。
接下来考虑一般情况。
对于模式串对 \(s=(s_1,s_2)\),我们可以找到它们第一个和最后一个不同的位置,构成一个极小替换区间 \([l,r]\),记区间内的内容为 \(s_1',s_2'\)。
那么 \(s\) 可以表示为 \((l+s_1'+r,l+s_2'+r)\)。
同理 \(t\) 可以表示为 \((L+t_1'+R,L+t_2'+R)\)。
则模式串对 \((s_1,s_2)\) 能对询问串对 \((t_1,t_2)\) 产生贡献,当且仅当:
- \(l\) 是 \(L\) 的后缀。
- \(r\) 是 \(R\) 的前缀。
- \((s_1',s_2')=(t_1',t_2')\)。
对于第三条,我们按哈希值(或用 Trie 树)分组,每个组独立处理即可。
第一、二条描述的前后缀关系,放在 Trie 树上就是祖先和后代的关系。按 DFS 序编号后,同样可以转化为二维数点求解。时间复杂度 \(O(L_1|\Sigma|+L_2+(n+q)(\log L_1+\log (n+q)))\)。
这里也可以用 AC 自动机来做:对所有 \(\overline{l\texttt{\#}s_1's_2'\texttt{\#}r}\) 建立 AC 自动机,询问串同样写成 \(\overline{l\texttt{\#}t_1't_2'\texttt{\#}r}\),在上面跑多模式匹配即可。时间复杂度应该是 \(O(L_1|\Sigma|+L_2)\)。但是我不会写(逃
注意先把 \(|t_1|\ne|t_2|\) 判掉。
Ref:代码参照 by 20_200
点击查看代码
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
#define fi first
#define se second
using namespace std;
using namespace __gnu_pbds;
typedef unsigned long long ull;
const int N=2e5+5,Q=2e5+5,L1=5e6+5,C=26,B=233;
gp_hash_table<ull,int> to;
int n,q,tn,rt[N<<1],idx,ans[Q];
pair<int,int> tmp[N];
inline ull hs(string &s){
ull a=0;
for(char i:s) a=a*B+i;
return a;
}
struct TRIE{
int f[L1][C],dfn[L1],dfm[L1],idx,tim;
inline int ins(int x,string &s){
for(char i:s){
if(!f[x][i-'a']) f[x][i-'a']=++idx;
x=f[x][i-'a'];
}
return x;
}
inline void dfs(int x){
dfn[x]=++tim;
for(int i=0;i<C;i++) if(f[x][i]) dfs(f[x][i]);
dfm[x]=tim;
}
inline int qry(int x,string &s){
for(char i:s){
if(f[x][i-'a']) x=f[x][i-'a'];
else return x;//注意找不到应直接 return,否则 x 可能从 0 开始走到错误的节点
}
return x;
}
}tr;
inline int lb(int x){return x&-x;}
struct BIT{
int s[L1];
inline void chp(int x,int v){for(;x<=tr.idx;x+=lb(x)) s[x]+=v;}
inline int qry(int x){int a=0;for(;x;x-=lb(x)) a+=s[x];return a;}
}bit;
struct Point{int x,y,k;bool t;}pt[4*N+Q];
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>q;
string s1,s2,sl,sr,s;
for(int i=1;i<=n;i++){
cin>>s1>>s2,sl=sr=s="";
int m=s1.size(),l=0,r=m-1;
while(l<m&&s1[l]==s2[l]) sl+=s1[l++];
while(r>l&&s1[r]==s2[r]) sr+=s2[r--];
reverse(sl.begin(),sl.end());
reverse(sr.begin(),sr.end());
for(int j=l;j<=r;j++) s+=s1[j],s+=s2[j];
ull h=hs(s);
int &x=to[h];
if(!x) rt[x=++tn]=++tr.idx,rt[++tn]=++tr.idx;
tmp[i]={tr.ins(rt[x],sl),tr.ins(rt[x+1],sr)};
}
for(int i=1;i<=tn;i++) tr.dfs(rt[i]);
for(int i=1;i<=n;i++){
pt[++idx]={tr.dfn[tmp[i].fi],tr.dfn[tmp[i].se],1,0};
pt[++idx]={tr.dfn[tmp[i].fi],tr.dfm[tmp[i].se]+1,-1,0};
pt[++idx]={tr.dfm[tmp[i].fi]+1,tr.dfn[tmp[i].se],-1,0};
pt[++idx]={tr.dfm[tmp[i].fi]+1,tr.dfm[tmp[i].se]+1,1,0};
}
for(int i=1;i<=q;i++){
cin>>s1>>s2,sl=sr=s="";
if(s1.size()!=s2.size()) continue;
int m=s1.size(),l=0,r=m-1;
while(l<m&&s1[l]==s2[l]) sl+=s1[l++];
while(r>l&&s1[r]==s2[r]) sr+=s2[r--];
reverse(sl.begin(),sl.end());
reverse(sr.begin(),sr.end());
for(int j=l;j<=r;j++) s+=s1[j],s+=s2[j];
ull h=hs(s);
if(to.find(h)==to.end()) continue;
int x=to[h];
pt[++idx]={tr.dfn[tr.qry(rt[x],sl)],tr.dfn[tr.qry(rt[x+1],sr)],i,1};
}
sort(pt+1,pt+1+idx,[](Point a,Point b){return a.x==b.x?a.t<b.t:a.x<b.x;});
for(int i=1;i<=idx;i++){
if(pt[i].t) ans[pt[i].k]=bit.qry(pt[i].y);
else bit.chp(pt[i].y,pt[i].k);
}
for(int i=1;i<=q;i++) cout<<ans[i]<<"\n";
return 0;
}
T4. P14364 [CSP-S 2025] 员工招聘 / employ
在补……?
后
期望得分 \(35+70+70+8=183\)。
云斗得分 \(45+72+80+12=209\)(数据太弱了吧)。
实测得分 \(40+72+60+8=180\)。
T1 自闭了,除了“还好没有砸太多时间死磕”还能说啥。人除我切,我已经菜如狗了。
本来有个很裸的 \(O(n^3)\) DP 结果临结束二十分钟左右发现大样例有一个数对不上,痛失至少 \(30\text{ pts}\)。
T2 写了个 \(60\text{ pts}\) 和特殊性质,大概是 \(70\text{ pts}\)。可惜的是正解(如果 CCF 少爷机发力的话 #1 也算正解罢)并不难想,只需要在开始时将城市之间的非 MST 边剔除掉就写出来了。一方面是主要打暴力去了,一方面是思路本身稍乱吧。学到的:不要假定题目难度顺序,这无异于给自己设限。
T3 暴力打得应该还行(怎么还挂了 \(10\text{ pts}\))。赛时用二维数点打出了特殊性质 B ,其实和正解只差一个 Trie 树(虽然这步转化也比较难想),如果多分配一些时间,也可能会有头绪(这道题第一眼联想到 Trie 树,虽然还没好好读)。
T4。暴力最难打的一次。打了 \(O(n!)\) 的 \(8\text{ pts}\) 后,在特殊性质上徘徊许久。结果愣是没注意到还有 \(m=n\) 这个点。痛失 \(4\text{ pts}\)。
一句话总结:想不到正解还挂分就是菜,只打暴力不动脑也是菜,不好好读题不规划时间还是菜。
浙公网安备 33010602011771号