E - LCM Sequence Editorial
by
Nyaan
\(A_n\) は単調に増加する数列です。よってこの問題の答えは
- \(1+\) (\(L+1 \leq i \leq R\) を満たす整数 \(i\) のうち \(A_{i-1} \lt A_i\) であるものの個数)
が答えとなります。(はじめの \(1\) は \(A_L\) の分)
\(A_{i-1} \lt A_i\) となる \(A_i\) がどこになるかを考えましょう。
\(A_n = \mathrm{LCM}(1,2,\dots,n)\) です。ここで \(A_n\) が \(p\) で何回割れるかを考えると、\(1,2,\dots,n\) の中で \(p\) で割れる回数が最も多いものは
- \(p^k \leq n\) (\(k\) は整数) を満たす最大の \(p^k\)
となり、割れる回数は \(k\) になることから、\(A_n\) もまた(上記の条件を満たす \(k\) を用いて) \(p\) で \(k\) 回割れることになります。
よって、\(n\) がある素数 \(p\) を用いて \(p\) の冪乗として表せる(これを 素数冪 と呼びます)とき、\(A_n\) は \(A_{n-1}\) に比べて \(p\) で \(1\) 回多く割れることになります。\(p\) 以外の素数において割れる回数は \(A_n\) と \(A_{n-1}\) で変わらないので、\(A_n\) は \(A_{n-1}\) の \(p\) 倍であることが言えます。
逆に \(n\) が素数冪でない場合、任意の素数 \(p\) について \(p\) で割れる回数は \(A_n\) と \(A_{n-1}\) で変わらないことがわかります。よってこの場合は \(A_n = A_{n-1}\) です。
以上の議論より、この問題の答えは
- \(1+\) (\(L+1 \leq i \leq R\) を満たす整数 \(i\) のうち素数冪であるものの個数)
になることがわかりました。
整数 \(L+1, \dots, R\) を全て素因数分解する処理は 区間篩 と呼ばれるアルゴリズムを用いて \(M = \max(R-L, \sqrt{R})\) として \(\mathrm{O}(M \log \log M)\) で計算できることが知られています。このアルゴリズムは過去にも出題されているので 過去の問題の解説 を参照してください。
以上よりこの問題を \(M = \max(R-L, \sqrt{R})\) として \(\mathrm{O}(M \log \log M)\) で解くことができて、これは十分高速です。
- 実装例(C++)
#include <cmath>
#include <iostream>
#include <vector>
using namespace std;
vector<int> prime_enumerate(int N) {
vector<bool> is_prime(N + 1, true);
vector<int> primes;
if (N < 2) return primes;
is_prime[0] = is_prime[1] = false;
for (int i = 2; i * i <= N; ++i) {
if (is_prime[i]) {
for (int j = i * i; j <= N; j += i) is_prime[j] = false;
}
}
for (int i = 2; i <= N; ++i) {
if (is_prime[i]) primes.push_back(i);
}
return primes;
}
int main() {
long long L, R;
cin >> L >> R;
vector<int> vis(R - L);
int ans = 1;
for (int p : prime_enumerate(sqrt(R) + 100)) {
for (long long x = (L / p + 1) * p; x <= R; x += p) {
if (vis[x - (L + 1)]) continue;
vis[x - (L + 1)] = 1;
long long y = x;
while (y % p == 0) y /= p;
if (y == 1) ans++;
}
}
for (int v : vis) ans += v == 0;
cout << ans << "\n";
}
posted:
last update: