C. Variety

先用每种宝石里面价值最大的那个宝石来凑出 \(M\) 种,然后用剩余的宝石凑出接下来的 \(K-M\)

代码实现
#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 n, k, m;
    cin >> n >> k >> m;
    
    vector<vector<int>> vs(n);
    rep(i, n) {
        int c, v;
        cin >> c >> v;
        --c;
        vs[c].push_back(v);
    }
    
    vector<int> top, other;
    rep(i, n) {
        if (!vs[i].size()) continue;
        ranges::sort(vs[i]);
        top.push_back(vs[i].back());
        vs[i].pop_back();
        
        ranges::move(vs[i], back_inserter(other));
    }
    ranges::sort(top);
    
    ll ans = 0;
    rep(i, m) {
        ans += top.back(); top.pop_back();
    }
    ranges::move(top, back_inserter(other));
    ranges::sort(other, greater<>());
    rep(i, k-m) ans += other[i];
    
    cout << ans << '\n';
    
    return 0;
}

D. Count Subgrid Sum = K

先把矩形拍成一维,那么对于一维问题就是有多少个区间满足区间和等于 \(K\),很经典的问题。可以考虑转化成求多少个区间满足区间和 \(\leqslant K\),不妨记为 \(f(K)\),那么原问题的答案就是 \(f(K)-f(K-1)\) 。关于 \(f(K)\),用双指针就能解决。

代码实现
#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, k;
    cin >> h >> w >> k;
    
    vector<string> s(h);
    rep(i, h) cin >> s[i];
    
    auto f = [&](int k) -> ll {
        if (k == -1) return 0;
        ll res = 0;
        rep(li, h) {
            vector<int> a(w);
            for (int ri = li; ri < h; ++ri) {
                rep(j, w) a[j] += s[ri][j]-'0';
                
                int l = 0, sum = 0;
                rep(r, w) {
                    sum += a[r];
                    while (sum > k) {
                        sum -= a[l];
                        ++l;
                    }
                    res += r-l+1;
                }
            }
        }
        return res;
    };
    
    ll ans = f(k)-f(k-1);
    cout << ans << '\n';
    
    return 0;
}

E. E-liter

每次操作某一行(或某一列)时,我们不需要真正去修改网格中的每一个格子,而是通过查看交点格子在上一次操作到当前操作期间的变化,来动态维护黑色格子的总数。

行操作(染黑):

假设当前在时间 \(t\) 染黑第 \(i\) 行,该行上一次被操作的时间是 \(l\)

  • 在时间 \(l\) 时,该行全被染成了黑色。
  • 在时间区间 \([l, t)\) 内,如果某些列被操作了(由于列操作是染白),它们会将与该行交点的格子变成白色。
  • 现在时间 \(t\) 重新将整行染黑,这些在 \([l, t)\) 期间变白的格子会重新变黑。
  • 因此,黑色格子数量的增量 = 最后一次操作时间在 \([l, t)\) 之间的列的数量。

列操作(染白):

假设当前在时间 \(t\) 染白第 \(i\) 列,该列上一次被操作的时间是 \(l\)

  • 在时间 \(l\) 时,该列全被染成了白色。
  • 在时间区间 \([l, t)\) 内,如果某些行被操作了(行操作是染黑),它们会将与该列交点的格子变成黑色。
  • 现在时间 \(t\) 重新将整列染白,这些在 \([l, t)\) 期间变黑的格子会重新变白。
  • 因此,黑色格子数量的减量 = 最后一次操作时间在 \([l, t)\) 之间的行的数量。

需要实现支持以下功能的数据结构:

  • 单点加
  • 区间求和

可以用线段树或树状数组,但还是推荐树状数组,因为常数更小

代码实现
#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 n, q;
    cin >> n >> q;
    int m = q+4;
    
    vector last(2, vector<int>(n));
    vector d(2, fenwick_tree<int>(m));
    rep(i, 2) {
        last[i] = vector<int>(n, i);
        d[i].add(i, n);
    }
    
    ll ans = 0;
    for (int t = 2; t < q+2; ++t) {
        int x, i;
        cin >> x >> i;
        --x; --i;
        int &l = last[x][i];
        int now = d[x^1].sum(l, t);
        if (x) ans -= now; else ans += now;
        cout << ans << '\n';
        
        d[x].add(l, -1);
        l = t;
        d[x].add(l, 1);
    }
    
    return 0;
}

F. Total Product is N

答案就是,对于每个大小 \(l\),把大小为 \(l\) 的集合的总和乘以 \(l!\) 后相加即可。

关于大小为 \(l\) 的集合的总和,可以考虑 \(\text{dp}\)
dp[i][p] 表示已经选了 \(i\) 个数且当前乘积为 \(p\) 时的集合的方案数以及总和。
其中 \(i\) 最多取到 \(13\),因为 \(2 \times 3 \times \dots \times 14 > 10^{12}\),大于 \(1\) 的不同因数最多选 \(12\) 个,加上数字 \(1\) 最多 \(13\)
注意到 \(p\) 可以取到很大,但需要用到的 \(p\) 的数量却比较少,所以可以用 \(\text{map}\) 来维护 \(\text{dp}\)

类似题:P3861

代码实现
#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;
using mint = modint998244353;

int main() {
    ll n;
    cin >> n;
    
    vector<ll> ds;
    {
        set<ll> s;
        for (ll i = 1; i*i <= n; ++i) {
            if (n%i == 0) s.insert(i), s.insert(n/i); 
        }
        ds = vector<ll>(s.begin(), s.end());
    }
    
    const int m = 13;
    vector<map<ll, pair<mint, mint>>> dp(m+1);
    dp[0][1] = {1, 0};
    dp[1][1] = {1, 1};
    for (ll d : ds) if (d > 1) {
        for (int i = m-1; i >= 0; --i) {
            for (auto& [p, cs] : dp[i]) {
                auto [c, s] = cs;
                if (n/p%d) continue;
                ll np = p*d;
                dp[i+1][np].first += c;
                dp[i+1][np].second += s + c*d;
            }
        }
    }
    
    vector<mint> fac(m+1, 1);
    rep(i, m) fac[i+1] = fac[i]*(i+1);
    
    mint ans;
    for (int i = 1; i <= m; ++i) {
        ans += dp[i][n].second*fac[i];
    }
    cout << ans.val() << '\n';
    
    return 0;
}

G. Graph Problem 2026

题目要求给每个点赋非负整数权值 \(W_j \le 2026\),满足每条边两端的权值和 \(W_u + W_v \le 2026\),并最大化总权值。

根据网络流/线性规划中关于顶点覆盖松弛变量的性质,这类问题的最优解中,每个点的权值一定可以只取三种可能:\(0\)\(\frac{2026}{2}=1013\)、或 \(2026\)
因此,我们可以令 \(W_j = 1013 \times x_j\),其中 \(x_j \in \{0, 1, 2\}\)

此时约束条件转化为:

  • \(x_j \in \{0, 1, 2\}\)
  • 对于每条边 \((u, v)\),有 \(x_u + x_v \le 2\)
  • 目标是最大化 \(\sum\limits_{j=1}^N x_j\)

为了用网络流求解 \(x_j\),我们可以把每个变量 \(x_j\) 拆成两个 \(0/1\) 变量:\(x_j = 2 - l_j - r_j\),其中 \(l_j, r_j \in \{0, 1\}\)

将此代入边的约束条件 \(x_u + x_v \le 2\):$$(2 - l_u - r_u) + (2 - l_v - r_v) \le 2 \implies (l_u + r_v) + (l_v + r_u) \ge 2$$

因为 \(l_j, r_j\) 只能是 \(0\)\(1\),要使上式成立,必须同时满足:

  • \(l_u + r_v \ge 1\)
  • \(l_v + r_u \ge 1\)

这正是二分图最小点覆盖的定义!我们将原图中的每个点 \(i\) 拆成左部点 \(L_i\) 和右部点 \(R_i\)。对于原图的边 \((a, b)\),我们在二分图中连两条边:\(L_a \to R_b\)\(L_b \to R_a\)。选择 \(l_i=1\) 表示选左部点,\(r_i=1\) 表示选右部点。

根据柯尼希定理,二分图的最大流等于该二分图的小点覆盖数,即 \(\sum (l_j + r_j)\) 的最小值。
我们要最大化 \(\sum x_j = \sum (2 - l_j - r_j) = 2N - \sum (l_j + r_j)\)
因此最大 \(\sum x_j\) 就等于 $2n \ - $ 最大流。
最后乘以系数 \(1013\) 还原得到原本的权值和。

另外,这题本质上就是一个 \(k=3\)(变量有 \(3\) 种离散取值)的“\(k\) 值最小割”问题。

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

using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    
    int sv = n*2, tv = sv+1;
    mf_graph<int> g(tv+1);
    rep(i, n) g.add_edge(sv, i, 1);
    rep(i, n) g.add_edge(n+i, tv, 1);
    rep(i, m) {
        int a, b;
        cin >> a >> b;
        --a; --b;
        g.add_edge(a, n+b, 1);
        g.add_edge(b, n+a, 1);
    }
    
    int ans = (n*2-g.flow(sv, tv))*1013;
    cout << ans << '\n';
    
    return 0;
}