CF Educational Round1 个人题解
-
- [A. Tricky Sum 传送门](#A.TrickySumhttps:codeforces.comcontest598problemA)
-
- [B. Queries on a String 传送门](#B.QueriesonaStringhttps:codeforces.comcontest598problemB)
-
- [C. Nearest vectors 传送门](#C.Nearestvectorshttps:codeforces.comcontest598problemC)
-
- [D. Igor In the Museum 传送门](#D.IgorIntheMuseumhttps:codeforces.comcontest598problemD)
-
- [E. Chocolate Bar 传送门](#E.ChocolateBarhttps:codeforces.comcontest598problemE)
-
- [F. Cut Length 传送门](#F.CutLengthhttps:codeforces.comcontest598problemF)
- 6.1. 题意
几场比赛打得太爆炸了,道心有点破碎,听教练的意见决定板刷CF Educational Round
CF Educational Round1(CF598) 个人题解
1. A. Tricky Sum 传送门
1.1. 题意
多组测试数据,给定整数 \(n(\leq 10^9)\),对于从 \(1 \sim n\) 当中的所有整数,如果是二的次幂就减去,否则加上,求最后的结果。
1.2. 做法
先计算 \(1 \sim n\) 的所有整数和,然后枚举二的次幂,如果 \(\leq n\) 就减去它的两倍(把算整数和的部分也减去)
1.3. 代码
// A. Tricky Sum
// 1000 ms
// 256 MB
// https://codeforces.com/contest/598/problem/A
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using real = long double;
int __stt = clock();
int tests = 1;
void solve() {
i64 n;
std::cin >> n;
i64 pow2 = 1;
i64 ans = n * (n + 1) / 2;
while (pow2 <= n) {
ans -= (pow2 << 1ll);
pow2 <<= 1ll;
}
std::cout << ans << '\n';
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
std::cin >> tests;
while (tests--) {
solve();
}
#ifdef LOCAL
int __edt = clock();
std::cerr << "Time: " << (__edt - __stt) << "ms\n";
#endif
return 0;
}
时间复杂度(真的还需要吗): \(O(T log N)\) (\(log\) 为枚举二的次幂,整数和计算为 \(O(1)\))
2. B. Queries on a String 传送门
2.1. 题意(何移位)
给定一个字符串 \(s(|s| \leq 10 ^ 4)\) 和 \(m(\leq 300)\) 次询问
每次询问给定三个整数 \(l, r, k(\leq 10 ^ 6)\)
表示将 \(s\) 中的 \(l ~ r\) 个字符向右循环移位 \(k\) 位
输出最后的字符串 \(s\)
2.2. 做法
发现循环移位是有周期性的,对于长度为 \(len\) 的字符串,如果循环移位 \(len\) 位相当于没动,所以我们可以将 \(k \ \% (r - l + 1)\), 即对移位的子串长度取模
那么这个 \(k\) 最大为 \(|s| \ (\leq 10^ 4)\), 又仅有不超过 \(300\) 次询问,所以直接暴力模拟即可,复杂度是 \(O(m \sum _{i = 1} ^ m (r_i - l_i + 1))\) , 最大为 \(O(m |s|)\)
2.3. 代码
// B. Queries on a String
// 2000 ms
// 256 MB
// https://codeforces.com/contest/598/problem/B
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using real = long double;
int __stt = clock();
int tests = 1;
void solve() {
std::string s;
int m;
std::cin >> s >> m;
while (m--) {
int l, r, k;
std::cin >> l >> r >> k;
--l, --r;
k %= (r - l + 1);
std::string t = s;
for (int i = 0; i < (r - l + 1); ++i)
t[l + i] = s[l + (i - k + (r - l + 1)) % (r - l + 1)];
s.swap(t);
}
std::cout << s << '\n';
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
while (tests--) {
solve();
}
#ifdef LOCAL
int __edt = clock();
std::cerr << "Time: " << (__edt - __stt) << "ms\n";
#endif
return 0;
}
3. C. Nearest vectors 传送门
3.1. 题意
给定 \(n(\leq 10 ^ 5)\) 个从原点出发到 \((x_i, y_i)\) 的向量,找出一对两个向量之间夹角最小的向量下标
3.2. 做法
在 C++ 中,有这么一个函数: \(atan2\)
它有什么用呢?
这个函数原型接受两个 double 类型变量 \(x, y\),然后返回一个 double 类型变量,为平面直角坐标系中,以 x轴的正半轴 为始边,原点到 \((x, y)\) 这一条射线为终边的角的大小。用弧度制表示,返回值范围为 \([-\pi, \pi]\)
利用此,我们可以将原向量排序成一个从第三象限出发到第四象限到第一象限到第二象限的顺时针顺序
差不多长这样

那一个简单的观察就是对于一个向量而言,只有相邻与它的两个向量有可能成为与该向量夹角最小的向量
即 sort 完后可以直接遍历每一条边,特判一下第一条和最后一条即可
时间复杂度 \(O(n log n)\) (\(log\) 来自 sort) 常数较大(cmath库函数一般较慢)
3.3. 代码
// C. Nearest vectors
// 2000 ms
// 256 MB
// https://codeforces.com/contest/598/problem/C
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using real = long double;
int __stt = clock();
int tests = 1;
void solve() {
const static real pi = acosl(-1);
int n;
std::cin >> n;
struct point {
real x, y;
point(real _x = 0.0, real _y = 0.0) : x(_x), y(_y) {}
bool operator<(const point& oth) const {
return atan2l(y, x) < atan2l(oth.y, oth.x);
}
};
std::vector<std::pair<point, int> > points(n + 1, {{0.0, 0.0}, 0});
for (int i = 1; i <= n; ++i) {
std::cin >> points[i].first.x >> points[i].first.y;
points[i].second = i;
}
std::sort(points.begin() + 1, points.end());
auto calc = [&](const point& a, const point& b) -> real {
real diff = atan2l(b.y, b.x) - atan2l(a.y, a.x);
if (diff < 0)
diff += 2 * pi;
return std::min(diff, 2 * pi - diff);
};
real mn = calc(points[1].first, points[n].first);
std::pair<int, int> ans = {points[1].second, points[n].second};
for (int i = 1; i < n; ++i) {
real cur = calc(points[i].first, points[i + 1].first);
if (cur < mn) {
mn = cur;
ans = {points[i].second, points[i + 1].second};
}
}
std::cout << ans.first << ' ' << ans.second << '\n';
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
while (tests--) {
solve();
}
#ifdef LOCAL
int __edt = clock();
std::cerr << "Time: " << (__edt - __stt) << "ms\n";
#endif
return 0;
}
一个需要注意的地方是这道题 double 可能会爆精度,所以需要用 long double
与之相应的 cmath 函数也要改用更高精的 \(atan2l\) 与 \(acosl\)
4. D. Igor In the Museum 传送门
4.1. 题意
给一个 \(n\) 行 \(m\) 列 \((n, m \leq 10 ^ 3)\) 的地图,由 * 和 . 组成
每一个 . 是一个展厅,每一个 * 是墙壁。每一个与展厅相邻的墙上会挂一个展品。
现在有 \(k \ (\leq \min(n \times m, 10 ^ 5))\) 次查询,每次查询给定一个展厅位置 \((x, y)\),问你从这个位置出发在不穿墙的情况下可以看多少个展厅
4.2. 做法
这道题有很多种做法(BFS,DFS,DSU……),但核心都是找到连通块然后计算
我这里写了一版先预处理每个展厅可看到的展品数量,然后两次BFS预处理出每个连通块可看到的展品数量,这样就是 \(O(nm)\) 预处理, \(O(1)\) 查询
其实每次遇到没有处理的展厅再从这个位置 BFS 的话,均摊下来也是 \(O(nm)\) 的,因为你每个点只会遍历到一次嘛
4.3. 代码
// D. Igor In the Museum
// 1000 ms
// 256 MB
// https://codeforces.com/contest/598/problem/D
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using real = long double;
int __stt = clock();
int tests = 1;
void solve() {
int n, m, Q;
std::cin >> n >> m >> Q;
std::vector<std::vector<char>> mp(n + 1, std::vector<char>(m + 1, '!'));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
std::cin >> mp[i][j];
// for (int i = 1; i <= n; ++i) {
// for (int j = 1; j <= m; ++j)
// std::cerr << mp[i][j];
// std::cerr << '\n';
// }
std::vector<std::vector<int>> cnt(n + 1, std::vector<int>(m + 1, 0));
static constexpr int dx[4] = {0, 1, 0, -1};
static constexpr int dy[4] = {1, 0, -1, 0};
for (int i = 2; i < n; ++i)
for (int j = 2; j < m; ++j)
if (mp[i][j] == '.')
for (int k = 0; k < 4; ++k)
cnt[i][j] += (mp[i + dx[k]][j + dy[k]] == '*');
std::vector<std::vector<int>> mem(n + 1, std::vector<int>(m + 1, 0));
std::vector<std::vector<bool>> vis1(n + 1, std::vector<bool>(m + 1, false));
std::vector<std::vector<bool>> vis2(n + 1, std::vector<bool>(m + 1, false));
// for (int i = 1; i <= n; ++i) {
// for (int j = 1; j <= m; ++j)
// std::cerr << cnt[i][j] << ' ';
// std::cerr << '\n';
// }
for (int x = 2; x < n; ++x) {
for (int y = 2; y < m; ++y) {
if (not vis1[x][y] and mp[x][y] == '.') {
// std::cerr << x << ' ' << y << '\n';
vis1[x][y] = true;
std::queue<std::pair<int, int>> q;
int sum = 0;
q.emplace(x, y);
while (not q.empty()) {
// std::cerr << "de" << '\n';
auto front = q.front();
sum += cnt[front.first][front.second];
q.pop();
for (int k = 0; k < 4; ++k) {
int nx = front.first + dx[k];
int ny = front.second + dy[k];
if (not vis1[nx][ny] and mp[nx][ny] == '.') {
// std::cerr << nx << ' ' << ny << '\n';
vis1[nx][ny] = true;
q.emplace(nx, ny);
}
}
}
// std::cerr << "Passed" << '\n';
vis2[x][y] = true;
std::queue<std::pair<int, int>> qq;
qq.emplace(x, y);
while (not qq.empty()) {
auto front = qq.front();
qq.pop();
mem[front.first][front.second] = sum;
for (int k = 0; k < 4; ++k) {
int nx = front.first + dx[k];
int ny = front.second + dy[k];
if (not vis2[nx][ny] and mp[nx][ny] == '.') {
vis2[nx][ny] = true;
qq.emplace(nx, ny);
}
}
}
// std::cerr << "Passed" << '\n';
}
}
}
while (Q--) {
int x, y;
std::cin >> x >> y;
std::cout << mem[x][y] << '\n';
}
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
while (tests--) {
solve();
}
#ifdef LOCAL
int __edt = clock();
std::cerr << "Time: " << (__edt - __stt) << "ms\n";
#endif
return 0;
}
5. E. Chocolate Bar 传送门
5.1. 题意
你有一块 \(n * m (n, m \leq 30)\) 的巧克力,你可以沿着行或列把它掰开
每一次掰巧克力操作之后,如果豁口长度为 \(k\), 那你这一次操作的代价为 \(k ^ 2\)
你想在若干次操作之后取出若干块巧克力使得它们的块数恰为 \(k \ (\leq \min(50, n \times m))\),同时使得代价最小,求出最小代价
5.2. 做法
一个较为显然的 DP / 记忆化搜索 题目,但是状态如何设计?
我们令 \(dp(i, j, k)\) 表示 \(i\) 行 \(j\) 列的巧克力掰完后可以凑出 \(k\) 块巧克力所需的最小代价
那么就可以尝试枚举每一次是横着切还是竖着切,切完之后的两段分别为结果贡献多少块巧克力
也就是
答案即为 \(dp(n, m, k)\) 这个式子对于记忆化搜索来说十分友好,所以这里采用了记忆化搜索做法,可以先预处理出来整张表,然后 \(O(1)\) 查询,这里我用的是全局记忆化,如果没有搜过就搜索一下,均摊下来是 \(O(状态数 \times 转移)\) = \(O(nmk^2 \ \max(n, m))\)
5.3. 代码
// E. Chocolate Bar
// 2000 ms
// 256 MB
// https://codeforces.com/contest/598/problem/E
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using u128 = unsigned __int128;
using real = long double;
int __stt = clock();
int tests = 1;
std::vector<std::vector<std::vector<int> > > dp(
31,
std::vector<std::vector<int> >(31, std::vector<int>(51, -1)));
void solve() {
int n, m, k;
std::cin >> n >> m >> k;
auto dfs = [&](auto&& dfs, int i, int j, int k) {
if (~dp[i][j][k])
return dp[i][j][k];
if (not k or i * j == k)
return 0;
if (i * j < k)
return 0x3f3f3f3f;
int dpval = 0x3f3f3f3f;
for (int a = 1; a < i; ++a)
for (int b = 0; b <= k; ++b)
dpval = std::min(dpval, j * j + dfs(dfs, a, j, b) +
dfs(dfs, i - a, j, k - b));
for (int a = 1; a < j; ++a)
for (int b = 0; b <= k; ++b)
dpval = std::min(dpval, i * i + dfs(dfs, i, a, b) +
dfs(dfs, i, j - a, k - b));
return dp[i][j][k] = dpval;
};
std::cout << dfs(dfs, n, m, k) << '\n';
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
std::cin >> tests;
while (tests--) {
solve();
}
#ifdef LOCAL
int __edt = clock();
std::cerr << "Time: " << (__edt - __stt) << "ms\n";
#endif
return 0;
}
6. F. Cut Length 传送门
6.1. 题意
给你按顺时针或逆时针排序的一个 \(n \ (\leq 10 ^ 3)\) 边形(保证不会打结),接下来给定 \(m \ (\leq 10^ 2)\) 条直线,每条直线由其上的两个点描述,问这条直线过这个多边形的长度为多少, 误差不超过 \(10 ^ {-6}\)
7. 做法
很不会计算几何,后面再来补吧

浙公网安备 33010602011771号