CSP-S 2026题解 [未完成]
前言
难度:黄绿紫黑
前两题是自己的考场上的做法,后两题参考了题解区个人认为最佳的做法,做了补充与详解。
T3: https://www.luogu.com.cn/article/1ukkypnp
T4: https://www.luogu.com.cn/article/st6ljm36
crystal
题意
给 \(n\) 个数,可以做至多 \(m\) 次给某数 \(+1\) 操作,最大化相同数数量。
\(n\le 5\times 10^5\)。
分析
注意到最优解必然是排序后一段区间,且目标数字必然在 \(a\) 数组里。枚举目标并动态维护这个区间的花费,双指针即可。
代码
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define ll long long
using namespace std;
const int N=5e5+10;
ll n,m,a[N],l,r,cst,ans;
int main(){
freopen("crystal.in","r",stdin);
freopen("crystal.out","w",stdout);
scanf("%lld%lld",&n,&m);
rep(i,1,n) scanf("%lld",&a[i]);
sort(a+1,a+n+1);
l=1,r=0;
while(r<n){
if(cst<=m) ans=max(ans,r-l+2);
else {
while(cst>m){
cst-=a[r+1]-a[l];
l++;
}
ans=max(ans,r-l+2);
}
r++;
cst+=(a[r+1]-a[r])*(r-l+1);
}
cout<<ans;
return 0;
}
subway
题意
题面真傻逼,qoj 上 \(800\) 多的差评不是白来的,相信有朝一日破千。
最神的是,这个人也是追忆的出题人,还是今年 J 组 T1、T2、T3、S 组 T1 的出题人。
有 \(m\) 个地铁站,各有一个权值 \(w\),代表从地铁口走下地铁站的时间。
有 \(n\) 条地铁线路,双向经过若干互不相同的站。对这第 \(i\) 条线里每个站 \(a_{i,j}\),从站厅走到该线路上要消耗时间 \(t_{i,j}\)。
同一站最多被 \(d\) 条线路经过,保证地铁网络连通。
乘坐地铁本身不计步行时间,换乘时需从原线路站台走到站厅,再从站厅走到另一线路站台。
有 \(q\) 次询问,每次给出 \(x,y\),求从第 \(x\) 个站的地铁口出发,到第 \(y\) 个站的地铁口,所需的最小总步行时间,包括进出站及所有换乘的步行时间。
\(n\le 500,m \le 5\times 10^5,d\le 20\)。
分析
注意到题目保证任意两点可以通过地铁到达。而且 \(n \le 500\),考虑对地铁建模比较好。
题目给了点线距,我们只需要搞定线线距。
任意两条地铁线路可以把他们的距离跑出来。由于 \(d \le 20\),只要枚举每个点,看经过其的任意两条线路,可以用点与线路的距离来更新线线距。之后对地铁跑一次 floyd 就能找出任意两条线路之间距离。
询问就是看 \(u\) 和 \(v\) 两个点分别走哪条线路,答案就是点线距线线距以及 \(w_u+w_v\) 之和最小值。这个 \(w\) 没用,完全可以不给。
时间复杂度 \(O(n^3+(m+q)d^2)\)。
代码
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define ll long long
#define pil pair<int,long long>
using namespace std;
const int M=5e5+10,N=510;
int casee,n,m,d,Q;
ll inf;
struct node{
ll w;
vector <pil> vec;
}a[M];
ll f[N][N];
int main(){
freopen("subway.in","r",stdin);
freopen("subway.out","w",stdout);
scanf("%d%d%d%d%d",&casee,&n,&m,&d,&Q);
rep(i,1,m) scanf("%lld",&a[i].w);
rep(i,1,n){
int k;scanf("%d",&k);
rep(j,1,k){
int id;ll len;
scanf("%d%lld",&id,&len);
a[id].vec.push_back({i,len});
}
}
memset(f,0x3f,sizeof(f));
inf=f[0][0];
rep(i,1,n) f[i][i]=0;
rep(i,1,m){
for(auto prs:a[i].vec){
for(auto prt:a[i].vec){
int s=prs.first,t=prt.first;
if(s==t) continue;
f[s][t]=min(f[s][t],prs.second+prt.second);
f[t][s]=min(f[t][s],prs.second+prt.second);
}
}
}
rep(k,1,n){
rep(i,1,n){
if(k==i) continue;
if(f[i][k]>=inf) continue;
rep(j,1,n){
if(k==i||i==j||j==k||f[k][j]>=inf) continue;
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
}
}
while(Q--){
int u,v;
ll ans=inf;
scanf("%d%d",&u,&v);
for(auto prs:a[u].vec){
for(auto prt:a[v].vec){
int s=prs.first,t=prt.first;
ans=min(ans,a[u].w+a[v].w+prs.second+prt.second+f[s][t]);
}
}
printf("%lld\n",ans);
}
return 0;
}
migrate
题意
给定 \(n\) 个位置 \(x_i\),以及一个 \(d\),表示从一个点 \(i\) 能一步跳到 \(j\) 当且仅当 \(|x_i-x_j| \le d\)。
对这样的无序对 \((u,v)\) 计数:从 \(u\) 起跳到 \(v\),最短路径走法数恰为 \(2\)。
\(n \le 2\times 10^6\)。
分析
以下钦定 \(u \le v\)。
考虑路径 \(u \to v\) 和 \(u \leftarrow v\),每一步都要尽力跳(如果最后一步尽力跳会跳过终点,就可以不尽力,直接到终点即可)。
如果这两条路径经过的点完全相同,说明 \((u,v)\) 只有一种走法。
观察这两条路径,我们可以发现,\(u \to v\) 这条路径由于每次都是要跳到下一个最远能到的,也就是说其字典序一定是最大的。同理,\(u \leftarrow v\) 的字典序一定是最小的。
我们要求只存在两种路径,换句话说,\(u \to v\) 这条与 \(u \leftarrow v\) 的不相等,且中间不能夹别的路径。
显然,他们只能有某一位有差别,且只能差 \(1\)。
说句题外话,这个由特殊性质 B 已经能大概推出来了。
你可以想象,存在某个平台(一段区间),只要上了这个平台,下一步尽全力跳,都只能跳到同一个终点。这也就会造成路径的多样。
所以,希望路径只有两条,那么只能存在一个这样的平台,且平台里只能由两座岛屿,那也就只能是 \(i\) 和 \(i+1\) 了。(这么讲应该很浅显了吧)
接下来考虑计数,枚举这样的 \((i+1,i)\),考虑其左边有多少能作为起点的点、右边有多少能作为终点的点。
可以双指针处理出每个点尽力往左、右跳能到最远的点 \(l_i\) 和 \(r_i\)。
从左往右做和从右往左做是对称的,改几个参数、变量名即可。细节参看代码,这里只讲解从左往右。
设 \(f_i\) 为只能通过一种最短走法走到 \(i\) 的起点 \(u\) 个数。
设 \(g_i\) 为跳到 \(i\) 和 \(i+1\) 的最短路都唯一,且一直尽力跳最后跳到 \(i+1\) 的起点 \(u\) 个数。
考虑如何算。首先枚举 \(i\) 作为起点的情况,尽力跳,下一步到 \(r_i\)。
若 \(r_i>i\),则 \(f_{r_i} \leftarrow f_{r_i}+1\)。
若 \(r_i-1>i\),则 \(g_{r_i-1} \leftarrow g_{r_i-1}+1\)。
考虑 \(i\) 作为中间点的情况,向后跳一步。
若 \(r_i>i\) 且 \(l_{r_i}=i\),说明能跳到 \(r_i\) 的岛屿之中,最左边是 \(i\),故 \(f_{r_i} \leftarrow f_{r_i}+f_i\)。
若 \(r_i-1>i\) 且 \(l_{r_i-1}=i\),说明存在了一个形如 \((x+1,x)\) 的东西,\(g_{r_i-1} \leftarrow g_{r_i-1}+f_{i}\)。
于是做完了,答案是:
时间复杂度 \(O(n)\)。
代码
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define int long long
using namespace std;
const int N=2e6+10;
int n,T,d,cas;
int l[N],r[N],x[N];
int f[N],g[N],ans[N];
signed main(){
freopen("migrate.in","r",stdin);
freopen("migrate.out","w",stdout);
scanf("%lld%lld",&cas,&T);
while(T--){
scanf("%lld%lld",&n,&d);
rep(i,1,n) scanf("%lld",&x[i]);
int p=1;
rep(i,1,n){
while(p<=n&&x[p]-x[i]<=d)p++;
r[i]=p-1;
}
p=n;
per(i,n,1){
while(p>=1&&x[i]-x[p]<=d)p--;
l[i]=p+1;
}
rep(i,1,n){
if(r[i]>i) f[r[i]]++;
if(r[i]-1>i) g[r[i]-1]++;
if(r[i]>i&&l[r[i]]==i) f[r[i]]+=f[i];
if(r[i]-1>i&&l[r[i]-1]==i) g[r[i]-1]+=f[i];
}
rep(i,1,n+1) ans[i]=g[i],f[i]=g[i]=0;
per(i,n,1){
if(l[i]<i) f[l[i]]++;
if(l[i]+1<i) g[l[i]+1]++;
if(l[i]<i&&r[l[i]]==i) f[l[i]]+=f[i];
if(l[i]+1<i&&r[l[i]+1]==i) g[l[i]+1]+=f[i];
}
int res=0;
rep(i,1,n-1) res+=ans[i]*g[i+1];
printf("%lld\n",res);
rep(i,0,n+1) x[i]=l[i]=r[i]=f[i]=g[i]=ans[i]=0;
}
return 0;
}
hub
题意
给定一棵树,要求选出 \(k\) 个点,它们构成不超过两个连通块。要使其余所有点到这两个连通块距离的较小值的最大值最小,即覆盖半径最小。
\(n \le 10^6\)。
分析
二分答案,考虑 check 一个半径 \(r\),先对原树剥去 \(r\) 轮叶子,剩下一颗核心树 \(C\)
代码
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=1e6+10;
int T,n,k;
int tim[N],deg[N],cnt[N];
int ok[N],a[N],b[N],c[N],f[N];
vector <int> g[N];
bool check(int r){
int len=cnt[r+1]-k;
if(len<=0) return 1;
if(len>2*r) return 0;
rep(i,1,n) ok[i]=(tim[i]>r+1)&&(c[i]<=r);
rep(i,1,n){
if(!ok[i]||ok[a[i]]&&ok[b[i]]) continue;
int u=i,v=ok[a[i]]?b[i]:a[i],cur=0,j=0;
f[0]=0;
while(ok[u]){
ok[u]=0,f[++j]=0;
int d=r-c[u],left=max(0,j+d-len),right=j-d-1;
if(left<=right) f[left]++,f[right+1]--;
if(j>=len){
cur+=f[j-len];
if(!cur) return 1;
}
int nxt=a[u]^b[u]^v;
v=u,u=nxt;
}
}
return 0;
}
int find(){
int l=0,r=n-1,ans=n-1;
while(l<=r){
int mid=(l+r)/2;
if(check(mid)) ans=min(ans,mid),r=mid-1;
else l=mid+1;
}
return ans;
}
signed main(){
freopen("hub.in","r",stdin);
freopen("hub.out","w",stdout);
ios::sync_with_stdio();cin.tie(0);cout.tie(0);
cin>>T>>T;
while(T--){
cin>>n>>k;
rep(i,1,n-1){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
deg[u]++,deg[v]++;
}
queue <int> tmp;
rep(i,1,n) if(deg[i]==1) tim[i]=1,tmp.push(i);
while(!tmp.empty()){
int u=tmp.front();tmp.pop();
for(auto v:g[u]) if(--deg[v]==1) tim[v]=tim[u]+1,tmp.push(v);
}
rep(i,1,n){
cnt[tim[i]]++;
for(auto v:g[i]){
if(tim[v]>tim[a[i]]) swap(v,a[i]);
if(tim[v]>tim[b[i]]) swap(v,b[i]);
c[i]=max(c[i],tim[v]);
}
}
per(i,n-1,0) cnt[i]+=cnt[i+1];
cout<<find()<<"\n";
rep(i,0,n+1) a[i]=b[i]=c[i]=cnt[i]=deg[i]=tim[i]=ok[i]=f[i]=0,g[i].clear();
}
return 0;
}

浙公网安备 33010602011771号