Скачиваний:
0
Добавлен:
15.04.2026
Размер:
19.85 Кб
Скачать
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()
Соседние файлы в папке Programs