Educational Codeforces Round 150 (Rated for Div. 2) A-E
阅读原文时间:2023年08月14日阅读:1

比赛链接

A

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

bool solve() {
    int n;
    cin >> n;
    if (n <= 4) cout << "Bob" << '\n';
    else cout << "Alice" << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

B

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

bool solve() {
    int q;
    cin >> q;

    bool ok = 1;
    int limit = -1;
    int pre = 0;
    while (q--) {
        int x;
        cin >> x;
        if (limit < 0) limit = x;
        if (ok) {
            if (pre <= x || x <= limit) {
                if (pre > x) ok = 0;
                cout << 1;
                pre = x;
            }
            else cout << 0;
        }
        else {
            if (pre <= x && x <= limit) cout << 1, pre = x;
            else cout << 0;
        }
    }
    cout << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

C

给定一个字符串 \(s\) ,仅含有 A-E \(5\) 个大写字母,其中字母代表一个值 |A| = 1, ..., |E| = 10000

一个字符串的值为 \(\displaystyle \sum_{i=1}^n (-1)^{\left[s_i < \max\limits_{i+1 \leq j \leq n}(s_i)\right]} \cdot |s_i|\) 。

方法一

知识点:线性dp,枚举。

考虑枚举每个位置作为修改的位置。

对于位置 \(i\) 的修改,只会影响到 \([1,i-1]\) 之间字符值的符号,而 \([i+1,n]\) 是不会被影响的。因此,我们需要预先知道, \([1,i-1]\) 在 \([i,n]\) 最大值确定时的值。

设 \(f_{i,j}\) 表示 \([1,i-1]\) 在 \([i,n]\) 的最大字符为 \(j\) 时的值。转移方程为:

\[f_{i,j} = f_{i-1,\max(s_{i-1},j)} + (-1)^{[s_{i-1}<j]} \cdot |s_{i-1}|
\]

随后,我们从后往前枚举修改位置 \(i\) 方便递推。设 \([i+1,n]\) 部分的值 \(sub\) 以及最大字符 \(mx\) ,若 \(s_i\) 修改为 \(j\) ,答案则为:

\[f_{i,\max(mx, j)} + (-1)^{[j < mx]} \cdot |j| + sub
\]

然后,我们递推 \(sub = sub + (-1)^{[s_i < mx]} \cdot |s_i|\) ,然后 \(mx = \max(mx,s_i)\) 。

最后,取 \(n\) 个修改位置的最大值即可。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

方法二

知识点:线性dp。

由于修改 \(i\) 时,\([i+1,n]\) 是不会被影响的,考虑从右往左递推。

设 \(f_{i,j,k}\) 表示 \([i,n]\) 的最大字符是 \(j\) 且修改了 \(k\) 次的最大值。转移方程为:

\[f_{i,\max(j,y),k + [s_{i} \neq y]} = f_{i+1,j,k} + (-1)^{[y<\max(j,y)]} \cdot |y|
\]

其中 \(y\) 是 \(s_{i}\) 的字符, \(k+[s_{i} \neq y]\) 应该小于 \(2\) 保证修改次数小于 \(2\) 。

这种方法比方法一更加通用好写。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

方法一

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int f[200007][5];//[1,i-1]在[i,n]的最大值为j时的值
const int mp[5] = { 1,10,100,1000,10000 };
bool solve() {
    string s;
    cin >> s;
    int n = s.size();
    s = "?" + s;

    for (int i = 2;i <= n;i++) {
        int x = s[i - 1] - 'A';
        for (int j = 0;j < 5;j++) f[i][j] = f[i - 1][max(j, x)] + (x < j ? -1 : 1) * mp[x];
    }

    int ans = -2e9;
    int sub = 0, mx = 0;
    for (int i = n;i >= 1;i--) {
        for (int j = 0;j < 5;j++) ans = max(ans, f[i][max(mx, j)] + (j < mx ? -1 : 1) * mp[j] + sub);
        int x = s[i] - 'A';
        sub += (x < mx ? -1 : 1) * mp[x];
        mx = max(x, mx);
    }

    cout << ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

方法二

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int f[2][5][2];//[i,n]的最大值为j且修改了k次时的值
const int mp[5] = { 1,10,100,1000,10000 };
bool solve() {
    string s;
    cin >> s;
    int n = s.size();
    s = "?" + s;

    for (int j = 0;j < 5;j++) f[0][j][0] = f[0][j][1] = -1e9;
    f[0][0][0] = 0;
    for (int i = n;i >= 1;i--) {
        for (int j = 0;j < 5;j++) f[1][j][0] = f[1][j][1] = -1e9;

        int x = s[i] - 'A';
        for (int j = 0;j < 5;j++) {
            for (int k = 0;k < 2;k++) {
                for (int y = 0;y < 5;y++) {
                    int jj = max(j, y);
                    int kk = k + (x != y); // k = 1,x != y,之前修改了这次也修改了,所以不合法
                    if (kk < 2) f[1][jj][kk] = max(f[1][jj][kk], f[0][j][k] + (y < jj ? -1 : 1) * mp[y]);
                }
            }
        }

        swap(f[0], f[1]);
    }

    int ans = -1e9;
    for (int j = 0;j < 5;j++) ans = max({ ans,f[0][j][0],f[0][j][1] });
    cout << ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

D

有 \(k\) 个线段, \(k\) 是偶数,如果能把他们分成两个一组,且满足要求:

  1. 每个线段只能出现在一个组里。
  2. 同一组的线段必须相交。
  3. 不同组的线段不能相交。

那么称这 \(k\) 个线段是合格的。

现在给你 \(n\) 个线段,问最少需要去掉多少条线段,才能使得剩下的线段是合格的。

知识点:贪心,枚举。

问题等价于最多能分多少组,就是活动安排问题的升级版,思路还是一样的。

先按右端点排序,贪心地从左往右选,是为了尽可能让后面选的线段不会与前面已经分好的组碰撞。我们用 \(maxr\) 记录已经分好组的线段的最大右端点,之后左端点小于等于它的都是不能选的。

我们还需要让同一组的线段相交,所以需要记录上一个选好的线段的右端点 \(lastr\) 。新的线段在大于 \(maxr\) 的基础上,若小于等于 \(lastr\) ,那么就可以与上一个选的线段直接贪心地合并成一组,这样保证新的组右端点最小,同样是为了尽可能让后面选的线段不会与前面已经分好的组碰撞,然后答案加 \(1\) 并且更新 \(maxr\) ;否则,用新的线段的右端点更新 \(lastr\) ,使得后面更有机会和这个线段合并。

时间复杂度 \(O(n \log n)\)

空间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int l[2007], r[2007];
int ord[2007];

bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) cin >> l[i] >> r[i];
    iota(ord + 1, ord + n + 1, 1);
    sort(ord + 1, ord + n + 1, [&](int a, int b) {return r[a] < r[b];});

    int ans = 0, maxr = -1, lastr = -1;
    for (int i = 1;i <= n;i++) {
        int id = ord[i];
        if (l[id] <= maxr) continue;
        if (l[id] <= lastr) {
            ans++;
            maxr = r[id];
        }
        else lastr = r[id];
    }
    cout << n - 2 * ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

E

给一个 \(n\times n\) 的矩阵,第 \(i\) 列 \([1,a_i]\) 的格子是黑的而 \([a_i+1,n]\) 的格子是白的。

有 \(1, \cdots ,m\) 的 \(m\) 个数字,要填在矩阵中,每个数字只能填一次,只能填在白色的格子里。

定义一个矩阵的值为数字 \(j\) 的数量,其中 \(j\) 满足 \(j+1\) 填在同一行下一列。

求矩阵的最大值。

方法一

知识点:单调栈,贪心,枚举。

模拟一下发现,每行白色格子被分成了若干段,要将数字填在尽量长的白色连续段内,因为一个白色连续段只会损失一个数字。因此,问题变成如何求出各种长度的白色连续段有多少个。

对于一个白色连续段的长度,其实就是它所在的位置能左右扩展直到遇到黑色格子的长度,即左右第一个白色列高小于自己的列高的位置。同时,我们发现一组相同位置和长度,但高度不同的连续白色段,一定是上下相邻的,且最大高度一定是跨越的白色列的高度最小值,最小高度一定是左右边界高度的最大值。

因此,考虑求出每个白色列以自己最大高度能扩展的左右边界,就是以这列高度为最大高度的白色连续段边界。这是个经典问题,可以用单调栈解决左右扩展边界的问题。

随后,我们就可以通过每个白色列的左右边界求出白色连续段的长度,列高减去左右边界的最大高度就是以这一列高度为最大高度往下的连续白色段的行数,即段数。

为了防止相同高度的白色列重复计算同一组白色连续段,考虑将左边界的意义改为第一个白色列高小于等于自己的列。这样只有最左侧的一列才会计算一次,其他列会因为左边界的高度等于自己的高度,从而计算出的段数为 \(0\) 。

得到了各种长度的白色连续段数,就可以贪心填数字了,注意数字不够填可以填段的部分。

时间复杂度 \(O(n)\)

空间复杂度 \(O(n)\)

方法二

知识点:STL,贪心,枚举。

注意到,白色连续段是从低到高,即行从大到小,逐渐被黑色格子分段的。

我们可以用 set 模拟分割过程,从低到高加入分段点,找到被分段的白色连续段端点可以计算出长度,再利用当前高度减去左右边界的最大高度,即左右边界的行最小值,就可以求出段数。

时间复杂度 \(O(n \log n)\)

空间复杂度 \(O(n)\)

方法一

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int a[200007];
int l[200007], r[200007];
ll cnt[200007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 1;i <= n;i++) {
        cin >> a[i];
        a[i] = n - a[i];
        l[i] = 0, r[i] = n + 1;
        cnt[i] = 0;
    }
    ll m;
    cin >> m;

    vector<int> stk;
    for (int i = 1;i <= n;i++) {
        while (stk.size() && a[stk.back()] > a[i]) {
            r[stk.back()] = i;
            stk.pop_back();
        }// 右开边界是第一个小于
        if (stk.size()) l[i] = stk.back();// 左开边界是第一个小于等于
        stk.push_back(i);
    }
    //! 左侧第一个小于等于,会让左侧第一个相同高度的有正确的左边界,其他的会在左侧等于的地方停止,避免之后重复计数
    //! 右侧第一个小于,是因为要让左侧第一个相同高度有正确的右边界,其他的虽然也会到正确的右边界,但是会因为左边界高度被排除
    //! 也可以左侧小于,右侧小于等于

    for (int i = 1;i <= n;i++) {
        int len = r[i] - l[i] - 1;
        int h = 0;
        if (l[i] >= 1) h = max(h, a[l[i]]);//! 相同的高度,但不是左侧第一个,h会等于a[i]
        if (r[i] <= n) h = max(h, a[r[i]]);
        cnt[len] += a[i] - h;
    }

    ll rest = m;
    ll ans = m;
    for (int i = n;i >= 1;i--) {
        if (i * cnt[i] < rest) {
            ans -= cnt[i];
            rest -= cnt[i] * i;
        }
        else {
            ans -= (rest + i - 1) / i;
            rest = 0;
        }
    }
    cout << ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}

方法二

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int a[200007];
vector<int> pos[200007];
ll cnt[200007];
bool solve() {
    int n;
    cin >> n;
    for (int i = 0;i <= n;i++) pos[i].clear();
    for (int i = 1;i <= n;i++) {
        cin >> a[i];
        pos[a[i]].push_back(i);
        cnt[i] = 0;
    }
    ll m;
    cin >> m;

    set<int> st;
    st.insert(0);
    st.insert(n + 1);
    a[0] = a[n + 1] = n;
    for (int i = n;i >= 0;i--) {
        for (auto x : pos[i]) {
            auto it = st.lower_bound(x);
            int pre = *prev(it);
            int nxt = *it;
            cnt[nxt - pre - 1] += min(a[pre], a[nxt]) - i;
            st.insert(x);
        }
    }

    ll rest = m;
    ll ans = m;
    for (int i = n;i >= 1;i--) {
        if (i * cnt[i] < rest) {
            ans -= cnt[i];
            rest -= cnt[i] * i;
        }
        else {
            ans -= (rest + i - 1) / i;
            rest = 0;
        }
    }
    cout << ans << '\n';
    return true;
}

int main() {
    std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--) {
        if (!solve()) cout << -1 << '\n';
    }
    return 0;
}