これも最近よく見る「整数の式で表された条件を扱う探索問題」の一味ですね!
問題概要
整数 が与えられる。
正の整数 の組であって、
を満たすものの個数を求めよ。
制約
考えたこと
もし計算時間をまったく気にしなくてよいならば、次のように 4 重の for
文を書くのが楽だろうと思う。
long long res = 0; for (long long A = 1; A <= N; ++A) { for (long long B = 1; B <= N; ++B) { for (long long C = 1; C <= N; ++C) { for (long long D = 1; D <= N; ++D) { if (A * B + C * D == N) { ++res; } } } } }
このコードの計算量は となる。流石に間に合わない。
Step 1: と に分離する
計算量を落とすにあたって、とても有効な考え方として、「ある量を固定して考える」というのがある。
今回は、 の 2 つを固定して考えてみよう。残りの変数 の組の個数を数えることを考える。ここで、 という式は、
というように変形できることに気づく。ここで、もし次のような配列があったら嬉しいなとなる。
num[v]
← 正の整数 の組であって、 を満たすものの個数
この配列がもし求められていたならば、次のコードによって答えが求められるのだ!
long long res = 0; for (long long v = 1; v <= N; ++v) { // A * B = v となる A, B の個数 long long ab = num[v]; // C * D = N - v となる C, D の個数 long long cd = num[N - v]; res += ab * cd; }
めっちゃ簡潔になった。この部分だけなら計算量は まで落ちた。残りは、配列 num[v]
を求める方法を考えよう。
Step 2:配列 num
を求める
色んな方法が考えられるが、一番簡単なのは次のコードで求める方法だと思われる。
vector<long long> num(N+1, 0); for (long long x = 1; x <= N; ++x) { for (long long y = 1; x * y <= N; ++y) { ++num[x * y]; } }
つまり、 をさまざまに動かして行ったときの、 の値の分布を集計するのだ。
このコードは一見すると、2 重の for
文なので の計算量を要するように見えるかもしれない。
しかし、 の値が大きくなると、 の動く範囲は小さくなる。具体的には、 のとき、 の動く範囲は となる。
この総和は
というように表せる。ここで、ABC でも時々登場する事実なのだが、実は
であることが知られている。よって、
となる。つまり、配列 num
を求めるコードの計算量は なのだ。
なお、上の事実がなぜ成り立つのかについて、関心のある方は次の記事など読んでみるとよいと思う。
まとめ
以上の解法をまとめる。
- 配列
num
(num[v]
は となる正の整数 の個数を表す) を求める - 配列
num
を用いて数え上げる
計算量は となる。
コード
#include <bits/stdc++.h> using namespace std; int main() { long long N; cin >> N; // 配列 num を求める vector<long long> num(N+1, 0); for (long long x = 1; x <= N; ++x) { for (long long y = 1; x * y <= N; ++y) { ++num[x * y]; } } // 答えを求める long long res = 0; for (long long v = 1; v <= N; ++v) { res += num[v] * num[N - v]; } cout << res << endl; }