OOP:数学では、進めるなら戻れるはず
このワークシートはMath by Codeの一部です。
これまで扱ったデザインパタンは
構造を前に進む探索について扱ったきました。
進みたいときもあれば、戻りたいときもあるでしょう。
処理を取り消したい。Undo。
可能な状態を行き来したい。StateTraveler。
過去の状態に一瞬で世界を上書きしたい。Warp。
いろんな戻り方があります。
今回のテーマは「数学では、進めるなら戻れるはず」
扱うパタンは
Commandクラスで記憶の追加と消去,
Stateクラスで主客逆転してすごろく化,
Mementoクラスは没入型の思い出写真。
の3つです。
数学でも日常生活でも重要な「可逆性の視点と方法」に取り組もう。
1.Commandクラスで記憶の追加と消去
<コマンド履歴パタン>
PCを動かしていると、Ctrl+Zで画面操作が1回取り消されます。
別に神様が監視して元に戻しているわけではありません。
アプリへのユーザの選んだコマンド選択が履歴として時系列で残します。
そのコマンドを取り消すことで、1つ前の画面状態に戻れるというしくみです。
コマンドパタン:
メソッド呼び出しのカプセル化をすることで、
メソッドログ保存やUndoの実装ができるようにする。
ということです。ただ、本によって実装方法もカプセル化もちがってますから、
ひとつの例として読んでください。
大昔のPCゲームでは2次元なので、コントローラのかわりに矢印キーで
自分の分身や攻撃の発射台など動かしてましたね。
敵のいないプレーンなゲームのフレームを作りたいとしましょう。
画面に現れた自分Meを矢印キー(←、→、↑、↓)のどれかを押すことができます。
キャレットを1つ前の位置に戻すにはZキーを押します。
ユーザのキー入力は呼び出しInvokeクラスのpressKeyコマンドで受け取ります。
それを情報として記録するための情報記録系のコマンドクラスを雇います。それがInvokeクラスのSetCommandです。ただし、コマンドクラスはインターフェースでしかないので、実際はその子クラスの情報部員MacroCommandになります。
キー入力に応じてInvokeのpresskeyのコマンドを通して、情報部員MacroCommandに情報が渡ります。
では、情報を受け取った情報部員MacroCommandのインスタンスは何をするのでしょうか。一番大切なのは指令の実行、executeであることは当然ですが、それだけではありません。
使えるメソッドはイニシャライズのときに受け取りMeInterfaceインターフェースの登録です。
Invokeのpresskeyに応じたコマンドがあります。
・appendは情報部員のログcommandslogにキー操作の追加。
・executeはappendしたあとにMeInterfaceに矢印キーに対応したleft,right, up,downを伝えます。
・undoはpresskeyがzなら、commandslogからのキー入力履歴の最新をPopで抜いておき、MeInterfaceにUndoを伝えます。
コマンドの最後の受け取りと実行をするのは受け取り系のインターフェースReceiverです。
ゲームで動く自分の分身にちなんで、MeInterfaceとします。
メソッドはleft,right,up,down,undo_actionの5つです。
この子クラスは、自分のposition、過去の位置の記録リストhistoryをもちます。
自分の位置の記録のための私的メソッド_save_current_positionを持ちます。
矢印キーの情報を時系列に追記し、連動してhistoryも時系列に追記します。
zキーを押すと、commandslogもhistoryもpopさせます。
最新の情報が消えるので、戻った状態になる。記憶を1つ消したのですね。
サブクラス化だけでなく、移譲(has)によって、情報をリレーして投げるようすは
次のようになります。
矢印キー→呼び出しのpresskey→MacroCommandのlogへのappendとexecute→Meクラスで実行。
zキー→→呼び出しのpresskey→MacroCommandのlogのpop→Meクラスのhistoryもpop。
<コマンド履歴パタンの実装例>
# Command.py
# ==========================================
# 末端の実行者である受取系(Receiver)のMeInterface
# ==========================================
class MeInterface:
"""(入力待ちの自分の分身)が持つべき具体的なアクションの規格"""
def left(self): raise NotImplementedError()
def right(self): raise NotImplementedError()
def up(self): raise NotImplementedError()
def down(self): raise NotImplementedError()
def undo_action(self): raise NotImplementedError() # 過去の位置に戻るアクション
# ==========================================
# サブクラス(現在位置と位置の履歴を持つアバター)
# 末端とはいえ、自分の情報と記憶を持っている。
# ==========================================
class Avator(MeInterface):
"""位置(x, y)と、自身の中に位置の履歴を持つ"""
def __init__(self):
self.position = [0, 0] # [x座標, y座標]
self.history = [] # 過去の位置座標を記録するスタック
def _save_current_position(self):
"""現在の位置を履歴スタックに保存する(可逆性のための記憶)"""
self.history.append(list(self.position))
def left(self):
self._save_current_position()
self.position[0] -= 1 # 左矢印
print(f"[Action] Left: 自分の分身が左に動きました。現在地: {self.position}")
def right(self):
self._save_current_position()
self.position[0] += 1 # 右矢印
print(f"[Action] Right: 自分の分身が右に動きました。現在地: {self.position}")
def up(self):
self._save_current_position()
self.position[1] += 1 # 上矢印
print(f"[Action] Up: 自分の分身が上に動きました。現在地: {self.position}")
def down(self):
self._save_current_position()
self.position[1] -= 1 # 下矢印
print(f"[Action] Down: 自分の分身が下に動きました。現在地: {self.position}")
def undo_action(self):
"""位置の巻き戻し(逆演算)"""
if self.history:
# 1つ前の位置を取り出して上書き(タイムトラベル)
self.position = self.history.pop()
print(f"[Undo] Z-Key: 1つ前の位置に戻しました。現在地: {self.position}")
else:
print("[Warning] これ以上戻る歴史(履歴)がありません。")
# ==========================================
# 【情報管理系】雇ったアバターにexcuteで実行させる
# ==========================================
class Command:
"""コマンド情報管理系の根底にある共通規格"""
def execute(self):
raise NotImplementedError()
# ==========================================
# 情報部員は情報を受け取って実行するアバターを雇う
# 情報部員はInvoke通して受け付けたキーのlogを持つ。
# ==========================================
class MacroCommand(Command):
"""[コマンド情報管理系] 受け取り実行アバターを保持し、操作の歴史を統括する"""
def __init__(self, receiver: MeInterface):
self.receiver = receiver
self.commands_log = [] # ユーザが行ったキー操作の文字列ログ
def append(self, key_name):
"""ユーザの操作を歴史ログに追加する"""
self.commands_log.append(key_name)
def execute(self):
"""最新のキー名に応じて、具体的なアクションをキックする"""
if not self.commands_log:
return
last_key = self.commands_log[-1] # 直近に追加されたキーを取得
if last_key == "left":
self.receiver.left()
elif last_key == "right":
self.receiver.right()
elif last_key == "up":
self.receiver.up()
elif last_key == "down":
self.receiver.down()
def undo(self):
"""取り消し:ログから最後の1件を削除し、逆演算を命令する"""
if self.commands_log:
removed_key = self.commands_log.pop()
print(f"[Macro] {removed_key} の操作を取り消します。")
self.receiver.undo_action()
else:
print("[Macro] 取り消すログが存在しません。")
# ==========================================
# ユーザーのキー入力を仲介する(Invoke)
# ==========================================
class KeyInvoker:
"""ユーザからのキー入力を受け取り、セットした情報部員にコマンドの実行を依頼する"""
def __init__(self):
self.macro_command = None
def setCommand(self, macro_command: MacroCommand):
"""操作対象となる情報部員をセットする"""
self.macro_command = macro_command
def press_key(self, key):
"""ユーザがキーボードから指令を送る(left, right, up, down, Z)"""
if not self.macro_command:
print("コマンドがセットされていません。")
return
if key == "Z":
# Zキーなら、マクロに対して取り消し(undo)を要求する
self.macro_command.undo()
else:
# それ以外の移動キーなら、履歴に追加(append)してから実行(execute)を依頼する
self.macro_command.append(key)
self.macro_command.execute()
# ==========================================
# 【作動テスト】ユーザのキーボード入力をシミュレート
# ==========================================
print("=== Command History Algorithm: Start ===\n")
# 1. 各系統のインスタンス化と連携の構築
Me = Avator() # 受取系アバターのインスタンスをMeとする。
my_macro = MacroCommand(Me) # Meを雇う情報部員my_macroインスタンス。
keyboard = KeyInvoker() # 呼出系のインスタンスをKeybordとする。
keyboard.setCommand(my_macro) # 呼出系のKeybordは情報部員を持つ。
# 2. ユーザのキーボード操作(指令の送信)
print("--- 1. 自分の分身を矢印キーで動かす ---")
keyboard.press_key("right")
keyboard.press_key("up")
keyboard.press_key("right")
keyboard.press_key("right")
keyboard.press_key("right")
keyboard.press_key("up")
keyboard.press_key("up")
keyboard.press_key("left")
print("\n--- 2. Zキーで操作を取り消してみる(Undo) ---")
keyboard.press_key("Z") #
keyboard.press_key("Z") #
keyboard.press_key("Z") #
keyboard.press_key("Z") #
[OUT]
=== Command History Algorithm: Start ===
--- 1. 自分の分身を矢印キーで動かす ---
[Action] Right: 自分の分身が右に動きました。現在地: [1, 0]
[Action] Up: 自分の分身が上に動きました。現在地: [1, 1]
[Action] Right: 自分の分身が右に動きました。現在地: [2, 1]
[Action] Right: 自分の分身が右に動きました。現在地: [3, 1]
[Action] Right: 自分の分身が右に動きました。現在地: [4, 1]
[Action] Up: 自分の分身が上に動きました。現在地: [4, 2]
[Action] Up: 自分の分身が上に動きました。現在地: [4, 3]
[Action] Left: 自分の分身が左に動きました。現在地: [3, 3]
--- 2. Zキーで操作を取り消してみる(Undo) ---
[Macro] left の操作を取り消します。
[Undo] Z-Key: 1つ前の位置に戻しました。現在地: [4, 3]
[Macro] up の操作を取り消します。
[Undo] Z-Key: 1つ前の位置に戻しました。現在地: [4, 2]
[Macro] up の操作を取り消します。
[Undo] Z-Key: 1つ前の位置に戻しました。現在地: [4, 1]
[Macro] right の操作を取り消します。
[Undo] Z-Key: 1つ前の位置に戻しました。現在地: [3, 1]
1.状態クラスはすごろく化だ
<状態クラスは主客逆転のすごろく化>
イベント駆動の開発が必要なときに、
あまりにもイベント中心思考となり、
状態を従となる制約条件としてみてしまうと、
大変なことが起きることがよくあります。
例えばイベントがX,Y,Zの4つあり、
それに連動して変わるstateがa,b,c,dの4つがあり、
メッセージm1,m2,m3,m4,m5,m6がある。
ゲームマシンContextの動作をイベント駆動でかくと、
イベントX{
if state==a{m1}
elif state==b{
m2
state =c }
elif state==c{m3}
elif state==d{
m4
state=a}
elif state==e{m5}
}
イベントY{
if state==a{
m2
state=b}
}
elif state==b{m3}
elif state==c{
m4
state=d}
elif state==d{m5}
elif state==e{m6}
}
イベントZ{
if state==a{m3}
elif state==b{m4}b-
elif state==c{
m5
state=a}
elif state==d{m6}
elif state==e{m1}
}
となったする。すごい大量のコードだ。
ここで逆転の発想をしましょう。
これを状態を中心に書き直してみよう。
状態a,b,c,dをクラスにしてStateA,StateB.StateC,StateDとする。
ゲームマシンContextは状態クラスのインターフェースIStateを持ち、
その子クラスが上にある4つのクラスだ。
Contextは3つのイベントX,Y,Zに対応するリクエストreqX,reqY,reqZができる。
これを受け取るとIStateはevntX,evntY,evntZ持つ。
全部はかかないけれど、StateAクラスのコードは簡単だ。
最初のイベント内のコードのif文の1行目から、
StateA{evntX{m1}; evntY{m2;changeState(stateB)};evntZ{m3}}
if文が消えたね。
そして、Contextクラスは現在の状態を格納する変数self.stateがある。
そうすることで、状態クラスをA→B→C→A, D→Aと推移することで、
Contextが移譲する状態子クラスが変化し、それに対応してイベントに対するメッセージが変わる。
このように、状態を中心に逆転した書き方をするだけでカンタンなるけれど、
状態をクラスにしたことで、移譲する子クラスを変えるという表現にすることで、
ゲームマシンコンテキストの状態推移が手にとるようにわかる。
記述も簡単になる。
状態たちを〇がこみして、そのときの推移を線でつなぐ。
外からイベントがやってきて、ただのメッセージで終わることもあれば、次の〇にすすめることもある。
まるで「すごろく」だね。
状態クラスを作り、状態つまりマスは次に進むだけです。
状態の子クラスは問題空間Contextで決められるし、あとで変更・追加も簡単にできるでしょう。
ストラテジーパタンのようには、持っている(移譲した)コメンテータを適当に選ぶことはできません。
でも、複数の状態を持てる(移譲する)ならば、それを渡る歩くStateTravelができます。
移譲でうまく切り替えるという意味では共通してますね。
<状態クラスの実装例>
# State.py
# ==========================================
# 状態の共通規格(インターフェース)
# ==========================================
class IState:
"""各状態(すごろくのマス目)が処理すべきイベント(X, Y, Z)の規格"""
def evntX(self, context): raise NotImplementedError()
def evntY(self, context): raise NotImplementedError()
def evntZ(self, context): raise NotImplementedError()
# ==========================================
# ゲームマシン(コンテキスト)
# ==========================================
class GameMachineContext:
"""状態を保持し、外からのリクエストを現在の状態へとリレーする"""
def __init__(self):
self.state = StateA() # 初期状態はマスA (state_a)
def change_state(self, new_state: IState):
"""状態を次のマス(子クラス)へと差し替えて推移(相転移)する"""
self.state = new_state
def reqX(self): self.state.evntX(self) # イベントXの受付
def reqY(self): self.state.evntY(self) # イベントYの受付
def reqZ(self): self.state.evntZ(self) # イベントZの受付
# ==========================================
# 状態の子クラス群(すごろくの各マス目)
# ==========================================
class StateA(IState):
"""状態aのマス:イベントYが来たら、m2を出してマスBへ進むルールに進化"""
def evntX(self, context): print("[Message] m1 (StateAのまま)")
def evntY(self, context):
print("[Message] m2 ➔ [Transit] マスA から マスB へ進みます。")
context.change_state(StateB()) # A ➔ B への推移
def evntZ(self, context): print("[Message] m3 (StateAのまま)")
class StateB(IState):
"""状態bのマス:イベントXが来たら、m2を出してマスCへ進む"""
def evntX(self, context):
print("[Message] m2 ➔ [Transit] マスB から マスC へ進みます。")
context.change_state(StateC()) # B ➔ C への推移
def evntY(self, context): print("[Message] m3 (StateBのまま)")
def evntZ(self, context): print("[Message] m4 (StateBのまま)")
class StateC(IState):
"""状態cのマス:イベントYならdへ進み、イベントZならaへ戻る"""
def evntX(self, context): print("[Message] m3 (StateCのまま)")
def evntY(self, context):
print("[Message] m4 ➔ [Transit] マスC から マスD へ進みます。")
context.change_state(StateD()) # C ➔ D への推移
def evntZ(self, context):
print("[Message] m5 ➔ [Undo/Transit] マスC から マスA へ戻ります!")
context.change_state(StateA()) # C ➔ A への引き戻し(StateTravel)
class StateD(IState):
"""状態dのマス:イベントXが来たら、最初のマスAへと引き戻される"""
def evntX(self, context):
print("[Message] m4 ➔ [Undo/Transit] マスD から 最初のマスA へ戻ります!")
context.change_state(StateA()) # D ➔ A への引き戻し(StateTravel)
def evntY(self, context): print("[Message] m5 (StateDのまま)")
def evntZ(self, context): print("[Message] m6 (StateDのまま)")
class StateE(IState):
"""状態eのマス:別の世界線"""
def evntX(self, context): print("[Message] m5 (StateEのまま)")
def evntY(self, context): print("[Message] m6 (StateEのまま)")
def evntZ(self, context): print("[Message] m1 (StateEのまま)")
# ==========================================
# 【作動テスト】ゲームマシンの状態すごろくをトラベルする
# ==========================================
print("=== State Traveler Algorithm: Start ===\n")
# 1. コンテキスト(ゲームマシン)の起動
machine = GameMachineContext()
print("--- 1. マスA:イベントYをトリガーして、正当にマスBへ進む ---")
machine.reqX() # マスAのまま(m1)
machine.reqY() # マスA ➔ マスB へ!
print("\n--- 2. マスB:イベントXをトリガーして、自動的にマスCへ進む ---")
machine.reqY() # マスBのまま(m3)
machine.reqX() # マスB ➔ マスC へ!
print("\n--- 3. マスC:イベントYをトリガーして、自動的にマスDへ進む ---")
machine.reqX() # マスCのまま(m3)
machine.reqY() # マスC ➔ マスD へ!
print("\n--- 4. マスD:イベントXをトリガーして、最初のマスAへと自動で「戻る」 ---")
machine.reqY() # マスDのまま(m5)
machine.reqX() # マスD ➔ 最初のマスA へ引き戻し!
[OUT]
=== State Traveler Algorithm: Start ===
--- 1. マスA:イベントYをトリガーして、正当にマスBへ進む ---
[Message] m1 (StateAのまま)
[Message] m2 ➔ [Transit] マスA から マスB へ進みます。
--- 2. マスB:イベントXをトリガーして、自動的にマスCへ進む ---
[Message] m3 (StateBのまま)
[Message] m2 ➔ [Transit] マスB から マスC へ進みます。
--- 3. マスC:イベントYをトリガーして、自動的にマスDへ進む ---
[Message] m3 (StateCのまま)
[Message] m4 ➔ [Transit] マスC から マスD へ進みます。
--- 4. マスD:イベントXをトリガーして、最初のマスAへと自動で「戻る」 ---
[Message] m5 (StateDのまま)
[Message] m4 ➔ [Undo/Transit] マスD から 最初のマスA へ戻ります!
3.Mementoクラスは没入型の思い出写真
<Mementoクラスは思い出写真>
Commandはユーザの入力記録を追加してから、画面に表示していました。
呼び出し係が情報部員を持ち、情報部員が行動係を持ち、という移譲の連鎖で
2種の情報スタックの最上部が1つPopされて、記憶も画面も1つ戻るという仕組みでした。
これに対して移譲とは言っても、
状態を連続的に丸ごと記録したり、すべてを公開するのはしたくない場合があるでしょう。
2つの素数の積を乱数で表示するアプリRandN_Appがあるとします。
このアプリはplayボタンをおすたびに、2素数のかけ算の式と答えで表示します。
たとえば、2×17=34、11×13=143、23×31=713、31×127=3937、…のように。
丸ごと記録ではなく、パシャとスナップ写真のように、
状況・画面を作る変数だけを状態のインスタンスとして記録するのが思い出Mementoです。
だから、713が素敵だと思ったら。createMementoコマンドでパシャと(23,31)を保存したとします。
素敵な713を思い出したいと思ったらrestoreMementoコマンドを選ぶと、23×31=713が表示される。
Mementoクラスは没入型の思い出写真。
時が過ぎても記録しておけば、その時の状態が丸ごと再現できるわけです。
ゲームとかならば、その時の状態からリプレーできたりするので、うれしい機能ですね。
細かくいうと、Mementoクラスの他に、
情報の中身は知らずに保存と呼び出しをするお世話係Caretakerクラスを用意します。
Mementoは1枚だけのアルバムの写真1枚としてのインスタンスで使われます。主体性はありません。
アプリRandN_Appが写真として作り、再現します。ユーザはCaretakerが持っているアルバムに写真を入れたり、出したりするという構図によって、情報が守られているのです。
<Mementoクラスの実装例>
# Memento.py
import random
# ==========================================
# 【スナップ写真】思い出アルバムの1枚(Memento)
# ==========================================
class Memento:
"""内部の秘密の変数(2つの素数)を外に漏らさず凍結保存するカプセル"""
def __init__(self, p1, p2):
self._p1 = p1 # 外部から直接書き換えられないように私的変数にする
self._p2 = p2
def get_saved_primes(self):
"""写真の現像:保存された素数のペアを返す"""
return self._p1, self._p2
# ==========================================
# 【アプリ本体】写真機能のある、2つの素数の積を表示するマシン(Originator)
# ==========================================
class RandN_App:
"""乱数で素数算をつくる。自身の写真を撮ったり、写真から復元する"""
def __init__(self):
self.p1 = 2
self.p2 = 3
self.prime_list = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 127] # 候補の素数たち
def play(self):
"""時間を進める:ランダムに2つの素数を選んで式と答えを表示"""
self.p1 = random.choice(self.prime_list)
self.p2 = random.choice(self.prime_list)
print(f"{self.p1} × {self.p2} = {self.p1 * self.p2}")
def createMemento(self) -> Memento:
"""カシャッ!とスナップ写真を撮影してMementoインスタンスを作る"""
print(f"[Save] 現在の {self.p1}×{self.p2} の瞬間をアルバムに記録しました。")
return Memento(self.p1, self.p2)
def restoreMemento(self, memento: Memento):
"""アルバムから写真を1枚選び、その時の世界線へ没入ワープする"""
self.p1, self.p2 = memento.get_saved_primes()
print(f"[Restore] アルバムから復元しました!")
print(f" {self.p1} × {self.p2} = {self.p1 * self.p2}")
# ==========================================
# 【アルバム帳とお世話係】(Caretaker)
# ==========================================
class AlbumKeeper:
"""[情報管理系] Mementoの中身には一切触れず、ただ写真を預かるだけの係"""
def __init__(self):
self.photo_album = {} # お気に入りの名前をつけて保存する辞書
def add_photo(self, label, memento: Memento):
self.photo_album[label] = memento
def get_photo(self, label) -> Memento:
return self.photo_album.get(label)
# ==========================================
# 【作動テスト】思い出アルバムによる時空ワープ
# ==========================================
print("=== Memento Album Algorithm: Start ===\n")
# 1. アプリとアルバム帳の準備
app = RandN_App()
album = AlbumKeeper()
print("--- 1. アプリを何回か遊ぶ ---")
app.play()
app.play() # 偶然素敵な数式が出たとしましょう
print("\n--- 2. おっ、お気に入りの数式が出たのでカシャッと保存! ---")
# ここで 23 × 31 = 713 が出たと仮定して、アルバムに「素敵」という名前で保存
saved_photo = app.createMemento()
album.add_photo("素敵", saved_photo)
print("\n--- 3. さらに時間は過ぎ、全く別の数式へ移り変わる ---")
app.play()
app.play()
app.play()
print("\n--- 4. あの時の「素敵」な思い出の数式にワープする(Restore) ---")
target_photo = album.get_photo("素敵")
if target_photo:
app.restoreMemento(target_photo)
[OUT]やるたびに中身は変わります。
=== Memento Album Algorithm: Start ===
--- 1. アプリを何回か遊ぶ ---
23 × 23 = 529
7 × 43 = 301
--- 2. おっ、お気に入りの数式が出たのでカシャッと保存! ---
[Save] 現在の 7×43 の瞬間をアルバムに記録しました。
--- 3. さらに時間は過ぎ、全く別の数式へ移り変わる ---
47 × 17 = 799
127 × 13 = 1651
47 × 127 = 5969
--- 4. あの時の「素敵」な思い出の数式にワープする(Restore) ---
[Restore] アルバムから復元しました!
7 × 43 = 301
振り返りをかねて、つぎの課題にとりくもう。
課題:フィボナッチ数列を使って3つのパタンを擬人化することで、探求のヒントをつかもう。
フィボナッチ数列が素数Pの倍数になる番号をみつけたいとする。
フィボナッチ数のM番目までのリストがあるとする。fibM={1,1,2,3,5,8,13,21,34,55,89,....}
コマンド君は、戻るとはいってもただの消去だから、基本的にリストに追加して記録するのが得意技だ。
ひたすらMを増やして周期を見つけることはできるだろう。
P=2なfibMの剰余はrem={1,1,0,1,1,0,....}となり、周期3が0 mod2となる。
ステート君は、計算結果を状態として、状態を主に考える逆算思考ができる。
だから、剰余が一度0になると、次は剰余が0の前の剰余xと同じになり、その次は0+x=xとなるから、
2周目目の剰余は1周目のx倍になるだけだから、調べなくても周期的になることに気づくだろう。
p=3での剰余になおすと、
最初の4つが1,1,2,0だから、次の4つは2倍して2,2,4,0,つまり、2,2,1,0になることを予想して
確かめられた。rem={1,1,2,0,2,2,1,0,....}となり、周期4の倍数番目が0 mod3となる。
メメント君は、剰余が0になるときの番号を取りためることで、kを色々変化させてデータをアルバムに追加する。
そのために、剰余が0になる番号のリストを作りアルバム化することに気づく。
素数P={2,3,5,7,11,13,17,19,23,...}に対してfibMがPの倍数になる周期cycleを作ることにした。
cycle2=3
cycle5=5
cycle7=8
p=5のときの周期は5だけど、
それ以外ではp+1またはp-1になることに気がつくかもしれないと気づくかもしれない。
そこで、ウォールの定理の存在につながっていく。
kの倍数は最初に0(modk)となる番号p(k)を周期にして現れます。
kが5以外の素数なら、kの倍数の周期p(k)は、p-1,p+1またはその約数になる。
これを参考に探求のきっかにしてみよう。
M=slider(1,100,1)
fibM=IterationList(a+b,a,b,{1,1},M)={1,1,2,3,5,8,13,21,34,55,89,....}
p={2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53}
n=slider(1,Length(p),1)
P=p(n)とすると、n番目の素数がPとなる。
rem=Sequence(Mod(fibM(s),P),s,1,M)
plus=DivisorsList(P-1)
minus=DivisorsList(P+1)
a=cycle ∈ plus
b=cycle ∈ plus
c=a ∨ b
text1="rem="+rem+
"//素数Pの倍数の周期はcycle="+cycle+
"//cycleはP+1の約数"+plus+"か
//P-1の約数"+minus+"の中にあるか。\\"+c
ウォールの定理からcはいつもtrueを表示するでしょう。
text2="素数"+P+"の倍数分布"
text3=fibM
text3があると目や暗算でも確かめやすくなりますね。