A - Uncommon/みんプロ2018本戦
問題概要
集合が与えられる。
各整数k = 1, 2, ..., Mについて、kと互いに素であるAの要素の個数を求めよ。
制約
解法
愚直にやると少なくともO(N*M)かかってしまうのでまとめて数え上げるなどの高速化が必要になります。
kを固定したときに、kとa_iが互いに素であるというのは、gcd(k, a_i) = 1であるというのが定義ですがこの条件はまとめて数え上げようとするときには少し使いにくいので別の同値な条件に言い換えてみます。
という感じにkを素因数分解したとします。(p_jは素数)
このとき、kとa_iが互いに素であることの必要十分条件は、a_iがどのp_jでも割りきれないこととなります。逆に、kとa_iが互いに素でないことは、a_iがあるp_jで割り切れることと同値です。
この事実を使ってkと互いに素でないa_iの個数を求めることにします。その個数をn(k)とします。
をxで割り切れるa_i全体の集合と定義すると、求めたいのは
となります。和集合で表されているものは一般には数え上げには使い辛いので、包除原理を使って書き直してやると
と求めることが出来ます。
後は、とかとかをどうやって求めるのかとか、それをn(k)に足したり引いたりするのをどうするのかというところです。
まず初めに、エラトステネスの篩風の前処理をしておくと各整数kに含まれる異なる素因数の積やその個数などをO(MlogM)で求めて置くことができます。
それをやっておいてから、整数dを2,3,...と動かしていき、d = (dに含まれる異なる素因数の積)となっていれば上で書いたようなみたいな感じの求めるべきものなのでそこをちゃんと数えます。つまりAに含まれるdの倍数を数えます。ただし、愚直にやるとここでO(N)かかってしまうので最初にbool配列を取っておいてそこにAの要素があるないのフラグを立てておくとdの倍数ごとにスキップしながら見るということが出来るので、全体での計算量がM + M/2 + M/3 + ... = O(MlogM)程度になってくれます。そしてこれを求めたらdに含まれる素因数の個数偶奇に応じて+か-か決めて、またdの倍数ごとにスキップしながら足し引きすべきn(k)に対してそれを足したり引いたりします。
こうやってやるとO(MlogM)程度で解くことが出来てちゃんと間に合うという感じになりました。
ソースコード
適宜省略
void main() { int n, m; scan(n, m); auto a = iota(n).map!(i => readln.chomp.to!int).array; auto b = new bool[](a.reduce!max + 1); foreach (ai ; a) { b[ai] = 1; } auto pd = new int[](m + 1); auto pcn = new int[](m + 1); pd[] = 1; foreach (p ; 2 .. m + 1) { if (pd[p] == 1) { for (int d = p; d < m + 1; d += p) { pd[d] *= p; pcn[d]++; } } } auto cnt = new int[](m + 1); foreach (i ; 2 .. m + 1) { if (pd[i] < i) continue; int res = 0; for (int d = i; d < b.length.to!int; d += i) { if (b[d]) res++; } int diff = res * (-1)^^((pcn[i] & 1) ^ 1); for (int d = pd[i]; d < m + 1; d += pd[i]) { cnt[d] += diff; } } writeln(n); foreach (i ; 2 .. m + 1) { writeln(n - cnt[i]); } }