G - 16 Integers Editorial by MMNMM


与えられる \(X\) が、\(b\) 個の \(0,1\) で添字づけられた長さ \(2 ^ b\) の数列であるとします(この問題においては、\(b=4\) です)。

この問題を expected \(O(4 ^ b+N+2 ^ b\log\operatorname{mod})\) 時間で解く方法について解説します。


\(2 ^ {b-1}\) 頂点のグラフに辺を貼るところまでは公式解説と同様です。 このグラフのオイラー路を数え(て適当な係数をかけ)ることで答えを求めることができます。

まず、答えが正であるためには、次のどちらかの条件が成り立っている必要があります。

  • すべての頂点について、入次数と出次数が等しい。
  • ある頂点がただひとつ存在して、入次数が出次数より \(1\) だけ多い。また、ある頂点がただひとつ存在して、入次数が出次数より \(1\) だけ少ない。これらの頂点を除いたすべての頂点について、入次数と出次数が等しい。

前者のとき、このグラフのオイラー回路の数を \(N\) 倍したものが求めるオイラー路の数です。
後者のとき、入次数が多い頂点から出次数が少ない頂点へ \(1\) 本だけ辺を貼ったグラフのオイラー回路の数が求めるオイラー路の数です。

よって、もとのグラフから \(O(2 ^ b)\) 時間で構成できるグラフ \(1\) つについてオイラー回路の数を求めればよいです。

公式解説と同様にして、構成したグラフの有向ラプラシアン行列における \((v,v)\) 余因子を求めることに帰着されます。

ここで、いま考えている行列が疎であることに注目します。 グラフの構成の方法から、この行列の非零成分はたかだか \(3\times2 ^ {b-1}\) 個です。 なので、black box linear algebra を用いて求める余因子を expected \(O(4 ^ b+2 ^ b\log\operatorname{mod})\) 時間で求めることができます(black box linear algebra は、行列を陽に持つ必要のない線型写像をベクトルへ作用させることを基礎としたアルゴリズムのことをいいます(たぶん)。「ランダムベクトルに作用させて特性多項式/最小多項式を得る」ようなパートを含むものが多い印象があります。詳しくは、 https://yukicoder.me/wiki/black_box_linear_algebra などを参照してください)。

階乗の前計算を \(O(N)\) 時間や \(O(N+\log\operatorname{mod})\) 時間で行うことで、この問題を expected \(O(4 ^ b+N+2 ^ b\log\operatorname{mod})\) 時間で解くことができました。

実装例は以下のようになります。 \(b=13\) などとしたテストケースでも十分高速に動作します。

#include <iostream>
#include <vector>
#include <array>
#include <numeric>
#include <algorithm>
#include <ranges>
#include <random>
#include <atcoder/modint>

// atcoder::static_modint を std::ostream でそのまま出力する(便利なので)
template<int m>
std::ostream &operator<<(std::ostream &os, const atcoder::static_modint<m> &mint) {
    os << mint.val();
    return os;
}

// N×N 疎行列の行列式を求める(実装は下に)
template<typename T>
T determinant(const std::vector<std::tuple<unsigned, unsigned, T>> &matrix, unsigned N);

int main() {
    using namespace std;
    using modint = atcoder::static_modint<998244353>;

    // 階乗の前計算
    // N の上限として Nmax = 1000000 をとり、O(Nmax + log mod) 時間
    constexpr unsigned fact_max{1000000};
    array<modint, fact_max + 1> fact{}, ifact{};
    iota(begin(fact), end(fact), modint{0});
    fact.front() = 1;
    inclusive_scan(begin(fact), end(fact), begin(fact), multiplies<>{});
    iota(begin(ifact), end(ifact), modint{1});
    ifact.back() = 1 / fact.back();
    inclusive_scan(rbegin(ifact), rend(ifact), rbegin(ifact), multiplies<>{});

    modint ans{1};

    constexpr unsigned b{10};
    constexpr unsigned X_length{1U << b}, state_size{X_length / 2};

    array<unsigned, state_size> in_degree{}, out_degree{}; // グラフの入次数、出次数
    array<array<modint, state_size>, state_size> dense_matrix{}; // グラフラプラシアンの密行列表現

    // 初期化
    ranges::fill(in_degree, 0);
    ranges::fill(out_degree, 0);
    for (auto &&row : dense_matrix)
        ranges::fill(row, 0);

    unsigned N{};

    for (unsigned i{}; i < X_length; ++i) {
        unsigned X;
        cin >> X;

        N += X;
        ans *= ifact[X]; // X 本の辺を区別しないため、X! をかけておく

        in_degree[i & state_size - 1] += X;
        out_degree[i / 2] += X;
        dense_matrix[i / 2][i & state_size - 1] -= X;
        dense_matrix[i & state_size - 1][i & state_size - 1] += X;
    }

    // オイラー路が存在するか確認する
    if (const auto missmatches{ranges::count_if(views::iota(0U, state_size), [&in_degree, &out_degree](auto i) {
            return in_degree[i] != out_degree[i];
        })}; missmatches == 2) {

        // 次数がずれている頂点が +1 と -1 の 1 つずつあるとき
        unsigned start{}, end{};
        for (unsigned v{}; v < state_size; ++v) {
            if (in_degree[v] + 1 == out_degree[v])
                start = v;
            if (in_degree[v] == out_degree[v] + 1)
                end = v;
        }
        if (start == end) { // (±2 以上のとき、0)
            cout << 0 << endl;
            return 0;
        }

        // そこに辺を貼る
        ++out_degree[end];
        ++in_degree[start];
        --dense_matrix[end][start];
        ++dense_matrix[start][start];
    } else if (missmatches == 0) // 次数ずれがなければ
        ans *= N; // 始点の位置が N 通りあるので N 倍する
    else {
        cout << 0 << endl; // いずれでもなければ、0
        return 0;
    }

    // 次数が非零の頂点のみ取ってくる
    array<unsigned, state_size> idx{};

    for (unsigned v{}, i{}; v < state_size; ++v)
        if (in_degree[v]) {
            ans *= fact[in_degree[v] - 1]; // (d+(v) - 1)! をかけておく
            idx[v] = i++;
        }

    const auto M{ranges::max(idx)}; // 行列のサイズ

    vector<tuple<unsigned, unsigned, modint>> sparse_matrix;

    for (unsigned v{}; v < state_size; ++v)
        for (unsigned u{}; u < state_size; ++u)
            if (dense_matrix[v][u] != 0 && idx[v] != M && idx[u] != M) // 行と列を 1 つ取り除いておく
                sparse_matrix.emplace_back(idx[v], idx[u], dense_matrix[v][u]);

    cout << ans * determinant(sparse_matrix, ranges::max(idx)) << endl;

    return 0;
}

// 2N+1 項の数列からそれが満たすたかだか N 次の線形漸化式を求める
// 実装は https://nyaannyaan.github.io/library/fps/berlekamp-massey.hpp を参考にしました
template<typename T>
std::vector<T> linear_recurrence(const std::vector<T> &sequence) {
    using std::size, std::begin, std::end, std::swap, std::rbegin;
    using value_type = T;
    const auto N{size(sequence)};
    std::vector<value_type> b{1}, c{1};
    b.reserve(N + 1);
    c.reserve(N + 1);
    for (value_type y{1}; const auto ed : std::views::iota(1U, N + 1U)) {
        const auto x{std::inner_product(begin(c), end(c), begin(sequence) + ed - size(c), T{})};
        b.emplace_back();
        const auto l{size(c)}, m{size(b)};
        if (x == value_type{}) continue;
        value_type freq{x / y};
        if (l < m) {
            swap(b, c);
            for (auto &&C : c) C *= -freq;
            for (unsigned i{}; i < l; ++i) rbegin(c)[i] += rbegin(b)[i];
            y = x;
        } else
            for (unsigned i{}; i < m; ++i) rbegin(c)[i] -= freq * rbegin(b)[i];
    }
    std::ranges::reverse(c);
    return c;
}

// N×N 疎行列の行列式を求める
// 非零要素を K 個として、expected O(N(N+K)) 時間
// 実装は https://nyaannyaan.github.io/library/matrix/black-box-linear-algebra.hpp を参考にしました
template<typename T>
T determinant(const std::vector<std::tuple<unsigned, unsigned, T>> &matrix, unsigned N) {
    using std::size, std::begin, std::end;
    using value_type = T;

    std::mt19937_64 mt{std::random_device{}()};
    std::uniform_int_distribution<unsigned> uid{1, T::mod() - 1};

    std::vector<value_type> D(N), x(N), tmp(N), y(N), result(2 * N + 1);
    while (1) {
        for (auto &&r : D)
            r = uid(mt);
        for (auto &&r : x)
            r = uid(mt);
        for (auto &&r : y)
            r = uid(mt);

        for (unsigned n{}; n < 2 * N + 1; ++n) {
            result[n] = std::inner_product(begin(x), end(x), begin(y), value_type{});
            swap(x, tmp);
            std::ranges::fill(x, value_type{});
            for (const auto&[i, j, c] : matrix)
                x[i] += tmp[j] * c;
            for (unsigned i{}; i < N; ++i)
                x[i] *= D[i];
        }

        const auto minimal_polynomial{linear_recurrence(result)};

        if (minimal_polynomial.back() == 0)return value_type{};

        if (size(minimal_polynomial) != N + 1) continue;

        return minimal_polynomial.back() * (N & 1 ? -1 : 1) /
               std::reduce(begin(D), end(D), value_type{1}, std::multiplies<>{});
    }
}

posted:
last update: