問題文で述べられた集合を とします。
また、 を超えない最大の整数を と表します。
のとき、 が成り立ちます。
これを利用して、次のような決め打ち二分探索を行います。
を 以上 以下の整数とする。「 の要素で 以下であるものが 個以上ある(☆)」が成立する最小の を とすると、
を満たす唯一の が答えになる。( 以下の要素は 個以上、 以下の要素は 個未満)
探索範囲は とします。 で(☆)は常に不成立、 では-1
のケースを先に取り除けば常に成立します。
Writer解では から始めるめぐる式二分探索を採用しています。
二分探索の結果 から を求めます。判定(Step 2)に各回 以上かけるため、 は かけて求めてもボトルネックにはなりません。
となる整数 が存在するような を、 から順に試して求めます。
これは であればよく、このとき です。
図1 : 判定問題の例(, 緑線は。×印は座標が互いに素でないため除く。)
の要素で 以下であるものを数えます。図1のように表すと見通しが良くなります。
まず、互いに素という条件を無視して求めます。
を「格子点 で を満たすものの個数」と定義します。
は、 であれば 、 であれば となります。
の項はfloor_sumの形ですが、 回求めることになるため愚直に累積和を準備すればよいです。
のうち がともに の倍数である格子点の数は、図1の全体を 倍に縮小して で求められます。
ここから、互いに素という条件を無視しない個数を次のように約数系包除原理で求めます。
points[1..M] := 互いに素という条件を無視しない格子点数 for (i = M; i >= 1; i--) { // '/'は小数点以下切り捨て points[i] = f(s, N / i, M / i); for (j = i * 2; j <= M; j += i) { points[i] -= points[j]; } }
実行後、points[1]
が求める要素数となります。
Writer解において、Step 2の計算量は約数包除がネックであり 、Step 1と合わせた計算量はとなります。
def solve(): # Step 2 def count_rational(x): floor_sums = [0] * (M + 1) points = [0] * (M + 1) for i in range(1, M + 1): floor_sums[i] = floor_sums[i - 1] + x * i // (M * M) for i in range(M, 0, -1): P = (M * M * N) // (x * i) if x <= M * N: points[i] = floor_sums[M // i] else: points[i] = floor_sums[P] + (N // i) * (M // i - P) for j in range(i * 2, M + 1, i): points[i] -= points[j] return points[1] def check(mid): return count_rational(mid) >= K # https://aotamasaki.hatenablog.com/entry/meguru_bisect def meguru_bisect(ok, ng): while abs(ok - ng) > 1: mid = (ok + ng) // 2 if check(mid): ok = mid else: ng = mid return ok # Step 1 N, M, K = map(int, input().split()) if not check(N * M * M): print(-1) return res = meguru_bisect(N * M * M, 0) ans_num = -1 ans_denom = -1 for i in range(1, M + 1): if (res - 1) * i // (M * M) < res * i // (M * M): ans_num = res * i // (M * M) ans_denom = i break assert ans_num != -1 if ans_denom == 1: print(ans_num) else: print(ans_num, '/', ans_denom, sep='') if __name__ == '__main__': T = int(input()) for _ in range(T): solve()