C. Security 2

要从反向模拟入手!
逆向思考后:

  • 按钮 \(A\):移除末尾的0
  • 按钮 \(B\):将S的所有数字减1

此时只需反复执行“按按钮 \(B\) 直到 \(S\) 末尾变为 \(0 \to\) 按按钮 \(A\) ” 即可。

如果每次按按钮 \(B\) 都实际改写整个字符串 \(S\),复杂度会达到 \(O(|S|^2)\) 导致TLE。其实只需记录按钮 \(B\) 被按过的次数 \(x\),现场计算当前 \(S\) 末尾的值即可!实际上这个值不需要真的算出来,只需判定末尾的值是否是 \(0\) 即可。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    string s;
    cin >> s;
    
    int ans = 0, x = 0;
    while (s != "") {
        while (1) {
            int d = s.back()-'0';
            if ((d-x)%10 == 0) break;
            ans++; x++;
        }
        s.pop_back();
        ans++;
    }
    
    cout << ans << '\n';
    
    return 0;
}

D. Domino Covering XOR

只需从左上角的格子开始,按顺序尝试「纵向放置多米诺」「横向放置多米诺」「不放置」这三种选择进行DFS就行了!

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

int main() {
    int h, w;
    cin >> h >> w;
    
    vector a(h, vector<ll>(w));
    rep(i, h)rep(j, w) cin >> a[i][j];
    
    ll ans = 0;
    vector covered(h, vector<bool>(w));
    auto f = [&](auto& f, int i, int j, ll x) -> void {
        if (j == w) j = 0, i++;
        if (i == h) {
            ans = max(ans, x);
            return;
        }
        if (covered[i][j]) {
            f(f, i, j+1, x);
        }
        else {
            f(f, i, j+1, x^a[i][j]);
            if (j+1 < w and !covered[i][j+1]) {
                covered[i][j] = covered[i][j+1] = true;
                f(f, i, j+1, x);
                covered[i][j] = covered[i][j+1] = false;
            }
            if (i+1 < h and !covered[i+1][j]) {
                covered[i][j] = covered[i+1][j] = true;
                f(f, i, j+1, x);
                covered[i][j] = covered[i+1][j] = false;
            }
        }
    };
    f(f, 0, 0, 0);
    
    cout << ans << '\n';
    
    return 0;
}

E. Most Valuable Parentheses

贪心
\(N\) 个左括号和N个右括号组成的字符串要构成合法括号序列的充要条件就是『任何前缀中左括号数都不少于右括号数』。
所以问题就转化为:『在长度为 \(2N\) 的序列中选 \(N\) 个数标记,要求任何前缀中被标记数的数量都不少于未标记数,求被标记数之和的最大值』。这个数量条件等价于『对于任意奇数 \(x\),前 \(x\) 位中至少有 \(\lceil\frac{x}{2}\rceil\) 个标记』,由于 \(x\) 越小条件越严格,所以只需要满足:

  • \(1\) 位至少有 \(1\) 个标记
  • \(3\) 位至少有 \(2\) 个标记
  • \(5\) 位至少有 \(3\) 个标记
  • \(\cdots\cdots\)

于是我们采取『在集合中先添加两个数,再标记其中一个』的贪心策略即可。用大根堆就能实现!

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

void solve() {
    int n;
    cin >> n;
    
    n *= 2;
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    ll ans = a[0];
    priority_queue<int> q;
    for (int i = 1; i < n-1; i += 2) {
        q.push(a[i]);
        q.push(a[i+1]);
        ans += q.top(); q.pop();
    }
    cout << ans << '\n';
}

int main() {
    int t;
    cin >> t;
    
    while (t--) solve();
    
    return 0;
}

F. Sums of Sliding Window Maximum

按照 \(A_i\) 的值从大到小处理『最大值恰好为 \(A_i\) 的连续子序列有多少个?』这个问题。只需要能实现等差数列的区间加法操作即可,用延迟线段树就能搞定!如果巧妙处理的话,用差分也可以实现。

再讲一下差分做法:
\(A_i\) 作为最大值时,将包含 \(A_i\) 的区间所产生的贡献加入答案(由于是区间线性关系,可用差分进行一次函数的区间加法计算)。不包含最大值的部分则分别在左右两侧递归处理,通过笛卡尔树(Cartesian Tree)实现递归。

代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

template<class T=long long>
struct CartesianTree {
    int n, root;
    vector<int> l, r;
    CartesianTree() {}
    CartesianTree(const vector<T>& a, bool _max=true) {
        n = a.size();
        l = r = vector<int>(n, -1);
        vector<int> st;
        rep(i, n) {
            int p = -1;
            while (st.size() and !((a[st.back()] < a[i]) ^ _max)) { 
                int j = st.back(); st.pop_back();
                r[j] = p; p = j;
            }
            l[i] = p;
            st.push_back(i);
        }
        rep(i, st.size()-1) r[st[i]] = st[i+1];
        root = st[0];
    }
};

int main() {
    int n;
    cin >> n;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    CartesianTree<int> t(a);
    vector<ll> d(n+2);
    auto f = [&](auto& f, int v) -> int {
        if (v == -1) return 0;
        int l = f(f, t.l[v])+1;
        int r = f(f, t.r[v])+1;
        d[0] += a[v]; d[l] -= a[v];
        d[r] -= a[v]; d[l+r] += a[v];
        return l+r-1;
    };
    f(f, t.root);
    
    rep(i, n+1) d[i+1] += d[i];
    rep(i, n+1) d[i+1] += d[i];
    rep(i, n) cout << d[i] << '\n';
    
    return 0;
}

G. Domino Covering SUM

正难则反,可以先求出被覆盖的格子的权值总和,然后用网格总权值减去它就得到了最终的答案。要使答案最大,那么就该让被覆盖的格子的权值和最小

将格点作为顶点,在相邻格点之间建一条边权为 \(A_{ij}+A_{i'j'}\) 的边构成一个二分图,然后求最小权匹配即可。实际操作时,可以先将所有边权加上一个常数 \(G\) 使之为非负数,再求最小费用流,最终结果就是 \(\min_k(\) 流量为 \(k\) 时的最小费用 \(- kG)\)

代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;
using ll = long long;

int main() {
    int h, w;
    cin >> h >> w;
    
    vector a(h, vector<ll>(w));
    rep(i, h)rep(j, w) cin >> a[i][j];
    
    const ll G = 2e12;
    
    int n = h*w;
    int sv = n, tv = sv+1;
    mcf_graph<int, ll> g(tv+1);
    rep(i, h)rep(j, w) {
        int v = i*w+j;
        if (i+j&1) {
            g.add_edge(v, tv, 1, 0);
        }
        else {
            g.add_edge(sv, v, 1, 0);
        }
        
        auto add = [&](int ni, int nj) {
            int u = ni*w+nj;
            if (i+j&1) {
                g.add_edge(u, v, 1, a[i][j]+a[ni][nj]+G);
            }
            else {
                g.add_edge(v, u, 1, a[i][j]+a[ni][nj]+G);
            }
        };
        if (i) add(i-1, j);
        if (j) add(i, j-1);
    }
    
    ll tot = 0;
    rep(i, h)rep(j, w) tot += a[i][j];
    
    ll ans = -1e18;
    for (auto [flow, cost] : g.slope(sv, tv)) {
        ll now = tot - (cost-G*flow);
        ans = max(ans, now);
    }
    
    cout << ans << '\n';
    
    return 0;
}