CUGBACM 春季训练1部分题解

队员李玮宸

本人高中零基础,是从大学才开始系统学习算法的,目前进度覆盖不全,所以后面几题能力有限,只写我会的。

A题—因为开题之前几分钟队长发了题单了,所以我先把这个写了,成功拿下一血

E题—我先看的B题,没看懂,就先找另一个A题,这个挺简单,脑筋急转弯,中间隔一个就行,就是2*x-1,成功拿下一血

B题—最难的是读题,第一次看全英文题面,有些句子理解地不顺畅(英语不好,我已急哭),不过其实看数字和样例可以大大加深理解程度,其实就是取一个最小值的问题,成功拿下一血

F题—另一个b题,哈希表存次数,然后为了防止麻烦的条件判断,可以新开一个字符串,成功拿下一血

string s1="";
for(int i=0;i<s.size();i++){
   if(mp[s[i]]!=maxn){
    s1+=s[i];
   }
}

截止到这里,都挺顺利的,看了看表,前四个题一共花了26分钟,稳居榜一,然后不出意外的话意外就要来了

G题

题目大意是:给你一个全是大写字母的字符串,每次你只能对它进行两种操作,要么从中加上一个A,要么从中删除一个A,再给你一个新字符串,问你能不能从原字符串变成新字符串,能的话输出最少几次可以变成。

这个对我来说看似不太简单,实则也确实有点难,事实上我一直是字符串模拟苦手,不过这个还好,比较考思维。这是第一个比较卡我的题,卡了大概半个小时

这个题的要点主要有两个,一个是判断能不能变成

对于这个来说,因为我们每次都可以对A进行操作,所以主要就是看A以外的字母。

首先,你不能有新的字母,必须是同一组字母,因为你只能对A操作

其次,非A字母出现的顺序也得一样,不然不可能变。

其实分析到这里,整个题目的流程已经可以想象出来了:

我们只需要把A当初分隔符,那么就是

S1:###B#####C##D##…

S2:#B##C######D#…

只有这样,才能在只对中间的分隔符操作的情况下变成另一个

在这里,我开了两个<int,char>哈希表,存S1和S2的非A字母和它的出现下标,如果把字母放在KEY位置,会重复,其实这里pair也可以,然后只需要比对mp1==mp2即可了

其二,也是这个题的核心逻辑:比对每个字母之间A的个数的差,多了就删,少了就加,反正想要变成新的字符串,只用把中间差的绝对值算出来就行

开四个数字,分别存S1和S2的非A字母的下标,以及它每个字母之间的差值(差分数组,算一下差就行)

int pos1[N];
int pos2[N];
int pos3[N];
int pos4[N];

然后,由于一开始也有可能有A,所以我们把pos1和2的[0]设成1,后面相减的时候就可以直接算出第一个字母前面有几个A

int cnt=0;
pos1[0]=1;
pos2[0]=1;
for(int i=0;i<s.size();i++){
     t[s[i]-'A'+1]=1;
     if(s[i]!='A'){
         mp[++cnt]=s[i];
         pos1[cnt]=i+1;
         pos3[cnt]=pos1[cnt]-pos1[cnt-1]; //这个就是存每个之间有几个A
     }
}

注意,字母后面有可能有A,所以我们把cnt算出来之后,还需考虑最后有几个A

pos3[cnt+1]=s.size()-pos1[cnt];

然后再跑一遍S2

cnt=0;  //记得归零
string news;
cin>>news;
int flag=0;
for(int i=0;i<news.size();i++){
    if(t[news[i]-'A'+1]==0)flag=1; //这里我当时直接开桶数组判断了,但回过神来好像直接比较mp就可以,🍬完了
    if(news[i]!='A'){
        mp1[++cnt]=news[i];
        pos2[cnt]=i+1;
        pos4[cnt]=pos2[cnt]-pos2[cnt-1];
    }
}
pos4[cnt+1]=news.size()-pos2[cnt];

然后,把每个pos3pos4数组对应的求差就行了

if(flag==1)cout<<-1<<endl;
else if(mp!=mp1)cout<<-1<<endl;
else{
    int ans=0;
    for(int i=1;i<=cnt+1;i++){ //记得开到cnt+1
        ans+=abs(pos3[i]-pos4[i]);
    }
    cout<<ans<<endl;
}

这个其实理清逻辑也还好,稳健一点不难想,难的是各种边界,嗯,到这里之后成功拿下五个一血,回归榜一,感觉良好

然后,咚咚咚

C题和D题

C题我看了一遍题目,看完能理解,关键是我一看这不是动态区间查询吗?我也没学线段树啊,想套st表,不过忘干净了…然后就放弃了,对K<=5没有一点想法

D题重量级的来了,本来我在这里五个一血,时间是绝对优势的,到此时也才进行了一个小时出头,然后我就去刚D题了:

这是一道树上DFS,大意是给了一颗树,每个节点有它的权值,问你每个节点,从1到这个节点的路径(不走回头路,一气呵成),如果路径上的节点有重复值,输出Yes,否则No

DFS我熟啊,然后直接上手了,然后就懵了,看来还是不能纸上谈兵,一上实战就露出马脚了

一开始,我甚至是开vis数组防止回头的,丝毫没想多传入一个fa就行了,然后又想的是怎么开哈希表标记权值,具体的我已经忘了,只记得逻辑一团乱麻,甚至还想每一次都跑一遍DFS

事实上,DFS核心的在哪return,在哪标记,在哪回溯我其实没有掌握好

最后几分钟甚至已经祭出AI了,还是没怎么明白

接下来我以我的视角讲一下这个题我最后的正确解法

int n;
int wei[N];
vector<int> adj[N];
bool flag[N]; //这个数组用来记录从1到i的路径是不是有重复

然后,就是最核心的DFS

void dfs(int x,int fa){ //传入的一个是节点,一个是父节点,用来防止往回走
    mp[wei[x]]++; //在这里把节点权值入哈希表
    if(mp[wei[x]]>1||flag[fa])flag[x]=1; //最抽象的一点:首先必须这个点的次数要大于1才能有重复,其次,如果前面以及有点是重复的了,就可以直接大大方方标记
    else flag[x]=0; 
    for(auto i:adj[x]){
        if(i==fa)continue;
        dfs(i,x);
    }
    mp[wei[x]]--; //回溯状态,需要跟标记对称,所以放到for循环外面
    return;
}

为什么我没想开,一个是我在标记和回溯的位置上卡住了,一个是flag数组的判断卡住了

经过和AI的深入交流,我现在对DFS的标记有了更深的理解:

DFS里,可以把整个函数体想象成一个房间,把for循环想象成房间里的几个分支,在我们进行树上DFS的时候,每次访问的都是一个节点,也就是一个独立的房间,所以可以放在for循环外面。为什么说可以呢?因为放在里面也不是不行,但是需要特殊判断,不然就把起点略过了,而且不够直观。

当我们传入dfs的时候,实际上传入了一个节点,这个节点是有具体物理意义的实体,是一个实例化对象,我们在循环外面标记,在循环外面回溯,是没有任何问题的,因为操作的是这个对象本身具有的属性。

而如果dfs传入的是一种抽象的,不具有实际意义的状态,那么就不能在for循环外面操作

例如:

void dfs(int step){
    if(step>n){
        for(int i=1;i<=n;i++){
            cout<<path[i]<<" ";
        }
        cout<<endl;
        return;
    }
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            path[step]=i;
            vis[i]=1;
            dfs(step+1);
            vis[i]=0;
        }
    }
}

这是一段全排列的代码,我们可以看到它传入的是step,意味着这是当前第几步了,给一个dfs传入step的时候,你能对它的属性访问吗?不能,比如vis数组,vis[step]是没有意义的,vis存的是当前下标的元素是否访问了,而vis数组的操作,很明显是只有在for循环里面,当我们遍历每个节点i的时候才能进行的。

想清楚这个之后,接下来就通关了

H题—H题是第二天补的,也是想了很久,没有想出来,其实只有一步之遥了,后来才知道这个是典型的贪心求特定子序列

最开始我的想法是:

开一个哈希表<char,int>;

遇到A了,mpa++,遇到B了,如果mpa存在,就让mpb++,遇到c了,如果mpb存在,就让mpc++

这样可以保证先后顺序,然后判断一下如果mpa,b,c都有,就cnt++,各种–就行了

关键的是:

例如:ABBBBACCCCA

A存进去,B存到4,A存到2,C存到1,走掉,这时候A变成1,B变成3,C又可以存进去了,又可以走了,发现没有,是存在A在B后面的情况的。

这道题的正统写法应该是:

开一个数组numa记录A,遇到A就++,再开一个数组numab记录AB,然后遇到B,如果A已经存在,就让numab++,把numa–,然后遇到C以此类推,把最后ABC的++,AB的–

为什么可以这么做呢?可以想象一个篮子,装A,如果有B了,就把A拿出来一个撞到B里面,组合成AB,那么A的篮子自然少一个,如果遇到C了,就把AB里面拿出来一个AB,组成ABC,AB篮子自然少了一个,这相当于是模拟了三个栈,一个A栈,一个AB栈,一个ABC栈。

那么我的写法自然也可以很简单的改正,只需要在mpb++时mpa–,mpc++时mpb–就行了

string s;
int cnt;
int na,nab;
int main(){
    cin>>s;
    for(int i=0;i<s.size();i++){
        if(s[i]=='A')na++;
        else if(na>0&&s[i]=='B'){
            na--;
            nab++;
        }
        else if(nab>0&&s[i]=='C'){
            nab--;
            cnt++;
        }
    }
    cout<<cnt<<endl;
    return 0;
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐