『あのカシオペア座を一つの式で表してみよう』【python】

# はじめに
みなさんはカシオペア座を見たことがありますか？(特に冬の)夜頃、空に浮かんでいる「ｗ」←こういう形の星座です。[〈 Wikipedia / カシオペヤ座 〉](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%82%B7%E3%82%AA%E3%83%9A%E3%83%A4%E5%BA%A7)
<br>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049281/f5a65415-7b8c-4434-95d4-7502c01b6d3f.png)


感受性に富んだ我々はこのカシオペア座を見て、広がる満天の星空に思いを馳せながら、

「ああ、なんてきれいなんだ」
「ちっぽけな悩みはどうだっていいんだ」
「あのカシオペア座を一つの式で表してみよう」

などと考えることでしょう。
<br>
今回は、平面上のカシオペア座を絶対値を使った一つの式で表してみることを目標に、
〈 1 〉絶対値関数式から絶対値グラフを描く
$\qquad$[ ⅰ ] [まずは簡単な例から](#まずは簡単な例から)
$\qquad$[ ⅱ ] [折点が複数ある絶対値関数の表し方](#折点が複数ある絶対値関数の表し方)
〈 2 〉連立方程式を行列で解く
$\qquad$[ ⅰ ] [2元連立方程式を解く](#2元連立方程式を解く)
$\qquad$[ ⅱ ] [多元連立方程式を解くには？～掃き出し法～](#多元連立方程式を解くには掃き出し法)
〈 3 〉pythonで絶対値のグラフを与える式を出力する
$\qquad$[ ⅰ ] [numpyを使った絶対値関数の出力](#numpyを使った絶対値関数の出力)
$\qquad$[ ⅱ ] [掃き出し法を使った絶対値関数の出力](#掃き出し法を使った絶対値関数の出力)
〈 4 〉[まとめ](#まとめ)

というふうに進めます。

# 絶対値関数式から絶対値グラフを描く
## まずは簡単な例から
だいぶ端折って考えます。まず、
```math
f_1(x)=m|x-a|+bx+c\qquad (m, a, b, c \in \mathbb{R}, m \ne 0)
```
で表される絶対値関数のみを考えます。これを解いてみましょう。

$[ⅰ]x≧a$のとき、
```math
f_1(x)=(b+m)x+(c-ma) \qquad (x≧a)
```
<br>$[ⅱ] x≦a$のとき、
```math
f_1(x)=(b-m)x+(c+ma) \qquad (x≦a)
```
<br>

これらの2つの半直線が平行でないとき、交点の$x$座標はこれら2式を連立させた
```math
(b+m)x+(c-ma)=(b-m)x+(c+ma)
```
の解であり、これを解くと、$x=a$です。
ここから、$y=f_1(a)=ab+c$とわかります。

さて、交点の$x$座標というのは、グラフが折れ曲がる部分のことです。
以降、絶対値関数が折れ曲がる点のことを「折点」と呼ぶことにします。
一般に、絶対値関数
```math
f_1(x)=m|x-a|+bx+c \qquad (m, a, b, c \in \mathbb{R}, m \ne 0)
```
は折点をもつ場合、その折点は1つで、その座標は、
```math
(x, y)=(a, ab+c)
```
であるといえます。
絶対値関数$f_1(x)$は1つの折点とその両側の2つの点で定義されることがわかりました。

## 折点が複数ある絶対値関数の表し方
一般に、折点を$n$個もつ絶対値関数式を考えます。


```math
f_n(x)=m_1|x-a_1|+m_2|x-a_2|+ \cdots +m_n|x-a_n|+bx +c
```
```math
(m_p, a_p, b, c \in \mathbb{R}, m_k \ne 0, a_{i}<a_{i+1}(1\leqq i \leqq n-1))
```
<br>簡単のために、3点
```math
(a_{j-1}, f_n(a_{j-1})),\space (a_j, f_n(a_j)),\space (a_{j+1}, f_n(a_{j+1}))\qquad(2\leqq j\leqq n-1)
```
は同一直線上にないものとします。(3点が同一直線上にある場合は、$(a_j,f_n(a_j))$は折点ではないということになります)
<br>$[ⅰ]x≧a_n$のとき、
```math
f_n(x)=(b+m_1+m_2+ \cdots+m_n)x\qquad\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad\qquad+(c-m_1a_1-m_2a_2- \cdots -m_na_n) \qquad (x≧a_n)
```

<br>$[ⅱ]a_i≦x≦a_{i+1}$のとき、
```math
f_n(x)=(b+m_1+\cdots+m_i-m_{i+1}-\cdots-m_n)x\qquad\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad\qquad+(c-m_1a_1-\cdots-m_ia_i+m_{i+1}a_{i+1}+\cdots+m_na_n)
```
```math
(a_i≦x≦a_{i+1})
```
<br>$[ⅲ]a_{1}≦x$のとき、
```math
f_n(x)=(b-m_1-m_2-\cdots-m_n)x\qquad\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad\qquad+(c+m_1a_1+m_2a_2+\cdots+m_na_n)
```
```math
(a_1≦x)
```
<br>これで、線分または半直線が$n+1$本出てくるので、隣り合う線同士を連立させて得た交点が折点です。

① $[ⅰ]$と$[ⅱ] (i=n-1)$を連立させると、
```math
(b+m_1+m_2+\cdots+m_n)x\qquad\qquad\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad+(c-m_1a_1-m_2a_2- \cdots -m_na_n)
```
```math
=(b+m_1+\cdots+m_{n-1}-m_n)x\qquad\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad\qquad+(c-m_1a_1-\cdots-m_{n-1}a_{n-1}+m_na_n)
```
すなわち解は、$x=a_n$となり
```math
y=f(a_n)=a_nb+\sum_{k=1}^nm_k(a_n-a_k)+c
```
となります。
<br>
② $[ⅱ] (i=r)$と$[ⅱ] (i=r+1)$ ($1\leqq r \leqq n-1$)を連立させると、
```math
(b+m_1+\cdots+m_{r-1}-m_{r}-\cdots-m_n)x\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad+(c-m_1a_1-\cdots-m_{r-1}a_{r-1}+m_{r}a_{r}+\cdots+m_na_n)
```
```math
=(b+m_1+\cdots+m_{r}-m_{r+1}-\cdots-m_n)x\qquad\qquad\qquad
```
```math
\qquad\qquad\qquad+(c-m_1a_1-\cdots-m_{r}a_{r}+m_{r+1}a_{r+1}+\cdots+m_na_n)
```
すなわち解は、$x=a_{r}$となり、
```math
y=f(a_{r})=a_{r}b+\sum_{k=1}^rm_k(a_{r}-a_k)+\sum_{k=r}^nm_k(a_k-a_r)+c
```
```math
\quad =a_{r}b+\sum_{k=1}^nm_k|a_{r}-a_k|+c
```
となります。
<br>
③ $[ⅱ] (i=1)$と$[ⅲ]$を連立させると、
```math
(b+m_1-m_2-\cdots-m_n)x+(c-m_1a_1+m_2a_2+\cdots+m_na_n)
```
```math
=(b-m_1-m_2-\cdots-m_n)x+(c+m_1a_1+m_2a_2+\cdots+m_na_n)
```
すなわち解は$x=a_1$となり、
```math
y=f(a_1)=a_1b+\sum_{k=1}^nm_k(a_k-a_1)+c
```
となります。
<br>

①, ②, ③から、一般に、
```math
f_n(x)=m_1|x-a_1|+m_2|x-a_2|+ \cdots +m_n|x-a_n|+bx +c
```
```math
(m_p, a_p, b, c \in \mathbb{R}, m_k \ne 0, a_{i-1}<a_i(1\leqq i \leqq n-1))
```
について、3点
```math
(a_{j-1}, f_n(a_{j-1})),\space (a_j, f_n(a_j)),\space (a_{j+1}, f_n(a_{j+1}))\qquad(2\leqq j\leqq n-1)
```
がつねに同一直線上にない場合、$f_n(x)$は折点を$n$個もち、その座標$(x_s,y_s)$は
```math
(x_s,y_s)=a_sb+\sum_{k=1}^nm_s|a_s-a_k|+c
```
で表される。
また、これらを構成する線分または半直線は$n+1$本あり、
関数$f_n(x)$を決定するには$x<a_1$と$a_n<x$の範囲でそれぞれ1点ずつ設定する必要があるため、計$n+2$点で$f_n(x)$は決定される。

ここから、折点と外側の2つの点が与えられたときに、絶対値関数$f_n(x)$が定まることがわかりました。
したがって、$m_k, b, c$の未知数を求めることができればよさそうです。これは各折点で連立方程式をたてることで解決できそうですね。


# 連立方程式を行列で解く
## 2元連立方程式を解く
(以下、行列での連立方程式の解き方が理解できている方は読み飛ばしてください。)


pythonで連立方程式を解く際には「行列」を使うこととします。
まずは行列を用いて、簡単な(解が一意に定まる)連立方程式を解くことを考えます。

```math
\left\{\begin{matrix}
ax+by=p\\
cx+dy=q\\
\end{matrix}\right.
```
係数行列$A$と定数ベクトル$B$を次のように定めます。
```math
A=\begin{pmatrix}
   a & b \\
   c & d
\end{pmatrix},
B=\begin{pmatrix}
   p \\
   q
\end{pmatrix}
```
するとこの式は、$A\begin{pmatrix}x \\ y\end{pmatrix}=B$となり、求める解は、
```math
\begin{pmatrix}
   x \\
   y
\end{pmatrix}
=A^{-1}B
```
となりました。$A^{-1}$は$A$の逆行列を表していて、$A$の行列式$\mathrm{det}A$を用いて、
```math
A^{-1}=\frac{1}{\mathrm{det}A}\begin{pmatrix}
   d & -b\\
   -c & a
\end{pmatrix}
```
となります。ただし、$\mathrm{det}A=ad-bc$ 。
したがって、求める解は、
```math
\begin{pmatrix}
   x \\
   y
\end{pmatrix}
=\frac{1}{ad-bc}\begin{pmatrix}
   d & -b\\
   -c & a
\end{pmatrix} \cdot
\begin{pmatrix}
   p \\
   q
\end{pmatrix}
```
```math
=\begin{pmatrix}
   \frac{dp -bq}{ad-bc} \\
   \frac{-cp +aq}{ad-bc}
\end{pmatrix}
```
となります。

解がない、または解が無数にある連立方程式は、$\mathrm{det}A=ad-bc=0$です。

## 多元連立方程式を解くには？～掃き出し法～
このように、係数行列と定数ベクトルの情報から多元連立方程式を解くことができます。しかし、計算量が多くなってしまうので、多元連立方程式を効率的に解くアルゴリズム「ガウス・ジョルダン法(掃き出し法)」を使ってみます。

掃き出し法の概要は、

①係数行列$A$と定数ベクトル$B$を拡大係数行列$(A|B)$にする。
②拡大係数行列$(A|B)$を$(I_n|C)$に変換する(ただし、$I_n$は$n$行$n$列の単位行列)。このとき、$C$が解。

です。

例として、次の3元連立方程式を考えます。
```math
\begin{cases}
x + 2y + z = 3 \\
2x - y + 3z = 14 \\
-x + 4y - 2z = -12
\end{cases}
```

<br>係数行列$A$は
```math
A=\begin{pmatrix}
   1 & 2 & 1 \\
   2 & -1 & 3 \\
   -1 & 4 & -2
\end{pmatrix}
```
また、定数ベクトル$B$は
```math
B=\begin{pmatrix}
   3 \\
   14 \\
   -12
\end{pmatrix}
```
となっていて、拡大係数行列$(A|B)$を
```math
(A\,|\,B)=\left(\begin{array}{rrr|r}
   1 & 2 & 1 & 3 \\
   2 & -1 & 3 & 14 \\
   -1 & 4 &-2 & -12
\end{array}\right)
```
と定め、これを
$[1]$定数倍する
$[2]$定数倍した別の列で差を取る

のいずれかの操作で、左側を単位行列$I_3$
```math
I_3=\begin{pmatrix}
   1 & 0 & 0 \\
   0 & 1 & 0 \\
   0 & 0 & 1
\end{pmatrix}
```
に変換していきます。
以下、対角成分をその列のピボット(支配値)と呼ぶことにします。
1, 2, 3行目の現在のピボットは$1$, $-1$, $-2$です。
もしピボットの値が$0$になってしまう場合は、他の行と入れ替える必要があります。

まず1行目のピボットを$1$にします。今回は操作を行う必要はありません。
次に、1行目以外の1列目の成分を$0$にするように2行目、3行目を引いていきます。
2行目について、(1行目)$\times 2$を引きます。
```math
\left(\begin{array}{ccc|c}
   2 & -1 & 3 & 14 \\
\end{array}\right)
- 2
\left(\begin{array}{ccc|c}
   1 & 2 & 1 & 3 \\
\end{array}\right)
=
\left(\begin{array}{ccc|c}
   0 & -5 & 1 & 8 \\
\end{array}\right)
```

<br>3行目について(1行目$\times (-1)$)を引きます。
```math
\left(\begin{array}{ccc|c}
   -1 & 4 & -2 & -12 \\
\end{array}\right)
- \Big(-
\left(\begin{array}{ccc|c}
   1 & 2 & 1 & 3 \\
\end{array}\right)\Big)
=
\left(\begin{array}{ccc|c}
   0 & 6 & -1 & -9 \\
\end{array}\right)
```

<br>現在$(A|B)$は
```math
(A\,|\,B)=\left(\begin{array}{rrr|r}
   1 & 2 & 1 & 3 \\
   0 & -5 & 1 & 8 \\
   0 & 6 & -1 & -9
\end{array}\right)
```

同様に、2行目のピボットを$1$にスケールし、1, 3行目の2列目を$0$にするように差を取ります。
3行目も同様に操作します。結果、
```math
(A\,|\,B)=\left(\begin{array}{rrr|r}
   1 & 0 & 0 & 2 \\
   0 & 1 & 0 & -1 \\
   0 & 0 & 1 & 3
\end{array}\right)
```
解は$(x, y, z)=(2, -1, 3)$となることがわかります。

# pythonで絶対値のグラフを与える式を出力する
## numpyを使った絶対値関数の出力
以下の問題を考えます。
「座標平面上の$N$個の点$P_k=(x_k, y_k)$が与えられる。(ただし、$N$は$3$以上の整数であり、すべての$1≦k≦N-1$に対して、$x_k<x_{k+1}$が成り立つ。)
$P_1$から$P_2$、$P_2$から$P_3$、……、$P_{N-1}$から$P_N$を順に直線で結んでできる折れ線を絶対値を用いた1つの式で表し、出力せよ。」
入力値はたとえば
```
5
1 0
2 4
6 3
10 2
13 6
```
のようになります。
numpyを使ってこの問題を解いてみると、こんな感じになります。

```python: numpyを使った絶対値関数の出力.py
import numpy as np

def FindInflection(Points):
    InflectionPoints = []
    for j in range(1, len(Points) - 1):
        if (Points[j-1][1] - Points[j][1]) / (Points[j-1][0] - Points[j][0]) * (Points[j+1][0] - Points[j-1][0]) + Points[j-1][1] != Points[j+1][1]:
            InflectionPoints.append(Points[j])
    return InflectionPoints

def FindCoefficientMatrix(StartPoint, InflectionPoints, EndPoint):
    Points = [StartPoint] + [Point for Point in InflectionPoints] + [EndPoint]
    rtnMatrix = [[0 for j in range(len(Points))] for i in range(len(Points))]
    for i in range(len(Points)):
        for j in range(len(InflectionPoints)):
            rtnMatrix[i][j] = abs(Points[i][0] - InflectionPoints[j][0])
        rtnMatrix[i][-2] = Points[i][0]
        rtnMatrix[i][-1] = 1
    return rtnMatrix

def FindFormula(Variables):
    def AddSign(num: float):
        if   num > 0: return " + " + str(num)
        elif num < 0: return " - " + str(-num)
        else        : return ""
    rtnstr = "y = "
    for i in range(len(InflectionPoints)):
        rtnstr += AddSign(Variables[i]) + "|x" + AddSign(-1 * InflectionPoints[i][0]) + "|"
    rtnstr += AddSign(Variables[-2]) + "x"
    rtnstr += AddSign(Variables[-1])
    return rtnstr

def FindVariables(CoefficientMatrix, ConstantVector):
    A = np.array(CoefficientMatrix)
    B = np.array(ConstantVector)
    det = np.linalg.det(A)
    if det == 0:
        return "そんなわけありませんが…(err: def = 0)"
    else:
        A_inv = np.linalg.inv(A)
        X = np.dot(A_inv, B)
        return X

# 入力
N = int(input("N: "))
Stars = [list(map(int, input(str(i + 1) + "番目の座標：").split())) for i in range(N)]

# 折点(InflectionPoints)の取得
InflectionPoints= FindInflection(Stars)

# 係数行列(CoefficientMatrix)と定数ベクトル(ConstantVector)の取得
CoefficientMatrix = FindCoefficientMatrix(Stars[0], InflectionPoints, Stars[-1])
ConstantVector = [Stars[0][1]] + [InflectionPoint[1] for InflectionPoint in InflectionPoints] + [Stars[-1][1]]

# 絶対値関数式を作成
Variables = FindVariables(CoefficientMatrix, ConstantVector)
Formula = FindFormula(Variables)
print(Formula)
```

出力結果：「y =  - 2.1249999999999987|x - 2| + 0.7916666666666667|x - 10| + 2.666666666666666x - 7.666666666666667」

出力が小数なので、グラフがわかりにくいし美しさが失われています。

あと私は自然派コーダーですので、importなんか使いたくありませんわ！
おほほ ですわ。

次は、モジュールを使わずに、かつ出力が小数ではなく分数になるようにしたいと思います。

## 掃き出し法を使った絶対値関数の出力
計算のすべてを分数で行えるよう、分数型を定義します。
今回は最小限の演算さえできればよいので、分数型はこんな感じにしておきます。
時間があればどこかで追加の記事を作ります。
```python: FractionClass.py
class Fc:
    def __init__(self, numerator, denominator):
        if denominator == 0: raise ValueError("ZeroDivisionError")
        self.numerator = numerator
        self.denominator = denominator
        self.ctrSign()
        self.ctrZero()
        self.numerator, self.denominator = self.reduce()
    def gcd(self):
        a, b = abs(self.numerator), abs(self.denominator)
        if a == 0: return 1
        if a < b: a, b = b, a
        q, r = a // b, a % b
        while r != 0:
            a, b = b, r
            q, r = a // b, a % b
        return b
    def reduce(self): return int(self.numerator // self.gcd()), int(self.denominator // self.gcd())
    def ctrSign(self): 
        if self.denominator < 0:
            self.numerator *= -1
            self.denominator *= -1
    def ctrZero(self):
        if self.numerator == 0: self.denominator = 1
    def __str__(self): return f"{self.numerator}/{self.denominator}"
    def reciprocal(self):
        if self.numerator == 0: raise ZeroDivisionError("ZeroDivisionError")
        return Fc(self.denominator, self.numerator)
    def __add__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        numerator = self.numerator * other.denominator + other.numerator * self.denominator
        denominator = self.denominator * other.denominator
        return Fc(numerator, denominator)
    def __sub__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        numerator = self.numerator * other.denominator - other.numerator * self.denominator
        denominator = self.denominator * other.denominator
        return Fc(numerator, denominator)
    def __mul__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        numerator = self.numerator * other.numerator
        denominator = self.denominator * other.denominator
        return Fc(numerator, denominator)
    def __truediv__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        if other.numerator == 0: raise ZeroDivisionError("ZeroDivisionError")
        return self.__mul__(other.reciprocal())
    def __radd__(self, other): return self.__add__(other)
    def __rsub__(self, other): return self.__sub__(other)
    def __rmul__(self, other): return self.__mul__(other)
    def __rtruediv__(self, other): return Fc(other, 1).__truediv__(self)
    def __eq__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        return self.numerator == other.numerator and self.denominator == other.numerator
    def __ne__(self, other):
        return not self.__eq__(other)
    def __lt__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        return self.numerator * other.denominator < self.denominator * other.numerator
    def __gt__(self, other):
        if isinstance(other, int) or isinstance(other, float): other = Fc(other, 1)
        return self.numerator * other.denominator > self.denominator * other.numerator
```


掃き出し法関数をpythonで実装してみましょう。
係数行列$A$と定数ベクトル$B$を引数に取り、
$[1]$定数倍する
$[2]$定数倍した別の列で差を取る
のいずれかの操作を$A$に行い、単位行列$I_n$に変換し、
操作の結果の解$C$を返します。



```python: gauss_jordan.py
class Fc: …

def Gauss_Jordan(A, B):
    # 拡大係数行列(A | B)をつくる。
    Ex_Matrix = [[Fc(i, 1) for i in a] + [Fc(b, 1)] for a, b in zip(A, B)]
    
    # ピボットが0の行があれば、他の行と入れ替える
    for i in range(len(Ex_Matrix)):
        if Ex_Matrix[i][i] == 0:
            for j in range(len(Ex_Matrix)):
                if Ex_Matrix[i][j] != 0 and Ex_Matrix[j][i] != 0:
                    Ex_Matrix[i], Ex_Matrix[j] = Ex_Matrix[j], Ex_Matrix[i]
                    break
    
    # 掃き出し法を行う
    for i in range(len(Ex_Matrix)):
        # ある行をそのピボットが1になるようにスケーリング
        pivot = Ex_Matrix[i][i]
        for j in range(len(Ex_Matrix[i])):
            Ex_Matrix[i][j] /= pivot

        # 選択中のピボットを含む列が0になるように行を引く
        for j in range(len(Ex_Matrix)):
            if i != j:
                scale = Ex_Matrix[j][i]
                for k in range(len(Ex_Matrix[j])):
                    Ex_Matrix[j][k] -= scale * Ex_Matrix[i][k]
    
    rtnList = [row[-1] for row in Ex_Matrix]
    return rtnList
```

あとは先程のプログラムで、係数行列(CoefficientMatrix)と定数ベクトル(ConstantVector)の取得までを行っているので、それに組み合わせると、目的のプログラムを得ます。

```python: 掃き出し法を使った絶対値関数の出力.py
class Fc:…(省略します)
def FindInflection(Points):
    InflectionPoints = []
    for j in range(1, len(Points) - 1):
        if (Points[j-1][1] - Points[j][1]) / (Points[j-1][0] - Points[j][0]) * (Points[j+1][0] - Points[j-1][0]) + Points[j-1][1] != Points[j+1][1]:
            InflectionPoints.append(Points[j])
    return InflectionPoints

def FindCoefficientMatrix(StartPoint, InflectionPoints, EndPoint):
    Points = [StartPoint] + [Point for Point in InflectionPoints] + [EndPoint]
    rtnMatrix = [[0 for j in range(len(Points))] for i in range(len(Points))]
    for i in range(len(Points)):
        for j in range(len(InflectionPoints)):
            rtnMatrix[i][j] = abs(Points[i][0] - InflectionPoints[j][0])
        rtnMatrix[i][-2] = Points[i][0]
        rtnMatrix[i][-1] = 1
    return rtnMatrix

def FindVariables(CoefficientMatrix, ConstantVector):
    # 拡大係数行列(A | B)をつくる。
    Ex_Matrix = [[Fc(i, 1) for i in a] + [Fc(b, 1)] for a, b in zip(CoefficientMatrix, ConstantVector)]
    
    # ピボットが0の行があれば、他の行と入れ替える
    for i in range(len(Ex_Matrix)):
        if Ex_Matrix[i][i] == 0:
            for j in range(len(Ex_Matrix)):
                if Ex_Matrix[i][j] != 0 and Ex_Matrix[j][i] != 0:
                    Ex_Matrix[i], Ex_Matrix[j] = Ex_Matrix[j], Ex_Matrix[i]
                    break
    
    # 掃き出し法を行う
    for i in range(len(Ex_Matrix)):
        # ある行をそのピボットが1になるようにスケーリング
        pivot = Ex_Matrix[i][i]
        for j in range(len(Ex_Matrix[i])):
            Ex_Matrix[i][j] /= pivot

        # 選択中のピボットを含む列が0になるように行を引く
        for j in range(len(Ex_Matrix)):
            if i != j:
                scale = Ex_Matrix[j][i]
                for k in range(len(Ex_Matrix[j])):
                    Ex_Matrix[j][k] -= scale * Ex_Matrix[i][k]
    
    rtnList = [row[-1] for row in Ex_Matrix]
    return rtnList

def FindFormula(Variables):
    def AddSign(num):
        if   num > 0: return " + " + str(num)
        elif num < 0: return " - " + str(-1 * num)
        else        : return ""
    rtnstr = "y = "
    for i in range(len(InflectionPoints)):
        rtnstr += AddSign(Variables[i]) + "|x" + AddSign(-1 * InflectionPoints[i][0]) + "|"
    rtnstr += AddSign(Variables[-2]) + "x"
    rtnstr += AddSign(Variables[-1])
    return rtnstr

# 入力
N = int(input("N: "))
Stars = [list(map(int, input(str(i + 1) + "番目の座標：").split())) for i in range(N)]

# 折点(InflectionPoints)の取得
InflectionPoints= FindInflection(Stars)

# 係数行列(CoefficientMatrix)と定数ベクトル(ConstantVector)の取得
CoefficientMatrix = FindCoefficientMatrix(Stars[0], InflectionPoints, Stars[-1])
ConstantVector = [Stars[0][1]] + [InflectionPoint[1] for InflectionPoint in InflectionPoints] + [Stars[-1][1]]

# 絶対値関数式を作成
Variables = FindVariables(CoefficientMatrix, ConstantVector)
Formula = FindFormula(Variables)
print(Formula)
```

出力結果：「y =  - 17/8|x - 2| + 19/24|x - 10| + 8/3x - 23/3」

これで私の満足する結果が出力できるようになりました。

# まとめ

今回は、絶対値関数から行列での連立方程式の解、さらに掃き出し法から、pythonでのクラス定義にまで手を伸ばすことができました。
欲を言えば、出力結果は途中に出てきたあのかっこいい$g_n(x)$にしたいですね……時間があれば追加します。
また、星は平面上の点ではないという主張の方は、ある点からみた空間上の点を平面上に移す関数を作ればよいと思います。
他にも、多価の式にする方法があれば教えて下さい。
これであなたも、カシオペア座を見て、一つの式にできますね★彡
この調子で、いろいろな星座を一つの式にしちゃってください★彡

カロリーの高い記事になってしまいました。お読みいただきありがとうございました。

ご不明点、気になる点があれば教えて下さい。