C. 1122 Substring 2

两种做法,中心扩展和字符串压缩

  • 固定满足 \(s_i+1 = s_{i+1}\) 的两个相邻位置,然后向两边进行扩展
代码实现
#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 n = s.size();
    
    int ans = 0;
    rep(i, n-1) {
        if (s[i]+1 == s[i+1]) {
            ans++;
            int l = i, r = i+1;
            while (1 <= l and r+1 < n) {
                if (s[l] != s[l-1]) break;
                if (s[r] != s[r+1]) break;
                --l; ++r;
                ans++;
            }
        }
    }
    
    cout << ans << '\n';
    
    return 0;
}
  • 先做一遍字符串压缩,如果当前段满足加 \(1\) 后就变成下一段的字符,那么这两个相邻段的贡献就是取两段的数量的较小值
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

int main() {
    string s;
    cin >> s;
    
    vector<pair<char, int>> d;
    for (char c : s) {
        if (d.size() and d.back().first == c) d.back().second++;
        else d.emplace_back(c, 1);
    }
    
    int ans = 0;
    rep(i, d.size()-1) {
        if (d[i].first+1 != d[i+1].first) continue;
        ans += min(d[i].second, d[i+1].second);
    }
    
    cout << ans << '\n';
    
    return 0;
}

D. 183183

\(B_i = A_i\) 的长度

\(f(A_i, Aj) = A_i \cdot 10^{B_j} + A_j\)

\(M \big| A_i \cdot 10^{B_j} + A_j \Leftrightarrow A_i \cdot 10^{B_j} + A_j \equiv 0 \pmod M \Leftrightarrow A_j \equiv - A_i \cdot 10^{B_j} \pmod 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, m;
    cin >> n >> m;
    
    vector<int> a(n);
    rep(i, n) cin >> a[i];
    
    vector<vector<int>> as(11);
    rep(i, n) as[to_string(a[i]).size()].push_back(a[i]%m);
    
    ll ans = 0;
    ll ten = 1;
    rep(b, 11) {
        auto na = as[b];
        ranges::sort(na);
        auto count = [&](int r) {
            return upper_bound(na.begin(), na.end(), r) - lower_bound(na.begin(), na.end(), r);
        };
        rep(i, n) {
            ll r = (m-ten*a[i]%m)%m;
            ans += count(r);
        }
        ten *= 10; ten %= m;
    }
    
    cout << ans << '\n';
    
    return 0;
}

E. Max Matrix 2

贪心按值从大到小放置

先把行最大值序列 \(x\) 和列最大值序列 y 各自拷贝并按降序排序(原始顺序通过 rid, cid \(\text{map}\) 保存,最终要把答案映回原下标)。

维护两个指针 \(\text{xi}\)(已处理的行最大值数)、\(\text{yi}\)(已处理的列最大值数),初始都为 \(0\);还有一个候选单元格集合 \(cand\)

\(v = hw\) 开始到 \(1\) 递减处理每一个数 \(v\)

  • 如果当前最大的剩余行最大值 \(x[\text{xi}] = v\),则说明这是一个新的“需要在第 \(\text{xi}\) 行放置的行最大值”。此时把当前已“激活”的列(即 \(j=0 \cdots \text{yi}-1\))与该行组合成候选单元格 \((\text{xi}, j)\),并把 \(\text{xi++}\)(标记这一行已被激活)。
  • 如果当前最大的剩余列最大值 \(y[\text{yi}] = v\),则类似地把当前已激活的行(\(i=0 \cdots \text{xi}-1\))与该列组合成候选单元格 \((i, \text{yi})\),并把 \(\text{yi++}\)
  • 此时 \(cand\) 表示可以放置当前 \(v\) 的所有单元格(这些单元格不在尚未激活的行列的“交叉外”),如果 \(cand\) 为空则无法放置 \(v\),问题无解。
  • 否则从 \(cand\) 中取出最后一个单元格并令 \(a[i][j] \gets v\)

循环结束后,\(a\) 是按排序后的行列索引构造出的矩阵。最后用 ridcid 把行列索引映回原输入顺序即可。

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

using namespace std;
using P = pair<int, int>;

void solve() {
    int h, w;
    cin >> h >> w;
    
    vector<int> x(h), y(w);
    rep(i, h) cin >> x[i];
    rep(i, w) cin >> y[i];
    
    map<int, int> rid, cid;
    rep(i, h) rid[x[i]] = i;
    rep(i, w) cid[y[i]] = i;
    
    ranges::sort(x, greater<>());
    ranges::sort(y, greater<>());
    
    vector a(h, vector<int>(w));
    {
        int xi = 0, yi = 0;
        vector<P> cand;
        for (int v = h*w; v >= 1; --v) {
            if (xi < h and x[xi] == v) {
                rep(j, yi) cand.emplace_back(xi, j);
                ++xi;
            }
            if (yi < w and y[yi] == v) {
                rep(i, xi) cand.emplace_back(i, yi);
                ++yi;
            }
            if (cand.size() == 0) {
                puts("No");
                return;
            }
            auto [i, j] = cand.back(); cand.pop_back();
            a[i][j] = v;
        }
    }
    
    vector ans(h, vector<int>(w));
    rep(i, h)rep(j, w) ans[rid[x[i]]][cid[y[j]]] = a[i][j];
    puts("Yes");
    rep(i, h)rep(j, w) cout << ans[i][j] << " \n"[j == w-1];
}

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

F. 1122 Subsequence 2

考虑将 \(00\cdots0 1 \cdots 11\)\(11\cdots1 2 \cdots 22\)\(\cdots\)\(88\cdots8 9 \cdots 99\) 这九种分出来单独算。
我们只需考虑一个 \(01\) 序列,固定右边第一个 \(1\) 的位置 \(i\),记 \(l\)\(i\) 左边 \(0\) 的个数,\(r\) 为从 \(i\) 开始右边的 \(1\) 的个数
那么这个 \(01\) 序列的贡献就是 \(\displaystyle\sum\limits_{j=1} \dbinom{l}{j} \dbinom{r}{j-1} = \sum\limits_{j=0} \dbinom{l}{j+1} \dbinom{r}{j} = \sum\limits_{j=0} \dbinom{l}{l-j-1} \dbinom{r}{j} = \dbinom{l+r}{l-1}\) (最后一个等号是范德蒙恒等式)

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

struct modinv {
  int n; vector<mint> d;
  modinv(): n(2), d({0,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(-d[mint::mod()%n]*(mint::mod()/n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} invs;
struct modfact {
  int n; vector<mint> d;
  modfact(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*n), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} facts;
struct modfactinv {
  int n; vector<mint> d;
  modfactinv(): n(2), d({1,1}) {}
  mint operator()(int i) {
    while (n <= i) d.push_back(d.back()*invs(n)), ++n;
    return d[i];
  }
  mint operator[](int i) const { return d[i];}
} ifacts;
mint comb(int n, int k) {
  if (n < k || k < 0) return 0;
  return facts(n)*ifacts(k)*ifacts(n-k);
}

mint f(vector<int> a) {
    int n = a.size();
    int l = 0, r = 0;
    rep(i, n) r += a[i];
    mint res;
    rep(i, n) {
        if (a[i] == 0) ++l; else --r;
        if (a[i] == 1) {
            res += comb(l+r, l-1);
        }
    }
    return res;
}

int main() {
    string s;
    cin >> s;
    int n = s.size();
    
    mint ans;
    rep(x, 9) {
        char a = '0'+x, b = a+1;
        vector<int> d;
        rep(i, n) {
            if (s[i] == a) d.push_back(0);
            if (s[i] == b) d.push_back(1);
        }
        ans += f(d);
    }
    
    cout << ans.val() << '\n';
    
    return 0;
}

G. Substring Game

当前构造的字符串 \(T\) 必须始终是原串 \(S\) 的子串。把所有可能的 \(T\) 看作在“后缀树”上从根出发的一条路径上任意位置(路径长度即 \(∣T∣\))。从某个位置往下再追加一个字母,相当于在这棵树上向下走一步。

在后缀树上,沿一条树枝内部每一步都是唯一的(没有分支),只有到达显式节点时才有多个不同字母可选(分支)。因此一条树枝长度 \(L\) 表示有 \(L\) 步的“强制”移动(没有选择),到达子节点后才出现选择点。强制段的步数奇偶性会影响谁是到达子节点时的“手牌”(轮到谁行动)。

后缀树的构造

  • 字符串 \(S\) 的后缀数组 sa\(\text{LCP}\) 数组 lcp,然后基于后缀数组和 \(\text{LCP}\) 通过扫描构造了一个压缩的后缀树。

  • 数据结构:

    • dep[v]:节点 \(v\) 对应的位置深度(从根到该节点的字符串长度)。
    • pa[v]:节点 \(v\) 的父节点。
    • to[v]:节点 \(v\) 的子节点列表。
  • 构造方式:按后缀长度从长到短(即遍历后缀数组),遇到新的后缀长度或 \(\text{LCP}\) 时创造新节点/拆分边,维护当前节点指针 \(v\),把需要的子节点加入 to。结果是一个每个边对应一段连续字符、节点深度等于那处子串长度的树。

博弈型 \(\text{DP}\)

  • 定义 \(f(v)\) 为:在节点 \(v\)(即当前 \(T\) 的位置对应树上的该深度),轮到当前玩家时,当前玩家是否能必胜(真表示先手必胜)。
  • 对某个子节点 \(u\),从 \(v\)\(u\) 的边长度为 \(L=\text{dep}[u]−\text{dep}[v]\)。若当前玩家选择走向这个子树,则会有 \(L\) 步被执行,走到 \(u\) 时下一次行动的是:当前玩家(若 \(L\) 偶数)或对手(若 \(L\) 奇数)。因此选择子节点 \(u\) 能使当前玩家获胜的充分必要条件是到达 \(u\) 后“作为当前到达时的行动方”的位置是失败态。
  • 因此递归地检查任一子节点满足该条件即可判定当前节点为赢;若没有子节点(叶子)或所有子节点都不满足条件则当前为输。
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)

using namespace std;

void solve() {
    string s;
    cin >> s;
    int n = s.size();
    
    auto sa = suffix_array(s);
    auto lcp = lcp_array(s, sa);
    
    vector<vector<int>> to(1);
    vector<int> pa(1, -1), dep(1, 0);
    {
        int v = 0;
        rep(i, n) {
            int d = n-sa[i];
            if (dep[v] < d) {
                int u = to.size();
                to.resize(u+1); pa.push_back(v); dep.push_back(d);
                to[v].push_back(u);
                v = u;
            }
            if (i == n-1) break;
            d = lcp[i];
            while (d < dep[v]) v = pa[v];
            if (d != dep[v]) {
                int u = to.size();
                to.resize(u+1); pa.push_back(v); dep.push_back(d);
                to[u].push_back(to[v].back());
                pa[to[v].back()] = u; to[v].back() = u;
                v = u;
            }
        }
    }
    
    auto dfs = [&](auto& f, int v) -> bool {
        for (int u : to[v]) {
            int l = (dep[u]-dep[v])%2;
            if (f(f, u)^l) return true;
        }
        return false;
    };
    
    if (dfs(dfs, 0)) puts("Alice"); else puts("Bob");
}

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