Python
scipy
sympy
数学
pandas

2015年センター試験数学IAを全てプログラム(Python)で解く

この記事はなんなの

  • 「センター試験程度であれば、数式と文章を愚直にプログラムに落としこむことさえできれば、昨今のツールを用いて、何も閃かずとも機械的に問題を解くことが出来る」ということの主張
  • 科学計算ライブラリ(特にSympy)の布教
  • 将来、働き先がなくなったとき、「私、私こういうことができるんです!!」って言えばどこかが拾ってくれないかなあ、という

使用するもの

  • Python (3系)
  • Scipy.org に載っている科学計算ライブラリ全て(タグが足りない!!)

共に、2015年6月現時点での最新版を使用します(特に、Scipyは今年1月に実装された最新版の機能を使用するので注意してください)。

数々のライブラリを一つ一つインストールするのはすごく面倒です。面倒なので、有名どころを固めたパッケージのようなものが複数存在します。
個人的にはいつもAnacondaを使ってまとめてインストールします。

問題

第1問:二次関数

平方完成?知らんなぁ・・・

二次関数
$$y = -x^2 + 2 x + 2 ・・・①$$
のグラフの頂点の座標は$([ア], [イ])$である。

頂点の座標はどうやって求めればいいのでしょうか・・・微分係数が0になる点を求めればいいはずです。

Scipyを用いて、微分係数と方程式の解を数値的に求めます:

from scipy.optimize import root
from scipy.misc import derivative

y = lambda x: -x**2 + 2*x + 2
dy = lambda x: derivative(y, x) # y'(derivative:微分)

# y' = 0 の解(root:根)を求める
# 解が一つしかないことは問題文からわかっている
root(dy, x0=0)
出力
 success: True
 message: 'The solution converged.'
  status: 1
     qtf: array([  4.36251035e-12])
    nfev: 4
       r: array([ 2.])
       x: array([ 1.])
    fjac: array([[-1.]])
     fun: array([  1.11022302e-16])

結果はx:の所を見ればよく、1になります。$y$座標はそのまま元の関数yに代入すればOK!:

y(1)
出力
3

よって $([ア], [イ]) = (1, 3)$


解答枠を使うのはずるくね?って思う人は第2問[2]を見てね

また
$$y = f(x)$$
は$x$の2次関数で、そのグラフは、①のグラフを$x$軸方向に$p$、$y$軸方向に$q$だけ平行移動したものであるとする。

(1)
$2 \leqq x \leqq 4$における$f(x)$の最大値が$f(2)$になるような$p$の値の範囲は
$$p[ウ(大小関係)][エ(値)]$$
であり、最小値が$f(2)$になるような$p$の値の範囲は
$$p[オ(大小関係)][カ(値)]$$
である。

問題文そのままに制約付き最大化(最小化)を行ってもいいのですが、それは第2問でやるので、ここではもっとせこい方法を使いましょう。

解答欄の形から、値が1桁の整数になることはわかるわけです・・・ということは、0から9まで全部試してしまえばいいのではないでしょうか。

ここで、Pythonで間違いなく最も使われている多次元配列ライブラリであるNumpyが登場します(実際には、Scipyの内部で使われているので既に登場していますが):

import numpy as np

f = lambda x, p, q: y(x-p) + q # 平行移動後の関数

# 2 <= x <= 4
# これが怪しいと思う場合はやはり制約付き最小化を行う
xs = np.linspace(2, 4, 51)

for p in range(10):
    maxval = np.max(f(xs, p, 0)) # 最大値

    # 最大値がf(2)になるか
    # abs(maxval - f(2, p, 0))等とする必要はない
    if maxval == f(2, p, 0):
        print("p = {} はOK".format(p))
出力
p = 0 はOK
p = 1 はOK

これより、$p[ウ][エ]$部分の答は$p \leqq 1$

$>$や$<$が答になることが無いということは流石に用いてもよいでしょうが、そこが気になる場合は整数以外も代入するようにすれば大丈夫です。

次もほぼ同じことをするだけです:

for p in range(10):
    minval = np.min(f(xs, p, 0))
    if minval == f(2, p, 0):
        print("p = {} はOK".format(p))
出力
p = 2 はOK
p = 3 はOK
p = 4 はOK
p = 5 はOK
p = 6 はOK
p = 7 はOK
p = 8 はOK
p = 9 はOK

よって、$p[オ][カ]$部分は$p \geqq 2$


いいから総当たりだ

(2)
2次不等式$f(x) > 0$の解が$-2 < x < 3$になるのは
$$p = \frac{[キク]}{[ケ]}, q = \frac{[コサ]}{[シ]}$$
のときである。

さて、[キク]及び[コサ]に入るのは-9~99であり、[ケ]及び[シ]に入るのは1~9です・・・ということは、やっぱり総当たりすればいいのではないでしょうか。

このような分数の組は100万通りぐらいありますが、きっと大丈夫でしょう。

以下では、簡単のために、不等号は等号つきのものにしています(等号をつけないようにも出来ますが、処理がわかり辛くなる上に計算時間が伸びます)。

from itertools import product
from fractions import Fraction

kiku = range(-9, 100) # キクの候補
ke = range(1, 10) # ケの候補
kosa = range(-9, 100) # コサの候補
shi = range(1, 10) # シの候補
# product:積(この場合は直積)
num_product = product(kiku, ke, kosa, shi)
# 分数の組を順に生成するジェネレータ
# Fraction:分数
fracs_pair = ( (Fraction(kiku, ke), Fraction(kosa, shi))
               for kiku, ke, kosa, shi in num_product)

xs = np.linspace(-2, 3, 51)
for p, q in fracs_pair:
    cond1 = all(f(xs, p, q) >= 0) # f(x) >= 0
    cond2 = f(-2, p, q) == 0 # f(-2) = 0 (f(x) >= 0 だけでは不十分)
    cond3 = f(3, p, q) == 0 # f(3) = 0 (f(x) >= 0 だけでは不十分)
    if cond1 and cond2 and cond3:
        print("p = {}, q = {}".format(p, q))
        break
出力(約1分後)
p = -1/2, q = 13/4

パソコンは最強なんだ。

第2問:論理と集合/平面図形

正直、これを書くために初めてロジックモジュール使いました

[1]

(1)
命題「$(p_1かつp_2) \Rightarrow (q_1かつq_2)$」の対偶は[ア]である。

選択肢:
⓪$(\overline{p_1}または\overline{p_2})\Rightarrow(\overline{q_1}または\overline{q_2})$
①$(\overline{q_1}または\overline{q_2})\Rightarrow(\overline{p_1}または\overline{p_2})$
②$(\overline{q_1}かつ\overline{q_2})\Rightarrow(\overline{p_1}かつ\overline{p_2})$
③$(\overline{p_1}かつ\overline{p_2})\Rightarrow(\overline{q_1}かつ\overline{q_2})$

ここでは記号計算ライブラリ(数式処理システムとも言う)を使います。馴染みのない方は、そのまま読み進めて貰えばどういうものかが分かると思います。

PythonにはSympyという記号計算ライブラリが存在するので、これを使うことにしましょう。

さて、対偶とは、(元の命題の結論の否定)⇒(元の命題の仮定の否定)としたものです。Sympyを使えば、それをそのまま行うことができます:

import sympy as sy
from sympy.logic import simplify_logic

# Ipython Notebook (Jupyter) 上だとMathjaxを用いた出力が得られる
sy.init_printing(use_latex="mathjax")

p1, p2, q1, q2 = sy.symbols("p1 p2 q1 q2")
assum = (p1 & p2) # 元の命題の仮定
conc = (q1 & q2) # 元の命題の結論
print("対偶の仮定:", simplify_logic(~conc))
print("対偶の結論:", simplify_logic(~assum))
出力
対偶の仮定: Or(Not(q1), Not(q2))
対偶の結論: Or(Not(p1), Not(p2))

少しわかり辛いですが、よく見るとこれは①そのものです。

なお、Ipython Notebook (Jupyter) を用いると、次のように、MathJaxを用いた綺麗な出力が得られます:

# A implies B: AならばB
sy.Implies(simplify_logic(~conc), simplify_logic(~assum))

MathJaxによる出力:
$$\left(\neg q_{1} \vee \neg q_{2}\right) \Rightarrow \left(\neg p_{1} \vee \neg p_{2}\right)$$

高尚な表記が用いられていますが、①と意味するところは全く同じです。


もう全部総当たりでいいんじゃないかな

(2)
自然数$n$に対する条件$p_1, p_2, q_1, q_2$を次のように定める:
$p_1: n$は素数である
$p_2: n + 2$は素数である
$q_1: n + 1$は5の倍数である
$q_2: n + 1$は6の倍数である

30以下の自然数$n$のなかで[イ]と[ウエ]は
命題「$(p_1かつp_2)\Rightarrow(\overline{q_1}かつq_2)$」
の反例となる。

ここもゴリ押ししましょう。30以下の全ての自然数について、全ての条件をチェックしてしまえばいいでしょう:

for n in range(1, 31):
    # nが素数かつn+2が素数で
    # sieve:ふるい(素数判定法である「エラトステネスのふるい」より)
    if (n in sy.sieve) and (n + 2 in sy.sieve):
        # 「n+1が5の倍数ではなく、6の倍数である」を満たさなければ
        if not ( ((n + 1) % 5 != 0) and ((n + 1) % 6 == 0) ):
            print("n = {} は反例".format(n))
出力
n = 3 は反例
n = 29 は反例

正弦・・・定理・・・?

[2]
△ABCにおいて、AB = 3, BC = 5, ∠ABC = 120°とする。
このとき、
$$ {\rm AC} = [オ], \sin ∠{\rm ABC} = \frac{\sqrt{[カ]}}{[キ]}$$
であり、
$$ \sin ∠{\rm BCA} = \frac{[ク] \sqrt{[ケ]}}{[コサ]}$$
である。

図形の問題はそのままだとプログラムに落とし込み辛いので、ベクトル(代数ベクトル)として解いてしまうのが得策です。

さて、点A, B, Cは条件さえ満たせばどこに置いてもいいのですが、次のように位置を決めると楽です:

${\rm A} = (3 \cos \frac{2}{3} \pi, 3 \sin \frac{2}{3} \pi)$
${\rm B} = (0, 0)$
${\rm C} = (5, 0)$

A = 3*sy.Matrix([sy.cos(2*sy.pi/3), sy.sin(2*sy.pi/3)])
B = sy.Matrix([0, 0])
C = sy.Matrix([5, 0])

こうするとプログラムが幾分綺麗になるというだけで、別にこのように「賢く」決める必要はありません。また、点Aの座標を決めるのに「公式」を使うのはずるい!と思う場合、後にやるように、内積と角度の条件から座標を決めることになります。

さて、辺の長さは普通に$\sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}$で計算することができます・・・が、辺の長さというのは、高尚な言葉を使うと、2点の間を結ぶベクトルの2-ノルムというものになります:

(A - C).norm() # 2-ノルム(辺の長さ)を求める
出力
7

これが辺ACの長さ、つまり[オ]になります。

次にsin∠ABCですが、これはどうやって求めるのでしょうか。

愚直に考えれば、∠ABCを求め、そのsinを計算すればいいでしょう。では、その∠ABCはどうやって求めるのでしょうか。

ベクトルx, y間の角度θというものは次のように定義されます:

$$\cos \theta = \frac{x \cdot y}{|x||y|} を満たす\theta$$

次の式は全く同じ意味です:

$$\theta = \arccos \frac{x \cdot y}{|x||y|}$$

これは「公式」ではなく、「角度θというのはこういうもののことである、という決まり」 - つまり、「定義」になります。

(高校数学で学ぶように幾何学的には角度を元に内積が決まりますが、代数学的には逆に内積を元に角度が決まります。)

実際にやってみましょう:

BA = A - B
BC = C - B
cosABC = BA.dot(BC)/(BA.norm()*BC.norm()) # 内積からcos∠ABCを求める
angle_ABC = sy.acos(cosABC) # acos(=arccos)はcosθから"逆に"θを求める
sinABC = sy.sin(angle_ABC)
sinABC
出力
sqrt(3)/2

[MathJax]
$$\frac{\sqrt{3}}{2}$$

sin∠BCAも同様です:

CA = A - C
CB = B - C
cosBCA = CA.dot(CB)/(CA.norm()*CB.norm()) # 内積からcos∠BCAを求める
angle_BCA = sy.acos(cosBCA) # acos(=arccos)はcosθから"逆に"θを求める
sinBCA = sy.sin(angle_BCA)
sinBCA
出力
3*sqrt(3)/14

[MathJax]
$$\frac{3 \sqrt{3}}{14}$$


長いだけの機械的作業

直線BC上に点Dを、AD = $3 \sqrt{3}$かつ∠ADCが鋭角、となるようにとる。点Pを線分BD上の点とし、△APCの外接円の半径をRとすると、Rのとり得る値の範囲は$\frac{[シ]}{[ス]} \leqq R \leqq [セ]$である。

点Pが線分BD間の範囲にある、と言っているわけですが、点Dがどこにあるのかわからないので、まずはこれを求めましょう。

今、直線BCはx軸そのものなので、その上にある点Dは$(d_x, 0)$とおくことが出来ます。この$d_x$の解を条件から求めればいいのです:

# 使用する変数の定義
d_x = sy.symbols("d_x", real=True)

# 点Dは直線BC上、つまりx軸上にあるのでy=0だが、xは不明なのでd_xとおく
D = sy.Matrix([d_x, 0])
AD = D - A

# (ADの長さ) = 3√3 の解d_xを求める
# sy.solve関数の中身は、移行して = 0 の形にしておく
sy.solve(AD.norm() - 3*sy.sqrt(3))
[-6, 3]

$d_x$の解が2つ出てきましたが、∠ADCが鋭角となるのは$d_x = -6$の場合だけです(cos∠ADCを求めて確認したほうがいいのでしょうが、ここでは省略します)。

これより、D = (-6, 0)が求まりました。

次に、△APCの外接円の式を求めなければなりません。

高校で学ぶように、円の方程式は次のようになります:
$$(x - x_0)^2 + (y - y_0)^2 - R^2 = 0$$

これが点A, P, Cを通るように$x_0, y_0, R$を求めればいいわけです:

# 使用する変数の定義
x, y, x_0, y_0, p_x, R = sy.symbols("x y x_0 y_0 p_x R", real=True)

P = sy.Matrix([p_x, 0]) # 点Pもx軸上にある

# circumcircle:外接円
circum_APC = (x-x_0)**2 + (y-y_0)**2 - R**2
# substitute:代入する
c1 = circum_APC.subs({x:A[0], y:A[1]}) # 点Aを通る
c2 = circum_APC.subs({x:P[0], y:P[1]}) # 点Pを通る
c3 = circum_APC.subs({x:C[0], y:C[1]}) # 点Cを通る
sol = sy.solve({c1, c2, c3}, {x_0, y_0, R}) # 3つの条件から円の式を解く
sol
出力
[{R: -7*sqrt(3*p_x**2 + 9*p_x + 27)/9, y_0: sqrt(3)*(13*p_x + 33)/18, x_0: p_x/2 + 5/2}, {R: 7*sqrt(3*p_x**2 + 9*p_x + 27)/9, y_0: sqrt(3)*(13*p_x + 33)/18, x_0: p_x/2 + 5/2}]

[MathJax]
$$\left [ \left \{ R : - \frac{7}{9} \sqrt{3 p_{x}^{2} + 9 p_{x} + 27}, \quad x_{0} : \frac{p_{x}}{2} + \frac{5}{2}, \quad y_{0} : \frac{\sqrt{3}}{18} \left(13 p_{x} + 33\right)\right \}, \quad \left \{ R : \frac{7}{9} \sqrt{3 p_{x}^{2} + 9 p_{x} + 27}, \quad x_{0} : \frac{p_{x}}{2} + \frac{5}{2}, \quad y_{0} : \frac{\sqrt{3}}{18} \left(13 p_{x} + 33\right)\right \}\right ]$$

問題は、解が2組出てきているところです。これは元の式中に$R$ではなく$R^2$を使っているからですが、当然、値が正になる方のみが求める解です:

R_sol = sol[1][R]
R_sol
出力
7*sqrt(3*p_x**2 + 9*p_x + 27)/9

[MathJax]
$$\frac{7}{9} \sqrt{3 p_{x}^{2} + 9 p_{x} + 27}$$

さて、これでRがp_x、つまり点Pの座標の関数として求まりました。点Pは線分BD上の点であり、点B、点Dの座標はそれぞれ(0, 0), (-6, 0)でした。つまり、この問題は

$$-6 \leqq p_x \leqq 0 \ のとき、上式の最小値と最大値を求めよ$$

という問題になります。

現状、記号計算を行うSympyには最小値・最大値を求める機能はありませんが、数値計算において、関数最小化[最大化]というのは非常にポピュラーな分野です。

今回の場合、制約付き最小化[最大化]ということを行います:

# differential evolution は大域(global)最小化アルゴリズムの一つ
# Scipyにおける、定義域を指定した大域最小化が行える唯一の方法
# 今回の場合、大域でやる必要はなく、局所的(local)なものでもいい
from scipy.optimize import differential_evolution

# 記号的に表されたR_solを、p_xを変数(引数)とする数値的なものに変換
numeric_R = sy.lambdify(p_x, R_sol)

# -6 <= p_x <= 0 の範囲で最小化の実行。
differential_evolution(numeric_R, [(-6, 0)])
出力
 message: 'Optimization terminated successfully.'
     nit: 3
     fun: 3.5000000000000053
       x: array([-1.50000014])
 success: True
     jac: array([ -1.33226763e-07])
    nfev: 66

fun:の部分が求める最小値です。3.5000000000000053という数値解が得られ、これが分数でいくつになるのかは明らかですが、一応、次のようにして最適な分数の近似値を求めます:

# 分母が9以下の最適な分数を求める
Fraction(3.5000000000000053).limit_denominator(9)
出力
Fraction(7, 2)

よって$\frac{[シ]}{[ス]} = \frac{7}{2}$

次に最大値ですが、一般的に、最大化は「元の関数の符号を逆にしたものを最小化する」という方法で行います:

# 符号を逆転した関数を準備する
minus_numeric_R = lambda p_x: -numeric_R(p_x)

# 最小化
differential_evolution(minus_numeric_R, [(-6, 0)])
 message: 'Optimization terminated successfully.'
     nit: 9
     fun: -7.0
       x: array([-6.])
 success: True
     jac: array([ 1.16666667])
    nfev: 154

-7.0という答が得られました。符号を逆転させたので、[セ] = 7です。

第3問:データの分析

実際のデータ値が欲しい

[1]
ある高校3年生1クラスの生徒40人について、ハンドボール投げの飛距離のデータを取った。次の図1は、このクラスで最初にとったデータのヒストグラムである。

hist.PNG

(1)
この40人のデータの第3四分位数が含まれる階級は、[ア]である。
選択肢:
⓪5m以上10m未満 ①10m以上15m未満 ②15m以上20m未満
③20m以上25m未満 ④25m以上30m未満 ⑤30m以上35m未満
⑥35m以上40m未満 ⑦40m以上45m未満 ⑧45m以上50m未満

Pythonにおける統計処理にはPandasを使用します。

飛距離の正確な値が不明ですが、各階級の中間の値を取ることとしましょう。第3四分位数とは、この場合30番目と31番目のデータの平均になります:

from pandas import Series

data = [7.5] * 1
data += [12.5] * 4
data += [17.5] * 6
data += [22.5] * 11
data += [27.5] * 9
data += [32.5] * 4
data += [37.5] * 3
data += [42.5] * 1
data += [47.5] * 1
data = Series(data)
(data[29] + data[30])/2
出力
27.5

箱ひげ図って現実に使われているんでしょうか? ←普通に使われてるだろいい加減にしろ

(2)
このデータを箱ひげ図にまとめたとき、図1のヒストグラムと矛盾するものは、[イ]、[ウ]、[エ]、[オ]である。

box_center.PNG

Pandasを用いて(内部的にはMatplotlibを用いて)箱ひげ図を実際に生成し、各線の位置を見比べ、いずれかの線が異なった階級に属するものを見つけます:

# boxplot: 箱ひげ図
# whis=np.infは、最後のデータをエラーではなく「適切な値」として扱うために必要
data.plot(kind="box", vert=False, whis=np.inf)

[出力]
box.png

この図より、第1~第4四分位数はそれぞれ5~10m, 15~20m, 25~30m, 45~50mの階級にあり、また、中央値は20~25mの階級になければなりません。

そのようになっていないものは、⓪、②、③、⑤になります。


全て解くと約束したな、あれは嘘だ。

(3)
後日、このクラスでハンドボール投げの記録を取りなおした。次に示したA~Dは、最初にとった記録から今回の記録への変化の分析結果を記述したものである。a~dの各々が今回取りなおしたデータの箱ひげ図となる場合に、⓪~③の組み合わせのうち分析結果と箱ひげ図が矛盾するものは、[カ]、[キ]である。

選択肢:
⓪ A-a
① B-b
② C-c
③ D-d

A : どの生徒の記録も下がった。
B : どの生徒の記録も伸びた。
C : 最初にとったデータで上位1/3に入るすべての生徒の記録が伸びた。
D : 最初にとったデータで上位1/3に入るすべての生徒の記録は伸び、下位1/3に入るすべての生徒の記録は下がった

box2.PNG

これ具体的な数値が何も与えられてないから相当汚い方法を使わない限り無理ではないかと・・・
ランダムなデータを多数生成して確認、って方法しか思い浮かばないけどそれだと超汚いコードが必要に・・・


いい問題だと思うんだけど、ここで見る分には最高につまらない

[2]
ある高校2年生40人のクラスで一人2回ずつハンドボール投げの飛距離のデータを取ることにした。次の図2は、1回目のデータを横軸に、2回目のデータを縦軸に取った散布図である。なお、一人の生徒が欠席したため、39人のデータとなっている。

table.PNG

1回目のデータと2回目のデータの相関係数に最も近い値は、[ク]である。

選択肢:
⓪0.67 ①0.71 ②0.75 ③0.79 ④0.83
⑤0.87 ⑥0.91 ⑦0.95 ⑧0.99 ⑨1.03

定義通りに計算するしかないでしょう:

# 相関係数 = (共分散)/(片方の標準偏差)*(もう片方の標準偏差)
54.30/(8.21*6.98)
出力
0.9475480666878394

よって⑦

第4問:場合の数と確率

すごく・・・簡単です・・・

赤色、緑色、青色のペンキを用いて、5つの横並びの正方形を、隣り合う正方形同士が異なる色となるように塗り分ける。ただし、塗り分ける際には、3色のペンキをすべて使わなければならないわけではなく、2色のペンキだけで塗り分けることがあっても良いものとする。

(1) このような塗り方は、全部で[アイ]通りある。

(2) 塗り方が左右対称となるのは、[ウエ]通りある。

(3) 青色と緑色の2色だけで塗り分けるのは、[オ]通りある。

(4) 赤色に塗られる正方形が3枚であるのは、[カ]通りある。

(5) 赤色に塗られる正方形が1枚である場合について考える。
・どちらかの端の1枚が赤色に塗られるのは、[キ]通りある。
・端以外の1枚が赤色に塗られるのは、[クケ]通りある。
よって、赤色に塗られる正方形が1枚であるのは、[コサ]通りある。

(6) 赤色に塗られる正方形が2枚であるのは、[シス]通りある。

全ての塗り方について、一つ一つ条件をチェックするだけです:

rgb = ("r", "g", "b") # 赤、緑、青
patterns = product(rgb, repeat=5) # 全ての塗り方

def check1(pattern): # (1)
    # 全ての正方形が同じ色になってはいけない
    if not all(color == pattern[0] for color in pattern):
        # 隣り合う色は異なる
        if all(pattern[i] != pattern[i+1] for i in range(4)):
            return True
    return False

def check2(pattern): # (2)
    # 左右対称
    if pattern[0] == pattern[4] and pattern[1] == pattern[3]:
        return True
    return False

def check3(pattern): # (3)
    # 赤は使わず、緑と青は使う 
    if "r" not in pattern and "g" in pattern and "b" in pattern:
        return True
    return False

def check4(pattern): # (4)
    # 赤が3枚
    if pattern.count("r") == 3:
        return True
    return False

def check5(pattern): # (5)
    # 赤が1枚
    if pattern.count("r") == 1:
        return True
    return False

def check5_sub1(pattern): # check5_sub2は存在しない
    # どちらかの端が赤
    # この関数はcheck5の後に来るので、両方が赤である可能性はない
    if pattern[0] == "r" or pattern[4] == "r":
        return True
    return False

def check6(pattern): # (6)
    # 赤が2枚
    if pattern.count("r") == 2:
        return True

n1 = 0
n2 = 0
n3 = 0
n4 = 0
n5 = 0
n5_sub1 = 0
n6 = 0

for pattern in patterns:
    if check1(pattern):
        n1 += 1
        if check2(pattern):
            n2 += 1
        if check3(pattern):
            n3 += 1
        if check4(pattern):
            n4 += 1
        if check5(pattern):
            n5 += 1
            if check5_sub1(pattern):
                n5_sub1 += 1
        if check6(pattern):
            n6 += 1

# [キ]と[クケ]よりも[コサ]が先に求まる
# よって、[クケ]は[コサ] - [キ]として求めれば良い        
n5_sub2 = n5 - n5_sub1
print(n1, n2, n3, n4, n5_sub1, n5_sub2, n5, n6)
出力
48 12 2 4 4 12 16 26

これより、
[アイ] = 48
[ウエ] = 12
[オ] = 2
[カ] = 4
[キ] = 4
[クケ] = 12
[コサ] = 16
[シス] = 26

第5問:整数問題

そもそも素因数分解って人間がやる作業じゃないよね

以下、$a = 756$とし、$m$は自然数とする。

(1)
$a$を素因数分解すると
$$a = 2^{[ア]} \cdot 3^{[イ]} \cdot [ウ]$$
である。
$a$の正の約数の個数は[エオ]個である。

そのものずばりな関数が存在します:

from sympy.ntheory import factorint, divisor_count
a = 756
print(factorint(a)) # 素因数分解(factorization:因数分解)
print(divisor_count(a)) # 約数を数える(divisor:約数)
出力
{2: 2, 3: 3, 7: 1}
24

前者は$2^2 \cdot 3^3 \cdot 7^1$を意味します。

よって、
[ア] = 2, [イ] = 3, [ウ] = 7
[エオ] = 24


この記事の半分はゴリ押しでできています

(2)
$\sqrt{am}$が自然数となる最小の自然数$m$は[カキ]である。
$\sqrt{am}$が自然数となる時、$m$はある自然数$k$により、$m = [カキ]k^2$と表される数であり、その時の$\sqrt{am}$の値は$[クケコ]k$である。

単に、1から99まで(解答欄の形を使うならば10から99まで)の全てを代入してみればいいのです:

for m in range(10, 100):
    # 整数ならば(integer:整数)
    if np.sqrt(a*m).is_integer():
        print(m)
        break
出力
21

よって、[カキ] = 21

[カキ]さえ求まれば、[クケコ]はほとんど自明なのですが、一応やっておくと:

# kをわざわざ使用する意味はほぼない
k = sy.symbols("k", integer=True, positive=True)
sy.sqrt(a*m*k**2)
出力
126*k

よって、[クケコ] = 126


専用の関数を引っ張りだしてこないといけないのがネック

(3)
次に、自然数$k$により$[クケコ]k$と表される数で、11で割った余りが1となる最小の$k$を求める。1次不定方程式
$$[クケコ]k - 11l = 1$$
を解くと、$k > 0$となる整数解$(k, l)$のうち$k$が最小のものは、
$k = [サ], l = [シスセ]$である。

a, b, cを整数としたax + by = cの形の方程式のことを(1次の)ディオファントス方程式といいます。 係数・定数部分を整数に限定した多項式による方程式のことをディオファントス方程式といいます:

from sympy.solvers.diophantine import diophantine
l = sy.symbols("l", integer=True)
diophantine(126*k - 11*l - 1)
出力
{(-11*t - 2, -126*t - 23)}

[MathJax]
$$\left\{\left ( - 11 t - 2, \quad - 126 t - 23\right )\right\}$$

これは$k = -11t -2, l = -126t - 23$を意味します。ここで$t$は整数です。

この時、$k > 0$となる最小の$k$は明らかに(明らかでない場合は逐一代入して求めて)$t = -1$の時で、その値は[サ] = 9、
また、$l = [シスセ] = 103$になります。


これが最後の総当たりだ

(4)
$\sqrt{am}$が11で割ると1余る自然数となる時、そのような自然数$m$のなかで最小のものは[ソタチツ]である。

$m$の候補は1000~9999です。やはり、全て試せばいいのです

for m in range(1000, 10000):
    val = np.sqrt(a*m)
    # 整数かつ11で割ると1余るならば
    if val.is_integer() and val % 11 == 1:
        print(m)
        break
出力
1701

よって[ソタチツ] = 1701

第6問:平面図形

ひらめきなんていらない

△ABCにおいて、AB = AC = 5、BC = $\sqrt{5}$とする。辺AC上に点DをAD = 3となるようにとり、辺BCのBの側の延長と△ABDの外接円との交点でBと異なるものをEとする。

CD・CB = [アイ]であるから、BE = $\sqrt{[ウ]}$である。

△ACEの重心をGとすると、AG = $\frac{[エオ]}{[カ]}$である。

ABとDEの交点をPとすると
$\frac{{\rm DP}}{{\rm EP}} = \frac{[キ]}{[ク]}$
である。

やはり座標平面と代数ベクトルを使って解いていきましょう。

A = (0, 0)、C = (0, 5)とおけるわけですが(そのようにおく必要があるわけではない)、CE・CBを求めるにはさらにEとBの座標が必要です。

EはBCの延長にあるので、まずはBを求めましょう:

# 使用する変数の定義
b_x, b_y = sy.symbols("b_x b_y", real=True)
A = sy.Matrix([0, 0])
B = sy.Matrix([b_x, b_y])
C = sy.Matrix([0, 5])
D = sy.Matrix([0, 3]) # A, C をy軸上に取るとこのようにおけるので楽
# AB = 5, AC = 5(点の置き方から自明), BC = √5 の条件を使ってBの座標を求める
B_sol = sy.solve({(A - B).norm() - 5, (A - C).norm() - 5, (B - C).norm() - sy.sqrt(5)}, {b_x, b_y})
B_sol
出力
[{b_y: 9/2, b_x: -sqrt(19)/2}, {b_y: 9/2, b_x: sqrt(19)/2}]

[MathJax]
$$\left [ \left \{ b_{x} : - \frac{\sqrt{19}}{2}, \quad b_{y} : \frac{9}{2}\right \}, \quad \left \{ b_{x} : \frac{\sqrt{19}}{2}, \quad b_{y} : \frac{9}{2}\right \}\right ]$$

解が2つ出てきましたが、別にBの位置は辺ACに対して右側でも左側でもいいはずです。

普通はABCを左回りに取るので、そうなるように右側のものを選んでおきましょう:

B = B.subs(B_sol[1])
B
出力
Matrix([
[sqrt(19)/2],
[       9/2]])

[MathJax]
$$\left[\begin{matrix}\frac{\sqrt{19}}{2}\\ \frac{9}{2}\end{matrix}\right]$$

次に、第2問(2)でやったのと同じように△ABDの外接円の式を求めます:

# 第2問(2)とやっていることは全く同じ
circum_ABD = (x-x_0)**2 + (y-y_0)**2 - R**2
c1 = circum_ABD.subs({x:A[0], y:A[1]})
c2 = circum_ABD.subs({x:B[0], y:B[1]})
c3 = circum_ABD.subs({x:D[0], y:D[1]})
circum_ABD_sol = sy.solve({c1, c2, c3}, {x_0, y_0, R})
circum_ABD_sol
出力
[{y_0: 3/2, x_0: 23*sqrt(19)/38, R: -5*sqrt(133)/19},
 {y_0: 3/2, x_0: 23*sqrt(19)/38, R: 5*sqrt(133)/19}]

[MathJax]
$$\left [ \left \{ R : - \frac{5 \sqrt{133}}{19}, \quad x_{0} : \frac{23 \sqrt{19}}{38}, \quad y_{0} : \frac{3}{2}\right \}, \quad \left \{ R : \frac{5 \sqrt{133}}{19}, \quad x_{0} : \frac{23 \sqrt{19}}{38}, \quad y_{0} : \frac{3}{2}\right \}\right ]$$

やはりRが正なものを選びましょう。求める外接円(= 0 としたときの左辺)は次のようになります:

circum_ABD = circum_ABD.subs(circum_ABD_sol[1])
circum_ABD
出力
(x - 23*sqrt(19)/38)**2 + (y - 3/2)**2 - 175/19

[MathJax]
$$\left(x - \frac{23 \sqrt{19}}{38}\right)^{2} + \left(y - \frac{3}{2}\right)^{2} - \frac{175}{19}$$

さて、直線BC上の点Eの位置は$k$を媒介変数として
$$ \vec{{\rm OE}} = \vec{{\rm OB}} + k \cdot \vec{{\rm BC}} $$
として表すことが出来ます:

k = sy.symbols("k", real=True)
E = B + k*(C - B) # 原点は(0, 0)なので数式に含む必要はない
E
出力
Matrix([
[-sqrt(19)*k/2 + sqrt(19)/2],
[                 k/2 + 9/2]])

[MathJax]
$$\left[\begin{matrix}- \frac{\sqrt{19} k}{2} + \frac{\sqrt{19}}{2}\\ \frac{k}{2} + \frac{9}{2}\end{matrix}\right]$$

そして、この点が先の△ABDの外接円上に存在する、という条件を用いて$k$が求まります:

sy.solve(circum_ABD.subs({x:E[0], y:E[1]}))
出力
[-1, 0]

解が2つ出てきましたが、$k = 0$は明らかにBそのものなので、$k = -1$が適する解です。これより、点Eの座標が求まります:

E = E.subs(k, -1)
E
出力
Matrix([
[sqrt(19)],
[       4]])

[MathJax]
$$\left[\begin{matrix}\sqrt{19}\\4\end{matrix}\right]$$

これより、点A, B, C, Eの位置が全て求まったため、CE・CB、そしてBEが求まります:

# CD・CB
(E - C).norm() * (B - C).norm()
出力
10

よってCE・CB = [アイ] = 10

# BE
(E - B).norm()
出力
sqrt(5)

[MathJax]
$$\sqrt {5}$$

よってBE = $\sqrt{5}$


重み無し平均

△ACEの重心をGとすると、AG = $\frac{[エオ]}{[カ]}$である。

重心の座標とは、(点に重みがついていなければ)頂点の座標の「平均」のことです:

G = (A + C + E)/3
(G - A).norm()
出力
10/3

よって$\frac{[エオ]}{[カ]} = \frac{10}{3}$


メネラウスの定理とか、現役時でさえ「だからなんなの」って思ってました。

ABとDEの交点をPとすると
$$\frac{{\rm DP}}{{\rm EP}} = \frac{[キ]}{[ク]} ・・・①$$
である。

直線ABと直線DEをそれぞれAB, DEの延長として表し、その交点を求めるだけです:

l = sy.symbols("l") # 媒介変数
p_x, p_y = sy.symbols("p_x p_y")
P = sy.Matrix([p_x, p_y])
line_AB = A + k*(B - A) # 直線AB
line_DE = E + l*(E - D) # 直線DE
k_l_P_sol = sy.solve([line_AB - line_DE, line_AB - P])
k_l_P_sol
出力
[{p_x: 3*sqrt(19)/8, k: 3/4, p_y: 27/8, l: -5/8}]

[MathJax]
$$\left [ \left \{ k : \frac{3}{4}, \quad l : - \frac{5}{8}, \quad p_{x} : \frac{3 \sqrt{19}}{8}, \quad p_{y} : \frac{27}{8}\right \}\right ]$$

この$p_x, p_y$がPの座標です:

P = P.subs(k_l_P_sol[0])
P
出力
Matrix([
[3*sqrt(19)/8],
[        27/8]])

[MathJax]
$$\left[\begin{matrix}\frac{3 \sqrt{19}}{8}\\ \frac{27}{8}\end{matrix}\right]$$

これにて点D, E, Pの座標が分かったため、$\frac{{\rm DP}}{{\rm EP}}$を計算することが出来ます:

(P - D).norm()/(P - E).norm()
出力
3/5

したがって、$\frac{[キ]}{[ク]} = \frac{3}{5}$


役に立たない導入

△ABCと△EDCにおいて、点A, B, D, Eは同一円周上にあるので∠CAB = ∠CEDで、∠Cは共通であるから
$${\rm DE} = [ケ]\sqrt{[コ]} ・・・②$$
である。

①, ②から、EP = $\frac{[サ] \sqrt{[シ]}}{[ス]}$である。

導入は無視します。点E, D, Pの座標は全て分かっているのですから、DEとEPの長さは簡単に求まるはずです:

# DE
(E - D).norm()
出力
2*sqrt(5)

[MathJax]
$$2 \sqrt{5}$$

# PE
(P - E).norm()
出力
5*sqrt(5)/4

[MathJax]
$$\frac{5 \sqrt{5}}{4}$$

これより、
$${\rm DE} = [ケ]\sqrt{[コ]} = 2\sqrt{5}$$
$${\rm EP} = \frac{[サ] \sqrt{[シ]}}{[ス]} = \frac{5 \sqrt{5}}{4}$$
である。

手で解いたほうが100倍はえーよ

手で解いた方が圧倒的に速い、というのは全く持ってその通りです。なぜならば、試験の問題というのは手で解けるような絶妙な調整がなされているからです。

人為的な調整のない現実の問題に対して、手計算というものは基本的に無力です。具体的には、例えば、第2問の[2]において、ABの長さが3ではなく4になっただけで手計算は不可能になります。

どうも、皆「自分の手を動かして作業をすること」を重要視しすぎているような気がするのです。しかし、手計算で解けるように調整された試験を高速にこなせた所で何が嬉しいのでしょうか。

私はこの風潮が学校教育の場だけのものだと思っていましたが、そんなことはないようなのです。(おそらく、上に立つものがそのような教育を受けてしまった結果として)最先端の研究分野でさえこのような風潮は存在し、専用ソフトや大規模マシンで一斉に作業をするなどけしからず、自分の手で一つ一つ処理しなければならない、といった雰囲気を持った場は確かに存在するのです(最初に何をやっているのか理解するために実際に手を動かしてみる、という話ではありません)。

私は、世間一般には一流と呼ばれる(国内の)大学に在籍しましたが、物理数学の授業を受けた時、「Mathematicaなんて使ってはいけない」ということを教えられました。これに対し、留学し国外で教育を受けた時、「自分で計算するやつは馬鹿」と言い切られたのをよく覚えています。

どちらかが100%正しいということはないはずです。しかし、「この意識の差じゃ外国には勝てんだろうなあ」ということだけは言えると思うのです。


読んでくれてありがとうございました。
「こうしたほうが良くない?」「ここおかしくない?」等といったご意見ありましたらお待ちしております。