import collections
import pprint
import typing
def to_flatten_tuple(any_arg: typing.Union[int, str, tuple, list, set]) -> tuple:
"""
Преобразует аргумент any_arg в кортеж (для использования в качестве ключа сортировки состояний и пр.)
42 -> ('42',) | (42, 24) -> ('42', '24') | [4, ['2', '4'], 2] -> ('4', '2', '4', '2')
([6, [[5]]], ([4], 3), 2, ['one', ['0']]) -> ('6', '5', '4', '3', '2', 'one', '0')
:param any_arg: число / строка / кортеж / список
:return: кортеж
"""
import re
return tuple(re.findall(pattern=r'\w+', string=str(any_arg)))
class PairByFirst:
"""Класс "Пара значений". Уникальность по первому значению."""
def __init__(self, first=None, second=None):
self.first = first
self.second = second
def __hash__(self) -> int:
return hash(self.first)
def __eq__(self, other) -> bool:
return isinstance(other, PairByFirst) and self.first == other.first or self.first == other
def __str__(self) -> str:
return f"{self.first} | {self.second}"
def __repr__(self) -> str:
return repr(str(self))
def create_mealy_vending_machine(cost_of_goods: int, coins: set) -> (dict, str):
"""
Создает вендинговый автомат Мили
Выходные сигналы:
- -- не выдавать ничего
0 -- выдать товар без сдачи
N -- выдать товар и N рублей сдачи
:param cost_of_goods: стоимость товара
:param coins: множество стоимостей монет/купюр (множество чисел)
:return: (вендинговый автомат Мили, начальное состояние)
"""
transitions = sorted(coins) # список переходов
machine = dict() # автомат Мили
seen = set() # множество уже обработанных состояний
initial_state = 0 # начальное состояние
str_initial_state = str(initial_state) # начальное состояние в строковом виде
q = collections.deque([initial_state]) # очередь состояний (0 рублей -- начальное состояние)
while q: # пока очередь не пуста
state = q.popleft() # выбираем первый элемент очереди
if state in seen: # если состояние уже было обработано
continue # идем к следующему элементу в очереди
seen.add(state) # или добавляем текущее состояние в список обработанных и обрабатываем его
str_state = str(state) # текущее состояние в строковом виде
machine.setdefault(str_state, dict()) # добавляем словарь переходов по меткам, если не был добавлен
for t in transitions: # для каждого перехода
if state + t < cost_of_goods: # если сумма меньше стоимости товара
machine[str_state][PairByFirst(str(t), '-')] = str(state + t) # добавляем в автомат состояние
q.append(state + t) # добавляем в очередь состояние для последующей обработки
else: # если сумма больше или равна стоимости товара, то выдаем сдачу и возвращаемся в initial_state
machine[str_state][PairByFirst(str(t), str(state + t - cost_of_goods))] = str_initial_state
return machine, str_initial_state
def create_moore_vending_machine(cost_of_goods: int, coins: set, z_state: str = 'z') -> (dict, PairByFirst):
"""
Создает вендинговый автомат Мура
Сигналы состояний:
- -- не выдавать ничего
0 -- выдать товар без сдачи
N -- выдать товар и N рублей сдачи
z-переход означает, что сдача выдана и автомат должен перейти в начальное состояние
:param cost_of_goods: стоимость товара
:param coins: множество стоимостей монет/купюр (множество чисел)
:param z_state: обозначение z перехода
:return: (вендинговый автомат Мура, начальное состояние)
"""
transitions = sorted(coins) # список переходов
machine = dict() # автомат Мура
seen = set() # множество уже обработанных состояний
initial_state = (0, '-') # начальное состояние
pair_initial_state = PairByFirst(str(initial_state[0]), str(initial_state[1])) # начальное состояние в виде пары
q = collections.deque([initial_state]) # очередь состояний (0 рублей -- начальное состояние)
while q: # пока очередь не пуста
state = q.popleft() # выбираем первый элемент очереди
if state in seen: # если состояние уже было обработано
continue # идем к следующему элементу в очереди
seen.add(state) # или добавляем текущее состояние в список обработанных и обрабатываем его
pair_state = PairByFirst(str(state[0]), str(state[1])) # текущее состояние в виде пары
machine.setdefault(pair_state, dict()) # добавляем словарь переходов по меткам, если не был добавлен
for t in transitions: # для каждого перехода
if state[0] + t < cost_of_goods: # если сумма меньше стоимости товара
new_state = (state[0] + t, '-')
pair_new_state = PairByFirst(str(new_state[0]), str(new_state[1]))
machine[pair_state][str(t)] = pair_new_state # добавляем в автомат состояние
q.append(new_state) # добавляем в очередь состояние для последующей обработки
else: # если сумма больше или равна стоимости товара, то выдаем сдачу и возвращаемся в initial_state
pair_new_state = PairByFirst(str(state[0] + t), str(state[0] + t - cost_of_goods))
machine[pair_state][str(t)] = pair_new_state
machine.setdefault(pair_new_state, dict()).setdefault(z_state, pair_initial_state)
return machine, pair_initial_state
def simulate_vending_machine(machine: dict, initial_state: typing.Any, coins: list, z_state: str = 'z') -> list:
"""
Симулирует поведение вендингового автомата (Мура или Мили)
:param machine: вендинговый автомат (Мура или Мили)
:param initial_state: начальное состояние автомата
:param coins: список монет/купюр, принятых вендинговым автоматом (строки либо числа)
:param z_state: обозначение z перехода (для автомата Мура)
:return: список состояний с переходами (включая начальное и последнее состояние)
"""
state = initial_state # начинаем с начального состояния
states_and_alphas_list = [state] # заводим список состояний с метками переходов
for coin in coins: # для каждой монеты/купюры
transition = machine.get(state) # находим возможные переходы из текущего состояния
if transition is None: # если переходов нет, то ошибка
raise ValueError(f'Нет переходов из состояния {state}')
for alpha in (coin, str(coin), z_state): # для переходов coin / str(coin) / z_state
state = transition.get(alpha) # проверяем, возможен ли переход
if state is not None: # если да, то
alpha = next(a for a in transition if a == alpha) # переполучаем метку (необходимо для автомата Мура)
break # завершаем цикл
if state is None: # если нужный переход не был найден
raise ValueError(f'Номинал в {coin} не может быть принят')
states_and_alphas_list.append(alpha) # добавляем в список метку перехода
states_and_alphas_list.append(state) # и новое состояние
# если есть переход z_state, то сразу переходим
transition = machine.get(state)
if transition is not None: # если есть переходы
next_state = transition.get(z_state)
if next_state is not None: # если есть переход по метке z_state
states_and_alphas_list.append(z_state) # добавляем в список метку перехода
states_and_alphas_list.append(next_state) # и новое состояние
state = next_state # обновляем текущее состояние
return states_and_alphas_list
def to_string_states(fsm: dict, initial_states: typing.Any, final_states: typing.Any):
"""
Преобразует состояния и переходы в строки
:param fsm: конечный автомат, например, {'1': {'a': {'1', '2'}}, '2': {'a': '1'}}
:param initial_states: множество начальных состояний КА (элементы множества -- строки / кортежи)
либо одно начальное состояние (строка / кортеж)
:param final_states: множество заключительных состояний КА (элементы множества -- строки / кортежи)
либо одно заключительное состояние (строка / кортеж)
:return: (конечный автомат, множество начальных состояний, множество заключительных состояний)
"""
new_fsm = dict()
initial_states = set(map(str, initial_states)) if isinstance(initial_states, (set, list)) else {str(initial_states)}
final_states = set(map(str, final_states)) if isinstance(final_states, (set, list)) else {str(final_states)}
for state, alphas in fsm.items():
str_state = str(state)
new_fsm.setdefault(str_state, dict())
for alpha, alpha_states in alphas.items():
str_alpha = str(alpha)
new_fsm[str_state].setdefault(str_alpha, set())
if isinstance(alpha_states, (set, list)):
new_fsm[str_state][str_alpha].extend(set(map(str, alpha_states)))
else:
new_fsm[str_state][str_alpha].add(str(alpha_states))
return new_fsm, initial_states, final_states
def fsm_plot(filename: str, fsm: dict,
initial_states: typing.Union[set, tuple, str],
final_states: typing.Union[set, tuple, str]) -> None:
"""
Генерирует диаграмму конечного автомата и сохраняет результат в файл <filename>.png
:param filename: имя файла изображения без расширения
:param fsm: конечный автомат, например, {'1': {'a': {'1', '2'}}, '2': {'a': '1'}}
:param initial_states: множество начальных состояний КА (элементы множества -- строки / кортежи)
либо одно начальное состояние (строка / кортеж)
:param final_states: множество заключительных состояний КА (элементы множества -- строки / кортежи)
либо одно заключительное состояние (строка / кортеж)
:return: None
"""
import graphviz
G = graphviz.Digraph()
G.attr('graph', rankdir="LR", fontname="arial") # направление -- сверху вниз; шрифт Arial для графа
G.attr('node', fontname="arial") # шрифт Arial для состояния
G.attr('edge', fontname="arial") # шрифт Arial для дуги
G.node('start', None, {'shape': 'point'}) # начальная точка, из которой идут дуги в начальные состояния
initial_states = set(initial_states) if isinstance(initial_states, (set, list)) else {initial_states}
final_states = set(final_states) if isinstance(final_states, (set, list)) else {final_states}
for state in fsm:
# если состояние является конечным, то двойной круг, иначе обычный круг
G.attr('node', shape='doublecircle' if state in final_states else 'circle')
G.node(','.join(state) if isinstance(state, tuple) else state)
for state, alphas in fsm.items():
if state in initial_states:
G.edge(tail_name='start', head_name=state, label='') # из начальной точки в начальное состояние
# формируем словарь для соответствий "целевое состояние" -> "метки дуг, входящих в целевое состояние"
arcs = dict()
for alpha, alpha_states in alphas.items(): # просматриваем все метки переходов из state
if isinstance(alpha_states, (str, tuple)): # если alpha_states -- это одно состояние
alpha_states = {alpha_states} # заводим множество с одним состоянием
for alpha_state in alpha_states: # просматриваем множество
if state != alpha_state: # петли обрабатываем в конце
arcs.setdefault(alpha_state, set()) # по умолчанию -- пустое множество
arcs[alpha_state].add(alpha) # добавляем метку в словарь
# обрабатываем добавленные в arcs состояния
for alpha_state, alpha_state_alphas in arcs.items():
G.edge(tail_name=','.join(state) if isinstance(state, tuple) else state,
head_name=','.join(alpha_state) if isinstance(alpha_state, tuple) else alpha_state,
label='\n'.join(sorted(alpha_state_alphas, key=to_flatten_tuple))) # рисуем дугу
# обрабатываем петли (чтобы не было много дуг: делаем одну с нужными метками через запятую)
state_alpha_state = set() # множество всех меток петель
for alpha, alpha_states in alphas.items():
if isinstance(alpha_states, (str, tuple)): # если alpha_states -- это одно состояние
alpha_states = {alpha_states} # заводим множество с одним состоянием
for alpha_state in alpha_states: # просматриваем множество
if state == alpha_state: # если петля
state_alpha_state.add(alpha) # добавляем метку
break # искать дальше по той же метке смысла нет -- метку уже добавили
if state_alpha_state: # если петли есть
G.edge(tail_name=','.join(state) if isinstance(state, tuple) else state,
head_name=','.join(state) if isinstance(state, tuple) else state,
label='\n'.join(sorted(state_alpha_state, key=to_flatten_tuple))) # рисуем петлю
G.render(filename, format='png', cleanup=True)
def main():
variants = [
# варианты в виде (стоимость товара, список монет, эксперименты-примеры)
# 1 вариант; товар: 2 руб.; монеты: 1, 2, 5, 10
(2, [1, 2, 5, 10], [[1, 2, 5, 10], [1, 1], [5, 10]]),
# 2 вариант; товар: 5 руб.; монеты: 1, 2, 5
(5, [1, 2, 5], [[1, 2, 5], [2, 2]]),
# 3 вариант; товар: 250 руб.; купюры: 50, 100, 200
(250, [50, 100, 200], [[50, 100, 200], [100, 200], [100]]),
# 4 вариант; товар: 300 руб.; купюры: 100, 200, 500
(300, [100, 200, 500], [[100, 200, 500], [200, 200]]),
# дополнительный 5 вариант; товар: 100 руб.; купюры: 10, 50, 100
(100, [10, 50, 100], [[10, 50, 100], [10, 50, 10, 50], [10, 25]])
]
for i, (cost_of_goods, coins, examples) in enumerate(variants, 1):
mealy_vending_machine, initial_state = create_mealy_vending_machine(cost_of_goods, set(coins))
mvm, init, final = to_string_states(mealy_vending_machine, initial_state, set())
fsm_plot(f'{i}_mealy_vending_machine', mvm, init, final)
print(f'Вендинговый автомат Мили для {i} варианта:')
pprint.pprint(mealy_vending_machine)
for example in examples:
try:
state = simulate_vending_machine(mealy_vending_machine, initial_state, example)
print(f'{example} -> {state}')
except Exception as e:
print(str(example), '->', ', '.join(e.args))
print()
moore_vending_machine, initial_state = create_moore_vending_machine(cost_of_goods, set(coins))
mvm, init, final = to_string_states(moore_vending_machine, initial_state, set())
fsm_plot(f'{i}_moore_vending_machine', mvm, init, final)
print(f'Вендинговый автомат Мура для {i} варианта:')
pprint.pprint(moore_vending_machine)
for example in examples:
try:
state = simulate_vending_machine(moore_vending_machine, initial_state, example)
print(f'{example} -> {state}')
except Exception as e:
print(str(example), '->', ', '.join(e.args))
print()
if __name__ == "__main__":
main()