1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 4 из 7

Что игре нужно доказать, а что - проверить или ограничить

Коротко

Каждое обещание игроку закрывается одним из четырёх способов. Доказательство - когда область конечна или свойство следует из устройства: честность кубика проверяется перебором всех 256 значений генератора, и этот перебор показывает, что b % 6 даёт четвёрке 43 шанса из 256, а шестёрке 42 - 16,41% вместо 16,67%. Сертификат - когда ответ можно проверить быстрее, чем найти, как кратчайший путь в разделе 26. Стресс-тест - когда доказательства нет: он не доказывает, а снижает риск. Предел - когда задача неразрешима: завершится ли скрипт мода, заранее узнать нельзя, поэтому сервер даёт ему 300 команд и останавливает - и честный мод тоже.

Если ты для каждой гарантии знаешь, чем её закрыть - доказательством, проверкой, тестом или пределом, листай до «Теперь сам».

Как думаешь, честный ли кубик

Генератор случайных чисел отдаёт байт от 0 до 255, все значения равновероятны. Грань кубика программист получает как b % 6 + 1. Честный ли кубик? Как это проверить - бросить миллион раз?

Ответь себе до чтения. Миллион бросков покажет частоты около 16,4% и 16,7%, и разницу в две десятые процента легко списать на случайность. Но область здесь конечна - 256 байтов, - и её можно перебрать целиком. Перебор конечной области - это не опыт, а доказательство: он отвечает точно. 256 не делится на 6, поэтому четыре грани получают на один байт больше двух других.

На что это похоже

Сдача экзамена по билетам. Если билетов 30, а выучено 29, не нужно гадать, каковы шансы, - их можно пересчитать точно: 29 из 30. Гадать приходится, только когда билетов бесконечно много или их нельзя пересмотреть. Так же с гарантиями игры: где область обозрима, её пересчитывают; где нет - проверяют, где нельзя даже проверить - ставят предел.

Механизм: четыре способа закрыть обещание

обещание игроку способ почему так раздел
все острова достижимы доказательство устройством генератор строит остов - он связен по построению 9, 11
кубик честный доказательство перебором генератор отдаёт конечное число значений 20
путь отряда кратчайший сертификат расстояния и родители проверяются за V + E 26
экономика без дыр стресс-тест и поиск отрицательного цикла правила меняются каждый патч 11, 26
расстановка башен лучшая не обещаем задача NP-трудна - обещаем «хорошая» или гарантию приближения, если она доказана (раздел 22) 22
мод не повесит сервер предел шагов завершимость неразрешима 21

Порядок выбора такой. Сначала спроси, конечна ли и обозрима ли область, - тогда переберите её. Потом - следует ли свойство из устройства: остов связен всегда, доказывать надо один раз. Потом - можно ли проверить готовый ответ быстрее, чем найти: тогда сертификат на каждый ответ. Если ни то, ни другое - стресс-тест против медленного эталона. А если сама задача неразрешима или NP-трудна, честное обещание - не «найдём», а «остановим» или «найдём не хуже».

Правило

Для каждой гарантии игроку назови способ: доказательство (область конечна или свойство следует из устройства), сертификат (ответ проверяется быстрее, чем ищется), стресс-тест (ни того, ни другого) или предел (задача неразрешима). Гарантия без способа - обещание, которое никто не выполняет.

Разбор: честный кубик полным перебором

Генератор отдаёт байт b от 0 до 255. Перебираем все 256 значений и считаем, сколько байтов дают каждую грань, - для двух способов.

# Генератор отдаёт байт 0..255, каждый с вероятностью 1/256. Нужна грань
# кубика 1..6. Проверяем не опытом, а полным перебором всех 256 байтов:
# на конечной области перебор и есть доказательство.

def kak_chasto(brosok):
    grani = [0, 0, 0, 0, 0, 0]
    for b in range(256):
        g = brosok(b)
        if g > 0:
            grani[g - 1] = grani[g - 1] + 1
    return grani

def po_modulyu(b):
    return b % 6 + 1

def s_otkazom(b):
    # 252 = 6 · 42: байты 252..255 отбрасываем и бросаем заново;
    # 0 - знак «байт не годится, бросай заново»
    if b >= 252:
        return 0
    return b % 6 + 1

for imya, f in [["b % 6 + 1:   ", po_modulyu],
                ["с отказом:   ", s_otkazom]]:
    g = kak_chasto(f)
    vsego = 0
    for x in g:
        vsego = vsego + x
    print(imya, g, "| шестёрка", round(100 * g[5] / vsego, 2), "%")
print("честная грань:", round(100 / 6, 2), "%")
b % 6 + 1:    [43, 43, 43, 43, 42, 42] | шестёрка 16.41 %
с отказом:    [42, 42, 42, 42, 42, 42] | шестёрка 16.67 %
честная грань: 16.67 %

256 = 6 · 42 + 4: остаток от деления на 6 проходит по кругу 42 полных раза, и ещё четыре байта, 252-255, дают остатки 0-3. Поэтому грани 1-4 получают по 43 байта, а 5 и 6 - по 42. Это смещение по модулю: остаток от деления равномерного числа неравномерен, если размер области не делится на модуль. Лечение - выборка с отказом: байты 252-255 выбрасывают и бросают заново. Оставшиеся 252 байта делятся на 6 ровно, и каждая грань получает по 42. Перебор считает исходы одного броска; повторный бросок снова равномерен на тех же 252 значениях, поэтому итог после всех повторов распределён так же, как принятые байты. Повторный бросок нужен с вероятностью 4 / 256, то есть в среднем бросков 256 / 252 ≈ 1,016. Граница правила: смещение растёт, когда область мала относительно модуля. Генератор на три бита (0-7) с % 6 даёт граням 1 и 2 вдвое больше шансов, чем остальным.

Разбор: мод, предел и честная потеря

Моды - программы игроков на мини-языке. Сервер выполняет их каждый ход и не может ждать. Узнать заранее, завершится ли программа, нельзя: это проблема останова из раздела 21, и по теореме Райса оттуда же нельзя заранее узнать никакое нетривиальное свойство поведения программы. Остаётся предел - 300 команд. Предел на число команд ограничивает время, только если каждая команда дешёвая: удвоение единицы 300 раз даёт 2³⁰⁰ - число из 301 бита, и арифметика с ним уже не стоит одну операцию. Поэтому настоящий сервер ограничивает и размер чисел, или меряет время мода, а не команды.

# Мод - программа на мини-языке над одной переменной x. Сервер выполняет
# каждый мод каждый ход и не может ждать: у мода предел - 300 команд.
LIMIT = 300

def vypolnit(programma, x):
    # команды: ["+", k], ["*", k], ["//", k]; переходы к команде i:
    # ["если >", k, i], ["если ==", k, i], ["если чётно", i],
    # ["перейти", i]; ["стоп"]
    i = 0
    shagov = 0
    while programma[i][0] != "стоп":
        shagov = shagov + 1
        if shagov > LIMIT:
            return "остановлен на пределе, x = " + str(x)
        k = programma[i]
        if k[0] == "+":
            x = x + k[1]
        elif k[0] == "*":
            x = x * k[1]
        elif k[0] == "//":
            x = x // k[1]
        elif k[0] == "если чётно" and x % 2 == 0:
            i = k[1]
            continue
        elif k[0] == "если >" and x > k[1]:
            i = k[2]
            continue
        elif k[0] == "если ==" and x == k[1]:
            i = k[2]
            continue
        elif k[0] == "перейти":
            i = k[1]
            continue
        i = i + 1
    return "закончил за " + str(shagov) + " команд, x = " + str(x)

# урожай: удваивать запас, пока не станет больше 1000
urozhaj = [["если >", 1000, 3], ["*", 2], ["перейти", 0], ["стоп"]]
# «счастливое число»: правило Коллатца из раздела 1, пока не дойдёт до 1
kollatc = [["если ==", 1, 7], ["если чётно", 5], ["*", 3], ["+", 1],
           ["перейти", 0], ["//", 2], ["перейти", 0], ["стоп"]]
# мод с ошибкой: забыл выход из цикла
vechnyj = [["+", 1], ["перейти", 0], ["стоп"]]

print("урожай с 3:    ", vypolnit(urozhaj, 3))
print("Коллатц с 6:   ", vypolnit(kollatc, 6))
print("Коллатц с 27:  ", vypolnit(kollatc, 27))
print("вечный цикл:   ", vypolnit(vechnyj, 0))
урожай с 3:     закончил за 28 команд, x = 1536
Коллатц с 6:    закончил за 35 команд, x = 1
Коллатц с 27:   остановлен на пределе, x = 3644
вечный цикл:    остановлен на пределе, x = 150

Урожай и Коллатц с 6 закончили. Вечный цикл остановлен - ради этого предел и ставили. Но Коллатц с 27 остановлен тоже, хотя без предела он заканчивается за 486 команд: правило Коллатца из раздела 1 у числа 27 идёт 111 шагов, и мод честный. Это цена предела, и она неустранима: отличить «скоро закончится» от «никогда не закончится» в общем случае нельзя, поэтому любой предел отрезает часть честных программ. Выбор предела - решение о том, сколько честных модов допустимо потерять ради того, чтобы ход длился 100 мс.

Что ломается без этого

Смещение, которое заметили игроки. Кубик с % 6 выдаёт шестёрки на 0,26 процентного пункта реже - на полтора процента в относительном счёте. За десятки тысяч бросков сообщество это посчитает, и «рандом подкручен» станет главной темой форума.

Сервер, который ждёт мод. Без предела один мод с забытым выходом из цикла останавливает ход для всех восьми игроков.

Обещание оптимума там, где его нет. «Автоматическая расстановка башен - лучшая возможная» - и игрок находит расстановку лучше. Задача NP-трудна; честная надпись - «хорошая» или гарантия приближения, если она доказана.

Зачем это в работе

Лимит времени выполнения у облачных функций, предел шагов у смарт-контрактов, тайм-аут у запроса к базе - всё это тот же ответ на неразрешимость: раз заранее не узнать, остановится ли код, его останавливают снаружи. А смещение по модулю - классическая ошибка при генерации паролей и токенов: random_byte % len( алфавит) с алфавитом из 62 символов даёт первым восьми символам лишний шанс.

Теперь сам

  1. Генератор отдаёт число 0-999, нужна карта из колоды в 52 карты. Как получить её без смещения? Какая доля значений уйдёт в отказ?
  2. Сервер поднял предел модов до 500 команд. Закончит ли теперь Коллатц с 27?
  3. Свойство «путь отряда кратчайший» - доказывать каждый раз или проверять сертификатом? Почему?

Ответ. 1. 1000 = 52 · 19 + 12: значения 988-999 отбрасывать, остальные - b % 52. В отказ уходит 12 / 1000 = 1,2% бросков.

  1. Да: ему нужно 486 команд. Но любой предел отрежет какой-то другой честный мод - например, Коллатца с числа, у которого путь ещё длиннее.

  2. Проверять: алгоритм доказан один раз, а сертификат на каждый ответ ловит ошибку реализации и порчу данных за V + E, дешевле самого поиска.

Главное

У каждой гарантии игроку должен быть назван способ - доказательство, сертификат, тест или предел. Где задача неразрешима, честная гарантия - не «найдём», а «остановим вовремя».

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.