2023第五届CCPC河南省赛经历和题解
经历
算法生涯第一场比赛正好是三年以来第一次线下省赛,因为学校不报销(QAQ),我和我们班两个同学三个大一一队跟着学长学姐一队一起自费趁着周六日假期从洛阳跑到郑州打比赛。
周六晚上六点十六的火车,下着雨三个人带了一把雨伞快六点了还在隧道里堵着,过安检时太激动工作人员扫一只胳膊我就想冲被拽回来了,转个身扫背面我又想冲又被拽回来了,估计给人家工作人员都搞懵了hhh,发车前一分钟正好赶到,这波运气可能也预示着我们拿银尾的运气哈哈哈。
晚上吃了顿火锅回酒店睡觉,我和我的队友lzh都醒了十几次(lzh呼噜隔张床都快震死我了,可能我的呼噜也是哈哈哈),睡着的时间加一起估计不到仨小时,不过hzx睡得倒真死(可能是猪变的)。
早早起来去郑轻参加比赛,参加了开幕式,看得出来人家学校真的很重视,什么时候我的学校也能重视一下,起码报名费总能报销一下吧(狗头保命),开幕式结束后就是重头戏五个小时的比赛。现场赛的氛围非常棒,没法拍照记录真的太可惜了,中午志愿者发冷餐有火腿酸奶还有面包蛋糕(蛋糕真的好吃)。
比完赛就去参加赛题分享和颁奖仪式,没有入场券但是跟着别的学校教练溜进去了,最后队友们也都进来了。滚榜的时候真刺激,铜牌马上要滚到我们了突然停了,再多滚一下就会爆炸,卡在了银奖最后一名太爽了,特别有趣的是坐我们前面的那一队是金奖最后一名,欢呼的时候旁边有个老师都笑了,真是风水宝地哈哈哈。
从火车卡最后一分钟的点上车到银尾尖尖,运气绝佳地结束了我们队第一次算法竞赛,火车坐了俩小时又睡了不到俩小时中午又连着五小时高强度竞赛可真累。不过不得不说,郑轻的志愿者和老师教练团队真的很负责,领导也很重视,好几年一直在这举办也是有道理的,总之是不错的线下赛体验呢。
复盘
A签到hzx直接上机写但是交的时候没看清题面不小心WA了一发(下次一定要把题看清),接着看出来E是dp但是dp学的不好推不出来方程,跟hzx讨论后hzx拿dfs写但是一直TLE突然发现时间复杂度不对又改成记忆化搜索但是直接WA了,最后也没想出来转移方程耽误一俩小时。hzx写E时我和lzh看了F题感觉能写,讨论了一会想暴力突然发现复杂度会爆,但是那么多人过感觉不会太难,突然想到用滑动窗口,但是因为我和lzh都不怎么会单调队列,就hzx比较熟悉,我讲的不好他一直没听懂思路大家都很着急,最后我写一段他写一段看着代码讨论半天硬合,又是看边界又是打印中间值,调了半天总算是过了,憋了半天我们仨直接狂奔厕所了。这个时候lzh开的H题也差不多了,这个题比赛时我是没看,找规律推式子能力很垃圾,交的时候有个小点卡住WA了,造了好几个样例都没发现问题,最后lzh跟hzx讲了他思路俩人调了半天总算发现问题过了。这个时候大概还剩下俩小时,看看排名七八十还可以,想着再过一道就稳银了,又感觉E的dp能写但是看了半天没思路又看见学长也写了不短时间才过就放弃了,在这个题上浪费了太多时间。然后我觉得B也不少人过于是去看B但是一直没啥思路,lzh觉得K能写就去开了K构造题但是也没明显思路,看G通过率25%是个大模拟机子空出来兴哥说估计要一个多小时直接上机写,写了一会突然发现思路小错要重改就不想写了,基本全员都陷入零进展了。最后一小时封榜一看排名87,约莫着应该能拿个银尾但是不太稳,我看B题换了两三种思路但都没有想出来,就剩一小时了hzx感觉模拟写不完了于是跟lzh一起看K题最后很可惜也没出赛后发现就是偶数反了不然直接过了,剩下的十分钟大概就是研究排名看看能滑多少名祈祷能有银牌哈哈哈。
题解
Problem A. 小水獭游河南
判断字符串是否由前部分是不重复的小写字母并且后部分是回文串拼接而成,因为小写字母一共26个所以直接从前往后暴力枚举26种情况遇到重复字母就跳出,每种情况判断后面是不是回文串。需要注意的是字符串必须同时包含两部分。
#include <bits/stdc++.h>
using namespace std;
void solve() {
bool cnt[30] = {false}, res = false;
string s; cin >> s;
cnt[s[0] - 'a'] = true;
for (int i = 1; i <= 26 && i < s.size(); i++) {
int l = i, r = s.size() - 1;
for (; l < r; l++, r--) {
if (s[l] != s[r]) break;
}
if (l >= r) {
res = true;
break;
}
if (cnt[s[i] - 'a'] == true) break;
else cnt[s[i] - 'a'] = true;
}
if (res) cout << "HE" << endl;
else cout << "NaN" << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int t; cin >> t;
for (int i = 0; i < t; i++) {
solve();
}
return 0;
}
Problem B. Art for Rest
易知,当k合法存在时,每一段都会满足:该段的最大值小于下一段的最小值。
则预处理每一段的最小值和最大值,遍历k进行合法性检验,可以开一个数组存放该处是否合法,可以证明预处理时间复杂度为O(n),遍历k时间复杂度为调和级数O(log n)。
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N], maxh[N], minh[N], st[N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
int maxv = -1;
for (int i = 1; i <= n; i++)//存储最大值
{
cin >> a[i];
if (a[i] > maxv)
maxv = a[i], maxh[i] = a[i];
else
maxh[i] = maxv;
}
int minv = 1e9 + 10;
for (int i = n ; i > 0; i--)//存储最小值
{
if (a[i] < minv)
minv = a[i], minh[i] = a[i];
else
minh[i] = minv;
}
//存储该点是否合法
for (int i = 1; i < n; i++)
if (maxh[i] <= minh[i + 1])
st[i] = 1;
int ans = 1;
for (int k = 1; k <= n - 1; k++)
{
bool flag = 1;
for(int i=k;i<=n-1;i+=k)
if (st[i] == 0)
{
flag = 0;
break;
}
if (flag)
ans++;
}
cout << ans;
}
Problem C. Toxel 与随机数生成器
题意即是让我们判断一个1e6的字符串是否出现长度在1e3到1e4的连续重复子串,可以想到KMP的next数组中存储的值即是当前匹配位置的最长相同前后缀长度,所以我们只需要求出这个字符串的next数组,遍历next数组的值找最大值,根据官方题解只要这个最大值大于50就有很大把握判断是错误代码了。
#include <iostream>
using namespace std;
#define int long long
const int N = 1000010;
int ne[N];
char s[N];
bool flag;
void solve() {
cin >> s + 1;
for (int i = 2, j = 0; i < N; i++) {
while (j && s[i] != s[j + 1]) j = ne[j];
if (s[i] == s[j + 1]) j++;
ne[i] = j;
}
for (int i = 1; i < N; i++) {
if (ne[i] > 1000) flag = true;
}
if (flag) cout << "No";
else cout << "Yes";
}
signed main() {
int t = 1;
// cin >> t;
for (int i = 0; i < t; i++) {
solve();
}
return 0;
}
Problem E. 矩阵游戏
题意很简单,就是让我们寻找出一条修改后含有1最多的路径,首先我们会想到dfs,但数据范围太大n!的复杂度一定会爆,自然我们就想到dp来优化我们的搜索。
首先,我们的每次决策的状态都是由三个维度所决定的,即
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k](行,列,可以将?转化为1的次数)。而转移方程就更简单了,如果当前格子是1,则
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
[
k
]
,
d
p
[
i
]
[
j
−
1
]
[
k
]
)
+
1
max(dp[i-1][j][k],dp[i][j-1][k])+1
max(dp[i−1][j][k],dp[i][j−1][k])+1,如果当前格子是0,则
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
[
k
]
,
d
p
[
i
]
[
j
−
1
]
[
k
]
)
max(dp[i-1][j][k],dp[i][j-1][k])
max(dp[i−1][j][k],dp[i][j−1][k]),如果当前格子是
?
?
?,则可以参考01背包的处理,每个
?
?
?都有选或不选两种决策,如果k>0,
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
[
k
]
,
d
p
[
i
]
[
j
−
1
]
[
k
]
)
+
1
max(dp[i-1][j][k],dp[i][j-1][k])+1
max(dp[i−1][j][k],dp[i][j−1][k])+1,否则
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
[
k
]
,
d
p
[
i
]
[
j
−
1
]
[
k
]
)
max(dp[i-1][j][k],dp[i][j-1][k])
max(dp[i−1][j][k],dp[i][j−1][k])
但是,500^3一定会MLE,我们发现每一次状态转移都是只使用上一层的数据,所以我们很自然地就可以优化掉一维,类似01背包,把
i
i
i取掉,将k倒序遍历即可。
对了,每次别忘记初始化边界
#include<iostream>
#include<cstring>
using namespace std;
char a[505][505];
int f[505][1005];
int n, m, x;
bool cheek(int x, int y)
{
if (x >= 1 && x <= n && y >= 1 && y <= m) return true;
else return false;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int tt;
cin >> tt;
while (tt--)
{
cin >> n >> m >> x;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> a[i][j];
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= x; k++) {
f[j][k] = -0x3f3f3f3f;
}
}
//初始化边界
if (a[1][1] == '1') {
f[1][0] = 1;
}
else if (a[1][1] == '0') {
f[1][0] = 0;
}
else {
f[1][1] = 1;
f[1][0] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int k = x; k >= 0; k--) {
if (a[i][j] == '1') {
if (cheek(i - 1, j)) {
f[j][k] = max(f[j][k], f[j][k] + 1);
}
if (cheek(i, j - 1)) {
f[j][k] = max(f[j][k], f[j - 1][k] + 1);
}
}
else if (a[i][j] == '0') {
if (cheek(i, j - 1)) {
f[j][k] = max(f[j][k], f[j - 1][k]);
}
}
else {
if (cheek(i - 1, j)) {
if (k - 1 >= 0) f[j][k] = max(f[j][k], f[j][k - 1] + 1);
}
if (cheek(i, j - 1)) {
f[j][k] = max(f[j - 1][k], f[j][k]);
if (k - 1 >= 0)f[j][k] = max(f[j][k], f[j - 1][k - 1] + 1);
}
}
}
}
}
int ans = 0;
for (int i = 0; i <= x; i++) {
ans = max(ans, f[m][i]);
}
cout << ans << endl;
}
}
Problem F. Art for Last
题意是在长度为n的序列中选k个数,求这k个数里面找出两个数之间差的最大值和两个数之间差的最小值的乘积的最小值。所以排个序枚举所有长度为k的区间,最大值为端点之差,最小值可以用滑动窗口来维护,找到乘积最小的就是答案。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 5e5 + 10;
int a[N], s[N], q[N], minh[N];
long long ans = 1e18 + 10;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
for (int i = 1; i < n; i++) s[i] = a[i] - a[i - 1];
int hh = 0, tt = -1;
for (int i = 1; i < n; i++) {
if (hh <= tt && q[hh] <= i - (k - 1)) hh++;
while (hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
minh[i] = s[q[hh]];
}
for (int i = k-1; i < n; i++) {
int maxv = a[i] - a[i - k + 1];
int minv = minh[i];
ans = min(ans, (long long)maxv * minv);
}
cout << ans;
}
Problem H. Travel Begins
题意是给定一个大小为
n
n
n 的值把这个值分成
k
k
k 个数,小数部分小于0.5的数的小数都会被舍去,所以我们让一个数的小数部分越从下接近0.5就会浪费掉越多的值,和就会取到最小值,让每个值都为0.5就会节约最多的值,和就会取到最大值。
#include<iostream>
#include<cmath>
using namespace std;
void solve() {
int n, k;
cin >> n >> k;
if (k > 2 * n) cout << 0 << " " << 2 * a << endl;
else
cout << (int)ceil(n - (k - 1) * 0.5) << " "
<< (int)ceil(n - (k - 1) * 0.5) + k - 1 << endl;
}
int main() {
int t; cin >> t;
for (int i = 0; i < t; i++) {
solve();
}
return 0;
}
Problem K. 排列与质数
很容易发现一个规律:所有偶数连着排一起和奇数连着排一起都是相差二,所以在满足条件的序列中一定大方向是以二做基准的,再次观察,我们发现当n是偶数时:1,3,5…n-3,n,n-2,n-4…8,6,4,当n是奇数时:1,3 5…n-3,n,n-3,n-5…8,6,4
可以发现只有2和n-1没有被插入,2可以插入5和7之间,n-1可以插入n-4,n-6之间。
即题目一定有解,只需n-4!=n-6,即n>=11即可。
小于11直接暴力依照上法构建即可
#include<iostream>
using namespace std;
int a[100005];
int main()
{
int n;
cin >> n;
if (n == 1 || n == 2 || n == 3 || n == 4)
cout << -1;
else if (n == 5)
cout << "4 1 3 5 2";
else if (n == 6)
cout << "1 3 5 2 4 6";
else if (n == 7)
cout << "1 6 3 5 2 7 4";
else if (n == 8)
cout << "1 3 5 7 2 4 6 8";
else if (n == 9)
cout << "1 3 8 5 2 7 9 6 4";
else if (n == 10)
cout << "1 3 5 2 7 10 8 6 9 4";
else if (n == 11)
cout << "1 3 5 10 7 2 9 11 8 6 4";
else
{
if (n & 1)
{
for (int i = 1; i <= n; i += 2)
if (i == 5)
cout << "5 2 ";
else if (i == n - 6)
cout << n - 6 << " " << n - 1 << " ";
else
cout << i << " ";
for (int i = n - 3; i >= 4; i -= 2)
cout << i << " ";
}
else
{
for (int i = 1; i <= n-3; i += 2)
if (i == 5)
cout << "5 2 ";
else
cout << i << " ";
for (int i = n; i >= 4; i -= 2)
if (i == n - 4)
cout << n - 4 << " " << n - 1 << " ";
else
cout << i << " ";
}
}
}