F - Not Adjacent 解説
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;
}
投稿日時:
最終更新: