2025.6.2-GDCPC-线上参赛总结
前言
我们此次是在PTA线上参赛的,本来是说9:30开始,但是不知道为什么,倒计时十秒结束,时间延长了10分钟;好不容易等到又一个倒计时十秒结束,又延长5分钟……到9:45时,比赛正式开始赛后补题链接
比赛开始
D题赛后补题链接

D题大意及做法:
注意到题目给的两种操作可以合为一种操作:选择一个非负整数x,花费 \(2^x\) 时间,贡献 \(10^x\) 的长度,所以我们只需要将询问 r 进行从r的末位进行逐位分解,对于第i位的数字A[i],答案增加\(2^{A[i]}\),可以预处理2的幂。记得开long long!
赛后复盘代码如下:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int pw2[20];
void solve(){
int n;
cin>>n;
int idx=0,res=0;
while(n){
int tmp=n%10;
res+=tmp*pw2[idx];
idx++,n/=10;
}
cout<<res<<endl;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int t=1;
cin>>t;
pw2[0]=1;
for(int i=1;i<=19;i++)pw2[i]=pw2[i-1]*2;
while(t--) solve();
}
F题赛后补题链接

F题大意及做法:
排序+贪心,模拟ICPC比赛赛制:我们通过的题数为a,总罚时为b,我们知道对手的答题情况,我们要统计他们每题的罚时,及通过的题数。对于封榜后提交的题目,记录它最开始提交的时间,并对它们进行排序。既然要看看对手至少在封榜后过了几题,我们就假设他们都是一发入魂,每过新的一题加上过的最早时间和之前的罚时,并和我们的情况进行比较即可。
赛后复盘代码如下:
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+10,INF=0x3f3f3f3f;
void solve(){
int n,a,b,m;
cin>>n>>a>>b;
cin>>m;
vector<int>wa(20,0),pd(20,INF);
vector<bool>ac(20,0);
int num=0,tim=0;
for(int i=1;i<=m;i++){
int t;
char c;
string str;
cin>>t>>c>>str;
int idx=c-'A';
if(ac[idx]) continue;
if(str=="ac"){
tim+=(t+wa[idx]*20);
num++;
ac[idx]=1;
}else if(str=="rj"){
wa[idx]++;
}else if(str=="pd"){
pd[idx]=min(pd[idx],t+wa[idx]*20);
}
}
if(num>a||num==a&&tim<b){
cout<<0<<endl;
return ;
}
sort(pd.begin(),pd.end());
for(int i=0;i<n;i++){
if(pd[i]==INF)break;
num++,tim+=pd[i];
if(num>a||num==a&&tim<b){
cout<<i+1<<endl;
return;
}
}
cout<<-1<<endl;
return ;
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _=1;
cin>>_;
while(_--)solve();
return 0;
}
G题赛后补题链接

G题大意:
给一个货币系统,f(x,n) 表示在尽量多换大面额纸币的情况下用多少张纸币才能凑数 x 元,给定 m 问满足 f(x,n) = m 的正整数 x 有多少个。
G题做法:
1.g[i] 表示支付金额 i 的最少张数:
用于预处理 g[i](即金额 i 的最小纸币张数),范围从 1 到 A[n]。
目的:利用 g[i] 的周期性,将无限范围的金额问题压缩到 [1, A[n]] 范围内。
将预处理放大10倍观察周期性(g[x + A[n]] = g[x] + 1),一个周期一行,注意:这样写会改变周期长度从而改变答案,只用于观察:

这里是AI的证明:

2.fre[i]:统计所有 x 中 g[x] == i 的数量。(频率数组)
由于 g[x] 的周期性,只需统计 x ∈ [0, A[n]] 的 g[x] 值即可覆盖所有可能的 x
3.ans[m] 表示所有金额 x 中,g[x] ≤ m 的数量。(前缀和数组)
fre[i] 统计了 [0, A[n]] 中 g[x] == i 的数量,通过前缀和累加即可得到 g[x] ≤ m 的总数;查询时,若 m ≥ A[n],答案为 ans[A[n]](因为更大的 x 的 g[x] 会随 周期 增长,但 g[x] ≤ m 总成立)。
赛后复盘代码如下:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int N=1e6+10,M=1010,mod=998244353,INF=0x3f3f3f3f;
int g[N],A[N],fre[N],ans[N];
//源自题目所给的公式
int f(int x,int y){
if(x==0)return 0;
if(g[x]!=-1)return g[x];//记忆化搜索,避免重复计算
int l=1,r=y;//二分查找支付x元时可最大的金额
while(l<r){
int mid=l+r+1>>1;
if(A[mid]<=x)l=mid;
else r=mid-1;
}
y=l;
return g[x]=x/A[y]+f(x%A[y],y-1);
}
void solve(){
int n,q;
cin>>n>>q;
memset(g,-1,sizeof g);
for(int i=1;i<=n;i++) cin>>A[i];
// 预处理所有可能的g[i]值
for(int i=1;i<=A[n];i++){
f(i,n);
}
// 统计频率
for (int r = 0; r < N; ++r) {
fre[g[r]]++;
}
//计算前缀和
ans[0]=fre[0];
for (int i=1;i<N;i++) {
ans[i]=ans[i-1]+fre[i];
}
// 处理查询
while (q--) {
int m;
cin >> m;
int res=0;
if(m>=N)res=ans[N-1];
else res=ans[m];
cout<<res<<" ";
}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int _=1;
//cin>>_;
while(_--) solve();
}
H题赛后补题链接

H题大意:
给定一个长为 n 的小写字母串 S 和非负整数 k,对于 S 的一个子序列 T,定义 \(pos_i\) 为 T 中第 i 个字符在S 中的下标,称 T 是好的当且仅当 \(pos_{i+1}\) − \(pos_i\) > k, 问 S 有多少本质不同的子序列是好的。
H题做法:
一、本质不同的子序列:
定义:指字符内容或顺序不同的子序列,而非仅位置不同。因此,统计本质不同子序列时,需去重,即只计算字符组合本身不同的子序列。
求解:设dp[i] 表示考虑前 i 个字符且以第 i 个字符结尾的本质不同子序列数目。last[c]:记录字符 c 上一次出现的位置。可用前缀和优化。
当S的第i个字母c第一次出现,dp[i] 可以从之前的所有状态转移过来,加上它本身一个字母,故状态转移方程为:$$dp[i]= {\textstyle \sum_{j=1}^{i-1}dp[j]} +1$$
当S的第i个字母c不是第一次出现,那么当 a[i] 出现过时 0 ∼ last[i]-1 这一段会被重复计算,需要减去,故状态转移方程为:$$dp[i]= {\textstyle \sum_{j=last[c]}^{i-1}dp[j]}$$
二、该题松散子序列:
当 k = 0 时,不同之处在于考虑 a[i] 时 dp[i − k] ∼ dp[i − 1]都没有贡献,我们可以用 i+k 偏移,确保子序列的下一个字符至少间隔 k 个位置。例如,当前字符在位置 i,下一个字符只能从 i+k+1 开始,故设dp[i+k]表示以第 i 个字符结尾的 k 松散子序列数量,ans[i+k]表示以前i个字符结尾的 k 松散子序列总数。只需在本质不同子序列里加上偏移量的修改就可以ac这题
赛后补题代码如下:
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int N=1e6+10,mod= 998244353;
int n,m,k,kk,u,v;
string s;
void solve(){
cin>>n>>k;
cin>>s;
vector<int>ans(n+k+1,0);
vector<int>dp(n+k+1,0);
vector<int>last(26,0);
for(int i=1;i<=n;i++){
int c=s[i-1]-'a';
if(last[c])dp[i+k]=((ans[i-1]-ans[last[c]-1])%mod+mod)%mod;
else dp[i+k]=ans[i-1]+1;
last[c]=i;
ans[i+k]=(dp[i+k]+ans[i+k-1])%mod;
}
//for(int i=1;i<=n+k;i++)cout<<dp[i]<<" ";
cout<<ans[n+k]<<endl;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
int _=1;
cin>>_;
while(_--) solve();
}
J题赛后补题链接

J题大意:
这是一个带管控条件的图上解锁问题,每个节点有一个初始参数 a[i],当某个节点被解锁后,它会通过边向其他节点传递“能量”(影响时间),但前提是目标节点的参数降到 0,存在k个强制刷新器,会在特定时间点将某些题目的参数重置为 0。
J题做法:
赛时思路:我们之前是考虑将每个刷新器读入后直接压入优先队列 q,然后在 Dijkstra 中当作普通事件处理,在指定时间或者无法操作时使用。但是这样可能导致刷新器提前生效,影响结果。无法证明这样可以获得最优解。
正确做法:
1.考虑bfs():节点的解锁是一个逐层扩散的过程:一旦某个节点被解锁,它就会通过边向其他节点传递能量。
2.刷新器应单独存储,并按时间排序。然后在Dijkstra 的过程中维护当前时间,每当当前时间>= 刷新器时间时才触发其效果。维护每个节点的最早解锁时间 ans[idx],当前节点解锁后,所有出边都应加上该节点的解锁时间,形成新的到达时间。
3.用小根堆排序最早事件,并用val区分节点和刷新器,一步步使得节点需求已满足后再更新其答案,并将其所有未解锁领边加上传播时间入队。
赛后补题代码如下:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10,mod=998244353,inf=0x3f3f3f3f3f3f3f3f;
int n,m,k;
int e[N],ne[N*2],h[N*2],w[N*2],idx;
int a[N],ans[N];
struct nod{
int time; // 时间戳
int tar; // 目标节点
int val; // 标记是否是初始状态(0是1否)
bool operator < (const nod &u) const {
return time > u.time;
}
};
priority_queue<nod> pq;
void add(int a, int b, int c) {
w[idx]=c, e[idx]=b, ne[idx]=h[a], h[a]=idx++;
}
void bfs(){
//如果某个节点的需求数量为0,则将其答案设为 0;
//并将其所有出边加入优先队列,表示这些边可以开始传播能量。
for(int i=1;i<=n;i++){
if(a[i]==0){
ans[i]=0;
for (int j=h[i];~j; j=ne[j]){
//将相邻节点加入优先队列,标记为非初始状态
pq.push({w[j],e[j],1});
}
}
}
while (!pq.empty()) {
auto tmp= pq.top();pq.pop();
int time=tmp.time,idx=tmp.tar,val=tmp.val;
if (a[idx]<=0) continue; // 如果节点需求已满足,跳过
if (val == 1)a[idx]--; //如果是非初始状态,减少节点的需求数量
if (val == 0)a[idx]=0; //如果是初始状态,参数赋值为0,用于去重
// 如果节点需求已满足,更新其答案,并将与其相连的节点加入优先队列
if (a[idx]==0) {
ans[idx]=time;
for (int i=h[idx];~i;i=ne[i]) {
int j=e[i];
if (a[j]!=0){
//将相邻节点加入优先队列,标记为非初始状态
pq.push({time + w[i],j, 1});
}
}
}
}
}
void solve() {
memset(h,-1,sizeof h);
cin>>n>>m>>k;
memset(ans,0x3f,sizeof ans);
for(int i=1; i<=n;i++)cin>>a[i];
for(int i=1;i<=k;i++){
int t,sc;
cin>>t>>sc;
for(int j=1;j<=sc;j++){
int x;
cin>>x;
pq.push({t,x,0}); //将节点加入优先队列,标记为初始状态
}
}
for(int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
bfs();
for (int i = 1; i <= n; i++) {
if (ans[i] == inf) cout << -1 << " ";
else cout << ans[i] << " ";
}
}
signed main() {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t = 1;
// cin >> t;
while (t--)solve();
}
我们队伍拿下了第一块奖牌。下次也要加油!

不经一翻彻骨寒,怎得梅花扑鼻香。 ——宋帆
浙公网安备 33010602011771号