summer_tree_home

Check iOでPython3をマスターするぜっ

Three Points Circle (Electronic Station) - 3点を通る円の方程式を求める

どんな問題?

Three Points Circle
http://www.checkio.org/mission/three-points-circle/

3点を通る円の方程式を求めよ。

ただし、中心が(a,b)、半径rの円の方程式は以下の通り。
(x-a)^2+(y-b)^2=r^2

その他の条件

  • 3点は一直線上に無いものとする。
  • x,y,r < 10 とする。(※)
  • 引数の3点の座標は "(2,2),(4,2),(2,4)" のような文字列で与えられる。
  • 戻り値の方程式は "(x-4)^2+(y-4)^2=2.83^2" のような文字列で返す。
  • 数字の余分なゼロや小数点は除去せよ。

問題文には書かれていないが、例を見る限り、数字は小数点2桁に丸めるようだ。余分なゼロや小数点は除去、というのは、3.0 や 3.00 は 3 に直せ、ということだろう。

(※ 今のところは x,y,r < 10 の場合だけらしいが、いずれテスト項目をもっと増やすらしい。)

例:

checkio("(2,2),(4,2),(2,4)") == "(x-4)^2+(y-4)^2=2.83^2"
checkio("(3,7),(6,9),(9,7)") == "(x-6)^2+(y-5.75)^2=3.25^2"


ところで、問題文に出てくる Cartesianって何だろうって思って調べたら、デカルトのことらしい。
(Cartesian coordinate system でデカルト座標系)
デカルト座標系って何だっけと思って調べたら、単なる直交座標系だった。(よく見るX軸とY軸の座標)

どうやって解く?

いや、これPythonというより数学の問題やないか?
流れとしては、

  1. 文字列から3点の座標を得る。'(2,2),(6,2),(2,6)' → (x1,y1),(x2,y2),(x3,y3)
  2. 3点から円の中心と半径を求める。
  3. 方程式(文字列)を作成して返す。

という3ステップになるだろう。2は数学の問題だから、あとでググろう。自分で解く気なし(笑)
3はformatで数字を埋め込めばいいとして、1が一番面倒そうだな。

文字列から3点の座標を得る

普通に考えれば、カンマでsplitしてから'('と')'を除去して、って感じかな。
そういや、先日の問題の答えで eval() というのがあったな。ちょっとテスト。

>>> print(eval("(2,2),(6,2),(2,6)"))
((2, 2), (6, 2), (2, 6))

あれま。evalすげー。

(x1, y1), (x2, y2), (x3, y3) = eval(data)

じゃあこれで。Pythonすごいな。

3点から円の中心と半径を求める

http://okwave.jp/qa/q6389934.html
http://okwave.jp/qa/q7112206.html
http://wilhelmtell.blog46.fc2.com/blog-entry-550.html
http://nobutina.blog86.fc2.com/blog-entry-674.html
これらのリンクを参考にして、Pythonで書いてみた。

import math

def get_circle_center_and_radius(x1, y1, x2, y2, x3, y3):
    """
    3点を通る円の中心と半径を取得
    """
    d = 2 * ((y1 - y3) * (x1 - x2) - (y1 - y2) * (x1 - x3))
    x = ((y1 - y3) * (y1 ** 2 - y2 ** 2 + x1 ** 2 - x2 ** 2) - (y1 - y2) * (y1 ** 2 - y3 ** 2 + x1 ** 2 - x3 ** 2)) / d
    y = ((x1 - x3) * (x1 ** 2 - x2 ** 2 + y1 ** 2 - y2 ** 2) - (x1 - x2) * (x1 ** 2 - x3 ** 2 + y1 ** 2 - y3 ** 2)) / -d
    r = math.sqrt((x - x1) ** 2 + (y - y1) ** 2)
    return (x, y), r

方程式(文字列)を作成して返す

ここが意外と手間取った。まず、浮動小数点を小数点2桁に丸めるには、round()を使ったり、format()を使えばいい。

>>> str(round(3.1415, 2))
'3.14'
>>> format(3.1415, '.2f')
'3.14'

末尾の「0」と「.」を消す方法だが、小数点2桁なんだから、末尾に'.0'と'.00'があれば削除すればいいか。(←注:後で気づくが、ここが間違っていた。)
文字列の末尾が○○なら削除する、という関数を作っておく。

def remove_suffix(s, suffix):
    # 文字列の末尾がsuffixならば削除する
    return s[:-len(suffix)] if s.endswith(suffix) else s

これを strのメソッドとして登録して、s.remove_suffix("abc") とかできればいいのに。しかし、残念なことにPythonでは組み込み型は拡張できない。(C#なら拡張メソッドでstringを拡張できるのになー。)

さて、あとは方程式を作成する。
問題には "(x-a)^2+(y-b)^2=r^2" と書いてあるが、単純に

return "(x-{})^2+(y-{})^2={}^2".format(a, b, r)

というわけにはいかない。
aが-1のときは (x--1)^2 ではなく (x+1)^2 だし、aが0のときは (x-0)^2 ではなく x^2 となる。

def make_equation(x, y, r):
    """
    円の方程式を作成
    """
    def format_float(f):
        # 末尾の0と.を削除
        result = str(round(f, 2))
        result = remove_suffix(result, '.00')
        result = remove_suffix(result, '.0')
        return result

    def make_part(name, value):
        # '(x-5)'や'(y+2)'の部分を作成
        num = format_float(abs(value))
        sign = '-' if value > 0 else '+'
        return name if num == '0' else '({0}{1}{2})'.format(name, sign, num)

    return "{}^2+{}^2={}^2".format(make_part('x', x), make_part('y', y), format_float(r))

まとめ

全部まとめたったら、こうなった。

import math

def get_circle_center_and_radius(x1, y1, x2, y2, x3, y3):
    d = 2 * ((y1 - y3) * (x1 - x2) - (y1 - y2) * (x1 - x3))
    x = ((y1 - y3) * (y1 ** 2 - y2 ** 2 + x1 ** 2 - x2 ** 2) - (y1 - y2) * (y1 ** 2 - y3 ** 2 + x1 ** 2 - x3 ** 2)) / d
    y = ((x1 - x3) * (x1 ** 2 - x2 ** 2 + y1 ** 2 - y2 ** 2) - (x1 - x2) * (x1 ** 2 - x3 ** 2 + y1 ** 2 - y3 ** 2)) / -d
    r = math.sqrt((x - x1) ** 2 + (y - y1) ** 2)
    return (x, y), r

def remove_suffix(s, suffix):
    # remove suffix if exists
    return s[:-len(suffix)] if s.endswith(suffix) else s

def make_equation(x, y, r):

    def format_float(f):
        # remove extraneous zeros
        result = str(round(f, 2))
        result = remove_suffix(result, '.00')
        result = remove_suffix(result, '.0')
        return result

    def make_part(name, value):
        # value=5 -> '(x-5)', value=-2 -> '(x+2)', value=0 -> 'x'
        num = format_float(abs(value))
        sign = '-' if value > 0 else '+'
        return name if num == '0' else '({0}{1}{2})'.format(name, sign, num)

    return "{}^2+{}^2={}^2".format(make_part('x', x), make_part('y', y), format_float(r))

def checkio(data):
    (x1, y1), (x2, y2), (x3, y3) = eval(data)
    (x0, y0), r = get_circle_center_and_radius(x1, y1, x2, y2, x3, y3)
    return make_equation(x0, y0, r)

これでテストクリア&公開!

他の人の答え

文字列から3点の座標を得る

正規表現を使う人、evalを使う人、普通にsplit(',')する人、とまちまち。evalを使うのが一番簡単だろう。

3点から円の中心と半径を求める

複素数を使っている。私にはさっぱりわからない。
http://www.checkio.org/mission/three-points-circle/publications/veky/python-27/cplx-strings/

方程式(文字列)を作成して返す

やはり、数字の末尾の「0」と「.」をどう削除するかというところで、みんな工夫していた。どうも自分の答えに自信がなくなってきて、あれこれ試してみた。

>>> str(round(3.14, 2))
'3.14'
>>> str(round(3.10, 2))
'3.1'
>>> str(round(3.00, 2))
'3.0'
>>> str(round(3, 2))
'3'

>>> format(3.14, '.2f')
'3.14'
>>> format(3.10, '.2f')
'3.10'
>>> format(3.00, '.2f')
'3.00'
>>> format(3, '.2f')
'3.00'

round(f,2)とformat(f,'.2f')って微妙に違うんだな。round(f,2)では末尾に'.00'がくることはないのか。
私のコードの

result = remove_suffix(result, '.00')

は必要なかったようだ。今回はround()を使っていたので良かったが、format()の場合なら '3.10'を'3.1'とする処理も必要になる。小数点2桁だから'.00'と'.0'を消せばいい、というわけではなかった。

他に気づいた点は、format()で+の符号を追加できるらしい。

>>> format(3.1415, '+.2f')
'+3.14'
>>> format(-3.1415, '+.2f')
'-3.14'

また、.rstrip('0').rstrip('.') とすれば、末尾の「0」と「.」を消すことができる。これなら '3.00'でも'3.0'でも'3.10'でも対応できる。