Codeforces Round 972 (Div. 2)
A. 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;
}
B1. 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;
}
B2. 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;
}
C. Lazy Narek
因为最后是的答案是\(score_n - score_c\),所以实际上我们可以直接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;
}
D. Alter the GCD
首先我们可以想到,如果枚举出了两个端点,就可以方便的计算出区间的最大公约数。
我们考虑,前缀\(\gcd\)的值的个数是不多,至多是\(\log n\)个。
比如我们枚举\(l,r\),表示分为三个区间\([0,l-1],[l,r-1],[r,n -1]\)。我们考虑三个区间分别怎么求解?对于\([0,l-1]\)可以用前缀最大公约数,\([r,n-1]\)我们可以用后缀最大公约数。中间的区间怎么求?
有一种解法是根据\(\gcd\)个数不超过\(\log\)个,我们可以枚举左端点,然后按照\(\gcd\)的值枚举右端点,中间的值用\(ST\)表实现。而枚举右端点,我们可以用二分查找找到与上一个右端点第一个不同点就好。这样的话复杂度可以实现\(O(N\log^3N)\)的复杂度。但是很可惜这种做法目前似乎无法通过了。
下面的解法来着 jly。
首先我们考虑枚举右段点\(r\),然后对左端点,我们统计出前缀\(\gcd\)每个值出现最靠后的位置,共\(\log\)个。然后我们可以计算出一个\(c[i]\),表示\([i,r-1]\)的区间\(\gcd\),这个数组的值依旧只有\(\log\)个,因此我们可以像统计前缀\(\gcd\)每个值出现的最靠后的位置,统计出中间区间\(\gcd\)的值出现的最靠后的位置。这样的话,如果我们要维护这个数组的代价就是\(\log\)的,
此时我们发现,对于当前的\(r\),其左端点可选的值有两个数组的前缀\(\gcd\)值出现的最靠后的位置,两个数组中间区间\(\gcd\)值出现的位置,一共只有\(4\log\)个,所以直接枚举就好,复杂度是\(O(N\log^2N)\)。
#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;
}
E1. Subtangle Game (Easy Version)
我们可以用SG函数来分析这到题目,我们可以\(SG(x,y,i)\)表示当应该从\((x,y)\)到\((n,m)\)选择\(a_i\)的 SG函数,然后我们只要找到范围的\(a_i\)并递归计算就好了。
但是如果我们直接暴力的扫描复杂度肯定是无法接受的。
注意到\(a_i\)的范围的其实不大,我们可以统计出对于所有的值出现某一行的某一列,然后就可以枚举加二分快速的找到值的位置并转移。
然后再加一个简单的记忆化就可以通过这道题目。
#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;
}
E2. Subtangle Game (Hard Version)
这道题目,主要是理解 jly 的代码为主。
我们做一些简单的约束,首先下标都是 0 开始,其次保证\(n < m\),如果不满足手动转置一下,最后\(l=\min(l,n)\),因为 1 行至多放一个,所以最多放\(n\)个。
用\((x,y)\)表示坐标,用\([x,y]\)表示\((x,y)\)到\((n-1,m-1)\)右下角的这个区域。
那么如何判断\([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] \ge f[i + 1][x]\),这样的话我们如果从大到小枚举\(x\),则\(f[i][x]\)可以从\(f[i][x + 1]\)继承过来。
那么考虑我在什么情况下,可以增大\(f[i][x]\),我们贪心的选择出\(x\)行中数字\(a_i\)最后一次出现的列\(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’+1\ge f[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 = n - 1\)时,我在最后一行,我没有\(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;
}