CSP-J复赛模拟赛4 王晨旭补题 2025.10.4
一,序语
依旧Rank2,然而并没有昨天开心,今天班一跟开桂了一样狂拉我70分,本来以为今天终于有希望冲班一了,这怎么赢?其实还是期望与现实不构成正比的原因(老师说这套题真的很简单,以为至少打个260呢),落差大了又开始否定自己,其实完全没有必要,好好分析问题才是王道(单押),本次考试还是有一定失误,比如错过的问题再错,审题不充分,总体来说没有昨天打得好,今晚调整一下再看明天成绩(我怎么神一场鬼一场的)
二,成绩
1,复读机【100/100】这题带回了半瓶水,剩下半瓶从我脑子里补
2,小可的矛与盾【70/100】这是回到了2024年吗
3,不合法字符串【20/100】哪有人会过第三题的
4,虚假的珂朵莉树【0/100】我真没做大慈善家
总分:【190/400】老夫子开大把我狠狠绑在了200分段
几个现象:班一第3题90pts(其余基本全0),恐怖如斯,第四题全班无得分人,全部爆零,第二题唯一一次,全班无满分
三,题一
p0:本题改编自2025csp-j初赛第一篇完善程序,略加改动
p1:题面,可以对照着初赛题目看,不完全相同
人类的本质是复读机。
小可喜欢在群里复读别人说过的话,这种人通常被称为复读机。现在小可想模拟复读机过程,过程如下:现在给定一个长度为 n 的仅包含小写字母和数字的字符串,字母表示需要复读的消息,数字表示要复读的次数。
例如:𝑘𝑑𝑦3,表示将𝑘𝑑𝑦复读3遍,输出为:𝑘𝑑𝑦𝑘𝑑𝑦𝑘𝑑𝑦;然而复读没有这么简单,小可想进行一个更复杂的复读模拟, 于是这个字符串中可能包含多个数字, 当多次出现数字时,例如 𝑎5𝑏2,我们从左到右解析这个字符串,𝑎5表示将𝑎复读 5 遍,即原字符串变为 𝑎𝑎𝑎𝑎𝑎𝑏 ,然后遇到数字 2 ,再将所有消息全部复读 2 遍,即 𝑎𝑎𝑎𝑎𝑎𝑏𝑎𝑎𝑎𝑎𝑎𝑏 。
上来先整一句小哲学,那题面的大致意思就是给我们一个字符串,要求我们将其中的数字转换为字母,其中该数字将会转换成这个数字数量的前面已经转换过后的字符串,求转换完了的字符串
p2:赛上这题也是迷迷糊糊,由于它模拟的过程太像曾经做过的一个队列题了(我怎么没有想到那个完善程序似乎用的不是队列),所以先整了队列
队列一是非常麻烦,想实现替换转换过后的字符串还得将原队列里的字符全部转到新队列,再循环入队,二是时间复杂度比我都大(嘿嘿不正经),O(n^4)果断放弃
p3:此题AC,依旧不放官方思路
可以想到,数字的替换操作既是在原字符串后拼接(数字-1)个原字符串,就达到了“复读”效果,而设计拼接,string中帮我们贴心重载好的“+”是不二选择
具体过程即为设立空串ans,遍历原字符串,遇到字母直接加在ans后面,遇到数字(不能直接加!可能是多位数)遍历到下一个字母,直接向后+(数字-1)个原字符串,再拼接字母
p4:那我们的AC代码基本也就出来了(其中细节ans不能直接自加,先保留下原字符串anss)
#include<bits/stdc++.h>
using namespace std;
int main(){freopen("repeater.in","r",stdin);freopen("repeater.out","w",stdout);int t,n;cin>>t;while(t--){string s,ans="";cin>>n>>s;int len=s.size()-1,cnt=0;for(int i=0;i<s.size();i++){if(s[i]>='a'&&s[i]<='z'){string anss=ans;for(int j=1;j<cnt;j++){ans+=anss;}cnt=0;ans+=s[i];}else if(s[i]>='0'&&s[i]<='9'){cnt*=10;cnt+=s[i]-'0';}}if(cnt){string anss=ans;for(int i=1;i<cnt;i++){ans+=anss;}} cout<<ans<<"\n";}
}
四,题二
p1:哈哈,long long掉20分;题目故意不显示关键信息,和老师视角完全不相同掉10分(我很平静)
我分没了啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊还我分啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊(谁要平静)
long long我纯活该,已经呼自己两巴掌了;预制菜AB题面?(紧跟时事)那恶意溢出屏幕了吧
p2:抛开所有第二题都在针对我之外(好像别的就没有针对我一样),先放最可气的题面
我很少直接截图,但这在法庭上就是物证(bushi)
下图是老师的题面(可看出排版区别)
这里是我们的题面
我们的题面中缺少了能够直接影响答案的pos范围,所有人都默认了pos范围为从1到n
然而,我跟老师说,老师的题上有,所以让我去审题。。。(窦娥冤啊啊啊啊)
理性的看待这件事情的话,它倒是让我们借此机会好好进行了审题(包括以后的题也得好好审)
p3:回到主题,完整的题面还差一句
如果字符串某一位是0,则表示小可充当队伍矛,否则为盾。
大致意思是:让我们确定一个分割点,使分割点左侧的权值与右侧相差值最小
权值计算方法:分割点左侧的权值为字符串分割点左侧中所有0的下标求和,右侧的权值即为字符串分割点(不包含分割点本身)右侧中所有1的下标求和
p4:赛上
这题赛上表现也不大行,优先考虑的是二分,双指针,二分序列无序,分不动,双指针写着写着都给写笑了,写的不知道什么玩意还能过样例,其实那时候已经有点摆烂了,隔壁小孩哥一语点醒梦中人“这题好像要用前缀和”,感谢小孩哥,就这一句直接有了思路,5分钟写完成功过掉样例(非失误即为正解)
同时,我很担心,在赛场上遇到这种题,如何想到正解呢?又为何我没有想到正解呢,我想到二分因为此题是在序列中求一个位置使收益最大,像二分答案,我想到双指针是因为分割点的移动似乎可以用双指针枚举,指针的移动靠贪心,但双指针的移动策略很复杂(大佬看看这题能不能用双指针整),本质依然是枚举位置,排除WA时间复杂度依然压不下去,正解前缀和是因为分割点分割出的两侧权值是一定的,分割点的改变不会改变这一段上的权值,这样在已提前求解出的基础上做简单的加减法,压掉计算权值所占用的时间复杂度,就可以直接O(n)枚举分割点了,而我之所以没有想到这种做法,是认为枚举O(n^2)必然会超时,没有再去考虑如何让枚举不超时的方法,或者说我的优化方式不是去优化权值的求解,而是去优化分割点的划定次数(问题的两侧是相对的),急于求成,还没看清答案每步来源(递推),就是能力不够,这也说明了我的思维能力是要比算法能力弱的
p5:本题我的思路完全正确(诶呦气呐),所以依然不放官方思路
首先正序递推求出a[i](表示从1到 i 的矛的权值),若该位非0,就继承上一位a[i-1]的权值,若该位为0,额外增加下标的收益 i,逆序递推从n到i求出b[i](表示从 i 到 n 盾的权值) ,若该位非1,就继承下一位b[i+1]位的权值若该位为1,额外增加下标的收益 i ,若该位非1,就继承下一位b[i+1]位的权值,从1到n遍历分割点,分割点i的权值就是abs(a[i],b[i+1]),比较存储i的权值的最小(注意i的范围要从0到n+1)
p6:直接放AC代码(写冒了又)
#include<bits/stdc++.h>
using namespace std;
long long w[100005],v[100005];//w[i]表示从1到i矛的攻击力总和,v[i]表示从n到i盾的防御力总和
int main(){freopen("spearshield.in","r",stdin);freopen("spearshield.out","w",stdout);string s;int n;cin>>n>>s;for(int i=1;i<=n;i++){w[i]=w[i-1];if(s[i-1]=='0'){w[i]+=i;}}for(int i=n;i>=1;i--){v[i]=v[i+1];if(s[i-1]=='1'){v[i]+=i;}}long long minn=0x7ffffffffffffffll; for(int i=0;i<=n;i++){if(abs(w[i]-v[i+1])<minn){minn=abs(w[i]-v[i+1]);}}//枚举pos,最小到1,最大到n-1cout<<minn;
}
五,题三
p0:不是牢弟,老师说这题简单你还真信啊,是CSP的风太大还是你听不见我题三说话啊
p1:special judge大样例显示AC我是心高气傲,成绩一出只得20分我是生死难料
还是太大意了,没有闪;题是简单,但不完全简单,分人啊(喜欢我的连环段子吗)
p2:首先先放上题面
小可是一名小说审核员,他的工作是看小说,然后把小说中不合法字符串和谐掉。
现在给出若干个不合法的字符串 s[i],和一篇小说 str ,小可需要把 str 中的不合法字符串用
*
和谐掉。当然小可是一个很聪明的审核员,他会用最少的*
和谐字符串。比如:
有三个不合法字符串:
abc
、ab
、a
。str=abcd
他会只和谐
a
,使得str=*bcd
,这样小说中就没了不合法字符串。请输出和谐之后的小说。
这是想让咱干啥呢?首先给你一些字符串,这些字符串是不和谐的,你现在要当游戏策划,把语音里的部分字符用星号替换,让整句语音中没有完整的不和谐字符串(王者荣耀策划做了这题所以让我们曹操玩家连名字都打不出来是吗)
p3:首先必排除暴力,正在我迷茫不知所措之时,老师的一句“这题要用贪心!!”,我直接醍醐灌顶气冲云霄才高八斗玉树临风英姿飒爽夹带私货写了把20行贪心
贪心的大致思路如下,遍历不合法字符串,遍历到一个不合法字符串,在原字符串中用find函数找,找到一个把这个不合法字符串的首个下标和谐掉,一直找直到返回值为-1,就开始遍历下一个不合法字符串,所有不合法字符串都遍历完了之后,输出原字符串
然而,这题面中的数据就能直接卡住(样例和大样例都卡不住可想而知有多水)
abc
、ab
、a
str=abcd
他会只和谐a
,使得str=*bcd
那我们打个排序吧!短的先遍历,这样避免重复打*
好,考场代码出现,只有20分
#include<bits/stdc++.h>
using namespace std;
int main(){freopen("illegality.in","r",stdin);freopen("illegality.out","w",stdout);int T;cin>>T;while(T--){int n;string s,a[15];cin>>n;for(int i=1;i<=n;i++){cin>>a[i];}sort(a+1,a+n+1);cin>>s;for(int i=1;i<=n;i++){while(s.find(a[i])!=-1){s[s.find(a[i])]='*';} }cout<<s<<"\n";}
}
很可惜,这样的贪心太贪了,无脑选最短的不合法字符串优先遍历并不可以!不一定最优哦
p4:贪心不成?直接给我枚举!
从前向后遍历小说字符串,这一位向前看看能不能取出不合法字符串的任意一个,如果能取出来,直接将这一位标为*号,所有的不合法字符串只要能被和谐就一定会先和谐掉,保证了答案的最优
#include<bits/stdc++.h>
using namespace std;
int main() {int T;cin>>T;while(T--) {int n;cin>>n;string s[15];for(int i=1; i<=n; i++) {cin>>s[i];}string a;cin>>a;int len = a.size();a = " "+a;for(int i=1; i<=len; i++) {for(int j=1; j<=n; j++) {if(i<s[j].size()) continue;if(a.substr(i-s[j].size()+1,s[j].size())==s[j]) {a[i]='*';}}cout<<a[i];}cout<<"\n";}return 0;
}
六,题四
p0:我补药再做图论题了,补药啊啊啊啊啊(其实因为缺乏自信,连暴搜都没写出来,不然能拿10分的呜呜)
p1:先放题面
在我的后园,可以看见墙外有两株树,一株是枣树,还有一株也是枣树。
小可有一棵树,有 n 个节点,根节点为 1, 每个节点都有一个权值。
设每个节点与根节点距离是这个节点的深度。
小可会在这棵树上增加 m 条虚假边,任意一条虚假边不会和原来的树边或其他虚假边重合(增加的虚假边不影响节点深度)。
之后小可会进行 q 次操作:
操作1: 让结点 u 的权值增加 k ,并对与结点 u 相邻的结点中,深度比结点 u 小的结点重复操作1。
操作2:让结点 u 的权值增加 k ,并对与结点 u 相邻的结点中,深度比结点 u 大的结点重复操作2。
小可想知道,经过 q 次操作之后,所有的节点的权值是多少(模1e9+7)。
大致就是,给你一棵树,树中有一些虚假边只连接点和点,使它们成为相邻的点
现在给你一些操作,分为操作1:将自身相邻的深度大的点增加权值,并对该点再次进行操作1,操作2:将自身相邻的深度小的点增加权值,并对该点再次进行操作2(类深搜),求出经过操作后的数的所有节点的权值是多少
p3:这题赛上更是惨烈,看到20%的数据只有操作2且没有虚假边,那我直接对每个需要进行操作的点打一次深搜,遍历到的点权值全部增加,连深搜求深度都不需要,多么完美啊
完美到直接暴0,我搜个蛋
正确的骗分思路是暴搜(10pts)
首先先建树,建树完了直接深搜打深度表(由于下一步连虚假边会影响深度,所以先打好表),连虚假边,开始操作打暴搜求出答案,输出结束(可过样例)
p4:每次操作都需要权值的累加显然超时,正解为懒处理(lazy算法),这里可参考2023年csp-j真题T3部署,采用相同策略
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+5;
const int mod=1e9+7;
int n,m,q,a[N],depth[N];
ll lazy1[N],lazy2[N];
vector<int> G[N];
vector<pair<int,int> > g;
void dfs(int u,int fa,int d) {g.push_back({d,u});depth[u]=d;for(int v:G[u]) {if(v==fa) continue;dfs(v,u,d+1);}
}
int main() {scanf("%d%d%d",&n,&m,&q);for(int i=1; i<=n; i++) scanf("%d",&a[i]);for(int i=1; i<n; i++) {int u,v;scanf("%d%d",&u,&v);G[u].push_back(v),G[v].push_back(u);}dfs(1,1,1);//因为m条虚边不影响深度,先求深度for(int i=1; i<=m; i++) { //m条虚边int u,v;scanf("%d%d",&u,&v);G[u].push_back(v),G[v].push_back(u);}while(q--) { //做q次操作,但是加法互不影响,所以每个点先记下来一起做int opt,u,k;scanf("%d%d%d",&opt,&u,&k);if(opt==1) lazy1[u]=(lazy1[u]+k)%mod;else lazy2[u]=(lazy2[u]+k)%mod;}sort(g.begin(),g.end());//按深度从小到大排序for(pair<int,int> p:g) { //先做所有操作2,因为深度小的不会被别的影响int u=p.second;for(int v:G[u]) { //操作2只向更深的点传递if(depth[u]<depth[v]) lazy2[v]=(lazy2[v]+lazy2[u])%mod;}}reverse(g.begin(),g.end());//翻转一下,就成了按照深度从大到小排序for(pair<int,int> p:g) { //同理,这里做操作1int u=p.second;for(int v:G[u]) { //操作1只向更浅的点传递if(depth[u]>depth[v]) lazy1[v]=(lazy1[v]+lazy1[u])%mod;}}for(int i=1; i<=n; i++) { //输出,原来的点权+操作1获得的总和+操作2获得的总和printf("%lld ",(a[i]+lazy1[i]+lazy2[i])%mod);}return 0;
}
七,结语
真切地希望自己补药再失误了,但其实失误也是实力欠缺的一部分,好好练吧,总有一天能够抵达成功彼岸的,对自己有信心一些,以良好的心情迎接每一天,要说考试上的话就是long long补药再错了,脑瓜子别卡死了,多想反例,多造样例,OI人永不服输!
八,祝福
祝:平安顺遂,顺顺利利!