CSP-J 2024 游寄&题解
省流:这是一个没 AK 的蒟蒻/kel
游寄
初赛糖没边了,无脑倒贴分,拿到了 J 组 86,S 组 80.5 的逆天成绩。
考前随便翻了翻历年 T4,感觉我不会啊!
(事实证明,确实如此)
考前一天,我妈不知道从哪儿来的野消息,说可以带零食进考场,我还是拒绝了。进考场后盯着监考员一个一个地说:“零食不能带”……
我是用不惯 Linux 的,还是用了 Windows 系统作答,反正也挂不到哪里去。
开考了。手还是有些抖啊,毕竟没啥经验,但还是很快适应了。
T1 shaber 题。
T2 shaber 题。
T3 感觉好熟啊。随便贪就行了吧。大样例都过了,但成功构造出几个 corner case 把自己 hack 掉了。花了大概 15min 调了调细节就继续了。
此时用时 1h,优势在我!
于是 T4……什么鬼。想了想有什么可能的做法,但发现整场都没用 DP,于是我就果断开始想 DP 了(
暴力 DP 挺好想的啊,随便弄个 \(f_{r,i,j}\) 再取或值暴力大转移。应该是 \(O(rk\sum{l})\) 的。貌似空间会炸?再想想,把 \(s\) 全串起来就行了。
然后我就开打了,调调细节,这个大暴力就打好了。一打开大样例的 ans,发现是乱码???我举手问监考员,他就说换种方法打开试试,但是我写字板和记事本打开都是乱码。
监考员:“嗯,有点奇怪……”
我:“那……咋办啊?”
监考员:“你,多试试吧。”(留给我一个潇洒的背影)
我:……??
但万幸,是能 fc 的。大样例全跑了一遍,全过了,最慢的跑了 10s。感觉能骗挺多分的?
此时过去了 2h。
于是我开始想优化。我就怼着这个定长区间去想,但想半天还是没有头绪。
于是考试结束了。
期望得分 340 左右?
出了考场,发现大众 300+。今年分数线不会暴涨到 300 吧??T4 好像很多人都是随便骗了 5 分,貌似我也没啥优势啊。
听到有人说 T4 是图论,就在想我不会是方向完全错了吧?怪不得我都优化不了。
事实上两者都可以做。
题解
T1
输入的扑克牌都是合法的,排序、去重后得到牌堆大小 \(s\),答案即为 \(52-s\)。
时间复杂度为 \(O(n\log{n})\)。
显然可以对每张牌 hash 一下再开桶做到 \(O(n)\)。
T2
开个 \(vis\) 数组,直接暴力模拟即可。
时间复杂度为 \(O(T(nm+k))\)。
T3
考虑贪心。首先我们需要最小化数字位数,在此基础上顺次最小化从高到低每一位。
用小木棒的数量最多的数字是 \(8\),用了 \(7\) 根小木棒。于是最小的数字位数就是 \(\left\lceil\frac{n}{7}\right\rceil\)。
然后从左到右去填数字。令 \(num_i\) 表示数字 \(i\) 的小木棒个数。对于当前位,考虑后面所有位都填 \(8\),于是这一位就填一个用了至少 \(n \bmod 7\) 个小木棒的数字中最小的一个,即 \(\min_{num_i\geq n \bmod 7}\{i\}\)。确定当前位填 \(i\) 后,令 \(n=n-num_i\)。
再加点简单特判即可:
- 第一位不能为 \(0\);
- 当余数为 \(0\) 时直接把后面所有位变成 \(8\);
- 最后一位必须填用的小木棒个数恰好等于余数的最小的数字,即 \(\min_{num_i=n \bmod 7}\{i\}\)。
时间复杂度 \(O(Tn)\)。实际算法时间规模远小于这个值,因为在填了非常少的位后余数就会变成 \(0\)。
T4
赛时做红温了,赛后调红温了
这里讲 DP 做法,参考了洛谷上的题解。
令 \(f_{r,i,j}\) 表示进行 \(r\) 轮接龙,以 \(s_{i,j}\) 结尾是否可行。
令 \(S_{i,j}\) 表示集合 \(\{s_{i,p}|p\in[j-k+1,j-1]\land p>0\}\)。考虑枚举起始点,容易列出状态转移方程:
我们从值域上考虑这个转移,结合当前这一轮接龙的人不能与上一轮接龙的人相同这一限制条件,可以想到前缀和优化。在更新 \(f\) 数组值的同时,我们更新以下前缀和数组:
于是状态转移方程可以相应被改写:
对于一个询问 \((r,c)\),答案即为 \(g_{r,c}\)。
\(f\) 的转移方式很经典。固定 \(r,i\),每次 \(j\) 增加时,决策集合必定会右端加入一个数,并至多从左端删除一个数。简单记录一下决策集合中 \(1\) 的个数,删除时判断一下即可做到 \(O(1)\) 转移。DP 的时间复杂度为 \(O(T\max(r)\sum{l})\)。
暴力地按照上面的方式存数组显然会 MLE。对于 \(f\) 数组,记录 \(l\) 数组的前缀和 \(suml\),每个 \((i,j)\) 对可以被存储为 \(suml_{i-1}+j\),存 \(f\) 数组就是 \(O(r\sum{l})\) 的(事实上,存 \(s\) 也是类似的)。
而对于 \(h\) 数组,我们注意到在使用 \(h_{r,i,x}\) 的值时,必然有 \(x\in s_i\),这启发我们用 \(x\) 在 \(s_i\) 中第一次出现的位置 \(pos_{i,x}\) 来表示 \((i,x)\) 。\(pos\) 数组开个值域上的数组 \(pre_x\) 即可整体 \(O(\sum{l})\) 的算出。于是我们就能 \(O(r\sum{l})\) 地存 \(h\) 数组了。
整个算法时间复杂度为 \(O(T\max(r)\sum{l})\),空间复杂度为 \(O(r(V+\sum{l}))\),滚动数组可以把 \(f\) 和 \(h\) 的第一维压掉。
感觉这题真的很好捏~~~
总结
分数:未出。等出了和代码一起放出来。
分数:100+100+100+0=300……T4 也是爆零了好吧。
T4 code:
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int MAX_R = 105, MAX_N = 1e5 + 5, MAX_Q = 1e5 + 5, MAX_SL = 2e5 + 5, MAX_V = 2e5 + 5;
int t, n, k, q, maxv, maxr;
int l[MAX_N], suml[MAX_N];
int s[MAX_SL];
bool f[MAX_R][MAX_SL];
int g[MAX_R][MAX_V], h[MAX_R][MAX_SL];
int pos[MAX_SL], pre[MAX_V];
pii queries[MAX_Q];
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> t;
while (t--) {
cin >> n >> k >> q;
for (int i = 1; i <= n; ++i) {
cin >> l[i];
for (int j = 1; j <= l[i]; ++j) {
int x = suml[i - 1] + j;
cin >> s[x];
if (!pre[s[x]]) pre[s[x]] = x;
pos[x] = pre[s[x]];
}
suml[i] = suml[i - 1] + l[i];
for (int j = 1; j <= l[i]; ++j) pre[s[suml[i - 1] + j]] = 0;
}
maxr = 0;
for (int i = 1; i <= q; ++i) {
cin >> queries[i].first >> queries[i].second;
maxr = max(maxr, queries[i].first);
}
memset(f, 0, sizeof(f));
memset(g, 0, sizeof(g));
memset(h, 0, sizeof(h));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= l[i]; ++j) {
int x = suml[i - 1] + j;
if (s[x] == 1) {
f[0][x] = true;
++g[0][1];
}
}
for (int r = 1; r <= maxr; ++r) {
for (int i = 1; i <= n; ++i) {
int b = suml[i - 1];
int cnt = 0;
for (int j = 2; j <= l[i]; ++j) {
if (j > k && g[r - 1][s[b + j - k]] != h[r - 1][pos[b + j - k]]) --cnt;
if (g[r - 1][s[b + j - 1]] != h[r - 1][pos[b + j - 1]]) ++cnt;
f[r][b + j] = !!cnt;
g[r][s[b + j]] += f[r][b + j];
h[r][pos[b + j]] += f[r][b + j];
}
}
}
for (int i = 1; i <= q; ++i)
cout << !!g[queries[i].first][queries[i].second] << '\n';
}
return 0;
}

浙公网安备 33010602011771号