feature image

2026年8月26日 | ブログ記事

世界のナベアツと Aho–Corasick オートマトン

この記事は 夏のブログリレー 10 日目の記事です。

こんにちは、23B の @hayatroid です。院試対策でオートマトンの勉強をしたので、同じく講義などでオートマトンの勉強をした人間を競プロに誘いです。

TL;DR

kuretchi さんのオートマトン上の DP に KMP オートマトンと Aho–Corasick オートマトンを impl です。そして次の問題を AC です。

ナベアツとはオートマトンである

ナベアツ[1]は、 の倍数と がつくときアホになる。数を読んでアホになるかどうかを決めるのだから、ナベアツはオートマトンといえる。

ナベアツ数を認識するオートマトン

非負整数がナベアツ数であるとは、 の倍数であるか がつくことをいう[2]

の倍数を認識するオートマトンと がつく数を認識するオートマトンは、それぞれ次のとおりである。

の倍数 がつく

ナベアツ数を認識するオートマトンは、この つの union で表せる。

たとえば は、 がつかないが の倍数であるため、ナベアツ数である。

の倍数 ( を読む) がつく ( を読む)

たとえば は、 の倍数でないが がつくため、ナベアツ数である。

の倍数 ( を読む) がつく ( を読む)

これらのオートマトンを実装すれば、次の問題が解ける。

No.220 世界のなんとか2

yukicoder No.220 世界のなんとか2 では、 以上 以下のナベアツ数を数え上げる必要がある。 は最大 である。

先の図のオートマトンを実装し、この問題を解く。

まず、決定性有限オートマトン[3] つ組をそのまま trait にする。

trait Dfa {
    type State;
    type Alphabet;
    fn init(&self) -> Self::State;
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State;
    fn accept(&self, q: &Self::State) -> bool;
}

先の図の左、 の倍数を認識するオートマトンを MultipleOf(3) として実装する。

struct MultipleOf(u64);
impl Dfa for MultipleOf {
    type State = u64;
    type Alphabet = u8;
    fn init(&self) -> Self::State {
        0
    }
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State {
        (q * 10 + (c - b'0') as u64) % self.0
    }
    fn accept(&self, q: &Self::State) -> bool {
        *q == 0
    }
}

先の図の右、 がつく数を認識するオートマトンを Seen(b'3') として実装する。

struct Seen(u8);
impl Dfa for Seen {
    type State = bool;
    type Alphabet = u8;
    fn init(&self) -> Self::State {
        false
    }
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State {
        *q || *c == self.0
    }
    fn accept(&self, q: &Self::State) -> bool {
        *q
    }
}

オートマトンの union もまたオートマトンである。 つのオートマトンを並べて走らせ、どちらかが受理すれば受理するオートマトンを Or として実装する。

struct Or<A, B>(A, B);
impl<A: Dfa, B: Dfa<Alphabet = A::Alphabet>> Dfa for Or<A, B> {
    type State = (A::State, B::State);
    type Alphabet = A::Alphabet;
    fn init(&self) -> Self::State {
        (self.0.init(), self.1.init())
    }
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State {
        (self.0.next(&q.0, c), self.1.next(&q.1, c))
    }
    fn accept(&self, q: &Self::State) -> bool {
        self.0.accept(&q.0) || self.1.accept(&q.1)
    }
}

オートマトン のすべての文字列を読ませ、 が受理する文字列を数える関数を として実装する。たとえば 進の数字とすれば、 桁の数字列のうち が受理するものを数える。先頭の を気にしないことにすれば、これは 未満の非負整数のうち が受理するものの個数とみなせる。

fn count<A, S>(dfa: A, sigma: S, len: usize) -> u64
where
    A: Dfa,
    A::State: Eq + Hash,
    S: Iterator<Item = A::Alphabet> + Clone,
{
    let mut dp = HashMap::new();
    dp.insert(dfa.init(), 1);
    for _ in 0..len {
        let mut ndp = HashMap::new();
        for (q, v) in dp {
            for c in sigma.clone() {
                *ndp.entry(dfa.next(&q, &c)).or_insert(0) += v;
            }
        }
        dp = ndp;
    }
    dp.iter()
        .filter_map(|(q, v)| dfa.accept(q).then_some(v))
        .sum()
}

あとはオートマトンを組み立てて長さ で数え上げるのみである。 はナベアツ数でなく、 はナベアツ数なので、 を引けば答えになる。

fn main() {
    input!(p: usize);
    let dfa = Or(MultipleOf(3), Seen(b'3'));
    println!("{}", count(dfa, b'0'..=b'9', p) - 1);
}

以上を提出すると AC が得られる

No.260 世界のなんとか3

実は先の問題はナベアツ方程式[4]で解けてしまう。それでも問題をオートマトンで解くことの真髄は、条件が増えてもオートマトンを足すだけで済むことにある。

yukicoder No.260 世界のなんとか3 では、 以上 以下の整数のうち、ナベアツ数であって の倍数でないものを で割った余りで数え上げる必要がある。 は最大 である。

これは、先の問題で用意したオートマトンに加えて、次の つのオートマトンを足せば解ける。

たとえば のとき、 以下を認識するオートマトンは次のとおりである。読んだ数字列が の接頭辞より小さいか、等しいか、大きいかを状態に持つ。

以下

上図のオートマトンを Le(b"31415") として実装する。

struct Le<'a>(&'a [u8]);
impl Dfa for Le<'_> {
    type State = (Ordering, usize);
    type Alphabet = u8;
    fn init(&self) -> Self::State {
        (Ordering::Equal, 0)
    }
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State {
        (q.0.then(c.cmp(&self.0[q.1])), q.1 + 1)
    }
    fn accept(&self, q: &Self::State) -> bool {
        q.0.is_le()
    }
}

Lt, And, Not もこれまで同様のノリで実装する。count で割った余りを取るよう書き換える。

あとはオートマトンを組み立てて、 以下の数え上げから 未満の数え上げを引くのみである。

fn main() {
    input!(a: Bytes, b: Bytes);
    let nabeatsu = || And(Or(MultipleOf(3), Seen(b'3')), Not(MultipleOf(8)));
    let dfa_le_b = And(Le(&b), nabeatsu());
    let dfa_lt_a = And(Lt(&a), nabeatsu());
    let le_b = count(dfa_le_b, b'0'..=b'9', b.len());
    let lt_a = count(dfa_lt_a, b'0'..=b'9', a.len());
    println!("{}", (le_b + MOD - lt_a) % MOD);
}

以上を提出すると AC が得られる

ナベアツが本気を出してきたらどうしよう

今度のナベアツは、 の倍数と がつくときアホになるという。

がつく数を認識するオートマトン

がつく数を認識するオートマトンは、読んだ数字列の末尾と の先頭が一致する長さのうち、最も長いものを状態に持てばよい。読んだ数字が の次の文字と一致しなかったら、その次に長いものへ戻り、そこから続きを読む。この戻る辺を failure link という[5] を一列に並べて failure link を足したものを KMP オートマトンという[5:1]

たとえば のとき、 がつく数を認識するオートマトンは次のとおりである。破線が failure link である。読む数字の辺が無いときは、破線を 本たどって読む。それでも読めなければ、また破線を 本たどって読む。 まで戻っても読めなければ、 に留まる。

がつく

たとえば は、 の倍数でないが がつくため、ナベアツ数である。

がつく ( を読む)

このオートマトンを実装すれば、次の問題が解ける。

No.2867 NOT FOUND 404 Again

yukicoder No.2867 NOT FOUND 404 Again では、 以下の正の整数のうち がつかないものを で割った余りで数え上げる必要がある。 は最大 である。

これは、先の問題のオートマトンに加えて、 がつく数を認識するオートマトン Contains を足せば解ける。

先の図のオートマトンを Contains::new(b"33") として実装する。

状態 において、数字 を読んだ次の状態を next[q][c]、破線を 本たどった先を fail[q] とする。 の辺が無いときは、破線をたどって読んだ先を next[q][c] とする。nextfail は、 の小さいほうから埋められる。

struct Contains {
    next: Vec<[usize; 10]>,
    accept: Vec<bool>,
}
impl Contains {
    fn new(w: &[u8]) -> Self {
        // 1. w を一列に並べる
        let n = w.len();
        let mut next = vec![[0; 10]; n + 1];
        let mut accept = vec![false; n + 1];
        for q in 0..n {
            next[q][(w[q] - b'0') as usize] = q + 1;
        }
        accept[n] = true;
        // 2. failure link を張る
        let mut fail = vec![0; n + 1];
        for q in 1..=n {
            for c in 0..10 {
                if next[q][c] != 0 {
                    fail[next[q][c]] = next[fail[q]][c];
                } else {
                    next[q][c] = next[fail[q]][c];
                }
            }
        }
        // 3. 受理状態に留まるようにする
        next[n] = [n; 10];
        Contains { next, accept }
    }
}
impl Dfa for Contains {
    type State = usize;
    type Alphabet = u8;
    fn init(&self) -> Self::State {
        0
    }
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State {
        self.next[*q][(c - b'0') as usize]
    }
    fn accept(&self, q: &Self::State) -> bool {
        self.accept[*q]
    }
}

count で割った余りを取るよう書き換える。

あとはオートマトンを組み立てて長さ で数え上げるのみである。 がつかないので、 を引けば答えになる。

fn main() {
    input!(n: Bytes);
    let dfa = And(Not(Contains::new(b"404")), Le(&n));
    println!("{}", (count(dfa, b'0'..=b'9', n.len()) + MOD - 1) % MOD);
}

以上を提出すると AC が得られる

Re: ナベアツが本気を出してきたらどうしよう

今度のナベアツは、 の倍数と のどれかがつくときアホになるという。

のどれかがつく数を認識するオートマトン

のどれかがつく数を認識するオートマトンは、KMP オートマトンの一列を の trie[6] に差し替え、同じように failure link を足したものである。これを Aho–Corasick オートマトン という[7]

たとえば のとき、 のどれかがつく数を認識するオートマトンは次のとおりである。破線が failure link である。受理状態に添えた集合が、そこで見つかる語である。見つかる語は、破線を 本たどった先で見つかる語を含む。その先の語も、また破線を 本たどった先の語を含む。

または がつく

たとえば は、 の倍数でないが もつくため、ナベアツ数である。

または がつく ( を読む)

このオートマトンを実装すれば、次の問題が解ける。

No.1269 I hate Fibonacci Number

yukicoder No.1269 I hate Fibonacci Number では、 未満の正の整数のうち、 以上 以下のフィボナッチ数がどれもつかないものを で割った余りで数え上げる必要がある。 は最大 は最大 である。

これは、先の問題のオートマトンに加えて、 のどれかがつく数を認識するオートマトン ContainsAny を足せば解ける。

先の図のオートマトンを ContainsAny::new(&[b"33".to_vec(), b"313".to_vec()]) として実装する。

状態 において、数字 を読んだ次の状態を next[q][c]、破線を 本たどった先を fail[q] とする。 の辺が無いときは、破線をたどって読んだ先を next[q][c] とする。nextfail は、根からの深さが小さいほうから埋められ、幅優先探索をすればよい。実装は OI Wiki の AC 自动机 を参考にした。

struct ContainsAny {
    next: Vec<[usize; 10]>,
    accept: Vec<bool>,
}
impl ContainsAny {
    fn new(ws: &[Vec<u8>]) -> Self {
        // 1. ws の trie をつくる
        let mut next = vec![[0; 10]];
        let mut accept = vec![false];
        for w in ws {
            let mut q = 0;
            for c in w {
                let c = (c - b'0') as usize;
                if next[q][c] == 0 {
                    next[q][c] = next.len();
                    next.push([0; 10]);
                    accept.push(false);
                }
                q = next[q][c];
            }
            accept[q] = true;
        }
        // 2. failure link を張る
        let mut fail = vec![0; next.len()];
        let mut bfs = VecDeque::new();
        for c in 0..10 {
            if next[0][c] != 0 {
                bfs.push_back(next[0][c]);
            }
        }
        while let Some(q) = bfs.pop_front() {
            for c in 0..10 {
                if next[q][c] != 0 {
                    fail[next[q][c]] = next[fail[q]][c];
                    bfs.push_back(next[q][c]);
                } else {
                    next[q][c] = next[fail[q]][c];
                }
            }
            accept[q] |= accept[fail[q]];
        }
        // 3. 受理状態に留まるようにする
        for q in 0..accept.len() {
            if accept[q] {
                next[q] = [q; 10];
            }
        }
        ContainsAny { next, accept }
    }
}
impl Dfa for ContainsAny {
    type State = usize;
    type Alphabet = u8;
    fn init(&self) -> Self::State {
        0
    }
    fn next(&self, q: &Self::State, c: &Self::Alphabet) -> Self::State {
        self.next[*q][(c - b'0') as usize]
    }
    fn accept(&self, q: &Self::State) -> bool {
        self.accept[*q]
    }
}

count で割った余りを取るよう書き換える。

あとはフィボナッチ数を列挙し、オートマトンを組み立てて長さ で数え上げるのみである。 はフィボナッチ数がつかないので、 を引けば答えになる。

fn main() {
    input!(n: usize, l: u64, r: u64);
    let mut ws = vec![];
    let (mut x, mut y) = (1, 1);
    while x < l {
        (x, y) = (y, x + y);
    }
    while x <= r {
        ws.push(x.to_string().into_bytes());
        (x, y) = (y, x + y);
    }
    let dfa = Not(ContainsAny::new(&ws));
    println!("{}", (count(dfa, b'0'..=b'9', n) + MOD - 1) % MOD);
}

以上を提出すると AC が得られる

Rust の trait があると、オートマトンの具体を与えずに、抽象からオートマトンを組み立てたり (Or)、抽象のまま数え上げたり (count) できて面白いです。

以上、オートマトン er を競プロに誘い記事と見せかけた、競プロ er を Rust に誘い記事でした。


  1. https://ja.wikipedia.org/wiki/桂三度 ↩︎

  2. https://x.com/Yuk3u/status/2076621350224179441 ↩︎

  3. https://ja.wikipedia.org/wiki/決定性有限オートマトン ↩︎

  4. https://dreamscience.iml.menhera.org/wiki/ナベアツ方程式 ↩︎

  5. https://qiita.com/hdbn/items/b3909ce29a46c5d0cb8c ↩︎ ↩︎

  6. https://oi-wiki.org/string/trie/ ↩︎

  7. https://oi-wiki.org/string/ac-automaton/ ↩︎

hayatroid icon
この記事を書いた人
hayatroid

大岡山にいます

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2025年9月15日
traPでの一年半を振り返る〜全班所属の体験記(?)〜
gurukun41 icon gurukun41
2024年9月17日
1か月でゲームを作った #BlueLINE
Komichi icon Komichi
2025年9月18日
泥タブに夢と希望を見出した男の物語 【Lenovo Yoga Tab Plus】
mutv625 icon mutv625
2024年8月21日
【最新版 / 入門】JUCEを使ってVSTプラグインを作ろう!!!!【WebView UI】
kashiwade icon kashiwade
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2022年9月26日
競プロしかシラン人間が web アプリ QK Judge を作った話
tqk icon tqk
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記