From 7bcc949bee99c072738191e5e5532f7853891389 Mon Sep 17 00:00:00 2001 From: Haruki Oshiro <68176059+Harui-i@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:03:05 +0900 Subject: [PATCH] Add composable automaton DFA utilities --- docs/dp/automaton/automaton.md | 132 ++++++++++- docs/dp/automaton/remainder.md | 26 ++- dp/automaton/automaton.hpp | 244 ++++++++++++++++++++- dp/automaton/remainder.hpp | 44 ++-- test/verify/aoj-0570.test.cpp | 85 +++++++ test/verify/yuki-372-itsautomatic.test.cpp | 5 +- 6 files changed, 482 insertions(+), 54 deletions(-) create mode 100644 test/verify/aoj-0570.test.cpp diff --git a/docs/dp/automaton/automaton.md b/docs/dp/automaton/automaton.md index e29d29d..524c774 100644 --- a/docs/dp/automaton/automaton.md +++ b/docs/dp/automaton/automaton.md @@ -3,23 +3,133 @@ title: 決定性有限オートマトン(DFA, Deterministic Finite Automaton) documentation_of: //dp/automaton/automaton.hpp --- -桁DPとかをわりと簡潔(?)に解くための仕組み?データ構造? +有限個の状態を `int` で管理する DFA。 -有限な状態数で、状態と入力が決まると次の状態も一意に決まりますよでおなじみの、決定性有限オートマトン(Deterministic Finite Automaton)を扱うことを考えている。 -決定性じゃない(状態と入力に対して、次の状態が複数考えられるような)場合は、遷移先を集合としてみて(?)bitDPをすると有限な場合に帰着できるらしいですよ。 +桁 DP などで、「上限以下」「余りが 0」「パターンを満たす」のような条件をそれぞれ DFA として書き、それらを積 DFA で合成して使うことを想定している。 -めちゃめちゃ参考にしました 1: [https://kuretchi.github.io/blog/entries/automaton-dp/](https://kuretchi.github.io/blog/entries/automaton-dp/) +## 使い方の流れ -めちゃめちゃ参考にしました 2: [https://shino16.github.io/blog/post/algo/%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3/](https://shino16.github.io/blog/post/algo/%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3/) -めちゃめちゃ参考にしました 3: [https://ja.wikipedia.org/wiki/%E6%B1%BA%E5%AE%9A%E6%80%A7%E6%9C%89%E9%99%90%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3](https://ja.wikipedia.org/wiki/%E6%B1%BA%E5%AE%9A%E6%80%A7%E6%9C%89%E9%99%90%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3) +1. 条件を 1 つずつ DFA として書く +2. `AndDfa` や `ProductPolicyDfa` で条件を合成する +3. `count_fixed_length` で長さ固定の列を全部流して数える +「`0 <= x <= N` かつ `x` が条件を満たす数を数える」タイプの桁 DP では、`N` の桁数に合わせて 0 埋めした文字列を考える。 -定義は3に書いてある。 1は"~な数の個数"だけではなくて"~な数についてxxした総和"みたいなものも扱えることを取り上げている(実際、ジグザグ数の総和を(個数ではなく!!)求めている)が、ちょっとよくわからない部分も多い。 +```cpp +vector alphabet = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; -2は実装の参考になった。 +DfaPtr less_equal = make_shared(N); +DfaPtr condition = make_shared>( + make_shared()); -オートマトンで考えることの嬉しさは、状態の管理が比較的らくになったり、「2つのオートマトンが受理する文字列のみを受理するオートマトン」などを簡単に[^1]扱えるところにあるんじゃないか。 +DfaPtr whole = make_shared>(less_equal, condition); +mint ans = count_fixed_length(*whole, alphabet, N.size()); +``` -Verify用の問題をどうすればいいかに悩んでいる。実装例があったほうがわかりやすいだろうし。AtCoderの問題をVerifyに使うにはDropboxのAPIキーがないといけないし、yukicoderは使ったことがないし、AOJにある問題はジグザグ数(苦行)しか知らない。 +`DecimalLessEqualDfa` は 0 埋めされた固定長文字列をそのまま `N` と比較する。 +一方で、通常の 10 進表記に対する条件 DFA は先頭 0 を読ませると意味が変わることがあるので、必要なら `LeadingZeroSkipDfa` で包む。 -[^1]: 要出典 +例: `N = "123"` のとき、DP は `"000"` から `"999"` までの長さ 3 の文字列を読む。 +`DecimalLessEqualDfa("123")` は `"007"` を 7 として `<= 123` と判定できる。 +`LeadingZeroSkipDfa` は内側の DFA に `"007"` を `"7"` として読ませる。 + +## DFA の書き方 + +```cpp +template +struct Dfa { + virtual int state_count() const = 0; + virtual int initial_state() const = 0; + virtual int next_state(int state, Alphabet c) const = 0; + virtual bool accept(int state) const = 0; +}; +``` + +状態は `0, 1, ..., state_count() - 1` の整数にエンコードする。 + +- `state_count()`: 状態数 +- `initial_state()`: 初期状態 +- `next_state(state, c)`: 状態 `state` で文字 `c` を読んだ後の状態 +- `accept(state)`: 最終状態 `state` を受理するか + +問題固有の条件はこの 4 つだけ実装すればよい。 + +```cpp +class Contains3Dfa : public Dfa { +public: + int state_count() const override { return 2; } + int initial_state() const override { return 0; } + + int next_state(int state, int digit) const override { + return state || digit == 3; + } + + bool accept(int state) const override { + return state == 1; + } +}; +``` + +## 合成 + +- `AndDfa`: 2 つの DFA の両方が受理する文字列を受理する積 DFA +- `ProductPolicyDfa`: 任意個の DFA を積にして、受理状態の真偽列から受理判定を決める DFA + +```cpp +DfaPtr a = make_shared(); +DfaPtr b = make_shared(); +DfaPtr both = make_shared>(a, b); +``` + +「3 つの条件のうちちょうど 1 つを満たす」のような受理条件は `ProductPolicyDfa` を使う。 + +```cpp +vector> conditions = {a, b, c}; + +DfaPtr exactly_one = make_shared>( + conditions, + [](const vector& ok) { + int cnt = 0; + for(bool x : ok) if(x) cnt++; + return cnt == 1; + }); +``` + +## 用意してある DFA / wrapper + +### `DecimalLessEqualDfa` + +固定長 0 埋め 10 進文字列として読んだ値が `N` 以下なら受理する。 +桁 DP の tight に相当する。 + +```cpp +DfaPtr less_equal = make_shared(N); +``` + +### `LeadingZeroSkipDfa` + +先頭 0 を内側の DFA に流さない wrapper。 +全桁が 0 の場合は、内側の DFA に 0 を 1 回だけ読ませた状態で受理判定する。 + +```cpp +DfaPtr normal_decimal_condition = make_shared>( + make_shared()); +``` + +`DecimalRemainderDfa` やジグザグ数 DFA のように「普通の 10 進表記」を読む DFA は、固定長 DP で使うとき基本的にこれで包む。 + +## 数え上げ + +`count_fixed_length(dfa, alphabet, length)` は、長さ `length` の列をすべて DFA に流し、受理されるものの個数を `T` で返す。 +`T` は `+=` と `T(0)`, `T(1)` が使えればよいので、`modint` を渡せる。 + +```cpp +mint ans = count_fixed_length(*dfa, alphabet, L); +``` + +## Verify + +- `test/verify/aoj-0570.test.cpp` + +AOJ 0570 は「`A <= x <= B`」「`x % M == 0`」「`x` がジグザグ数」の 3 条件を DFA として合成して解いている。 +区間 `[A, B]` は `count(<= B) - count(<= A - 1)` で処理している。 diff --git a/docs/dp/automaton/remainder.md b/docs/dp/automaton/remainder.md index 9c055ee..1ffa9f3 100644 --- a/docs/dp/automaton/remainder.md +++ b/docs/dp/automaton/remainder.md @@ -3,7 +3,25 @@ title : あまりを管理するオートマトン(remainder.hpp) documentation_of: //dp/automaton/remainder.hpp --- -これいるか? nextは、数字の右側に桁を加えるような操作を考えている。 '998'に'2'を与えてnextすると'9982'に対応する状態が返ってくるイメージ。 -わざわざライブラリにするほどのものでもなくね??? -ライブラリにするほど複雑でもないから、1から書けばいいし、ライブラリにするなら「よく出るオートマトンは事前に書いておく」かつ「オートマトンのAND合成もライブラリにある」という状態が望ましいけど、 -AND合成のオートマトンは書くの難しいし、どうせなら2つだけじゃなくて任意個の合成もできたほうが便利だけどそれもやっぱり難しいしで、オートマトンをライブラリにして有効に使うのは難しそう。 \ No newline at end of file +10 進表記を左から読んで、値の `mod` での余りを状態として持つ DFA。 + +```cpp +DecimalRemainderDfa dfa(mod); +``` + +状態数は `mod`。受理状態は余り `0`。 + +```cpp +int next_state(int state, int digit) { + return (state * 10 + digit) % mod; +} +``` + +固定長 0 埋めの桁 DP で「通常の 10 進表記としての値」を扱いたい場合は、`LeadingZeroSkipDfa` で包む。 + +```cpp +DfaPtr multiple_of_m = make_shared>( + make_shared(m)); +``` + +余りだけなら先頭 0 を読んでも値は変わらないが、他の通常表記向け DFA と同じ形で合成できるので、桁 DP では包んでおくと扱いが揃う。 diff --git a/dp/automaton/automaton.hpp b/dp/automaton/automaton.hpp index 147b6d5..81da46b 100644 --- a/dp/automaton/automaton.hpp +++ b/dp/automaton/automaton.hpp @@ -1,11 +1,233 @@ -// https://shino16.github.io/blog/post/algo/%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3/ -// Dfaインターフェース -template -class Dfa { -public: - virtual State init() const = 0; // 初期状態を返す - virtual State next([[maybe_unused]] State s, [[maybe_unused]] Alphabet a, [[maybe_unused]]int i) const = 0; // sにaを入力として与えた時の次の状態を返す - virtual bool accept([[maybe_unused]] State s) const = 0; // sをオートマトンが受理するかどうか - virtual bool successful([[maybe_unused]] State s) const { return false; } // どういうふうにnextしていこうが、絶対にacceptされる状態かどうか - virtual bool unsuccessful([[maybe_unused]] State s) const { return false; } // どういうふうにnextしていこうが、絶対にaccpetされない状態かどうか -}; \ No newline at end of file +#ifndef HARUILIB_DP_AUTOMATON_AUTOMATON_HPP +#define HARUILIB_DP_AUTOMATON_AUTOMATON_HPP + +#include +#include +#include +#include +#include +#include + +template +struct Dfa { + using alphabet_type = Alphabet; + using state_type = int; + + virtual int state_count() const = 0; + virtual int initial_state() const = 0; + virtual int next_state(int state, Alphabet c) const = 0; + virtual bool accept(int state) const = 0; + virtual ~Dfa() = default; +}; + +template +using DfaPtr = std::shared_ptr>; + +template +class ProductPolicyDfa : public Dfa { + std::vector> dfas; + std::vector base; + int states; + std::function&)> policy; + +public: + ProductPolicyDfa(std::vector> dfas, + std::function&)> policy) + : dfas(std::move(dfas)), states(1), policy(std::move(policy)) { + base.resize(this->dfas.size()); + for(int i = 0; i < (int)this->dfas.size(); i++) { + base[i] = states; + states *= this->dfas[i]->state_count(); + } + } + + int encode(const std::vector& component_states) const { + int state = 0; + for(int i = 0; i < (int)dfas.size(); i++) { + state += component_states[i] * base[i]; + } + return state; + } + + std::vector decode(int state) const { + std::vector component_states(dfas.size()); + for(int i = 0; i < (int)dfas.size(); i++) { + component_states[i] = (state / base[i]) % dfas[i]->state_count(); + } + return component_states; + } + + int state_count() const override { + return states; + } + + int initial_state() const override { + std::vector component_states(dfas.size()); + for(int i = 0; i < (int)dfas.size(); i++) { + component_states[i] = dfas[i]->initial_state(); + } + return encode(component_states); + } + + int next_state(int state, Alphabet c) const override { + std::vector component_states = decode(state); + for(int i = 0; i < (int)dfas.size(); i++) { + component_states[i] = dfas[i]->next_state(component_states[i], c); + } + return encode(component_states); + } + + bool accept(int state) const override { + std::vector component_states = decode(state); + std::vector accepted(dfas.size()); + for(int i = 0; i < (int)dfas.size(); i++) { + accepted[i] = dfas[i]->accept(component_states[i]); + } + return policy(accepted); + } +}; + +template +class AndDfa : public Dfa { + DfaPtr lhs; + DfaPtr rhs; + int rhs_states; + +public: + AndDfa(DfaPtr lhs, DfaPtr rhs) + : lhs(std::move(lhs)), rhs(std::move(rhs)), rhs_states(this->rhs->state_count()) {} + + int encode(int lhs_state, int rhs_state) const { + return lhs_state * rhs_states + rhs_state; + } + + std::pair decode(int state) const { + return {state / rhs_states, state % rhs_states}; + } + + int state_count() const override { + return lhs->state_count() * rhs->state_count(); + } + + int initial_state() const override { + return encode(lhs->initial_state(), rhs->initial_state()); + } + + int next_state(int state, Alphabet c) const override { + auto [a, b] = decode(state); + return encode(lhs->next_state(a, c), rhs->next_state(b, c)); + } + + bool accept(int state) const override { + auto [a, b] = decode(state); + return lhs->accept(a) && rhs->accept(b); + } +}; + +template +T count_fixed_length(const Dfa& dfa, const std::vector& alphabet, int length) { + std::unordered_map dp, next_dp; + dp[dfa.initial_state()] = T(1); + + for(int i = 0; i < length; i++) { + next_dp.clear(); + for(const auto& [state, value] : dp) { + for(const Alphabet& c : alphabet) { + next_dp[dfa.next_state(state, c)] += value; + } + } + dp.swap(next_dp); + } + + T answer = T(0); + for(const auto& [state, value] : dp) { + if(dfa.accept(state)) answer += value; + } + return answer; +} + +class DecimalLessEqualDfa : public Dfa { + std::vector digits; + +public: + explicit DecimalLessEqualDfa(const std::string& n) { + digits.reserve(n.size()); + for(char c : n) digits.push_back(c - '0'); + } + + int encode(int pos, int rel) const { + return pos * 3 + rel; + } + + std::pair decode(int state) const { + return {state / 3, state % 3}; + } + + int state_count() const override { + return ((int)digits.size() + 1) * 3; + } + + int initial_state() const override { + return encode(0, 0); + } + + int next_state(int state, int digit) const override { + auto [pos, rel] = decode(state); + if(pos == (int)digits.size()) return encode(pos, 2); + + if(rel == 0) { + if(digit < digits[pos]) rel = 1; + if(digit > digits[pos]) rel = 2; + } + return encode(pos + 1, rel); + } + + bool accept(int state) const override { + auto [pos, rel] = decode(state); + return pos == (int)digits.size() && rel != 2; + } +}; + +template +class LeadingZeroSkipDfa : public Dfa { + DfaPtr inner; + Alphabet zero; + int inner_states; + +public: + LeadingZeroSkipDfa(DfaPtr inner, Alphabet zero = Alphabet()) + : inner(std::move(inner)), zero(zero), inner_states(this->inner->state_count()) {} + + int encode(bool started, int inner_state) const { + return (started ? inner_states : 0) + inner_state; + } + + std::pair decode(int state) const { + if(state < inner_states) return {false, state}; + return {true, state - inner_states}; + } + + int state_count() const override { + return inner_states * 2; + } + + int initial_state() const override { + return encode(false, inner->initial_state()); + } + + int next_state(int state, Alphabet c) const override { + auto [started, inner_state] = decode(state); + if(!started && c == zero) return encode(false, inner_state); + return encode(true, inner->next_state(inner_state, c)); + } + + bool accept(int state) const override { + auto [started, inner_state] = decode(state); + if(started) return inner->accept(inner_state); + + int zero_state = inner->next_state(inner->initial_state(), zero); + return inner->accept(zero_state); + } +}; + +#endif diff --git a/dp/automaton/remainder.hpp b/dp/automaton/remainder.hpp index 4599757..b391e6e 100644 --- a/dp/automaton/remainder.hpp +++ b/dp/automaton/remainder.hpp @@ -1,37 +1,31 @@ -#include "automaton.hpp" +#ifndef HARUILIB_DP_AUTOMATON_REMAINDER_HPP +#define HARUILIB_DP_AUTOMATON_REMAINDER_HPP -// nextは数字の右端に書き加えるイメージ。つまり、いろいろな桁数を考えられる。 -// しかし、固定した桁数に対して左から埋めていくパターンで使いたい場合もありそう。 -// 数字のMの倍数のみ受理するオートマトン -template -class RemainderAutomaton : public Dfa { - const int M; - const int N_siz; +#include "dp/automaton/automaton.hpp" + +class DecimalRemainderDfa : public Dfa { + int mod; public: - using State = int; - RemainderAutomaton(int _N_siz, int _M) : M(_M), N_siz(_N_siz) {} + explicit DecimalRemainderDfa(int mod) : mod(mod) {} - State init() const override { - return State(0); + int state_count() const override { + return mod; } - State next(State s, char c, [[maybe_unused]] int i) const override { - State ret = ((long long)s*10 + (long long)(c - '0') )%M; - - return ret; + int initial_state() const override { + return 0; } - bool accept(State s) const override { - return s == 0; + int next_state(int state, int digit) const override { + return ((long long)state * 10 + digit) % mod; } - bool successful ([[maybe_unused]] State s) const override { - return false; + bool accept(int state) const override { + return state == 0; } +}; - bool unsuccessful([[maybe_unused]] State s) const override { - return false; - } - -}; \ No newline at end of file +using RemainderAutomaton = DecimalRemainderDfa; + +#endif diff --git a/test/verify/aoj-0570.test.cpp b/test/verify/aoj-0570.test.cpp new file mode 100644 index 0000000..99afbf2 --- /dev/null +++ b/test/verify/aoj-0570.test.cpp @@ -0,0 +1,85 @@ +#define PROBLEM "https://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0570" + +#include "template/template.hpp" +#include "math/modint.hpp" +#include "dp/automaton/automaton.hpp" +#include "dp/automaton/remainder.hpp" +#include +#include +#include + +using mint = modint<10000>; + +class ZigZagDfa : public Dfa { + static constexpr int START = 30; + static constexpr int NG = 31; + + int encode(int last_digit, int direction) const { + return last_digit * 3 + direction; + } + +public: + int state_count() const override { + return 32; + } + + int initial_state() const override { + return START; + } + + int next_state(int state, int digit) const override { + if(state == NG) return NG; + if(state == START) return encode(digit, 0); + + int last_digit = state / 3; + int direction = state % 3; + if(digit == last_digit) return NG; + + int next_direction = (last_digit < digit ? 1 : 2); + if(direction != 0 && direction == next_direction) return NG; + return encode(digit, next_direction); + } + + bool accept(int state) const override { + return state != START && state != NG; + } +}; + +string decrement(string s) { + int i = (int)s.size() - 1; + while(i >= 0 && s[i] == '0') { + s[i] = '9'; + i--; + } + if(i >= 0) s[i]--; + + int pos = 0; + while(pos + 1 < (int)s.size() && s[pos] == '0') pos++; + return s.substr(pos); +} + +mint count_less_equal(const string& n, int m) { + vector alphabet = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + + DfaPtr less_equal = make_shared(n); + DfaPtr multiple_of_m = make_shared>( + make_shared(m)); + DfaPtr zig_zag = make_shared>( + make_shared()); + + DfaPtr condition = make_shared>(multiple_of_m, zig_zag); + DfaPtr whole = make_shared>(less_equal, condition); + + return count_fixed_length(*whole, alphabet, n.size()); +} + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + string A, B; + int M; + cin >> A >> B >> M; + + cout << count_less_equal(B, M) - count_less_equal(decrement(A), M) << '\n'; +} diff --git a/test/verify/yuki-372-itsautomatic.test.cpp b/test/verify/yuki-372-itsautomatic.test.cpp index 04aa783..bff88be 100644 --- a/test/verify/yuki-372-itsautomatic.test.cpp +++ b/test/verify/yuki-372-itsautomatic.test.cpp @@ -17,8 +17,7 @@ int main() { vector alphabet = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; - RemainderAutomaton ra(S.size(), M); - + DecimalRemainderDfa ra(M); mint ans = 0; vectordp1(M), dp2(M); @@ -47,4 +46,4 @@ int main() { return 0; -} \ No newline at end of file +}