accuber

导航

2025.6.2-GDCPC-线上参赛总结

前言

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

比赛开始

D题赛后补题链接

image

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题赛后补题链接

image

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题赛后补题链接

image

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),一个周期一行,注意:这样写会改变周期长度从而改变答案,只用于观察:

image
这里是AI的证明:
image

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题赛后补题链接

image

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题赛后补题链接

image

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();
}


我们队伍拿下了第一块奖牌。下次也要加油!
微信图片_20250822141619


不经一翻彻骨寒,怎得梅花扑鼻香。 ——宋帆

posted on 2025-06-04 22:35  accuber  阅读(67)  评论(0)    收藏  举报