Pythonで蟻本2-6 - 数学的な問題を解くコツ

背景:蟻本をやります - 競プロはじめました
テキスト:

【2021.05.02〜05.04】

ユークリッドの互除法

ユークリッドの互除法については,次で解説しています.代数の本を買えば,大体載っていると思います.

Pythonでは,math.gcd(x, y)で最大公約数が得られます(Pythonで最大公約数と最小公倍数を算出・取得 | note.nkmk.me).本文と同じように再帰関数で書くと

def gcd(a, b):
  if b==0:
    return a
  else:
    return gcd(b, a%b)

です(elseはなくてもよい(最初のreturnで抜ければ実行されないため)).

線分上の格子点の個数

【入力】
1 11
5 3
【出力】
3

【コード】

import math

x1, y1 = map(int, input().split())
x2, y2 = map(int, input().split())

if x1==x2 and y1==y2:
  exit(print('0'))
else:
  print(math.gcd(abs(x1-x2), abs(y1-y2)) -1 )

【ポイント】
本文のように図を書いてみる(※)のがわかりやすいと思いますが,若干補足です.

※本文の方法:2点間の傾きを表す大きな三角形を,辺の長さが整数の相似な三角形に分割する方法を考える.

【式を使った考え方】
2点$(x_{1},y_{1})$,$(x_{2},y_{2})$を通る直線の傾きは

\begin{aligned}
\frac{y_{2}-y_{1}}{x_{2}-x_{1}}
\end{aligned}
です.

これを約分して既約分数にした結果を$q/p$とすれば,$x_{1}$から$\pm x$方向に$p$進んだ直線上の点,あるいは$y_{1}$から$\pm y$方向に$q$進んだ直線上の点が一番近い格子点になります.次の格子点も同じ間隔で存在します.

よって,$|\frac{x_{2}-x_{1}}{p}| -1$または$|\frac{y_{2}-y_{1}}{q}| -1$が2点間に存在する格子点の個数です.ここで,$|(y_{2}-y_{1})/q| = |(x_{2}-x_{1})/p|$は,(既約分数の定義から)$|y_{2}-y_{1}|$と$|x_{2}-x_{1}|$の最大公約数です.

【補足 (もう少し厳密に)】
直線の方程式は

\begin{aligned}
y-y_{1}
=\frac{y_{2}-y_{1}}{x_{2}-x_{1}} (x-x_{1})
\end{aligned}
で与えられます.$\frac{y_{2}-y_{1}}{x_{2}-x_{1}}$の既約分数を$q/p$とすれば,
\begin{aligned}
(y-y_{1})p = (x-x_{1})q
\end{aligned}
となります.

$x,y$が格子点上にあることと,$x-x_{1}, y-y_{1}$が整数であることは同値です.したがって,直線上の点$(x,y)$について,①「$x,y$が格子点上にある」とき,上式で$p,q$は互いに素であることから,②「$p$は$(x-x_{1})$の約数で,$q$は$(y-y_{1})$の約数」です.逆に,②が成り立つとき,$x-x_{1}, y-y_{1}$は整数なので,①が成り立ちます.

よって,$(x-x_{1})/p = (y-y_{1})/q$が整数となる$(x,y)$が格子点を意味します.


拡張ユークリッドの互除法 - 双六

これも,大概の代数の本でユークリッドの互除法と一緒に解説されていると思います.ちなみに,$ax+by=\gcd(a,b)$なる$x,y$の存在を示す際,与えられた$a,b$を予め最大公約数で割って考えれば,互いに素(i.e. $\gcd(a,b)=1$)な$a,b$に対し$ax+by=1$を満たす$x,y$の存在を示せば十分です.

Wikipediaの行列演算による解説がわかりやすいですね:https://ja.wikipedia.org/wiki/ユークリッドの互除法



本と同じようにx, yを変数にすると上手くいかないなぁ,と思っていましたが,次のようにすればよいのですね:Pythonで理解する蟻本「2-6 双六」(p.108) - クルトンのプログラミング教室

「拡張ユークリッドの互除法」に関するコードは以下(双六の問題は,出力方法を少し書き換えるだけなので略します).
【入力】
4 11

【出力】
4*3+11*-1=1

【コード】

a, b = map(int, input().split())

def extgcd(a, b):
  '''
  ax+by=gcd(a, b)となるx,yに更新
  返り値はgcd(a, b), x, y
  '''
  d = a
  if b!=0:
    # (1) by + (a%b)x = gcd(b, a%b) の解x, yに更新
    d, y, x = extgcd(b, a%b)
    # (1) <=> (2) ax + b(y - (a//b)x) = gcd(a, b)
    y -= (a//b) * x
  else:
    x, y = 1, 0
  return d, x, y

d, x, y = extgcd(a, b)
print('{}*{}+{}*{}={}'.format(a, x, b, y, d))

素数

素数判定

iの初期値が2であることに注意.

最後のreturn n != 1がスマートですね.$2, 3,... \leq\sqrt{n}$のどの自然数でも割り切れなかった$n$に対し,それが1ならFalse, 1以外ならTrueを返すことができます(本来は,2以上の値が入力されているかを,最初に判定すべきなのかもしれませんが).

def is_prime(n):
  '''
  素数ならTrue, そうでないならFalse
  '''
  i = 2
  while i**2 <= n:
    if n % i == 0:
      return False
    i += 1
  return n != 1

# --- テスト ---
test = [1, 2, 4, 53, 295927]
ans = [[x, is_prime(x)] for x in test]
print(ans)
# [[1, False], [2, True], [4, False], [53, True], [295927, False]]

約数の列挙

iの初期値が1であることに注意.

def divisor(n):
  i = 1
  res = []
  while i**2 <= n:
    if n % i == 0:
      res.append(i)
      if i != n//i:
        # √nより大きいものも追加
        res.append(n//i)
    i += 1
  res.sort()
  return res

# --- テスト ---
test = [1, 2, 4, 53, 295927]
ans = [[x, divisor(x)] for x in test]
print(ans)
# [[1, [1]], [2, [1, 2]], [4, [1, 2, 4]], [53, [1, 53]], [295927, [1, 541, 547, 295927]]]

素因数分解

collections.defaultdictを使いました.
8.3. collections --- コンテナデータ型 — Python 3.6.13 ドキュメント

iの初期値が2であることに注意.

①辞書的に表現すると良い点,②n //= i で2つのwhile文のnを書き換えることで簡単に書いている点は,なかなか思いつけなぁ,という感想です.

from collections import defaultdict

def prime_factor(n):
  i = 2
  # res[i]=約数iの個数
  res = defaultdict(int)
  while i**2 <= n:
    while n % i == 0:
      # i で割れる回数を記録
      res[i] += 1
      n //= i
    i += 1
  if n != 1:
    res[n] = 1
  return res

# --- テスト ---
test = [1, 2, 4, 53, 295927]
ans = [[x, sorted(prime_factor(x).items())] for x in test]
print(ans)
# [[1, []], [2, [(2, 1)]], [4, [(2, 2)]], [53, [(53, 1)]], [295927, [(541, 1), (547, 1)]]]

エラトステネスの篩(ふるい)

素数の個数

ふるいって,こんな漢字なんですね.読めない...
この方法は,小・中学生のころ「数の悪魔」という本で読んだ記憶があります.
数の悪魔―算数・数学が楽しくなる12夜

数の悪魔―算数・数学が楽しくなる12夜

def sieve(n):
  p = 0  # 素数の個数
  prime = ['']*n  # prime[i]=i番目の素数
  
  # 初期化 (0,1は素数でない)
  is_prime = [True]*(n+1)  # is_prime[0]~is_prime[n]まで必要
  is_prime[0] = is_prime[1] = False
  
  for i in range(2, n+1):
    # 調べていない最小のiは素数
    if is_prime[i]:
      p += 1
      prime[p] = i
      for j in range(2*i, n+1, i):
        # iの倍数は素数でない
        is_prime[j] = False
  return p, prime

# --- テスト ---
test = [11, 1000000]
ans = [[x, sieve(x)] for x in test]
print(ans)
# [[11, (5, ['', 2, 3, 5, 7, 11, '', '', '', '', ''])], [1000000, (78498, ['', 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, ...

区間内の素数の個数

iの倍数でa以上となる最小のもの = (a+i-1)//i * iとなることは,次の記事にまとめました.
床関数・天井関数と関係式 - Notes_JP

書籍中のコードでは,$[a,b)$のふるいで,max(2, )としている部分がありました.これがないと,$j=i$となるとき($a \leq i < \sqrt{b}$の場合に起きる)に素数$i$が除かれてしまいます.例えば$[2,5)$のときに2が素数から除かれます(i=2が一番外側のwhileで弾かれないb > 2**2の場合に,i=a=2でj=2が素数から除かれるというバグが出る).

import math

def segment_sieve(a, b):
  """
  入力は 2<=a < b
  """
  prime = ['']*(b-a)  # prime[i-a]=i番目の素数
  
  # 初期化
  ## [a,b)の表.(b-1)-a+1=b-a要素
  ## iが素数 <=> is_prime[i-a]=True
  is_prime = [True for _ in range(b-a)] 
  ## [0,√b)の表
  b2 = math.ceil(math.sqrt(b)-1)  # √b未満の最大の整数
  is_prime_small = [True for _ in range(b2+1)]
  
  i = 2
  while i**2 < b:
    # [0,√b)で調べていない最小のiは素数
    if is_prime_small[i]:
      
      j = 2*i
      while j**2 < b:
        # iの倍数は素数でない
        is_prime_small[j] = False
        j += i
        
      # iの倍数でa以上となる最小のもの = (a+i-1)//i * i
      # iが素数なので,素数でないjはiの2倍以上
      j = max(2, (a+i-1)//i) * i
      while j < b:
        # [0,√b)の素数の倍数を[a,b)から除く
        is_prime[j-a] = False
        j += i
    i += 1
  prime = [i+a for i in range(b-a) if is_prime[i]]
  return len(prime), prime

# --- テスト ---
test = [[2,5], [22, 37], [22801763489, 22801787297]]
ans = [[x, segment_sieve(*x)] for x in test]
print(ans)
# [[[2, 5], (2, [2, 3])], [[22, 37], (3, [23, 29, 31])], [[22801763489, 22801787297], (1000, [22801763489, 22801763513, 22801763527, ...

べき乗の高速計算

Carmichael Numbers

def mod_pow(x, n, mod):
  res = 1
  while n > 0:
    # n=0 なら1を返す
    if n & 1:
      # i(=0,1,...)番目のビットが1なら res * x^(2^i)
      res = res * x % mod
    x = x**2 % mod  # x = x^(2^(i+1))
    n >>= 1
  return res

# --- テスト ---
def is_prime(n):
  '''
  素数ならTrue, そうでないならFalse
  '''
  i = 2
  while i**2 <= n:
    if n % i == 0:
      return False
    i += 1
  return n != 1

def solve(n):
  if is_prime(n):
    return 'No ({}は素数)'.format(n)
    
  for x in range(2, n):
    res = mod_pow(x, n, n)
    if res != x:
      return 'No ({}^{}={} (mod {}))'.format(x, n, res, n)
  return 'Yes'
      
test = [17, 561, 4]
ans = [[n, solve(n)] for n in test]
print(ans)
# [[17, 'No (17は素数)'], [561, 'Yes'], [4, 'No (2^4=0 (mod 4))']]

mod_powとして,代わりに次を使うこともできます.

def mod_pow(x, n, mod):
  if n == 0:
    return 1
  res = mod_pow(x**2 % mod, n//2, mod)
  if n & 1:
    # nが奇数ならxを掛ける
    res = res * x % mod
  return res

Pythonで**とpowのどちらが速いかは,一概には言えないようです: