振り返りです。

AtCoder Beginner Contest 292 - AtCoder
AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online.
def test_all(f): for i, data in enumerate(eval(f'test_data{f.__name__[0]}')): exp = data["out"] ans = f(*data["in"]) result = "AC" if exp == ans else "WA" print(f"{i+1} {result}: expected: {exp}, output: {ans}")

A - CAPS LOCK

提出:#39404553

B - Yellow and Red Card

提出:提出 #39410010 - AtCoder Beginner Contest 292

C - Four Variables

やってみよう!

C関数を完成させて「Run」ボタンをクリックしよう!

test_dataC = [ { "in":[4], "out": 8 },{ "in":[292], "out": 10886 },{ "in":[19876], "out": 2219958 }, ] def C(N): pass test_all(C)

自由欄

解説を見る:$O(N \sqrt{N})$

コンテスト中にACできず…
考えたのは、$1 \le AB, CD \le N - 1$だから$1 \le X \le N - 1$の全てのXについて素因数分解して、素因数の数(X:10の場合、2, 5で2つ)を$p$としたら$2^p$がABまたはCDの組み合わせになって・・・みたいな。

でも、そもそも素因数分解がだるいし、X:16(=$2 ^ 4$)の時って「選ぶ・選ばない」でAB作っても4 × 4になるような0101, 1100,...みたいなのを重複して数えることになっちゃうから「$2^p$がABまたはCDの組み合わせになる」って考えるのは間違ってるな・・・

結果、結構な時間を使って考えた挙げ句、むっっっっずーーーーーーーー無理!!!!!!!!!となって飛ばしました。

という訳で解説 - AtCoder Beginner Contest 292を見ます。
文章だけ読んだ状態で「なるほどAを決め打ってA≦Bとしたら$O(N \sqrt{N})$でイケんのか」と実装したのが以下。

from math import ceil, sqrt def C(N): a = [0] * N a[1] = 1 for i in range(2, N): for j in range(1, ceil(sqrt(i))): if i % j == 0: a[i] += 1 a[i] *= 2 if int(sqrt(i)) ** 2 == i: a[i] += 1 # print(a) ans = 0 for i in range(1, N): ans += a[i] * a[N - i] return ans test_all(C)

結果:#39555777

ACやったー
jでイテレートしてるrangeの終点の設定が分かりづらいですね。。

自由欄

解説を見る:--約数の個数を調べる--$O(N \log{N})$

さらに解説を見ます。

$AB=X$を満たす正整数$A,B$の個数は$X$の正の約数の個数に等しいです

😲😲😲😲😲😲😲😲😲😲😲😲😲😲😲😲❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗

そっか…素因数分解じゃなくて約数調べたらよかったのか…
んで、その計算量は$\frac{N}{1} + \frac{N}{2} + \dots + \frac{N}{N} = N(\frac{1}{1} + \frac{1}{2} + \dots + \frac{1}{N})$で、カッコ内が調和数というものらしく、増大度は n の自然対数 ln(n) と同程度の速さであるようです。
つまり$O(N \log{N})$になるわけですね。

numpyを使って実装するとこんな感じ。

import numpy as np def C(N): a = np.ones(N, dtype=int) for i in range(2, N): sli = slice(i, N, i) a[sli] += 1 a = a[1:] return sum(a * a[::-1]) test_all(C)

結果:#39592679

自由欄

解説を見る:--やっぱり素因数分解する--

もう一つのより高速な解法の解説を見てみます。
線形篩とはなんぞ?エラトステネスの篩とは違うの?

ということで、解説内でも示されているえびちゃん🍑🍝🦃(@rsk0315_h4x)さんのライブラリ(下記1.)や、例によってググったらヒットしたけんちょんさんの記事(下記2.)を参考にクラス化してみました。

が、結論としては37zigenさんが言及しているとおりエラトステネスの篩の方が高速なため、「最小の素因数を求める」というアイディアと素因数分解や約数列挙の考え方・実装のみ援用しています。
結局、線形篩は$O(N)$ですがエラトステネスの篩$O(N \log\log{N})$(参照)の方が高速で、「各iがlpf(i)で何回割り切れるか」も前処理として$O(N)$で求めておくことで(下記41行~)、iの昇順に素因数分解を$O(1)$で得られることになります。
そして素因数分解が分かっていれば、約数の個数は簡単に計算できてしまいます。(参照)

一般に$N = p_{1}^{e_{1}} p_{2}^{e_{2}} \dots p_{K}^{e_{K}}$と素因数分解できるとき、約数の個数は$(e_{1} + 1)(e_{2} + 1) \dots (e_{K} + 1) 個$となります。

「約数の個数を求めるために素因数分解する」という着想自体は、(けんちょんさんの指摘通りオーバーキルだとしても)間違ってなかったんですね。

ちなみに、線形篩というキーワードだけで横道に逸れた結果、もとの解説とはもはや別物です。(というか、解説をよく分かってない。)

参考🔎

  1. LinearSieve in nekolib::math - Rust
  2. けんちょんさんの記事
  3. Linear Sieve - Algorithms for Competitive Programming
from enum import Enum from functools import lru_cache, reduce from operator import mul from itertools import product class Sieve: method = Enum("method", ["ERATOSTHENES", "LINEAR"]) def __init__(self, n, method=method.ERATOSTHENES): if method == self.method.ERATOSTHENES: primes = [False, False] + [True] * (n - 1) self.lpf = list(range(n + 1)) for p in range(2, int(n ** (1 / 2)) + 1): if not primes[p]: continue self.lpf[p] = p for q in range(p * 2, n + 1, p): primes[q] = False if self.lpf[q] == q: self.lpf[q] = p elif method == self.method.LINEAR: self.lpf = list(range(n + 1)) primes = [] for i in range(2, n + 1): if self.lpf[i] == i: primes.append(i) lpf_i = self.lpf[i] for p in primes: if p > lpf_i or i * p > n: break self.lpf[i * p] = p else: raise ValueError(f"'method' must be instance of '{self.__class__.__name__}.method'.") # 各iがlpf(i)で何回割り切れるか self.lpf_e = [[1, 0] for _ in range(n + 1)] for i in range(2, n + 1): p = self.lpf[i] j = i // p self.lpf_e[i] = ( [self.lpf_e[j][0] * p, self.lpf_e[j][1] + 1] if self.lpf[j] == p else [self.lpf[i], 1] ) @lru_cache(None) def factors(self, n): lpfs = [(self.lpf[n], self.lpf_e[n][1])] if self.lpf_e[n][0] == n: return lpfs else: return lpfs + self.factors(n // self.lpf_e[n][0]) @lru_cache(None) def divisors(self, n): if n == 1: return [1] elif self.lpf[n] == n: return [1, n] else: return sorted([ a * b for a, b in product( self.divisors(n // self.lpf_e[n][0]), [self.lpf[n] ** i for i in range(self.lpf_e[n][1] + 1)] ) ]) def divisors_count(self, n): return reduce(mul, [e + 1 for _, e in self.factors(n)]) def C(N): s = Sieve(N) return sum(s.divisors_count(N - i) * s.divisors_count(i) for i in range(1, N)) test_all(C)

結果:#39594096(線形篩の場合:#39594745)

おぉ、実行時間が危なっかしいところですが無事AC。

自由欄

D - Unicyclic Components

やってみよう!

D関数を完成させて「Run」ボタンをクリックしよう!

test_dataD = [ { "in":[3, 3, [[2, 3], [1, 1], [2, 3]]], "out": "Yes" },{ "in":[5, 5, [[1, 2], [2, 3], [3, 4], [3, 5], [1, 5]]], "out": "Yes" },{ "in":[13, 16, [[7, 9], [7, 11], [3, 8], [1, 13], [11, 11], [6, 11], [8, 13], [2, 11], [3, 3], [8, 12], [9, 11], [1, 11], [5, 13], [3, 12], [6, 9], [1, 10]]], "out": "No" }, ] def D(N, M, uv): pass test_all(D)

自由欄

解説を見る①

コンテスト中はお馴染みのDSUクラスを、連結成分の数ではなく辺の数を持つように変更を加えることでACに至りましたが、その過程で2WAとなってしまいました😇

解説を見ます。あっ、そんな面倒なことせずともDSUクラスのままでイケたんや…
という訳で以下。

from collections import defaultdict class DSU(): def __init__(self, n=None): self.dsu = defaultdict(lambda: -1) if n: for i in range(n): self.dsu[i] def leader(self, i): if self.dsu[i] < 0: return i self.dsu[i] = self.leader(self.dsu[i]) return self.dsu[i] def merge(self, i, j): '''サイズの大きい方(x)に小さい方(y)をマージ''' if i == j: return x = self.leader(i) y = self.leader(j) if x == y: return if self.dsu[x] > self.dsu[y]: x, y = y, x self.dsu[x] += self.dsu[y] self.dsu[y] = x def members(self, i): l = self.leader(i) return [k for k, v in self.dsu.items() if self.leader(k) == l] def __len__(self): return len([1 for v in self.dsu.values() if v < 0]) def D(N, M, uv): dsu = DSU(N + 1) for i in uv: dsu.merge(*i) edge = defaultdict(int) for u, v in uv: edge[dsu.leader(u)] += 1 for l, e in edge.items(): if abs(dsu.dsu[l]) != e: return "No" return "Yes" if len(uv) == N else "No" test_all(D)

結果:#39661034

自由欄

解説を見る②

ユーザ解説を見ます。

答えは、$N=M$かつすべての連結成分がサイクルを持つことです。

ということで超頂点を導入し、サイクルができたら超頂点と繋ぐとのこと。
実装を見ると、UnionFindクラスでunionメソッドの返り値が「既に連結成分かどうか」を返してるっぽいので、我がDSUクラスでも同様の挙動になるように変更を加えて(下記15, 18行目)、こんな感じ。

from collections import defaultdict class DSU(): def __init__(self, n=None): self.dsu = defaultdict(lambda: -1) if n: for i in range(n): self.dsu[i] def leader(self, i): if self.dsu[i] < 0: return i self.dsu[i] = self.leader(self.dsu[i]) return self.dsu[i] def merge(self, i, j): '''サイズの大きい方(x)に小さい方(y)をマージ''' # if i == j: return x = self.leader(i) y = self.leader(j) if x == y: return True if self.dsu[x] > self.dsu[y]: x, y = y, x self.dsu[x] += self.dsu[y] self.dsu[y] = x def members(self, i): l = self.leader(i) return [k for k, v in self.dsu.items() if self.leader(k) == l] def __len__(self): return len([1 for v in self.dsu.values() if v < 0]) def D(N, M, uv): dsu = DSU(N + 1) for u, v in uv: if dsu.merge(u, v): dsu.merge(0, u) return "Yes" if N == M and abs(dsu.dsu[dsu.leader(0)]) == N + 1 else "No" test_all(D)

結果:#39661999

自由欄

E - TransitivityE - Transitivity

やってみよう!

E関数を完成させて「Run」ボタンをクリックしよう!

test_dataE = [ { "in":[4, 3, [[2, 4], [3, 1], [4, 3]]], "out": 3 },{ "in":[292, 0, []], "out": 0 },{ "in":[5, 8, [[1, 2], [2, 1], [1, 3], [3, 1], [1, 4], [4, 1], [1, 5], [5, 1]]], "out": 12 }, ] def E(N, M, uv): pass test_all(E)

自由欄

try

コンテスト中はBFSっぽい実装で愚直に操作を繰り返して数え上げました。
それが以下。

from collections import defaultdict, deque def E(N, M, uv): nxt = set() edge = defaultdict(list) for u, v in uv: edge[u].append(v) nxt.add(v) ans = 0 for a in range(1, N + 1): q = deque(edge[a]) while q: b = q.popleft() if b == a: continue for c in edge[b]: if c in [a, b]: continue if c not in edge[a]: ans += 1 edge[a].append(c) q.append(c) # print(a, b, c) return ans test_all(E)

結果:#39443129

TLE x 3😇

自由欄

解説を見る

解説によると、「最終的なグラフの状態において存在する頂点$x$を始点とする有向辺は、初期状態において頂点$x$から到達可能な頂点」らしいです。
そうなんだ… あー、まぁ、そうか…

で結局全ての頂点についてBFSです。

from collections import defaultdict, deque def E(N, M, uv): edge = defaultdict(set) for u, v in uv: edge[u].add(v) ans = 0 for i in range(1, N + 1): q = deque(edge[i]) seen = edge[i].copy() seen.add(i) while q: now = q.popleft() for nxt in edge[now]: if nxt in seen: continue q.append(nxt) seen.add(nxt) ans += len(seen) - 1 - len(edge[i]) return ans test_all(E)

結果:#39663652

自由欄

まとめ

自由欄