【赛后小结】Codeforces Round #786 (Div. 3)
比赛相关信息
比赛信息
比赛名称: Codeforces Round #786 (Div. 3)
比赛地址: Codeforce
部分题解与小结
C - Infinite Replacement
小评
挺水的构造题,要注意分类讨论,错了一次就是因为讨论的时候较为混乱。
题意
\(T(1 \le T \le 10^4)\) 组样例。给出一串只含 \(\tt a\) 的字符串 \(S\) ,以及一条只含小写字母的字符串 \(T\) ,两者长度均小于 \(50\) 。规定操作:
- 每次从 \(S\) 种选择一个 \(\tt a\) ,将其替换为 \(T\) ;
无限进行操作,询问可以获得的不同的 \(S'\) 的数量,若为无限,输出 \(-1\) 。
思路
易得以下规律
- \(T\) 中含 \(\tt a\) 且 \(Len_T \neq 1\),即为 \(-1\) ;
- \(T\) 为 \(\tt a\) ,即为 \(1\) ;
- 只要 \(T\) 中不含 \(\tt a\) ,即为 \(2^{Len_S}\) ;
AC代码
点击查看代码
void Solve() {
string s, t; cin >> s >> t;
if (t == "a") cout << 1 << endl;
else if (t.find('a') != -1) cout << -1 << endl;
else cout << (1LL << s.sz) << endl;
}
D - A-B-C Sort
小评
初见以为是很难的找规律题,于是果断跳。做完 \(\tt E\) 回来推了推规律,发现不难,三十分钟就直接切掉了。
题意
\(T(1 \le T \le 2 * 10^ 4)\) 组样例。给出三个数组 \(A,B,C\) ,初始 \(A\) 数组中有 \(N\) 个元素,\(B,C\) 均为空。规定如下操作:
- 当 \(A\) 不为空时,每一步都取出 \(A\) 末尾的元素并放置到 \(B\) 的中间位置:特别的,当 \(B\) 中含奇数个元素时,你可以任意放置到中间位置的左侧或者右侧;
- 当 \(A\) 为空但 \(B\) 不为空时,每一步都取出 \(B\) 中间的元素并放置到 \(C\) 的末尾位置:特别的,当 \(B\) 中含偶数个元素时,你可以任意选择中间位置的左侧或者右侧的元素;
给出 \(A\) 中的初始元素,询问经上述操作后能否在 \(C\) 中得到一个非递减的序列。
思路
假设 \(A\) 中序列为 \(\tt abcde\) ,操作如下:
至此,我们可以得到,当 \(N\) 为奇数时:
- 第一位 \(\tt a\) ;
- 第二位 \(\tt c、b\) ;
- 第三位 \(\tt d、e\) ;
即有规律:当 \(N\) 为奇数时,第一位取出的一定是 \(A\) 中最后一个放进去的,而此后每一位均有两种不同的情况。相对应的,我们也可以得到 \(N\) 为偶数时的情况,根据规律直接输出即可。
自己思路
构建排序过后的 \(A\) 数组 \(A'\) ,按以下方式进行模拟:
- 对于奇数,特判 \(A[1]\) 是否等于 \(A'[1]\) ,之后两两比对;
- 对于偶数,直接两两比对;
蒋老师思路
由上述规律可以进一步抽象得到规律:从 \(A\) 到 \(C\) 的操作即将 \(A\) 中元素进行了如下的排序:
故我们只需要按此规律进行排序,再判断排序过后的 \(A\) 数组是否非递减即可。这里我们可以使用到一个函数 is_sorted
,函数原型如下:
bool is_sorted(iterator begin, iterator end);
AC代码
点击查看代码
自己思路
int a[N];
void Solve() {
int n; cin >> n;
FOR (i, 1, n) {
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + 1 + n);
int flag = 0;
if (n % 2 == 1) {
if (a[1] != b[1]) flag = 1;
FOR2 (i, 2, n) {
if ((a[i] == b[i] && a[i + 1] == b[i + 1]) || (a[i] == b[i + 1] && a[i + 1] == b[i]) );
else flag = 1;
}
}else {
FOR2 (i, 1, n) {
if ((a[i] == b[i] && a[i + 1] == b[i + 1]) || (a[i] == b[i + 1] && a[i + 1] == b[i]) );
else flag = 1;
}
}
if (flag) NO;
else YES;
}
蒋老师思路
int a[N];
void Solve() {
int n; cin >> n;
for (int i = 1; i <= n; ++ i) cin >> a[i];
for (int i = n - 1; i >= 1; i -= 2) {
if (a[i] > a[i + 1]) swap(a[i], a[i + 1]);
}
if (is_sorted(a + 1, a + 1 + n)) YES;
else NO;
}
E - Breaking the Wall
小评
赛时跳过 \(\tt D\) 直接来做的 \(\tt E\) ,结果发现跟 \(\tt D\) 一样也是一道找规律题,但因为没有上一题这么数学,所以还算能接受(虽说找完规律之后发现这道题确实比上一题要难上这么一些的),但是需要仔细的进行分类讨论。
题意
地上有 \(N\) 堵墙,每一堵都有生命值,你需要操作大炮进行攻击,攻击伤害如下:
- 假设你选择第 \(i\) 堵墙,那么会对 \(i\) 号墙造成 \(2\) 点伤害,会对 \(i-1\) 和 \(i+1\) 号墙造成 \(1\) 点伤害;
现在需要将两堵墙的生命值攻击至低于 \(0\) ,请你输出最少需要的攻击次数。
思路
由于需要两堵墙的生命值低于 \(0\) ,故对于这两堵墙的选择存在显然的贪心策略:
- \(i\) 和 \(i+1\) ;
- \(i-1\) 和 \(i+1\) ;
- 对生命值最少的两堵墙;
记生命值较小的墙的生命值为 \(x\) ,较大的为 \(y\) ,按照上述贪心策略进一步思考:
- 选择 \(i\) 和 \(i+1\)
- 若 \(x \le \lfloor \frac{y}{2} \rfloor\) ,答案为 \(\lceil \frac{y}{2} \rceil\) ;
- 否则,设分别攻击 \(K_1,K_2\) 下,则有 \(\left\{\begin{matrix} 2K_1+K2 \ge x \\ 2K_2 + K_1 \ge y\end{matrix}\right.\) ,联立易得解 \(K_1+K_2 \ge \frac{x+y}{2}\) ,答案为 \(\lceil \frac{x+y}{2} \rceil\) ;
- 选择 \(i-1\) 和 \(i+1\)
- 首先攻击 \(i\) 直到 \(x \le 0\) ,随后剩下那堵墙直到 \(y \le 0\) ,答案为 \(x+\lceil \frac{y-x}{2} \rceil\) ;
- 对生命值最少的两堵墙
最后输出上述答案的最小值即可。
AC代码
点击查看代码
int t[N];
void Solve() {
int n; cin >> n;
FOR (i, 1, n) cin >> t[i];
int ans = INFF;
FOR (i, 2, n) {
int x = min(t[i - 1], t[i]);
int y = max(t[i - 1], t[i]);
if (x <= y / 2) cmin(ans, (y + 1) / 2);
else cmin(ans, (x + y + 2) / 3);
}
FOR (i, 3, n) {
int x = min(t[i - 2], t[i]);
int y = max(t[i - 2], t[i]);
cmin(ans, x + (y - x + 1) / 2);
}
sort(t + 1, t + 1 + n);
cmin(ans, (t[1] + 1) / 2 + (t[2] + 1) / 2);
cout << ans;
}
F - Desktop Rearrangement
小评
拿到这道题的时候还有相当的时间,在 看完题意之后快速的联想到了“使用线段树进行“单点修改”,确定思路无误之后便直接开码,结果码到一半编译器吃不住压力,自动纠错炸掉了,随之电脑黑屏,于是强制重启了一次。
在还剩余20分钟时交了第一发,错误的原因是结构体存线段树忘记开四倍空间;
非常快速的找到错误后又交了第二发,超时第42个点,想了很久为什么,最后发现错误的原因是不是所有的查询都需要使用到线段树(绝大多数查询通过一个数组即可完成);
在还剩余2分钟的时候交了第三发,结果错了第42个点,直到比赛结束没能找到为什么。赛后发现错误的原因是数组越界,如下图
但是这个提示也不是每一次都有,就很鬼畜。详细分析后发现原因在于结构体四倍空间开的还不够大,这就更鬼畜了……
题意
想象电脑桌面的图标排列,我们使用 *
代表图标,.
代表空格。现在有一个大小为 \(N*M(1 \le N,M \le 10^4)\) 的电脑桌面,当所有的图标是从左向右、从上向下排列的,这样的桌面我们称之为”干净的“。我们每一步操作都可以将任意一个图标移动到任意一个空格上去。现在给出 \(Q(1 \le Q \le 2 * 10^5)\) 次询问,每次询问需要进行的操作如下:
- 给出坐标 \((x,y)\) ,若这一格上有图标,则删除;反之,则新建一个图标;
- 每次修改都是长期延续的;
对于每一个询问,分别输出让桌面达到”干净的“还需要进行几步操作。
思路
使用一个变量 \(Sum\) 储存桌面上的图标数量,显然,需要输出的答案即为从左到右、从上到下的桌面上的前 \(Sum\) 个格子中空格的数量。
自己思路(线段树/树状数组):
对每一列分别建立一棵线段树——将每次询问想象成线段树的单点修改,将每次输出想象成线段树的区间查询,得解。但是这里有一个优化,即在输出时,对于完整的列,我们可以直接使用一个数组来统计空格数,而只需要对不完整的列进行区间查询即可,复杂度由 \(\mathcal O\Big((N+Q*M)*logN \Big)\) 优化至 \(\mathcal O\Big((N+Q)*logN \Big)\) ,可解。
别的大佬的思路(规律):
将桌面从左至右一列一列的进行拆分,压缩为一个一维结构,首先计算初始状态时达到“干净的”需要的步骤数量,此后,对于每一次询问分类讨论:
-
若将某一格从空格修改至图标,
- 新扩入填充区间的格子若为图标,则操作数减一;
- 若这一格位于需要填充的区间外,则操作数加一;
-
若将某一格从图标修改至空格,
- 新从填充区间删去的格子若为图标,则操作数加一;
- 若这一格位于需要填充的区间外,则操作数减一;
AC代码
点击查看代码
char a[N];
int num, ans;
void Solve() {
int n, m, q; cin >> n >> m >> q;
FOR (i, 1, n) FOR (j, 0, m - 1) {
cin >> a[i + j * n];
if (a[i + j * n] == '*') ++ num;
}
FOR (i, 1, num) if (a[i] == '.') ++ ans;
FOR (t, 1, q) {
int x, y; cin >> x >> y;
int z = x + (y - 1) * n;
if (a[z] == '.') {
++ num;
if (a[num] == '*') -- ans;
if (z > num) ++ ans;
a[z] = '*';
}else {
if (a[num] == '*') ++ ans;
-- num;
if (z > num) -- ans;
a[z] = '.';
}
cout << ans << endl;
}
}
文 / WIDA
2022.05.12 成文
首发于WIDA个人博客,仅供学习讨论