なんかユーザー解説の数がすごいことになっててビビる!
問題概要
以下の正の整数 の組であって、 が平方数であるようなものの個数を求めよ。
制約
考えたこと
この手の問題では「ある変数を固定して考える」という常套手段がある!!
今回は を固定して考えよう。つまり私たちは次の問題を解決したいということになる。
整数 が与えられる。以下の条件を満たす整数 の個数を求めよ。
- は平方数である
- である
次に考えるべきことは、「一体どういうときに が平方数になるのだろうか」という部分だ。
が平方数になる を求める
まずは、 を素因数分解してみよう。
と表せるとする。ここで、 が奇数であるような素因数 たちを改めて とすると、ある整数 を用いて
と表せることになる。具体例をあげると、1440 を素因数分解すると
となる。これを上のように表すと
となる。つまり、 ということだ。
このとき、 が平方数であるということは、 が平方数であるということと同値になる。
さらに、 が平方数であるということは、 を素因数分解したときに、次の条件と同値である!
- は を素因数に持ち、しかもそれらの指数は奇数である
- のそれら以外の素因数については、指数が偶数である
これをさらに分かりやすく言い換えると、次のようになる。 とおくことにする。
は平方数 を用いて、 と表せる
ここまで来ると、実はすでに効率のよい解法が得られている!! それを次にまとめる。
解法まとめ
解法をまとめると、次のようになる。
に対して、 が平方数となる () の個数を次のように求める。
- を素因数分解したときに、指数が奇数であるような素因数の積を とする
- このとき、 が平方数であることは、整数 を用いて と表せることと同値である
- よって、条件を満たす の個数は「 以下の平方数の個数」に一致する
最後に計算量を見積もる。各 を素因数分解するのに の計算量を要することから、全体の計算量は となる。
なお、高速素因数分解を用いると、 の素因数分解を の計算量で求めることもできる。その技法を用いると、全体の計算量も となる。
コード
解法 1:愚直に素因数分解する方法
計算量は となる。提出すると 182ms だった。
#include <bits/stdc++.h> using namespace std; // 素因数分解 vector<pair<long long, long long> > prime_factorize(long long n) { vector<pair<long long, long long> > res; for (long long p = 2; p * p <= n; ++p) { if (n % p != 0) continue; int num = 0; while (n % p == 0) { ++num; n /= p; } res.push_back(make_pair(p, num)); } if (n != 1) res.push_back(make_pair(n, 1)); return res; } int main() { long long N; cin >> N; long long res = 0; for (long long i = 1; i <= N; ++i) { // i を素因数分解して、指数が奇数である素因数の積を求める const auto &pf = prime_factorize(i); long long Q = 1; for (auto [p, e] : pf) if (e & 1) Q *= p; // N/Q 以下の平方数の個数を求める long long sq = (long long)sqrt(N/Q); res += sq; } cout << res << endl; }
解法 2:高速素因数分解を用いる方法
エラトステネスの篩を用いる。 となる。提出すると 33ms だった。
#include <bits/stdc++.h> using namespace std; // isprime[n] := is n prime? // min_factor[n] := the min prime-factor of n struct Eratos { vector<int> primes; vector<bool> isprime; vector<int> min_factor; Eratos(int MAX) : primes(), isprime(MAX+1, true), min_factor(MAX+1, -1) { isprime[0] = isprime[1] = false; min_factor[0] = 0, min_factor[1] = 1; for (int i = 2; i <= MAX; ++i) { if (!isprime[i]) continue; primes.push_back(i); min_factor[i] = i; for (int j = i*2; j <= MAX; j += i) { isprime[j] = false; if (min_factor[j] == -1) min_factor[j] = i; } } } // prime factorization vector<pair<int,int>> prime_factors(int n) { vector<pair<int,int> > res; while (n != 1) { int prime = min_factor[n]; int exp = 0; while (min_factor[n] == prime) { ++exp; n /= prime; } res.push_back(make_pair(prime, exp)); } return res; } }; int main() { long long N; cin >> N; // エラトステネスの篩を準備 Eratos er(N + 1); long long res = 0; for (long long i = 1; i <= N; ++i) { // i を素因数分解して、指数が奇数である素因数の積を求める const auto &pf = er.prime_factors(i); long long Q = 1; for (auto [p, e] : pf) if (e & 1) Q *= p; // N/Q 以下の平方数の個数を求める long long sq = (long long)sqrt(N/Q); res += sq; } cout << res << endl; }