2023年莆田市C++专项第三期学员友谊赛题解

本次比赛共有 $80$ 人提交代码,最高分 $375$,$200$ 分及以上 $15$ 人。

恭喜俞伯洋同学获得本次比赛第一名!

| | 提交数 | 通过数 | 通过率 | 与出题组预期相比 |

| :-----------: | :-----------: | :-----------: | :-----------: | :-----------: |

| A | $140$ | $16$ | $11.43\%$ | 略低于预期 |

| B | $131$ | $4$ | $3.05\%$ | 符合预期 |

| C | $109$ | $0$ | $0\%$ | 远低于预期 |

| D | $139$ | $4$ | $2.88\%$ | 符合预期 |

# 比赛出锅:

- C 数据强度不够,导致有人通过奇葩骗分算法得到 $40$ 分。

- D 题样例 #3 有误。

在这里出题组向大家道歉。

下面是本次比赛的题解。

# A

出题&题解:by tyx。

判断单曲循环很简单,难点在于后面两种模式。

要想知道这个歌单是什么播放模式,就必须先找出 $k$。我们可以依次枚举 $k$ 的值,但很显然这种算法的时间复杂度是平方级别,只能得到 50pts。

想要 AC,就必须使用 $O(n)$ 做法。经过推理,我们可以证明,如果该歌单是列表循环模式,$k$ 的值是唯一的。证明如下:

- 假设存在一个 $k$ 满足列表循环的条件。

- 对于每一个小于 $k$ 的正整数 $k'$,由于 $a_1=a_{k+1}$ 且 $a_1$ 到 $a_k$ 的所有数均不相同,所以 $a_1$ 和 $a_{k'+1}$ 一定不等(因为 $k'<k$,所以 $k'+1 \le k$)。因此 $k'$ 不满足条件。

- 对于每一个大于 $k$ 的正整数 $k''$,由于 $a_1=a_{k+1}$ 且 $a_1$ 到 $a_k$ 的所有数均不相同,所以 $a_1$ 至 $a_{k''+1}$ 中一定至少有两个数相等(因为 $k''>k$,所以 $k+1 \le k''$)。因此 $k''$ 不满足条件。

- 所以,$k$ 的值唯一。

因此我们在枚举时,只要找到第一个满足 $a_1$ 到 $a_i$ 全部不等且 $a_1=a_{i+1}$ 的数,就可以终止枚举了。此时我们再判断 $i$ 是否满足题目条件即可,时间复杂度 $O(n)$。

最后还有一个要注意的点,就是 $a_1$ 到 $a_n$ 全部不等时,我们可以选择 $k=n$,因此这个列表的播放方式是列表循环。

## AC code:

```cpp

#include<bits/stdc++.h>

using namespace std;

int T,n,t[200005],k;

bool book[200005];

int main()

{

cin>>T;

while(T--)

{

k=0;

cin>>n;

for(int i=1;i<=n;i++)

cin>>t[i];

for(int i=1;i<=n&&book[t[i]]==0;i++)

{

book[t[i]]=1;

k++;

}

for(int i=1;i<=k;i++)

{

for(int j=1;i+j*k<=n;j++)

{

if(t[i]!=t[i+j*k])

{

cout<<"3\n";

goto end;

}

}

}

if(k==1)

cout<<"1\n";

else

cout<<"2\n";

end:1;

for(int i=1;i<=n;i++)

book[i]=0;

}

return 0;

}

```

# B

出题&题解:by hzf。

先分析一下这道题。

先想一想朴素算法,从大到小枚举 $b$,$O(n)$ 判断,总复杂度为 $O(nm)$,预计 50pts。

接下来思考正解。

假设 $b$ 与 $a$ 中的每个数都互质,那么对于每个 $a_i=c_{i_1}^{d_{i_1}}c_{i_2}^{d_{i_2}}...c_{i_k}^{d_{i_k}}$(其中 $c_{i_j}$ 为 $a_i$ 的质因数),都有 $b$ 不为 $c_{i_j}$ 的倍数,那么只需要标记所有 $c_{i_j}$ 的倍数即可。

在标记完后,我们从 $m$ 到 $1$ 枚举 $b$ 的值,如果未被标记便输出 $b$(这一部分的时间复杂度为 $O(m)$)。

然后就是求出所有 $c_{i_j}$。

### 算法 1:

将每个 $a_i$ 分解质因数,然后依次标记,这样的最劣复杂度大约是 $O(nm\sqrt{m})$。

### 算法 2:

在**算法 1** 的基础上优化一下,存储所有的 $c_{i_j}$,那么时间复杂度就是 $O(n\sqrt m)$。

### 算法 3:

我们能否枚举 $c_{i_j}$ 呢?

答案是可以的,在跑质数筛的同时,若 $i$ 为质数,则判断 $ij$ 是否为 $a$ 中任意一个数(其中 $j$ 为正整数且 $i×j \le m$),如果是则标记 $i$ 的所有倍数。该算法的时间复杂度为 $O(m)$。

因此,我们便解决了这道题。

## AC code:

```cpp

#include<bits/stdc++.h>

#define int long long

using namespace std;

int n,m;

bool book[5000005],vis[5000005],pd[5000005];

main(){

cin>>n>>m;

while(n--){int a;cin>>a,vis[a]=true;}

for(int i=2;i<=m;++i)if(book[i]==false){

book[i]=true;

for(int j=1;i*j<=m;++j)if(vis[i*j]==true){

for(int v=1;i*v<=m;++v)pd[i*v]=true;

break;

}

for(int j=1;i*j<=m;++j)book[i*j]=true;

}

for(int i=m;i>=1;--i)if(pd[i]==false){cout<<i;return 0;}

return 0;

}

```

# C

出题:by tyx&hzf。

题解:by hzf。

先简要分析:

这题显然是一道 $dp$ 题,我们可以用 $f_{i,j,k}$ 来存储三种操作的数量,即使用 $i$ 次与,$j$ 次或,$k$ 次异或。

注意,此处有一错误做法,就是用 $f_{i,j,k}$ 来存储最优状态,以 $(7 \oplus 8) > (8 \oplus 8)$ 为例解释。

那么如何存储呢?

注意,$a_i \le 2^8-1$,所以所有数操作后的结果必然小于 $64$,由于 $x,y,z \le 100$,因此可以定义一个四维 `bool` 数组 $f$,$f_{a,i,j,k}=1$,表示使用 $i$ 次与,$j$ 次或,$k$ 次异或后可以得到 $a$ 。

那么该如何转移呢?

当 $f_{a,i,j,k}=1$ 时:

- $f_{a \land A_{i+j+k+1},i+1,j,k}=1$

- $f_{a \lor A_{i+j+k+1},i,j+1,k}=1$

- $f_{a \oplus A_{i+j+k+1},i,j,k+1}=1$

其中 $f_{0,0,0,0}=1$ 为动态规划边界。

最后,找到最大的满足 $f_{a,x,y,z}=1$ 的 $a$ 并输出即可。

## AC code:

```cpp

#include<bits/stdc++.h>

using namespace std;

int n,x,y,z,a;

bool f[64][105][105][105];

int main(){

cin>>n>>x>>y>>z>>a,f[a][0][0][0]=true;

for(int i=2;i<=n;++i){

cin>>a;

for(int j=0;j<i;++j)for(int v=0;j+v<i;++v){

int k=i-1-j-v;

if(k<=z&&j<=x&&v<=y)for(int b=0;b<=63;++b){

if(j>0&&f[b][j-1][v][k])f[b&a][j][v][k]=true;

if(v>0&&f[b][j][v-1][k])f[b|a][j][v][k]=true;

if(k>0&&f[b][j][v][k-1])f[b^a][j][v][k]=true;

}

}

}

for(int i=63;i>=0;--i)if(f[i][x][y][z]){cout<<i;return 0;}

return 0;

}

```

# D

出题:by hzf。

题解:by tyx。

此题初看似乎有些难以入手,但其实核心思路并不难。

首先这个 D 类串中每个字符的选取都是有范围的,比如在 `abcde` 这个字符串中找到一个长度为 $2$ 的 D 类串,那么这个 D 类串的第一位肯定不会是 `e`,第二位肯定不会是 `a`。可以证明,对于 D 类串的第 $i$ 个字符,其选取范围是 $s_{i-1}$ 至 $s_{n-k+i-1}$,这里 $s$ 的下标从 $0$ 开始。

由于我们要选择的 D 类串要满足字典序最大,所以根据贪心策略,D 类串的第 $1$ 个字符肯定为 $s_0$ 到 $s_{n-k}$ 中最大值,如果有最大值肯定选择下标最小的一个,正确性请读者自己证明。

找到 D 类串的第 $1$ 个字符后,由于第 $2$ 个字符在第 $1$ 个字符之后,所以我们从 D 类串第 $1$ 个字符在 $s$ 中的下标开始枚举。接下来一直枚举到 第 $k$ 个字符。

此方法最劣时间复杂度 $O(kn)$,可以得到 70pts。如果加上特判和卡常可以得到 80pts。

## 70pts code:

```cpp

#include<bits/stdc++.h>

using namespace std;

int n,k,sum,maxn;

string s;

int main()

{

cin>>n>>k>>s;

if(k==0)

return 0;

for(int i=1;i<=k;i++)

{

maxn=sum;

sum++;

for(int j=sum-1;j<n-k+i;j++)

{

if(s[j]>s[maxn])

{

maxn=j;

sum=j+1;

}

}

cout<<s[maxn];

}

return 0;

}

```

思考正解。

上面的代码为什么会 TLE?例如在 `yxwvwhfreuowregohrwoieigithigrigu` 这个字符串中找出一个长度为 $2$ 的 D 类串,其实我们在 $s_0$ 就已经找到 D 类串的第一个字符了,但是我们要枚举到倒数第二位的 `g`,而且在找 D 类串的第二个字符时重新从 $s_1$ 开始枚举,这样浪费了很多时间。

那应该怎么办呢?

假如我们给定了一个字符串 `ppppppphzfakioi`,在其中找出一个长度为 $7$ 的 D 类串。此时我们可以发现:由于 `h` 的后面有一个 `z`,所以我们在所有选取范围包含 `z` 的 D 类串中的字符时,肯定不会选到 `h`(因为 `h` 比 `z` 小且比 `z` 先失效)。因此当 `z` 进入选取范围时,我们就不用再考虑是否要选择 `h` 了,这和单调队列的性质很相似。因此此题可以使用单调队列解题,当一个字符入队时弹出所有小于它的字符。

## AC code:

```cpp

#include<bits/stdc++.h>

using namespace std;

int n,k;

string s;

deque<int>q;

int main()

{

cin>>n>>k>>s;

for(int i=0;i<n;i++)

{

while(!q.empty()&&s[q.back()]<s[i])

q.pop_back();

q.emplace_back(i);

if(i>n-k-1)

{

cout<<s[q.front()];

q.pop_front();

}

}

return 0;

}

```

此题也可以使用 ST 表解决,代码如下:

```cpp

#include<bits/stdc++.h>

using namespace std;

int n,k,Max[200005][25];

string s;

inline int fin(int l,int r){

int lg=std::log2(r-l+1);

if(s[Max[l][lg]]>=s[Max[r-(1<<lg)+1][lg]])return Max[l][lg];

return Max[r-(1<<lg)+1][lg];

}

int main(){

cin>>n>>k>>s,s=' '+s;

for(int i=1;i<=n;++i)Max[i][0]=i;

for(int i=1;i<=20;++i)for(int j=1;j+(1<<i)-1<=n;++j){

if(s[Max[j][i-1]]>=s[Max[j+(1<<(i-1))][i-1]])Max[j][i]=Max[j][i-1];

else Max[j][i]=Max[j+(1<<(i-1))][i-1];

}

int now=1;

for(int i=1;i<=k;++i)now=fin(now,n-k+i),cout<<s[now++];

return 0;

}

```

当然也可以使用线段树等数据结构,这里不放代码,留给读者自行思考。

全部评论

相关推荐

后端测试双料特工:这能忍住不骂?
找实习记录
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务