Official

F - Not Adjacent Editorial by MMNMM


この問題は半分全列挙を用いて解くことができます。

フィボナッチ数列 \(\operatorname{fib} _ 0=1,\operatorname{fib} _ 1=2,\operatorname{fib} _ {i+2}=\operatorname{fib} _ {i+1}+\operatorname{fib} _ i\) について、長さ \(N\) の列の(連続するとは限らない)部分列 \(2 ^ N\) 個のうち、連続する \(2\) つの要素を選ばないものが \(\operatorname{fib} _ N\) 個であることが知られています。

与えられた長さ \(N\) の列を、前半 \(\lfloor N/2\rfloor\) 項と後半 \(\lceil N/2\rceil\) 項に分けることを考えます。 すると、それぞれの部分列のうち連続する \(2\) つの要素を選ばないものは \(\operatorname{fib} _ {\lfloor N/2\rfloor}\) 個と \(\operatorname{fib} _ {\lceil N/2\rceil}\) 個になります。 \(N\le60\) のとき \(\operatorname{fib} _ {\lfloor N/2\rfloor}\le\operatorname{fib} _ {\lceil N/2\rceil}\le2178309\) が成り立つので、 \(\operatorname{fib} _ {\lfloor N/2\rfloor}\) 個と \(\operatorname{fib} _ {\lceil N/2\rceil}\) 個の部分列すべてに対して、それぞれの部分列に含まれる値の総和を \(M\) で割った余りを求めておくことができます。

この情報を用いて、前半と後半の部分列を組み合わせて \(M\) の倍数になる組み合わせの数を \(O(\operatorname{fib} _ {\lfloor N/2\rfloor}\log\operatorname{fib} _ {\lceil N/2\rceil})=O(N((1+\sqrt5)/2)^{N/2})\) 時間 で求めることができます。

この値には、分割された境界に接している \(2\) つの項(前半の末尾の項と、後半の先頭の項)を両方使う組み合わせが含まれているため、これを取り除く必要があります。 これも同様のアルゴリズムで求めることができるため、この問題を \(O(N((1+\sqrt5)/2)^{N/2})\) 時間で解くことができます。

実装例は以下のようになります。

実装方針によっては、\(N=1\) の場合を特別に扱う必要があることに注意してください(分割した列の一方が空列になるため)。

#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>

int main() {
    using namespace std;
    unsigned N, M;
    cin >> N >> M;
    vector<unsigned> A(N);
    for (auto&& a : A)
        cin >> a;

    // 与えられた列の連続する 2 要素を選ばない部分列の総和を列挙する
    // 末尾の要素を使うものも同時に求める
    const auto solve{[M](const auto& seq) -> pair<vector<unsigned>, vector<unsigned>> {
        vector<unsigned> used, unused;
        unused.emplace_back();
        for (const auto x : seq) {
            swap(used, unused);
            for (auto&& v : used) {
                unused.emplace_back(v);
                (v += x) %= M;
            }
        }
        vector<unsigned> all(used);
        ranges::copy(unused, back_inserter(all));
        ranges::sort(used);
        ranges::sort(all);
        return make_pair(used, all);
    }};

    // 前半に対する結果
    const auto [first_used, first_all]{solve(A | views::take(N / 2))};
    // 後半に対する結果
    const auto [second_used, second_all]{solve(A | views::reverse | views::take((N + 1) / 2))};

    unsigned long ans{};
    for (const auto a : first_all)
        ans += ranges::upper_bound(second_all, (M - a) % M) - ranges::lower_bound(second_all, (M - a) % M);

    // 境界で連続しているものを引く
    for (const auto a : first_used)
        ans -= ranges::upper_bound(second_used, (M - a) % M) - ranges::lower_bound(second_used, (M - a) % M);

    cout << ans << endl;

    return 0;
}

posted:
last update: