ntk log ntk

競プロや非競技プロのやったことを書いています.

DISCO presents ディスカバリーチャンネル コードコンテスト2016 予選 C - ロト2

C - ロト2

思考過程

与えられた整数A_iのそれぞれについて,他の整数A_j (j \neq i)のそれぞれと積を取り,Kの倍数であるか確認する解法がまず思いつきます. が,計算量がO(N^2)になり,今回の入力サイズはN = 2 \times 10^5となるのでTLEしてしまいます.

ここでサンプル1を見てみましょう.

4 6
1 3 2 6

積を考えたいので,それぞれの要素に何をかければKの倍数になるか考えます. すると,A_iにはK // A_iをかければいいことに気づきます. K // A_iは,Kの約数です.必要そうなので,ここら辺でKの約数をとりあえず列挙しておきます. では前処理として,約数ごとにi番目以降のその約数の個数を計算して,配列に保存しておけばいいのではないでしょうか. これができたら,A_iを順にみていって,かけたらいいK//A_iの個数がO(1)で求まり,答えがわかります. 前処理をした配列を値とし,対応する約数をキーとした辞書を持っておけば実装できそうです! しかし残念ながらこれも前処理にO((約数の個数) \times N)かかり,今回の入力だとTLEしてしまいます….

解法

Kの約数について考えるのはいいことでした.さらにKの約数なら高々1500個くらいで抑えられるので1O((Kの約数の個数)^2)ならしていいです.ここで,K_iはi番目のKの約数とします.そして,K_iKとの最大公約数として持つAの要素の個数を,K_iごとに数えておきます2

以降,K_iKとの最大公約数として持つAの要素の個数のK_iの個数と単に言います. K_iK_jの個数の積を取ると,K_iK_jの積を因数に持つペアの個数だということを発想します.ここは例を考えるとわかりやすくて,サンプルを考えましょう.サンプル1から要素を1つ増やしたものです.

5 6
1 3 2 6 4

このとき,6の約数は1, 2, 3, 6であり,1の個数は1個,2の個数は2個,3の個数は1個,6の個数は1個です.1の個数と6の個数をかけると 1 \times 6を含むペアは1通りです(1と6).2の個数と3の個数をかけると,2 \times 3を含むペアは2通りです(3と2,3と4).1の個数と4の個数をかけると1 \times 4を含むペアは1通りです(1と4).2 \times 6を含むペアは1通りです(2と6).2 \times 3を含むペアと,2 \times 6を含むペアというのは区別します3

これに対し,K_iK_jをかけてKの約数になるようなペアだけ数え上げればいいです.具体的には,約数を二重ループで回して,順番を気にせずにかけてKの約数になるなら和を取ってしまいます.そうすると 1 \times 6を含むペアと 6 \times 1を含むペアが同じなので,最終的な和は答えより2倍大きい値になります.ちょうど2倍になっているので2で割ってあげると求めたい答えが得られます.

ただし,一つ注意する点があります.6の個数と6の個数をかけると 6 \times 6を含むペアの総数が得られるとしたいですが,同じものを2つ選んだペアも数え上げてしまっているので,答えが正しいものより大きくなってしまします(実際,上のサンプルでは6の個数は1個でペアはできないのに,6 \times 6を含むペアを一つ数えてあげてしまうことになります).これを避けるために,K_iK_jが同じものだったときは,(K_iの個数) \times ((K_iの個数) - 1)を足してあげればいいです.これは(K_iの個数)という集合から2つ選ぶという組合せの総数と考えることができます4

提出コード

提出コード

from math import gcd
import sys
input = sys.stdin.buffer.readline


def main():
    N, K = (int(i) for i in input().split())
    A = [int(i) for i in input().split()]

    def enum_divisors(n):
        # 約数列挙
        divs = []
        for i in range(1, n+1):
            if i*i > n:
                break
            if n % i == 0:
                divs.append(i)
                if n//i != i:
                    # i が平方数でない
                    divs.append(n//i)
        return divs

    divs = enum_divisors(K)

    G = [gcd(a, K) for a in A]
    B = {d: 0 for d in divs}
    for g in G:
        B[g] += 1

    ans = 0
    for g in divs:
        for rg in divs:
            if (g*rg) % K == 0:
                ans += B[g] * (B[rg] - (g == rg))
    print(ans//2)


if __name__ == '__main__':
    main()

僕はこの問題を解説ACしたのですが,解説pdfを読んでも全然わからなかったのでTwitterで質問しまくりました.はてぃーぽったーさん,茶碗蒸しさんありがとうございます.

はてぃーぽったーさんとのやりとり 茶碗蒸しさんのツイート


  1. これはどう調べればいいでしょう?僕はいつも高度合成数でググります.OEISのA002182です

  2. ここは「なぜこうなる?」が言語化できませんでした….gcdが出てくるのは約数考えたら次に連想することが経験上多いからでしょうか

  3. ここも「なぜ?」が言語化できませんでした

  4. 逆に,K_i \neq K_jのときは異なる集合から一つずつ選ぶ組み合わせの総数なので,(K_iの個数) \times (K_jの個数)と解釈することもできます