メビウス関数を用いた約数系包除原理を使いこなそう!
問題概要 (表現改)
文字 '.' と '#' のみからなる長さ の文字列 が与えられる。今、文字 '#' をいくつか '.' に書き換えることによって、文字列 が周期的文字列となるようにしたい。
なお、 が周期的文字列であるとは、 より真に小さい の約数 が存在して、 () を満たすことをいうものとする。
書き換えて得られる周期的文字列の個数を 998244353 で割ったあまりを求めよ。
制約
考えたこと:最小周期を考える
たとえば次の文字列を見てみよう。
#.#.#.#.#.#.#.#.
この文字列は周期 2 だが、周期 4 であるとも言えるし、周期 6 であるとも言える。つまり、同じ文字列であっても、いくつかの周期が考えられることが厄介だ。
そこで、理想的には、最小周期で場合分けして数え上げることにしたい。最小周期が であるような文字列の個数を と表すと、答えは だ。
最小周期が難しいので、先に周期を考える
しかし、ここで壁にぶつかるのだ。最小周期がちょうど であるような文字列を数え上げるのは簡単ではない。最小周期が 6 であるような文字列の個数を考えるためには、上の "#.#.#.#.#.#.#.#." のような文字列は除外しなければならない。この文字列は確かに周期 6 をもつが、最小周期は 2 なのだ。
そこで方針を変えよう。最小周期が であるような文字列を数え上げるのではなく、やっぱり、先に周期が であるような文字列の個数を数えることにするのだ。これは比較的容易に求められる。
周期が であるような文字列の個数を と表すことにしよう。 は次のように求められる。
'#'
を満たすような () の個数を としたとき、
である
本当に求めたいのは の方だが、それは難しいので、代わりに の方を求めたというわけだ。
メビウスの反転公式と、約数系包除原理
最後に、(約数系) 包除原理を活用して、 (周期 をもつ文字列の個数) から (最小周期が である文字列の個数) を求める方法を考えよう。
たとえば、 としてみよう。12 の約数として がある。周期が であるとき、その最小周期としては の 4 通りがありうる。よって、次の式が成り立つ。
同様に、次のように立式できる。
これらは についての連立方程式ともみなせる。実は、次の定理が成り立つ。メビウスの反転公式などと呼ばれている。ここで、 はメビウス関数である。
【メビウスの反転公式】
のとき、
この反転公式を活用して の値を求める処理は、約数系包除原理と呼ばれる方法そのものでもある。この反転した式に を適用した式を具体的に書き下してみると、「確かに!」になると思う。
ここで、私たちが求めたい値は であることを思い出そう。
たとえば の場合を考えると、求めたい値は である。ここで、
であることを考慮すると、求めたい値は に一致する。よって、メビウスの反転公式を適用することで、求めたい値は
と表せる。 の値は容易に求められる。
一般の についても同様に、求めたい値は
(メビウスの反転公式より)
によって計算できる。計算量は、 の約数の個数を として、 となる。
補足 1:メビウス関数値の列挙
エラトステネスの篩を用いると、メビウス関数値 の値を列挙するのを の計算量で効率よくできます。
詳しいことは次の記事に書きました。この記事では、メビウスの反転公式が「累積和の逆演算」を意味することなども書いていますので、気になる方はぜひ読んでみてください。
補足 2:メビウスの反転公式の証明
次の記事に証明があります。
コード
#include <bits/stdc++.h> using namespace std; // modint template<int MOD> struct Fp { // inner value long long val; // constructor constexpr Fp() noexcept : val(0) { } constexpr Fp(long long v = 0) noexcept : val(v % MOD) { if (val < 0) val += MOD; } constexpr long long get() const noexcept { return val; } constexpr int get_mod() const noexcept { return MOD; } // arithmetic operators constexpr Fp operator - () const noexcept { return val ? MOD - val : 0; } constexpr Fp operator + (const Fp &r) const noexcept { return Fp(*this) += r; } constexpr Fp operator - (const Fp &r) const noexcept { return Fp(*this) -= r; } constexpr Fp operator * (const Fp &r) const noexcept { return Fp(*this) *= r; } constexpr Fp operator / (const Fp &r) const noexcept { return Fp(*this) /= r; } constexpr Fp& operator += (const Fp &r) noexcept { val += r.val; if (val >= MOD) val -= MOD; return *this; } constexpr Fp& operator -= (const Fp &r) noexcept { val -= r.val; if (val < 0) val += MOD; return *this; } constexpr Fp& operator *= (const Fp &r) noexcept { val = val * r.val % MOD; return *this; } constexpr Fp& operator /= (const Fp &r) noexcept { long long a = r.val, b = MOD, u = 1, v = 0; while (b) { long long t = a / b; a -= t * b, swap(a, b); u -= t * v, swap(u, v); } val = val * u % MOD; if (val < 0) val += MOD; return *this; } constexpr Fp pow(long long n) const noexcept { Fp res(1), mul(*this); while (n > 0) { if (n & 1) res *= mul; mul *= mul; n >>= 1; } return res; } constexpr Fp inv() const noexcept { Fp res(1), div(*this); return res / div; } // other operators constexpr bool operator == (const Fp &r) const noexcept { return this->val == r.val; } constexpr bool operator != (const Fp &r) const noexcept { return this->val != r.val; } friend constexpr istream& operator >> (istream &is, Fp<MOD> &x) noexcept { is >> x.val; x.val %= MOD; if (x.val < 0) x.val += MOD; return is; } friend constexpr ostream& operator << (ostream &os, const Fp<MOD> &x) noexcept { return os << x.val; } friend constexpr Fp<MOD> modpow(const Fp<MOD> &r, long long n) noexcept { return r.pow(n); } friend constexpr Fp<MOD> modinv(const Fp<MOD> &r) noexcept { return r.inv(); } }; // エラトステネスの篩を用いた、メビウス関数値列挙、高速素因数分解、約数列挙 struct Eratos { vector<bool> isprime; vector<int> mebius; vector<int> min_factor; Eratos(int MAX) : isprime(MAX+1, true), mebius(MAX+1, 1), min_factor(MAX+1, -1) { isprime[0] = isprime[1] = false; min_factor[0] = 0, min_factor[1] = 1; for (int i = 2; i <= MAX; ++i) { if (!isprime[i]) continue; mebius[i] = -1; min_factor[i] = i; for (int j = i*2; j <= MAX; j += i) { isprime[j] = false; if ((j / i) % i == 0) mebius[j] = 0; else mebius[j] = -mebius[j]; if (min_factor[j] == -1) min_factor[j] = i; } } } // prime factorization vector<pair<int,int>> prime_factors(int n) { vector<pair<int,int> > res; while (n != 1) { int prime = min_factor[n]; int exp = 0; while (min_factor[n] == prime) { ++exp; n /= prime; } res.push_back(make_pair(prime, exp)); } return res; } // enumerate divisors vector<int> divisors(int n) { vector<int> res({1}); auto pf = prime_factors(n); for (auto p : pf) { int n = (int)res.size(); for (int i = 0; i < n; ++i) { int v = 1; for (int j = 0; j < p.second; ++j) { v *= p.first; res.push_back(res[i] * v); } } } return res; } }; int main() { const int MOD = 998244353; using mint = Fp<MOD>; // 入力 int N; string S; cin >> N >> S; Eratos er(N + 10); // 約数系包除原理 mint res = 0; const auto &div = er.divisors(N); for (auto d : div) { if (d == N) continue; // '#' か '.' かを選べるマスの個数 num を求める vector<bool> can(d, true); for (int i = 0; i < N; ++i) { if (S[i] == '.') can[i % d] = false; } long long num = 0; for (int i = 0; i < d; ++i) if (can[i]) ++num; // 包除原理 res -= mint(2).pow(num) * er.mebius[N / d]; } cout << res << endl; }