G - Wildcard Construction 解説
by
MMNMM
印刷ページ
\(X,Y\) が与えられたとき、良い文字列全体の集合は正規言語になります。 よって、良い文字列だけをすべて受理する決定性有限オートマトンが存在します。 このオートマトンを十分高速に構成できるなら、オートマトンの状態数を \(N\) として、この問題を \(O(N|S|)\) で解くことができます。
まず、\(t\) としてありえる列のみをすべて受理する非決定性有限オートマトンを考えると、これは次のようになります。

また、\(X\) のみを受理する非決定性有限オートマトンは次の \(3\) 種類のパーツを使って以下のように記述することができます。


同様に \(Y\) のみを受理する非決定性有限オートマトンを構成することができ、これらをあわせて良い文字列のみをすべて受理する非決定性有限オートマトンを以下のように構成することができます。

これは \(|X|+|Y|+4\) 状態の非決定性有限オートマトンです。 受理状態を \(X\) や \(Y\) のみを受理する非決定性有限オートマトンの先頭や末尾とまとめることで、\(|X|+|Y|\) 状態の \(\varepsilon\) 遷移をもたない非決定性有限オートマトンにすることができます。
あとは、この非決定性有限オートマトンをもとに、「どの状態にいる可能性があるか」を状態とする \(O(2 ^ {|X|+|Y|})\) 状態の決定性有限オートマトンを構成することができます(詳細は powerset construction などで検索してください)。
あとは、これを利用して数え上げを行うことで、オートマトンの構成に \(O((|X|+|Y|)2 ^ {|X|+|Y|})\) 時間や \(O(2 ^ {|X|+|Y|})\) 時間、良い文字列の数え上げに \(O(|S|2 ^ {|X|+|Y|})\) 時間となり、十分高速にこの問題を解くことができます。
実装例は以下のようになります。
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
#include <bit>
#include <atcoder/modint>
int main() {
using namespace std;
using modint = atcoder::static_modint<998244353>;
string S, X, Y;
cin >> S >> X >> Y;
const auto N{size(X)}, M{size(Y)}, L{N + M};
const auto T{X + Y};
// 良い文字列のみをすべて受理する(ε遷移をもたない)非決定性有限オートマトンを構成する
// nfa[i] = {0 で遷移できる頂点の集合, 1 で遷移できる頂点の集合}
vector<pair<unsigned, unsigned>> nfa(L);
for (const auto &[i, c] : T | views::enumerate) {
const auto succ{1u << ((i + 1) % L)};
if (c != '1') nfa[i].first |= succ;
if (c != '0') nfa[i].second |= succ;
}
// X を読み終わってから再度 X を読んでもよい
if (X[0] != '1') nfa[N].first |= 2;
if (X[0] != '0') nfa[N].second |= 2;
// 非決定性有限オートマトンから決定性有限オートマトンを構成する
vector<pair<unsigned, unsigned>> dfa(1u << L);
for (auto&& [i, t] : dfa | views::enumerate | views::drop(1))
t = {dfa[i & i - 1].first | nfa[countr_zero(i + 0ul)].first, dfa[i & i - 1].second | nfa[countr_zero(i + 0ul)].second};
// 数え上げ
vector<modint> dp(1u << L), prev(1u << L);
dp[1u << N] = 1;
for (const auto c : S) {
swap(dp, prev);
ranges::fill(dp, modint{});
if (c != '1')
for (const auto& [next, v] : views::zip(dfa | views::elements<0>, prev))
dp[next] += v;
if (c != '0')
for (const auto& [next, v] : views::zip(dfa | views::elements<1>, prev))
dp[next] += v;
}
modint ans{};
// 受理状態を含むところの値を足しあげたものが答え
for (const auto& [i, v] : dp | views::enumerate) if (i & (1 | 1 << N)) ans += v;
cout << ans.val() << endl;
return 0;
}
投稿日時:
最終更新:
