配点: 475 点 / 実行時間制限: 2 sec / メモリ制限: 1024 MB / Difficulty: 1253
問題概要
長さ の数列
と
以下の正整数
が与えられる。
について以下のクエリを処理せよ。
- クエリ :
を含むように
個の要素を
から選ぶとき、それらの最大公約数としてあり得る最大値を求めよ。
制約
- 入力は全て整数。
考察
まずは K 個を適当に選んでみる
まず、一旦 を選ぶことは忘れて、
の中から適当に選んできた
個の要素
の最大公約数
が
の倍数
になるかを考えよう。 ということを示すには、ユークリッドの互除法などを用いる必要があり、少々面倒...
これは、以下のように言い換えられる。
- 条件(#) :
がそれぞれ
の倍数であるか?
理由(クリックで展開)
正整数 が次のように素因数分解されるとする。
このとき、 の最大公約数
は、
と計算できる。これは2つでも、4つ以上でも同様。
ここで、 ならば、
は少なくとも
の倍数であることが分かる。先ほどの素因数分解の式も合わせると、
が
の倍数
がそれぞれ
の倍数
と言い換えることができる。
ここで、以下のような配列を定義してみる。
の要素のうち
の倍数の個数
これが分かっていれば、もしある数 について
が成り立つならば、最大公約数として
が答えの候補に挙がるということになる。
A_i を含めて考える
では、元の問題に戻って に
を含めることを考えよう。
このとき、条件(#)の成立を仮定するならば、 も
の倍数でなければならない。
言い換えれば「 は
の約数である」ということになるので、素直に考えると「
の約数を列挙して、その数に対する
が
以上であるか」を確認していけばよいということになる(方針☆)。
...しかし、この方針は計算量が厳しい(詳細は後述)。
方針を変えよう。
となる
は明らかに条件を満たさないので除くとする。
その上で、残った を小さい順に見ていき、
で初期化した配列
に対して以下のように値を更新していく。
を正整数として、
とする。
こうすることで、この配列は
の要素のうち倍数として
個以上含まれる
の約数の中で最大のもの
が格納されることになり、各クエリの答えは に一致する。
※ 正直、自分で書いていて日本語がかなり怪しいので、具体例も含めて公式解説配信も参照することをオススメする。
計算量のお話
ここからは計算量について考えたい。以降、 とする。
まず、 の構築について。
これは、予めmap
などに各数の出現回数を記録しておき、 に対して(これが今考えている倍数)、
を
から
まで
ずつ増やしながら
に
を加算していくことで実現できる。
この処理は、見かけ上二重for
ループだが、実は の計算量で実現可能である。
理由(クリックで展開)
に関するループの中の処理が呼び出される回数を、全体に対する割合で考えていくと、
: 毎回呼び出されるので
。
: 1回おきに(2回に1回)呼び出されるので
。
: 2回おきに(3回に1回)呼び出されるので
。
...
:
回に1回呼び出されるので
。
となり、これを合計すると と書ける(調和級数の一部)。
ここで、少々唐突だが以下のグラフを考える。

ここから、以下のような評価ができる( 個の長方形と曲線が囲む面積とを比較している)。
したがって、オーダーでは となる。
の外側では
のループが
回実行されているので、この部分の計算量は
と見積もることができる。
続いて、 の構築について。
実はこの部分も先ほどと同じく、「外側で 回、内側で
回の二重
for
ループ」である(実装例も参考に)。
したがって、この部分も計算量は なのだ。
入出力にはそれぞれ 時間かかるので、全体としては
の計算量であることが分かった。
制約より、最大でも 程度なので、一応2秒以内に収まるかなといった感じ。
ただ、結構ギリギリなのでいつものようにcout << endl
で答えの改行出力を行うと TLE してしまう。
これは、endl
による出力がバッファを吐き出すものである(= 出力ごとに時間がかかる) からである。
対策としては、cout << '\n'
と書くことによって、バッファに内容を溜め続けて最後に吐き出すようにすることが最も簡単だと思う。
あとは、map
をvector
に置き換えるというのもアリかも。
ちなみに、前述した(方針☆)の計算量についてだが、ある整数 の約数列挙には一般的に
の計算量がかかる。
したがって、 の構築部分に
かかってしまい、ここが全体のボトルネックとなって TLE するのが濃厚である。
実装例
#include <bits/stdc++.h> using namespace std; #define rep(i, start, end) for (auto i = (start); (i) < (end); (i)++) #define repe(i, start, end) for (auto i = (start); (i) <= (end); (i)++) using ll = long long; // ======================================== // const int MAX = 1000100; int main() { int N, K; cin >> N >> K; vector<int> A(N); unordered_map<int, int> mp; rep(i, 0, N) cin >> A[i], mp[A[i]]++; vector<int> multiples(MAX, 0); repe(i, 1, MAX) { for (int j = i; j <= MAX; j += i) { multiples[i] += mp[j]; } } vector<int> max_g(MAX, 0); repe(i, 1, MAX) { if (multiples[i] >= K) { for (int j = i; j <= MAX; j += i) { max_g[j] = i; } } } rep(i, 0, N) cout << max_g[A[i]] << '\n'; return 0; }
実装時間: 60分
コメント
これ普通に難しくないか...?自分が数弱だということを抜きにしても、計算量解析も結構複雑なように感じた。
「 以下の整数について、それぞれその
以下の倍数を全探索する処理は調和級数の計算量
で可能」という話はたまに出てくるので覚えておいてもいいかもしれない。