AtCoder Beginner Contest 407 ABC-407 A-F 题解
A - Approximation
显然对于 \(\dfrac A B\) 来说离其最近的整数是 \(\lfloor \dfrac A B \rfloor\) 或者 \(\lceil \dfrac A B \rceil\).
CODE
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <math.h>
using namespace std;
double a, b;
int main() {
cin >> a >> b;
double c = a / b;
if (ceil(c) - c > c - floor(c)) cout << floor(c);
else cout << ceil(c);
return 0;
}
B - P(X or Y)
枚举每一种可能方案, 然后判断满足题目中条件的方案的个数 \(res\), 一共有 \(6 \times 6 = 36\) 种可能方案, 所以答案是 \(\dfrac {res} {36}\)
CODE
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <iomanip>
using namespace std;
int x, y;
int main() {
cin >> x >> y;
int res = 0;
for (int i = 1; i <= 6; i ++)
for (int j = 1; j <= 6; j ++) {
if (i + j >= x || abs(i - j) >= y) res ++;
}
// 输出位数多点, 防止误差
cout << fixed << setprecision(20) << (double)res / 36.0;
return 0;
}
C - Security 2
我们反向考虑, 即考虑将字符串 \(S\) 转为空字符串需要多少步, 操作也反过来:
- 操作 \(A\), 若字符串最后一位为 \(0\), 那么将其删去.
- 操作 \(B\), 将字符串中的所有数字减小 \(1\), 特别地, \(0\) 转为 \(9\).
那么对于一个字符串 \(S\), 如果它的最后一位不为 \(0\), 那么它只能执行操作 \(B\).
如果它的最后一位为 \(0\), 它可以删去最后一位或者将最后一位转为 \(9\), 如果将最后一位转为 \(9\), 显然又要再把 \(9\) 转为 \(0\) 后才能执行删一位的操作, 操作次数更多.
所以我们的思路是将字符串的最后一位降为 \(0\) 后删去该数字.
这时又有另一个问题, 操作 \(B\) 是将字符串中所有数字都减小 \(1\), 如果每次都执行这样的操作显然会 TLE.
但是我们发现, 我们每次对 \(S\) 最后一位操作时都用不到其他位的数, 所以我们可以维护一个变量 \(total\), 表示我们一共执行了多少次操作 \(B\). 然后每次删去最后一位数后, 对新的最后一位一次性执行 \(total\) 次操作 \(B\).
如何在 \(O(1)\) 时间执行 \(total\) 次操作 \(B\) ?
我们可以发现当执行 \(total\) 次操作 \(B\) 时, 相当于 \((S_i - total) \bmod 10\), 但在 C++ 中, 负数的取余和数学中负数的取余不太一样, 所以我们要保证 \(S_i - total \ge 0\), 解决方法是加上一个可以被 \(10\) 整除的数 \(k\), 且 \(k \ge (S_i - total)\).
我们可以发现一个数执行 \(10\) 次操作 \(B\) 和不执行是一样的, 因为转了一圈又回来了, 所以执行 \(total\) 次操作 \(B\) 相当于执行 \(total \bmod 10\) 次操作 \(B\), 即 $$(S_i - total) \bmod 10 = (S_i - (total \bmod 10)) \bmod 10$$
\(S_i - (total \bmod 10)\) 一定大于 \(-10\), 所以 \(k\) 为 \(10\) 就可以了.
CODE
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
string s;
int total = 0;
int res = 0;
void f(int pos) {
if (pos == -1) return;
int current = s[pos] - '0';
current = ((current - total + 10) % 10); // 执行 B 操作
res += current + 1; // 一共要执行 current 次 B 操作, 还有删去最后一位的 A 操作.
total = (current + total) % 10; // 提前计算 total % 10
f(pos - 1);
}
signed main() {
cin >> s;
f(s.size() - 1);
cout << res;
return 0;
}
D - Domino Covering XOR
\(HW \le 20\) 数据范围很小, 直接暴力即可.
但是我们发现统计有点麻烦, 每次都要遍历整个图, 优化方法为:
根据异或操作的定义我们可以发现 \(x = x \oplus y \oplus y\), 也就是说, 一个数异或两次同样的数, 相当于没有异或过. 也就是说, 如果我们先将图中所有格子的异或值 \(tot\) 计算出来, 然后在 DFS 的过程中, 对每个放了多米诺骨牌的格子再进行一次异或, 就相当于所有没放多米诺骨牌的格子的异或值.
CODE
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <bitset>
using namespace std;
#define int long long
const int N = 25;
int mp[N][N];
int n, m, tot = 0; // tot 图中所有点的异或值
struct Point {
int x, y;
};
// 获得当前点之后要处理的下一个点
Point getnext(Point x) {
// 到最后一个点, 没有下一个点, 返回 { -1, -1 }
if (x.x >= m && x.y >= n) {
return Point { -1, -1 };
}
if (x.x >= m) return Point { 1, x.y + 1 };
return Point { x.x + 1, x.y };
}
// 判断是否超出边界
bool check(Point x) {
return x.x >= 1 && x.x <= m &&
x.y >= 1 && x.y <= n;
}
// vis_i_j 坐标 i, j 的格子是否已经被放了多米诺骨牌
bool vis[N][N];
int res = -1;
void dfs(Point curr, int score) {
// 统计结果
res = max(res, score);
// 两种放的方式: 水平放和垂直放
Point horizontal = curr; horizontal.x ++;
Point vertical = curr; vertical.y ++;
Point next = getnext(curr);
// 边界
if (next.x == -1 || next.y == -1) return;
// 若想放的两格都可以放, 且没有超出边界
if (check(horizontal) && !vis[curr.y][curr.x] && !vis[horizontal.y][horizontal.x]) {
// 如果这里你看不懂的话, 请去学习 dfs 相关知识
vis[curr.y][curr.x] = true;
vis[horizontal.y][horizontal.x] = true;
// 如上所说, 对每个放了多米诺骨牌的格子再进行一次异或, 就相当于所有没放多米诺骨牌的格子的异或值
dfs(next, score ^ mp[curr.y][curr.x] ^ mp[horizontal.y][horizontal.x]);
vis[horizontal.y][horizontal.x] = false;
vis[curr.y][curr.x] = false;
}
// 同上
if (check(vertical) && !vis[curr.y][curr.x] && !vis[vertical.y][vertical.x]) {
vis[curr.y][curr.x] = true;
vis[vertical.y][vertical.x] = true;
dfs(next, score ^ mp[curr.y][curr.x] ^ mp[vertical.y][vertical.x]);
vis[vertical.y][vertical.x] = false;
vis[curr.y][curr.x] = false;
}
// 还有不放的一种情况
dfs(next, score);
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= m; j ++) {
cin >> mp[i][j];
// 计算 tot
tot ^= mp[i][j];
}
}
// 如分析中所说, 对每个放了多米诺骨牌的格子再进行一次异或, 就相当于所有没放多米诺骨牌的格子的异或值
dfs({ 1, 1 }, tot);
cout << res;
return 0;
}
考试的时候时间复杂度算错了痛失425分
E - Most Valuable Parentheses
若一个括号序列 \(S\) 对于任意 \(1 \le i \le |S|\), 其 \(S[1..i]\) 中的左括号数 \(\ge\) 右括号数, 那它一定可以通过在尾部添加 N 个右括号使该括号序列合法. (\(|S|\) 为 \(S\) 的长度)
即对于一个长度为 \(2N\) 合法括号序列 \(S\), 他一定满足对于任意 \(1 \le i \le 2N\), 其 \(S[1..i]\) 中的左括号数 \(\ge\) 右括号数.
也就是说, 我们可以构造这样一个合法的最优括号序列 \(S\), 设 \(A\) 为题目中给的序列:
- \(S_1\) 一定为
(. - 循环 \(i = 1+2\cdot1,\ 1+2\cdot2,\ 1+2\cdot3,\ \cdots,\ 1+2\cdot(N-1)\)
- 将 \(A_i, A_{i-1}\) 两个数加入备选列表.
- 在备选列表中找出一个数 \(A_j\), 将 \(S_j\) 设为左括号, 这样就满足了对于任意 \(1 \le i' \le i\), 其 \(S[1..i']\) 中的左括号数 \(\ge\) 右括号数.
- 将 \(A_j\) 从备选列表删去.
- 显然, 如果我们每次 \(A_j\) 都选备选列表最大的那个数, 那么对于所有 \(A_k\) 满足 \(S_k =\)
左括号加起来就是最大的.
- \(S_{2N}\) 一定为
).
因为我们每两个数只加一个左括号, 所以最后右括号数一定等于左括号数.
我们可以用一个优先队列来维护备选列表最大值, 什么是优先队列/堆?
CODE
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
using ll = long long;
// N = 2e5, 2N = 4e5
const int N = 4e5 + 10;
int n, arr[N], T;
void solve() {
// 备选列表
priority_queue<int> Q;
cin >> n;
for (int i = 1; i <= n * 2; i ++) cin >> arr[i];
ll res = 0;
for (int i = 1; i <= n * 2; i += 2) {
// 对于 1 特判, 直接将 arr[1] 加入备选列表, 此时 arr[1] 当然是最大的, 会直接选为 '('
if (i == 1) {
Q.push(arr[i]);
}
else {
// 加入备选列表
Q.push(arr[i]);
Q.push(arr[i - 1]);
}
// 将标为 '(' 的数加到最后的结果
// 因为题目并没有要求我们输出最后的括号序列, 所以我们就不用标 '(' 了.
res += Q.top(); Q.pop();
}
cout << res << '\n';
}
int main() {
cin >> T;
while (T --) solve();
return 0;
}
F - Sums of Sliding Window Maximum
设 \(max\) 为序列 \(A\) 的最大值, 其在一个滑动窗口中它为最大值的次数为它对该滑动窗口的贡献

如图所示, 我们可以发现, \(max\) 对从小到大的长度的滑动窗口的贡献值的变化是先小再到大, 之后到某一点不变, 再到某一点开始变小, 类似一个梯形.
设 \(max\) 在 \(A\) 中下标为 \(i\), 进一步观察, 我们可以发现, 从 \(1..i\) 逐渐上升, \((i+1)..(|A|-i+1)\) 不变, 其中 \(|A|\) 表示 \(A\) 的长度, \((|A|-i+1)..|A|\) 逐渐下降. 这样我们就可以求出 \(max\) 对不同长度的滑动窗口的贡献了.
而我们可以发现, 对于任意一个 \(A_i\), 都存在一个区间 \(L_i..R_i\), 在该区间内的所有数字内 \(A_i\) 为最大, 且该区间为满足前一个条件中最大的区间, 那么 \(A_i\) 的对不同长度的滑动窗口的贡献就可以通过上面的方法计算出来了.
我们可以遍历 \(A\) 中所有数, 并计算每个数对不同长度的滑动窗口的贡献, 而对于一个滑动窗口它在每个位置的最大值的总和, 就是所有 \(A_i \times A_i \rm 的对该滑动窗口贡献\) 加起来了.
如何求 \(A_i\) 对应的 \(L_i..R_i\) 区间? 我们只需要求 \(A_i\) 左右侧第一个大于 \(A_i\) 的数的下标即可, 若没有就为 \(0\) 和 \(|A| + 1\). 这一步可以用单调栈求出, 什么是单调栈 / 例题
但是还是不够快, 我们仍需要 \(O(N)\) 的时间来求出一个 \(A_i\) 对不同长度的滑动窗口的贡献.
如果你学过差分的话, 你可能知道在一个数组上加上一个长方形, 即给数组 \(L..R\) 加上同一个数, 可以用差分在 \(O(1)\) 的时间解决, 其实给一个数组加上梯形的操作也可以通过做两次差分完成, 什么是差分
具体地, 设 \(L, R\) 为 \(A_i\) 左右侧第一个大于 \(A_i\) 的数的下标, 若没有就为 \(0\) 和 \(|A| + 1\), \(ans_j\) 为 \(A_i\) 对长度为 \(j\) 的滑动窗口的贡献, 那么令
\((ans_1) + 1\)
\((ans_{i-L+1}) - 1\)
\((ans_{R-i+1}) - 1\)
\((ans_{R-L+1})+1\)
如对于动画中的样例
1 0 0 -1 0 -1 0 0 1
接下来执行两次前缀和
1 1 1 0 0 -1 -1 -1 0
1 2 3 3 3 2 1 0 0
这样我们就可以在快速给数组加上一个梯形了.
所以我们进行以下步骤:
- 对数组 \(A\) 利用单调栈求对于每一位数 \(A_i\) 左右侧第一个大于 \(A_i\) 的数的下标 \(L,R\), 若没有就为 \(0\) 和 \(|A| + 1\).
- 对于每一个 \(A_i\), 令
\((ans_1) + 1 \times A_i\)
\((ans_{i-L+1}) - 1 \times A_i\)
\((ans_{R-i+1}) - 1 \times A_i\)
\((ans_{R-L+1})+1 \times A_i\)
对于一个滑动窗口它在每个位置的最大值的总和, 就是所有 \(A_i \times A_i \rm 的对该滑动窗口贡献\) 加起来, 所以我们干脆在这一步就乘以 \(A_i\). - 对 \(ans\) 求两次前缀和, 求出答案
CODE
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <deque>
#include <stack>
using namespace std;
#define int long long
const int N = 2e5 + 10;
int arr[N], n, l[N], r[N];
int ans[N];
signed main() {
cin >> n;
for (int i = 1; i <= n; i ++) {
cin >> arr[i];
}
stack<int> stk;
// 将左右边界设到最高, 防止一个数左右没有比它更大的数
arr[n + 1] = 1e9;
arr[0] = 1e9;
// 单调栈经典操作
for (int i = 1; i <= n + 1; i ++) {
while (!stk.empty() && arr[stk.top()] <= arr[i]) {
int k = stk.top(); stk.pop();
r[k] = i;
}
stk.push(i);
}
// 清空栈
while (!stk.empty()) stk.pop();
// 单调栈经典操作 2
for (int i = n; i >= 0; i --) {
while (!stk.empty() && arr[stk.top()] < arr[i]) {
int k = stk.top(); stk.pop();
l[k] = i;
}
stk.push(i);
}
for (int i = 1; i <= n; i ++) {
/*
(ans[1]) += 1 * A[i]
(ans[i-L+1]) -= 1 * A[i]
(ans[R-i+1]) -= 1 * A[i]
(ans[R-L+1]) += 1 * A[i]
*/
int L = i - l[i], R = r[i] - i;
ans[1] += arr[i];
ans[L + 1] -= arr[i];
ans[R + 1] -= arr[i];
ans[R + L + 1] += arr[i];
}
// 两次前缀和
for (int i = 1; i <= n; i ++) {
ans[i] += ans[i - 1];
}
for (int i = 1; i <= n; i ++) {
ans[i] += ans[i - 1];
}
// 答案输出
for (int i = 1; i <= n; i ++) {
cout << ans[i] << '\n';
}
return 0;
}
本文图像文字内容采用 CC BY-SA 4.0 许可协议进行许可, 代码内容采用 Apache License, Version 2.0 许可证进行许可.

浙公网安备 33010602011771号