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;
}
posted @ 2024-09-17 19:31  PHarr  阅读(441)  评论(0编辑  收藏  举报