CSP-J 2021 题解
蒟蒻の估分
作为一个学了一年多还只在入门组的高龄 \(OIer\),\(T1\) 居然写挂了……
\(T1\) 是一道简单的数学题,考场上把问题想得太过复杂了,答案居然是由4个值中的最大值来决定的,鬼知道我是怎么想到的,期望得分 \(80/100 ~ pts\)
\(T2\) 是纯暴力,题目保证了修改次数不超过 \(5000\),自然是为 \(O(n^2)\) 修改、\(O(1)\) 查询奠定下了刚好卡时过的基础。但是考场上我用的方法是 \(O(1)\) 修改、\(O(n)\) 查询,似乎能过吧,期望得分 \(76/100 ~ pts\)。
\(T3\) 是一道大大的模拟,判断\(ERR\) 属实揪心,一大堆特判,从此恨上地址,花了大概 \(1h\),好在样例 \(3\) 给得十分的良心,还是调出来了,期望得分 \(100/100 ~ pts\)。
\(T4\) 的时候,我旁边的“大佬”就开始发作了,那脏话飙的啊(毕竟在三亚这一小渔村嘛),心态被影响了,最后最简单的暴力也没打出来,期望得分 \(10/100 ~ pts\)。
综上,这次的 \(CSP-J\) 的期望总分就是 \(266/400 ~ pts\)。
题解
T1 分糖果
给定的 \(L\) 和 \(R\) 的范围到了恐怖的 \(10^9\),遍历出解显然不现实,考虑能否 \(O(1)\) 出解。
再仔细读一遍题,捋清楚逻辑后不难发现:对于这道题来说,对答案有贡献的就是 \(L\) 和 \(R\) 除以 \(n\) 后的余数,可以分两种情况讨论:
- 当 \(R - L \ge n\) 时,根据抽屉原理,可以得到在这段区间内一定有一个数 \(x\) 满足 \(x \mod n = n - 1\),直接输出 \(n - 1\) 即可。
- 当 \(R - L < n\) 时,毫无头绪,那就再分情况:
- 如果存在一个正整数 \(k\) 满足 $ L < kn \le R$ (也即 \(L \div n \ne R \div n\))时,不难发现可以通过拿 \(kn -1\) 个糖果来使奖励糖果数量最大,也可以直接输出 \(n - 1\)。
- 如果不存在正整数 \(k\),则令 \(k\) 为保证 \(kn \le L\) 的最大值。此时,\(L,R\) 的关系就是 \(kn \le L \le R < (k + 1)n\),自然是拿的越多余数越大,输出 \(R \mod n\) 即可。
上代码:
#include <bits/stdc++.h>
using namespace std;
int n, L, R;
int main() {
scanf("%d%d%d", &n, &L, &R);
if (L / n != R / n) printf("%d", n - 1);
else printf("%d", R % n);
return 0;
}
T2 插入排序
做这道题的时候,没看见下面的修改操作不超过5000次,于是就有了 \(O(1)\) 修改、\(O(n)\) 查询的考场代码。
主要思想是在这一题的情况下,对于数组中的一个数 \(a_x\),其排序后的位置取决于它前面所有小于它的数的个数的和它后面所有小于等于它的数的个数之和。
#include <bits/stdc++.h>
#define MAXN 8100
using namespace std;
int n, Q, a[MAXN];
int main() {
scanf("%d%d", &n, &Q);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int op, x, v;
while (Q--) {
scanf("%d", &op);
if (op == 1) {
scanf("%d%d", &x, &v);
a[x] = v;
} else {
scanf("%d", &x);
int ans = 0;
for (int i = 1; i <= x; i++) {
if (a[i] <= a[x]) ans++;
}
for (int i = x + 1; i <= n; i++) {
if (a[i] < a[x]) ans++;
}
printf("%d\n", ans);
}
}
return 0;
}
当然,上面的做法会 \(TLE\),也就希望 \(O2\) 能救救我。
正解来了(\(O(\log n)\) 的修改和 \(O(\log n)\) 的查询)
既然题目给出了排序不会对后面的结果产生影响,那么我们大可不必在 \(a\) 数组中排序,而是可以采用一个 \(vector\) 类型的 \(f\) 数组来存储排序后的序列。
其次,每一次修改都只是单点修改,这就意味着之前的排序结果可以重用,进而降低时间复杂度。具体怎么重用呢?这就是接下来的重点了:
对于每一次修改,我们需要:
- 在 \(f[]\) 中找到并删去这个元素。
- 往 \(f[]\) 中推入新的元素,同时维护 \(f[]\) 的单调递增性。
而对于每一次询问 \(a[x]\) 排序后的位置,只需要通过 \(a[x]\) 的值找到其在 \(f[]\) 中的位置,再将其输出就行了。
乍一看,两次操作都是 \(O(n)\) 时间复杂度的啊,哪来的 \(O(\log n)\) 呢?
仔细想想,对于每一次操作,我们都要扫一遍整个 \(f[]\) 来查找对应位置,而 \(f[]\) 本身是单调递增的,这就意味着一个新的优化诞生了:二分优化查找过程!
只要每次在 \(f[]\) 中查找对应位置时均用二分优化,无论是修改还是查询,时间复杂度都会降到优秀的 \(O(\log n)\)。
上代码:
#include <bits/stdc++.h>
#define MAXN 8100
using namespace std;
int n, Q;
struct Node {
int v, id;
bool operator<(const Node &rhs) const {
if (v == rhs.v) return id < rhs.id;
return v < rhs.v;
}
} a[MAXN];
vector<Node> f;
int main() {
scanf("%d%d", &n, &Q);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].v);
a[i].id = i;
f.insert(lower_bound(f.begin(), f.end(), a[i]), a[i]);
}
int op, x, v;
while (Q--) {
scanf("%d", &op);
if (op == 1) {
scanf("%d%d", &x, &v);
f.erase(lower_bound(f.begin(), f.end(), a[x]));
a[x].v = v;
f.insert(lower_bound(f.begin(), f.end(), a[x]), a[x]);
} else {
scanf("%d", &x);
printf("%d\n", lower_bound(f.begin(), f.end(), a[x]) - f.begin() + 1);
}
}
return 0;
}
T3 网络连接
巨大无比的模拟,判断 \(ERR\) 属实揪心。
因为对 \(STL\) 掌握不熟练,所以考场上索性放弃写 \(map\)。
好了言归正传,判断 \(ERR\) 时,除了题目中已经给出的情况,还有很多的特判,列举如下:
- \(:\) 的个数少于 \(1\) 个或 \(.\) 的个数少于 \(3\) 个。
- \(:\) 出现在 \(.\) 的前面。
- 不以数字开头。
- \(.\) 或 \(:\) 后面没有数字(体现为以 \(.\) 或 \(:\) 结尾或二者直接相邻)。
- ……
判断完 \(ERR\),这题也就完成一半了。
因为每一次操作都需要访问之前的服务机,所以我用了一个 \(ser\) 数组来存储对应服务机的下标。
然后就挨个遍历每一台机器,先判断地址是否 \(ERR\),然后再根据首字母判断机器类型:是服务机则与之前所有服务机比对,有相同则输出 \(FAIL\),反之输出 \(OK\);是客户机则与之前所有服务机比对,一旦有相同,立马输出对应服务器在初始序列中的下标并 \(break\),如果一直没有相同的,则输出 \(FAIL\)。
比对两串字符是否相同就是裸裸的按位比对。
关于两台服务机地址相同则后者 \(FAIL\) 的问题,客户机在连接时也是从前往后遍历的,因此只会连接上第一台出现此地址的服务器,完美地解决了这个问题。
#include <bits/stdc++.h>
#define MAXN 1100
using namespace std;
int n, ser[MAXN];
char op[MAXN][10], ad[MAXN][30];
bool check(int x) {
int ln = strlen(ad[x] + 1);
int cnt1 = 0, cnt2 = 0;
long long t = 0;
if (!('0' <= ad[x][1] && ad[x][1] <= '9')) return 1;
if (ad[x][ln] == '.' || ad[x][ln] == ':') return 1;
for (int i = 1; i <= ln; i++) {
if (ad[x][i] == '.') {
if (ad[x][i - 1] == '.' || ad[x][i - 1] == ':') return 1;
if (cnt2) return 1;
if (ad[x][i + 1] == '0' && ('0' <= ad[x][i + 2] && ad[x][i + 2] <= '9')) return 1;
cnt1++;
} else if (ad[x][i] == ':') {
if (ad[x][i - 1] == '.' || ad[x][i - 1] == ':') return 1;
if (ad[x][i + 1] == '0' && ('0' <= ad[x][i + 2] && ad[x][i + 2] <= '9')) return 1;
cnt2++;
} else {
t = (t << 3) + (t << 1) + ad[x][i] - '0';
if (ad[x][i + 1] == '.' || ad[x][i + 1] == ':') {
if (t > 255) return 1;
t = 0;
}
}
}
if (cnt2 != 1 || t > 65535) return 1;
return 0;
}
bool cmp(char a[], char b[]) {
int lena = strlen(a + 1), lenb = strlen(b + 1);
if (lena != lenb) return 0;
for (int i = 1; i <= lena; i++) {
if (a[i] != b[i]) return 0;
}
return 1;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s%s", op[i] + 1, ad[i] + 1);
if (op[i][1] == 'S') ser[++ser[0]] = i;
}
for (int i = 1; i <= n; i++) {
if (check(i)) {
printf("ERR\n");
continue;
}
bool fl = 0;
if (op[i][1] == 'S') {
for (int j = 1; j <= ser[0] && ser[j] < i; j++) {
if (cmp(ad[i], ad[ser[j]])) {
fl = 1;
printf("FAIL\n");
break;
}
}
if (!fl) printf("OK\n");
} else {
for (int j = 1; j <= ser[0] && ser[j] < i; j++) {
if (cmp(ad[i], ad[ser[j]])) {
fl = 1;
printf("%d\n", ser[j]);
break;
}
}
if (!fl) printf("FAIL\n");
}
}
return 0;
}
然而,\(AC\) 了洛谷上的民间数据,在 \(CCF\) 却挂成了可怜的 \(65\),去逛了逛讨论区,发现了一个奇妙的数据:
1
Server 1.1.1.1:9999999999999999
用 \(int\) 来存 \(a, b, c, d, e\) 必然会爆,再考虑 \(long ~ long\),题目中给出地址串的长度至多为 \(25\),除去 \(3\) 个 .
,一个 :
,以及极限状态下的 \(4\) 个 \(1\),留给最后一个极大值的位数只有 \(17\) 位了,\(long ~ long\) 毫无压力地存下。
还是过不去 \(\dots\dots\)
然后又思索了一会儿,发现对于这样一个数据:
1
Client 090.228.145.77:8080
没判出来 \(ERR\),原因是对于 \(a\) 中的前导零根本没有判断,加上之后就得到了正解。
同时,比对可以用 \(map\) 来优化,读入数字的时候可以用一个不大常用的东西 \(sscanf\)。
#include <bits/stdc++.h>
using namespace std;
int n;
char type[30], ad[30];
map<string, int> mp;
bool check() {
int len = strlen(ad), cnt1 = 0, cnt2 = 0;
if (ad[0] == '0' && ('0' <= ad[1] && ad[1] <= '9')) return 1;
for (int i = 1; i <= len; i++) {
if (ad[i] == '.') {
cnt1++;
if (ad[i + 1] == '0' && ('0' <= ad[i + 2] && ad[i + 2] <= '9')) return 1;
} else if (ad[i] == ':') {
cnt2++;
if (ad[i + 1] == '0' && ('0' <= ad[i + 2] && ad[i + 2] <= '9')) return 1;
}
if (cnt1 > 3 || cnt2 > 1) return 1;
}
long long a, b, c, d, e;
if (sscanf(ad, "%lld.%lld.%lld.%lld:%lld", &a, &b, &c, &d, &e) != 5) return 1;
if (a > 255 || b > 255 || c > 255 || d > 255 || e > 65535) return 1;
return 0;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s%s", type, ad);
if (check()) {
printf("ERR\n");
continue;
}
if (type[0] == 'S') {
if (mp[ad]) printf("FAIL\n");
else {
mp[ad] = i;
printf("OK\n");
}
} else {
if (!mp[ad]) printf("FAIL\n");
else printf("%d\n", mp[ad]);
}
}
return 0;
}
T4 小熊的果篮
最后一道题了,肯定会稍微的有些难度(考完试返程途中才想到正解太淦了)。
直接来讲 \(100pts\) 的思路吧,这题满分解法有很多,起码我现在看到的就有 \(3\) 种了,但我选择的必定是代码最短最好理解的。
因为每一次拿走水果都相当于是从序列里删除一个元素,那么就可以用到链表。
再顺着往下推,推出正解也就不远了。
可以用一个 \(vector\) 容器(令其为 \(b\))来存储每个块的块头,然后不断更新。具体如何更新呢?更新必须满足条件:去掉块头后这个块内仍然有元素。否则跳过(那块都消失了你还管人家干啥)。
最最最后的代码了
#include <bits/stdc++.h>
#define MAXN 200100
using namespace std;
int n, a[MAXN], l[MAXN], r[MAXN];
vector<int> b;
int main() {
scanf("%d", &n);
a[0] = a[n + 1] = -1, r[0] = 1, l[n + 1] = n;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
if (a[i] != a[i - 1]) b.push_back(i);
l[i] = i - 1, r[i] = i + 1;
}
while (r[0] != n + 1) {
vector<int> tmp;
for (int i = 0; i < b.size(); i++) {
printf("%d ", b[i]);
int u = l[b[i]], v = r[b[i]];
r[u] = v, l[v] = u;
if (a[b[i]] != a[u] && a[b[i]] == a[v]) tmp.push_back(v);
}
puts("");
b = tmp;
}
return 0;
}
蒟蒻の得分
终究还是挂了 \(\dots\dots\)