という制約の意味に気がつくのにかなり時間がかかった。これのおかげで「 の中に の倍数が何個あるか」というのが、 ではなく でできる。
問題概要
個の相異なる整数 と、整数 が与えられる。各 に対して、
- のうち と互いに素なものが何個あるか
を求めて出力せよ。
制約
考えたこと
いかにも包除っぽいん。
としてあげて、とりあえず が 1 より大きいやつは、1 にしたやつと答えは一緒になるので、
と表せるものだけ考えればよい。包除原理やると、 回の
- の中に の倍数が何個あるか
を求める作業を行って足し引きをすればよい。 の値は最大でも 6 (2 × 3 × 5 × 7 × 11 × 13 = 30030) なので、 の方はほぼ定数と思えるのだが、、、
問題はこの何個あるかを求める作業が愚直にやると 1 回 1 回に かかってしまう。自分は最初
- この作業をやるべき は、素因数分解したときの指数部が 1 のものだけやればいいから、それでギリギリ間に合うのではないか
と考えて提出したけど TLE した。制約をよくみると とあってこれを活かすと、 の中に の倍数が何個あるかを ()で求めることができる。ここで
であることを思い出すと、全部の について実施しても で実施できる。全体として で実施できて間に合う。
なお、 回素因数分解する部分については、通称 osa_k 法と呼ばれている方法によって、 から に高速化できたりする。
#include <iostream> #include <vector> #include <cstring> using namespace std; const int MAX = 110000; int N, M; bool isina[MAX]; int memo[MAX]; // memo[i] := a の中に i で割り切れるものが何個あるか int num(int v) { if (memo[v] != -1) return memo[v]; int res = 0; for (int i = v; i < MAX; i += v) if (isina[i]) ++res; return memo[v] = res; } int main() { cin >> N >> M; memset(isina, 0, sizeof(isina)); memset(memo, -1, sizeof(memo)); for (int i = 0; i < N; ++i) { int a; cin >> a; isina[a] = true; } vector<int> res(M+1, 0); res[1] = N; for (int k = 2; k <= M; ++k) { vector<int> primes; int tk = k; for (int p = 2; p*p <= tk; ++p) { if (tk % p != 0) continue; primes.push_back(p); while (tk % p == 0) tk /= p; } if (tk > 1) primes.push_back(tk); int all = 1; for (auto p : primes) all *= p; if (all < k) res[k] = res[all]; else { for (int bit = 0; bit < (1<<(int)primes.size()); ++bit) { int con = 0, val = 1; for (int i = 0; i < primes.size(); ++i) if (bit & (1<<i)) ++con, val *= primes[i]; if (con & 1) res[k] -= num(val); else res[k] += num(val); } } } for (int k = 1; k <= M; ++k) cout << res[k] << endl; }
別解:添字 GCD 畳み込み
添字 GCD 畳み込みを用いて で解く方法が以下の noshi さんの記事で紹介されている!