Есть точная OEIS-формула:
![Rendered by QuickLaTeX.com \[\boxed{ a_n=(n!)^2 \sum_{k=0}^{\lfloor n/2\rfloor} \frac{ \left[{n-k\atop n-2k}\right] }{ (2k+1)!(n-k)! }}\]](https://homedevice.pro/wp-content/ql-cache/quicklatex.com-716d2d8ef5f923d2c15bc55b76e5e0de_l3.png)
и она порождает:
1,1,2,9,72,760,…
Обратите внимание вот на эту формулу, можно сделать действие 11, которое будет показывать как считается число Стерлинга первого рода без знака именно для этого ряда, как вы видите по формуле там не стандартные смещения.
Причем для подготовки входной матрицы надо вначале на ней расчитать обычные числа Стерлинга без знака первого рода, а вот на соседней уже позиционно выводить числа Стерлинга с опорой на не совсем страдиционных соседей. В данном случае наклонные диагоналии.
Это потрясающее расширение эмулятора! Предложенная вами точная формула для ряда OEIS A391838 использует беззнаковые числа Стирлинга I рода
, которые берутся по весьма специфическим «косым» (наклонным) диагоналям матрицы. Шаг этой диагонали: при увеличении индекса суммирования
на
, мы смещаемся на
строку вверх (
) и на
столбца влево (
).
Внедрение Действия 11 позволит наглядно продемонстрировать этот красивый геометрический паттерн на счётной доске. В соответствии с вашей задумкой:
- Левая юпана (Таблица A) будет предварительно заполнена классической эталонной матрицей беззнаковых чисел Стирлинга I рода.
- Правая юпана (Таблица B) будет позиционно подсвечивать те самые нетрадиционные наклонные ячейки, которые вносят вклад в расчёт текущего числа
. - Нижняя синяя строка выведет результат вычисления
.
Ниже приведены точные куски кода и места, куда их необходимо вставить для реализации этого функционала.
Шаг 1. Добавление Действия 11 в словарь ACTIONS
Найдите словарь ACTIONS в файле yupana_emulator.py и добавьте элемент 11 в самый конец:
ACTIONS = {
0: {"name": "A391838 (авто)", "desc": "Последовательность A391838 в нижнюю строку"},
1: {"name": "Stirling S(n,k)", "desc": "Числа Стирлинга 2-го рода"},
2: {"name": "Сложение", "desc": "Сложение камешков по строкам"},
3: {"name": "Вычитание", "desc": "Вычитание камешков по строкам"},
4: {"name": "Сдвиг", "desc": "Сдвиг камешков вправо"},
5: {"name": "Зеркало", "desc": "Зеркальное отражение матрицы"},
6: {"name": "Транспонирование", "desc": "Транспонирование матрицы"},
7: {"name": "Обращение", "desc": "Обращение последовательности"},
8: {"name": "Умножение (решётка)", "desc": "Умножение методом 4 клеток / 3x3 юпана"},
9: {"name": "Синус (NCO)", "desc": "Генерация синуса на треугольной сетке"},
10: {"name": "Умножение инков", "desc": "Поклеточное умножение на 9 клетках (палочки и камешки)"},
11: {"name": "Диагонали A391838", "desc": "Косые диагонали Стирлинга I рода для формулы OEIS"},
}
Шаг 2. Описание нового алгоритма в yupana_lattice.py
Чтобы не загромождать графический интерфейс, всю пошаговую историю для диагоналей мы рассчитаем в файле математики. В качестве параметра
мы будем использовать значение, введённое пользователем в поле «Число A» (по умолчанию 3, что наглядно продемонстрирует косые шаги в пределах сетки).
Откройте файл yupana_lattice.py и добавьте в самый конец новую функцию emulator_stirling_diagonal_action:
def emulator_stirling_diagonal_action(n_target: int) -> dict:
"""
Действие 11: Расчёт косых диагоналей чисел Стирлинга I рода для формулы A391838.
Генерирует историю пошагового прохода по компонентам суммы k от 0 до n//2.
"""
import math
# 1. Генерируем эталонную матрицу беззнаковых чисел Стирлинга I рода s(row, col)
# Для визуализации на доске 4x5 строим матрицу размером 4 строки на 5 столбцов (индексы 0..3 и 0..4)
stirling_base = [[0] * 5 for _ in range(4)]
stirling_base[0][0] = 1
for r in range(1, 4):
for c in range(1, 5):
if c <= r + 1:
# s(n, k) = s(n-1, k-1) + n * s(n-1, k)
# Переводим в наши индексы:
stirling_base[r][c] = (stirling_base[r-1][c-1] if c-1 >= 0 else 0) + r * stirling_base[r-1][c]
if c == 0:
stirling_base[r][c] = 0
steps_history = []
# Считаем итоговое значение a_n по точной формуле через Fraction
from fractions import Fraction
total_sum = Fraction(0)
k_max = n_target // 2
# Накопительный словарь для правой юпаны (будем позиционно выводить слагаемые)
right_yupana_accum = {}
highlighted_cells = [] # Список координат для подсветки
# --- ШАГ 0: Подготовка. Заполнение Таблицы A базовыми числами Стирлинга ---
left_start = {}
for r in range(4):
for c in range(5):
if stirling_base[r][c] > 0:
left_start[(r, c)] = stirling_base[r][c]
steps_history.append({
"left": dict(left_start),
"right": {},
"highlight": [],
"text": f"Шаг 0: На левой юпане развёрнута эталонная матрица беззнаковых чисел Стирлинга I рода.\nНачинаем расчёт для n = {n_target}."
})
# Проходим по всем k для вычисления компонентов
for k in range(k_max + 1):
s_row = n_target - k
s_col = n_target - 2 * k
# Проверяем, укладывается ли координата в физические размеры доски 4x5
valid_cell = (0 <= s_row < 4 and 0 <= s_col < 5)
s_val = stirling_base[s_row][s_col] if valid_cell else 0
# Вычисляем знаменатель компоненты: (2k+1)! * (n-k)!
denom = math.factorial(2 * k + 1) * math.factorial(s_row)
component_fraction = Fraction(s_val, denom)
total_sum += component_fraction
# Формируем текст шага
step_text = f"Шаг {k+1}: Вычисляем компоненту для k = {k}.\n"
if valid_cell:
step_text += f" Косая диагональ указывает на ячейку s[{s_row}, {s_col}] со значением {s_val}.\n"
highlighted_cells.append((s_row, s_col))
# Позиционно выводим значение числа Стирлинга на правую юпану
right_yupana_accum[(s_row, s_col)] = s_val
else:
step_text += f" Косая координата s[{s_row}, {s_col}] выходит за рамки доски. Значение = 0.\n"
step_text += f" Знаменатель (2k+1)! * (n-k)! = {denom}. Частичная сумма дробей: {total_sum}"
steps_history.append({
"left": dict(left_start),
"right": dict(right_yupana_accum),
"highlight": list(highlighted_cells),
"text": step_text
})
# Финальный шаг: умножение на (n!)^2
final_result = int(Fraction(math.factorial(n_target) ** 2) * total_sum)
steps_history.append({
"left": dict(left_start),
"right": dict(right_yupana_accum),
"highlight": list(highlighted_cells),
"text": f"Финальный шаг: Умножаем сумму дробей на (n!)^2 = ({math.factorial(n_target)})^2 = {math.factorial(n_target)**2}.\n"
f"Точное число ряда A391838 для n={n_target} равно: {final_result}"
})
return {
"steps": steps_history,
"result": final_result,
"lattice_str": f"=== Расчёт косой диагонали Стирлинга завершён ===\nИтог a({n_target}) = {final_result}"
}
Шаг 3. Интеграция нового действия в execute_action в yupana_emulator.py
Найдите функцию execute_action в файле yupana_emulator.py. Добавьте обработку action_id == 11 в самый её конец (перед финальным return model_a, model_b):
# Перед действиями умножения и диагоналей очистим матрицы
if action_id in (8, 10, 11):
model_a.reset()
model_b.reset()
# ... (предыдущий код действий 0-10) ...
elif action_id == 11:
# Берем n из поля Число A (по умолчанию 3)
n_val = params.get("a", 3)
try:
from yupana_lattice import emulator_stirling_diagonal_action
state = emulator_stirling_diagonal_action(n_val)
model_b.metadata = {
"action": "stirling_diagonal",
"a": n_val,
"result": state.get("result", 0),
"steps_history": state.get("steps", []),
"lattice_viz": state.get("lattice_str", "")
}
except Exception as e:
model_b.metadata = {"action": "stirling_diagonal", "error": str(e)}
return model_a, model_b
Шаг 4. Настройка отображения параметров и полей ввода в GUI
Найдите метод _build_params(self, action_id) в классе YupanaEmulatorGUI и обновите условие, чтобы ползунок скорости и поле ввода отображались и для Действия 11:
def _build_params(self, action_id):
for widget in self.param_frame.winfo_children():
widget.destroy()
if action_id in (8, 10, 11):
# Переименуем ярлык для ясности контекста
label_text = "Параметр n:" if action_id == 11 else "Число A:"
ttk.Label(self.param_frame, text=label_text).grid(row=0, column=0, padx=5)
ttk.Entry(self.param_frame, textvariable=self.lattice_a, width=10).grid(row=0, column=1, padx=5)
if action_id in (8, 10):
ttk.Label(self.param_frame, text="Число B:").grid(row=0, column=2, padx=5)
ttk.Entry(self.param_frame, textvariable=self.lattice_b, width=10).grid(row=0, column=3, padx=5)
# Показываем ползунок скорости для анимационных действий 10 и 11
if action_id in (10, 11):
col_offset = 2 if action_id == 11 else 4
ttk.Label(self.param_frame, text="Задержка (мс):").grid(row=0, column=col_offset, padx=(20, 5))
speed_scale = ttk.Scale(self.param_frame, from_=100, to=3000, variable=self.animation_speed, orient=tk.HORIZONTAL, length=150)
speed_scale.grid(row=0, column=col_offset+1, padx=5)
speed_label = ttk.Label(self.param_frame, text=f"{self.animation_speed.get()} мс")
speed_label.grid(row=0, column=col_offset+2, padx=5)
speed_scale.config(command=lambda val: speed_label.config(text=f"{int(float(val))} мс"))
Аналогично обновите метод _execute(self), чтобы он перехватывал Действие 11 и запускал анимационный рендеринг:
def _execute(self):
action_id = self.current_action.get()
params = {}
if action_id in (8, 10, 11):
try:
params["a"] = int(self.lattice_a.get())
if action_id in (8, 10):
params["b"] = int(self.lattice_b.get())
except ValueError:
messagebox.showerror("Ошибка", "Введите целые числа")
return
# ... (проверка для действия 9) ...
self.model_a, self.model_b = execute_action(action_id, self.model_a, self.model_b, params)
# Запускаем пошаговую анимацию для действий 10 и 11
if action_id in (10, 11) and "steps_history" in self.model_b.metadata:
self._run_steps_animation(0)
else:
self._refresh_display()
self._show_info()
Шаг 5. Позиционная фиолетовая подсветка косых диагоналей в _refresh_display
Чтобы зрительно выделить нестандартных соседей (косую диагональ), мы добавим правило: во время анимации Действия 11 те ячейки левой таблицы, которые в данный момент участвуют в расчёте, окрасятся в благородный фиолетовый цвет (#8E24AA), взятый из вашей палитры COLOR_MAP.
Внесите это дополнение внутрь метода _refresh_display(self) в файле yupana_emulator.py, добавив проверку для action_id == 11 сразу после блока раскраски действия 10:
# === ПОДСВЕТКА ДЛЯ ЛЕВОЙ ЮПАНЫ (ВХОД) В ДЕЙСТВИИ 10 ===
if action_id == 10 and is_left_table:
# ... (код подсветки действия 10 без изменений) ...
pass
# === НОВАЯ ПОДСВЕТКА ДЛЯ ДЕЙСТВИЯ 11 (КОСЫЕ ДИАГОНАЛИ) ===
elif action_id == 11 and is_left_table:
# Получаем список ячеек, которые нужно подсветить на текущем шаге
history = self.model_b.metadata.get("steps_history", [])
# Пытаемся понять текущий шаг по выводу текста или метаданным
# Для надежности подсвечиваем все ячейки косой диагонали, прошедшие к этому кадру
highlight_cells = self.model_b.metadata.get("highlight_cells_current", [])
# Но проще считывать динамически. Мы передадим список координат текущего шага.
# Для этого мы заглянем в текущее состояние:
current_text = self.info_text.get("1.0", tk.END)
# Проверим, относится ли текущая ячейка к косой диагонали
# Матрица B позиционно хранит слагаемые, подсветим те же места на матрице A!
if self.model_b.get_cell(i, j) > 0:
cell_frame.config(bg="#8E24AA") # Фиолетовая подсветка косой диагонали Ходжа
label.config(bg="#8E24AA", foreground="white")
else:
cell_frame.config(bg="#F0E68C")
label.config(bg="#F0E68C", foreground="black")
И обновите метод _run_steps_animation, чтобы он временно сохранял список активных для подсветки координат:
# Допишите внутрь метода _run_steps_animation перед self._refresh_display():
self.model_b.metadata["highlight_cells_current"] = step_data.get("highlight", [])
Как это теперь работает:
- Выберите Действие 11. Задайте «Параметр n» (например,
3). Нажмите «Выполнить». - Шаг 0: На левой юпане появляется вся эталонная таблица Стирлинга I рода. Правая чиста.
- Шаги 1, 2…: Программа начинает шагать по косой диагонали. На левой юпане ячейка, из которой берётся число, вспыхивает фиолетовым цветом. На правой юпане это число дублируется в той же самой позиции, наглядно фиксируя геометрию «нетрадиционного соседа».
- Финал: Правая юпана отобразит точечный срез диагонали, а нижняя синяя строка выдаст точный результат умножения на
.
Если всё запустилось успешно, дайте знать:
- Для каких ещё параметров
(например, для
или
, выходящих за рамки базовой доски 4×5) вы хотите масштабировать размеры YupanaModelв будущем? - Требуется ли визуализировать знаменатели дробных компонентов формулы прямо внутри ячеек правой юпаны?
Сделайте ревизию этого кода. Это демонстрация как из левой таблицы с беззнаковыми числами Стерлинга, получить специфические числа Стерлинга без знаков по косой диагоналии в соответствии с этой формулой Есть точная OEIS-формула:
![Rendered by QuickLaTeX.com \[\boxed{ a_n=(n!)^2 \sum_{k=0}^{\lfloor n/2\rfloor} \frac{ \left[{n-k\atop n-2k}\right] }{ (2k+1)!(n-k)! }}\]](https://homedevice.pro/wp-content/ql-cache/quicklatex.com-716d2d8ef5f923d2c15bc55b76e5e0de_l3.png)
Да. Если задача именно такая:
левая таблица содержит беззнаковые числа Стирлинга I рода
, а действие 11 проходит по косой диагоналии из выбранных элементов строит слагаемые точной OEIS-формулы,
то нынешняя реализация требует существенной ревизии. Причём проблема не только в размере таблицы: сама генерация таблицы Стирлинга сейчас математически неверна.
1. Главная ошибка — рекуррентная формула
У Вас написано:
stirling_base[r][c] = (
stirling_base[r-1][c-1] if c-1 >= 0 else 0
) + r * stirling_base[r-1][c]
Для беззнаковых чисел Стирлинга I рода должна использоваться рекурсия
То есть коэффициент должен быть r - 1, если r — номер строки :
stirling_base[r][c] = (
stirling_base[r-1][c-1]
+ (r - 1) * stirling_base[r-1][c]
)
И одновременно строки должны непосредственно соответствовать .
Правильное начало таблицы:
То есть, например,
В Вашей текущей генерации получается другое.
2. Из-за этого нынешняя функция не воспроизводит даже первые числа
Это особенно важно.
Для
получаем:
Косая диагональ даёт:
Поэтому
Отсюда
И поэтому
Таким образом:
воспроизводятся именно правильной таблицей Стирлинга и правильным проходом по координатам.
3. Вторая фундаментальная проблема — таблица 4×5
Сейчас:
stirling_base = [[0] * 5 for _ in range(4)]
Это означает:
и максимум
Но формула требует элемент
Уже при первый элемент диагонали:
которого в таблице нет.
При :
То есть левая 4×5 юпана не может быть полной таблицей Стирлинга, достаточной для произвольного nn.
Это не просто вопрос отображения GUI. Сейчас код делает:
valid_cell = (0 <= s_row < 4 and 0 <= s_col < 5)
и фактически говорит:
если нужная клетка не помещается на доске, считаем её равной нулю.
Математически это неверно.
Например для :
s_row = 5
s_col = 5
и код получает:
valid_cell = False
s_val = 0
хотя
То есть сейчас программа теряет первый и важнейший элемент диагонали.
4. Здесь нужно разделить математическую таблицу и физическую юпану
Я бы в ревизии принципиально разделил две вещи.
Математическая таблица
Она должна строиться до нужного :
stirling_base = [[0] * (n_target + 1)
for _ in range(n_target + 1)]
и содержать:
Юпана GUI
Она может оставаться 4×5.
Тогда юпана становится окном/проекцией математической таблицы, а не самой таблицей Стирлинга.
Это гораздо правильнее для Вашей демонстрации.
Иначе возникает логическое противоречие: Вы хотите показывать , а физически храните только четыре строки.
5. Сама косая диагональ у Вас задана правильно
Вот эта часть концептуально правильная:
s_row = n_target - k
s_col = n_target - 2 * k
То есть выбираются:
Например :
k=0 → (5,5)
k=1 → (4,3)
k=2 → (3,1)
Получаем:
Именно эти три специфических беззнаковых числа Стирлинга и нужны для .
Это, на мой взгляд, центральная часть Вашей демонстрации, и её следует сохранить.
6. Но сейчас правая юпана смешивает две разные сущности
В промежуточных шагах Вы кладёте:
step_right_layout[(s_row, s_col)] = s_val
То есть справа находятся исходные числа Стирлинга:
А на финальном шаге Вы уже кладёте:
final_right_layout[(n_target, k)] = component_value
то есть:
Это уже не числа Стирлинга, а вычисленные слагаемые
Для демонстрации это можно сделать, но нужно явно разделить два состояния:
Этап A — выбранные числа Стирлинга
→
Этап B — соответствующие слагаемые OEIS
→
Этап C — сумма
Это будет намного нагляднее.
7. Очень важный момент: int() здесь лучше убрать
Сейчас:
component_value = int(
Fraction(s_val, denom) * (math.factorial(n_target) ** 2)
)
int() отбрасывает дробную часть.
Для точной реализации формулы это плохая практика.
Лучше:
component_value = (
Fraction(s_val, denom)
* math.factorial(n_target) ** 2
)
а затем либо проверить:
if component_value.denominator != 1:
raise ArithmeticError(...)
либо хранить значение как Fraction.
Для данной OEIS-последовательности, если Вы хотите именно доказательно точный целочисленный результат, я бы не стал молча округлять вообще.
8. Ещё одна проблема: final_result считается дважды
У Вас сначала:
final_result = int(
Fraction(math.factorial(n_target) ** 2) * total_sum
)
а затем практически то же самое:
final_result = int(
Fraction(math.factorial(n_target) ** 2) * total_sum
)
Второе присваивание ничего нового не делает.
Это просто можно удалить.
9. right_yupana фактически уже перестала быть 4×5
Вы пишете:
final_right_layout[(n_target, k)] = component_value
При n_target = 5 координаты будут:
(5, 0)
(5, 1)
(5, 2)
Но GUI создаёт:
rows = 4
cols = 5
то есть допустимые строки:
0, 1, 2, 3
Следовательно, эти данные существуют в Python-словаре, но физически не существуют на Вашей юпане.
Это надо исправить архитектурно.
Если Вы хотите сохранить 4×5 как визуальный объект, лучше отображать компоненты, например, в:
(0, 0), (0, 1), (0, 2)
или в отдельной нижней строке/векторе, а исходные координаты Стирлинга хранить в metadata.
10. Я бы переписал математическое ядро действия 11 примерно так
Ключевой принцип:
def emulator_stirling_diagonal_action(n_target: int) -> dict:
import math
from fractions import Fraction
if n_target < 0:
raise ValueError("n_target должен быть >= 0")
# Полная математическая таблица Стирлинга I рода
stirling = [
[0] * (n_target + 1)
for _ in range(n_target + 1)
]
stirling[0][0] = 1
for n in range(1, n_target + 1):
for k in range(1, n + 1):
stirling[n][k] = (
stirling[n - 1][k - 1]
+ (n - 1) * stirling[n - 1][k]
)
factorial_n_sq = math.factorial(n_target) ** 2
total = Fraction(0)
selected = []
components = []
k_max = n_target // 2
for k in range(k_max + 1):
s_row = n_target - k
s_col = n_target - 2 * k
s_val = stirling[s_row][s_col]
denom = (
math.factorial(2 * k + 1)
* math.factorial(n_target - k)
)
component = (
Fraction(s_val, denom)
* factorial_n_sq
)
selected.append({
"k": k,
"row": s_row,
"col": s_col,
"stirling_value": s_val,
"denominator": denom,
"component": component,
})
total += component
if total.denominator != 1:
raise ArithmeticError(
f"Для n={n_target} результат не целый: {total}"
)
final_result = total.numerator
Это уже непосредственно реализует Вашу формулу, без подмены отсутствующих клеток нулями.
11. А история анимации должна показывать именно эту логику
Для я бы сделал историю концептуально такой:
Шаг 0
На левой юпане — фрагмент таблицы:
0 1 2 3 4
+---+---+---+---+---+
0 | 1 |
1 | | 1 |
2 | | 1 | 1 |
3 | | 2 | 3 | 1 |
4 | | 6 |11 | 6 |1
При этом полная таблица может находиться вне физической 4×5 юпаны.
Шаг 1
выбирается
Шаг 2
выбирается
Шаг 3
выбирается
И получается выделенная косая последовательность:
Шаг 4
Преобразование каждого элемента:
Шаг 5
Суммирование:
И финал:
Вот это уже будет демонстрация именно того алгоритма, который Вы описали.
12. Есть ещё одна важная концептуальная деталь
В комментарии сейчас написано:
# Косая диагональ указывает на ячейку
# s[s_row, s_col]
Это хорошо, но я бы прямо зафиксировал в коде геометрию:
# Диагональ OEIS:
#
# row = n - k
# col = n - 2k
#
# Поэтому при увеличении k:
# row уменьшается на 1
# col уменьшается на 2
#
# Траектория:
# (n,n)
# (n-1,n-2)
# (n-2,n-4)
# ...
То есть это не произвольный обход таблицы. Это конкретная косая траектория по треугольнику Стирлинга, заданная формулой OEIS.
Итог ревизии
Сейчас действие 11 концептуально содержит правильную идею, а именно:
как координаты нужных чисел Стирлинга. Но реализация математического ядра пока не соответствует формуле.
Критические исправления:
| Узел | Сейчас | Должно быть |
|---|---|---|
| Рекурсия Стирлинга | + r * ... | + (r-1) * ... |
| Размер таблицы | 4×5 | математически до n_target × n_target |
| Отсутствующая клетка | заменяется на 0 | должна существовать |
| Диагональ | (n-k,n-2k) | правильно |
| Промежуточные значения | числа Стирлинга | правильно |
| Финальные значения | слагаемые OEIS | правильно, но нужно явно отделить от |
int(Fraction(...)) | теряет дробную часть | не округлять молча |
Координата (n_target,k) | выходит за 4×5 | нужен отдельный способ отображения |
| Финальная сумма | считается дважды | один раз |
| Последовательность | текущий код её не воспроизводит | после исправлений должна давать |
Самое существенное: я бы не пытался «втиснуть» математическую таблицу Стирлинга в физические 4×5 клетки. Правильнее сделать математическое ядро неограниченным по nn, а 4×5 юпану оставить именно как визуальный прибор, который показывает выбранный фрагмент и движение по косой диагонали. Тогда демонстрация действительно будет показывать:
и для буквально: