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\) 是按排序后的行列索引构造出的矩阵。最后用 rid、cid 把行列索引映回原输入顺序即可。
代码实现
#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;
}
浙公网安备 33010602011771号