Codeforces Round 972 (Div. 2)

1|0A. Simple Palindrome


考虑到对于同一种字母无论怎么摆放,对答案的影响是相同的。所以我们可以直接把同一种字母放在一起,考虑不同中字母间为了消除回文串,必须是的同一种字母不会出现在另一种字母的两侧。因此我们只要尽可能的均分五种字母就好了。

#include <bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; #define int i64 using vi = vector<int>; using pii = pair<int, int>; const i32 inf = INT_MAX / 2; const i64 INF = LLONG_MAX / 2; const i64 mod = 1e9 + 7; const string s = "aeiou"; void solve() { int n; cin >> n; vi a(5, n / 5); n %= 5; for (int i = 0; i < n; i++) a[i]++; for (int i = 0; i < 5; i++) { for (int j = 0; j < a[i]; j++) cout << s[i]; } cout << "\n"; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--)solve(); return 0; }

2|0B1. The Strict Teacher (Easy Version)


如果学生在老师的两侧,则只有一种情况就是把学生赶到边界上。

如果学生在老师的中间,则两个老师向中间逼,而学生的策略一定是先逃到两个老师的正中间,然后等待。

#include <bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; #define int i64 using vi = vector<int>; using pii = pair<int, int>; const i32 inf = INT_MAX / 2; const i64 INF = LLONG_MAX / 2; const i64 mod = 1e9 + 7; void solve() { int n, m, q; cin >> n >> m >> q; int l, r; cin >> l >> r; if (l > r) swap(l, r); int p; cin >> p; if (p < l) { cout << l - 1 << "\n"; } else if (p > r) { cout << n - r << "\n"; } else { int x = p - l, y = r - p, res = 0; if (x < y) swap(x, y); res += (x - y) / 2, x -= res * 2; res += min(x, y); cout << res << "\n"; } return; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--)solve(); return 0; }

3|0B2. The Strict Teacher (Hard Version)


根据B1的结论,其实我们可以考虑到,能够决定答案的就是,离学生最近的两个老师。

#include <bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; #define int i64 using vi = vector<int>; using pii = pair<int, int>; const i32 inf = INT_MAX / 2; const i64 INF = LLONG_MAX / 2; const i64 mod = 1e9 + 7; int calc(int l, int r, int p, int n) { int x = p - l, y = r - p, res = 0; if (x < y) swap(x, y); res += (x - y) / 2, x -= res * 2; res += min(x, y); return res; } int calc(int l, int p, int n) { if(l > p) return l - 1; return n - l; } void solve() { int n, m, q; cin >> n >> m >> q; vi b(m); for (auto &i: b) cin >> i; ranges::sort(b); for (int p, l, r; q; q--) { cin >> p; auto it = ranges::lower_bound(b, p); if (it == b.end()) { l = *prev(it); cout << calc(l, p, n) << "\n"; } else if (it == b.begin()) { l = *it; cout << calc(l, p, n) << "\n"; } else { l = *prev(it), r = *it; cout << calc(l, r, p, n) << "\n"; } } return; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--)solve(); return 0; }

4|0C. Lazy Narek


因为最后是的答案是scorenscorec,所以实际上我们可以直接dp答案,也就是把 Narek 的得分记为正,GPT的得分记为负

然后对于单词与单词之间,我们可以用01背包求解单词是否选择。

对于单词的内部,我们如果知道了前一个单词选择到哪个字母了,那么当前单词就可以贪心的求出最优贡献。

当然了,题目有说过,对于末尾的部分,如果不完整,则无法得分,且GPT要得分,所以最后输出答案的时候,要把末尾的贡献剪掉。

#include <bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; #define int i64 using vi = vector<int>; using pii = pair<int, int>; const i32 inf = INT_MAX / 2; const i64 INF = LLONG_MAX / 2; const string T = "narek"; int sgn(char c) { for (int i = 0; i < 5; i++) if (c == T[i]) return i; return -1; } void solve() { int n, m; cin >> n >> m; vi f(5, -INF); f[0] = 0; string s; for (int i = 1; i <= n; i++) { cin >> s; auto g = f; for (int j = 0, sum, t; j < 5; j++) { sum = f[j], t = j; for (int h; auto c: s) { h = sgn(c); if (h == -1) continue; if (h == t) sum++, t = (t + 1) % 5; else sum--; } g[t] = max(g[t], sum); } f = move(g); } int res = 0; for (int i = 0; i < 5; i++) res = max(res, f[i] - i * 2); cout << res << "\n"; return; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--)solve(); return 0; }

5|0D. Alter the GCD


首先我们可以想到,如果枚举出了两个端点,就可以方便的计算出区间的最大公约数。

我们考虑,前缀gcd的值的个数是不多,至多是logn个。

比如我们枚举l,r,表示分为三个区间[0,l1],[l,r1],[r,n1]。我们考虑三个区间分别怎么求解?对于[0,l1]可以用前缀最大公约数,[r,n1]我们可以用后缀最大公约数。中间的区间怎么求?

有一种解法是根据gcd个数不超过log个,我们可以枚举左端点,然后按照gcd的值枚举右端点,中间的值用ST表实现。而枚举右端点,我们可以用二分查找找到与上一个右端点第一个不同点就好。这样的话复杂度可以实现O(Nlog3N)的复杂度。但是很可惜这种做法目前似乎无法通过了。

下面的解法来着 jly。

首先我们考虑枚举右段点r,然后对左端点,我们统计出前缀gcd每个值出现最靠后的位置,共log个。然后我们可以计算出一个c[i],表示[i,r1]的区间gcd,这个数组的值依旧只有log个,因此我们可以像统计前缀gcd每个值出现的最靠后的位置,统计出中间区间gcd的值出现的最靠后的位置。这样的话,如果我们要维护这个数组的代价就是log的,

此时我们发现,对于当前的r,其左端点可选的值有两个数组的前缀gcd值出现的最靠后的位置,两个数组中间区间gcd值出现的位置,一共只有4log个,所以直接枚举就好,复杂度是O(Nlog2N)

#include<bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; using vi = vector<int>; void solve() { int n; cin >> n; vi a(n); for (auto &i: a) cin >> i; vi b(n); for (auto &i: b) cin >> i; vi prea(n + 1), preb(n + 1); // pre 是 [0, i) 的 gcd for (int i = 0; i < n; i++) { prea[i + 1] = gcd(prea[i], a[i]); preb[i + 1] = gcd(preb[i], b[i]); } vi sufa(n + 1), sufb(n + 1); // suf 是 [i, n) 的 gcd for (int i = n - 1; i >= 0; i--) { sufa[i] = gcd(sufa[i + 1], a[i]); sufb[i] = gcd(sufb[i + 1], b[i]); } vector<array<int, 2>> pa, pb; // 记录有多少种前缀gcd for (int i = 0; i <= n; i++) { if (i == n or prea[i] != prea[i + 1]) pa.push_back({prea[i], i}); if (i == n or preb[i] != preb[i + 1]) pb.push_back({preb[i], i}); } int res = -1; i64 cnt = 0; vector<array<int, 2>> fa{{0, 0}}, fb{{0, 0}}; // 记录 [i, r) 的后缀gcd for (int r = 1; r <= n; r++) { // 枚举右区间 int t = a[r - 1]; for (int i = fa.size() - 1; i >= 0; i--) t = gcd(t, fa[i][0]), fa[i][0] = t; int k = 0; for (int i = 0; i < fa.size(); i++) { if (k > 0 and fa[k - 1][0] == fa[i][0]) { fa[k - 1][1] = fa[i][1]; } else { fa[k++] = fa[i]; } } fa.resize(k), fa.push_back({0, r}); t = b[r - 1]; for (int i = fb.size() - 1; i >= 0; i--) t = gcd(t, fb[i][0]), fb[i][0] = t; k = 0; for (int i = 0; i < fb.size(); i++) { if (k > 0 and fb[k - 1][0] == fb[i][0]) { fb[k - 1][1] = fb[i][1]; } else { fb[k++] = fb[i]; } } fb.resize(k), fb.push_back({0, r}); int ipa = 0, ipb = 0, ifa = 0, ifb = 0, lst = -1; while (true) { int u = min({pa[ipa][1], pb[ipb][1], fa[ifa][1], fb[ifb][1]}); // 区间组成为 a[0,u-1] b[u, r-1] a[r, n-1] if (u >= r) break; if (u > lst) { int ans = gcd(pa[ipa][0], gcd(fb[ifb][0], sufa[r])) + gcd(pb[ipb][0], gcd(fa[ifa][0], sufb[r])); if (res < ans) { res = ans, cnt = u - lst; } else if (res == ans) { cnt += u - lst; } } lst = u; if (pa[ipa][1] == u) ipa++; if (pb[ipb][1] == u) ipb++; if (fa[ifa][1] == u) ifa++; if (fb[ifb][1] == u) ifb++; } } cout << res << " " << cnt << "\n"; return; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--) solve(); return 0; }

6|0E1. Subtangle Game (Easy Version)


我们可以用SG函数来分析这到题目,我们可以SG(x,y,i)表示当应该从(x,y)(n,m)选择ai的 SG函数,然后我们只要找到范围的ai并递归计算就好了。

但是如果我们直接暴力的扫描复杂度肯定是无法接受的。

注意到ai的范围的其实不大,我们可以统计出对于所有的值出现某一行的某一列,然后就可以枚举加二分快速的找到值的位置并转移。

然后再加一个简单的记忆化就可以通过这道题目。

#include <bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; #define int i64 using vi = vector<int>; const i32 inf = INT_MAX / 2; int n, m, l; vi a; vector<vector<vi>> b; vector<vector<vi>> f; bool sg(int x, int y, int i) { if (i == l) return false; if (x > n or y > m) return false; if (f[x][y][i] != -1) return f[x][y][i]; for (int p = x, q; p <= n; p++) { q = ranges::lower_bound(b[a[i]][p], y) - b[a[i]][p].begin(); for (; q < b[a[i]][p].size(); q++) { if (sg(p + 1, b[a[i]][p][q] + 1, i + 1) == false) return f[x][y][i] = true; } } return f[x][y][i] = false; } void solve() { cin >> l >> n >> m; a = vi(l); for (auto &i: a) cin >> i; b = vector(8, vector<vi>(n + 1)); for (int i = 1; i <= n; i++) { for (int j = 1, x; j <= m; j++) { cin >> x; b[x][i].push_back(j); } } f = vector(n + 1, vector(m + 1, vi(l, -1))); if (sg(1, 1, 0)) cout << "T\n"; else cout << "N\n"; return; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--)solve(); return 0; }

7|0E2. Subtangle Game (Hard Version)


这道题目,主要是理解 jly 的代码为主。

我们做一些简单的约束,首先下标都是 0 开始,其次保证n<m,如果不满足手动转置一下,最后l=min(l,n),因为 1 行至多放一个,所以最多放n个。

(x,y)表示坐标,用[x,y]表示(x,y)(n1,m1)右下角的这个区域。

那么如何判断[x,y]的胜负状态?如果[x,y]的后继为空,或者[x,y]的后继全部是必败态,则[x,y]是必胜态。

我们设状态f[i][x],表示第i个数字,在第x行,给对手留下最大[x+1,f[i][x]]的选择区域是必胜的,换言之我需要最小[x,f[i][x]1]的选择区域才能保持必胜。

因此,可以推导出,f[i][x]f[i+1][x],这样的话我们如果从大到小枚举x,则f[i][x]可以从f[i][x+1]继承过来。

那么考虑我在什么情况下,可以增大f[i][x],我们贪心的选择出x行中数字ai最后一次出现的列y。如果说y+1>f[i+1][x+1]1那么我是可以更新的f[i][x]。为什么?因为对于对手来说,至少需要[x+1,f[i+1][x+1]1]的选择范围才能保持必胜。但是我选择了(x,y)点后,留给对手的范围是[x+1,y+1],因此对手必败,所以当前这个状态是必胜的。

对于刚才的条件,我们可以转换为y+1f[i+1][x+1],这个主要是为了方便理解 jly 的代码。

因此f[i][x]=max(f[i][x+1],y)。这里的y,如果我们维护出了每个数字在每一行出现的所有位置,我就可以二分出来。

然后在我和 jly 的代码中u=f[i+1][x+1],v=f[i][x+1]。因此当x=n1时,我在最后一行,我没有f[i][x+1],对手也没有f[i+1][x+1],因此u=0,v=0

考虑答案,如果说f[0][0]=0,是先手必败,因为先手必胜需要[0,1]的选择范围,而此时先手拥有的选择范围是[0,0]

然后就是刚刚说过的x是从大到小枚举,因此我们只要先更新u,再更新f[x]就可以优化的第一维空间。

#include <bits/stdc++.h> using namespace std; using i32 = int32_t; using i64 = long long; using vi = vector<int>; using pii = pair<int, int>; const i32 inf = INT_MAX / 2; void solve() { int n, m, l; cin >> l >> n >> m; vi a(l); for (auto &i: a) cin >> i, i--; vector b(n, vi(m)); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) cin >> b[i][j], b[i][j]--; if (n > m) { swap(n, m); vector c(n, vi(m)); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) c[i][j] = b[j][i]; b = move(c); } vector<vector<pii>> vec(n * m); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) vec[b[i][j]].emplace_back(i, j); l = min(l, n); vi f(n); for (int i = l - 1; i >= 0; i--) { int u = 0, v = 0; for (int x = n - 1; x >= 0; x--) { auto it = ranges::lower_bound(vec[a[i]], pair(x + 1, 0)); if (it != vec[a[i]].begin()) { // x 行最后一个 a[i] it--; if (it->first == x and it->second + 1 >= u) { v = max(v, it->second + 1); } } u = f[x], f[x] = v; } } cout << "NT"[f[0] > 0] << "\n"; return; } i32 main() { ios::sync_with_stdio(false), cin.tie(nullptr); int T; cin >> T; while (T--)solve(); return 0; }

__EOF__

本文作者PHarr
本文链接https://www.cnblogs.com/PHarr/p/18417409.html
关于博主:前OIer,SMUer
版权声明CC BY-NC 4.0
声援博主:如果这篇文章对您有帮助,不妨给我点个赞
posted @   PHarr  阅读(482)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示