Дам детальный анализ алгоритма Dice Roll Encoding — один из самых инновативных частей проекта.
Отлично, теперь дам очень подробный анализ.
🎲 ОЧЕНЬ ПОДРОБНЫЙ АНАЛИЗ: DICE ROLL ENCODING
ЗАДАЧА
Что нужно решить?
У нас есть 3 кубика, каждый может выпасть одной из 6 фигур:
- Пешка (P), Конь (N), Слон (B), Ладья (R), Ферзь (Q), Король (K)
Сырой подход: Всего 6³ = 216 различных комбинаций кубиков.
Проблема: Нам важны неупорядоченные комбинации:
- PPB, PBP, BPP — это одна и та же комбинация
- Нужно отобразить их в ОДИН индекс
Решение: Сжать 216 вариантов в 56 уникальных комбинаций (неупорядоченных).
Давайте проверим это число:
C(6+3-1, 3) = C(8, 3) = 56
Это звёздочки и полочки — кол-во способов распределить 3 кубика между 6 типами.
ТРЕУГОЛЬНИК ПАСКАЛЯ КАК КЛЮЧ
Что это?
consteval auto make_pascal_triangle() {
std::array<std::array<unsigned, 9>, 9> p{};
for (size_t i = 0; i < 9; ++i) {
p[i][0] = 1; // C(n, 0) = 1
p[i][i] = 1; // C(n, n) = 1
for (size_t j = 1; j < i; ++j) {
p[i][j] = p[i-1][j-1] + p[i-1][j]; // C(n,k) = C(n-1,k-1) + C(n-1,k)
}
}
return p;
}Результат для N=DICE_COUNT+PIECES_TYPES_COUNT-1=8:
p[0] p[1] p[2] ...
p[0]: [1]
p[1]: [1, 1]
p[2]: [1, 2, 1]
p[3]: [1, 3, 3, 1]
p[4]: [1, 4, 6, 4, 1]
p[5]: [1, 5, 10, 10, 5, 1]
p[6]: [1, 6, 15, 20, 15, 6, 1]
p[7]: [1, 7, 21, 35, 35, 21, 7, 1]
p[8]: [1, 8, 28, 56, 70, 56, 28, 8, 1]
Ключевые значения для нас:
- p[8][3] = 56 — всего комбинаций 3 кубиков по 6 типам ✅
- p[7][2] = 21 — комбинации 2 кубиков по 6 типам
- p[6][1] = 6 — комбинации 1 кубика по 6 типам
ЛЕКСИКОГРАФИЧЕСКИЙ ПОРЯДОК
Идея: Перечисляем комбинации в определённом порядке:
Индекс 0: ------ (0 кубиков)
Индекс 1: P (1 кубик, первый тип)
Индекс 2: N
Индекс 3: B
Индекс 4: R
Индекс 5: Q
Индекс 6: K (1 кубик, последний тип)
Индекс 7: PP (2 кубика)
Индекс 8: PN
...
Индекс 27: KK (последняя комбинация 2 кубиков)
Индекс 28: PPP (3 кубика)
...
Индекс 55: KKK (последняя комбинация 3 кубиков)
20 строк кода соответствуют 56 комбинациям!
ГЛУБОКИЙ АНАЛИЗ: ENCODE()
Формально, count[i] = количество раз, когда выпала фигура типа i.
Пример: PPB означает count = [2, 0, 1, 0, 0, 0]
Шаг 1: Подсчёт “смещения” для гуппы с sum кубиков
int sum = this->total_rolls(); // Для PPB: sum = 3
for (int i = 0; i < sum; ++i)
ret += pascal[PIECES_TYPES_COUNT + i - 1][i];Разбор:
i=0: ret += pascal[5][0] = 1i=1: ret += pascal[6][1] = 6i=2: ret += pascal[7][2] = 21
ret = 1 + 6 + 21 = 28
❓ Что это означает?
Это сумма всех комбинаций с <3 кубиками:
- C(5,0) = 1 (комбинация с 0 кубиков)
- C(6,1) = 6 (комбинации с 1 кубиком)
- C(7,2) = 21 (комбинации с 2 кубиками)
- Итого: 28 комбинаций до наших 3 кубиков
Шаг 2: Определение позиции внутри группы sum=3
for (size_t i = 0; i < PIECES_TYPES_COUNT; ++i) { // i = 0..5 типов
for (int _ = 0; _ < count[i]; ++_) { // Для каждого кубика типа i
int len = PIECES_TYPES_COUNT - i - 1; // Сколько типов ПОСЛЕ этого
ret += pascal[sum + len - 1][sum];
sum--;
}
}Для PPB: count[0]=2, count[2]=1, остальное 0
Итерация 1 (i=0, первая P):
- len = 6 - 0 - 1 = 5
- ret += pascal[3 + 5 - 1][3] = pascal[7][3] = 35
- ret = 28 + 35 = 63
- sum = 2
Итерация 2 (i=0, вторая P):
- len = 6 - 0 - 1 = 5
- ret += pascal[2 + 5 - 1][2] = pascal[6][2] = 15
- ret = 63 + 15 = 78
- sum = 1
Итерация 3 (i=2, B):
- len = 6 - 2 - 1 = 3
- ret += pascal[1 + 3 - 1][1] = pascal[3][1] = 3
- ret = 78 + 3 = 81
- sum = 0
ФИНАЛЬНЫЙ РЕЗУЛЬТАТ: ret = 81 для PPB
ЧТО ПРОИСХОДИТ В ЦИКЛЕ?
Ключевое понимание:
Когда мы добавляем i-й кубик типа X, мы добавляем количество всех комбинаций, которые лексикографически МЕНЬШЕ.
Для PPB на позиции первой P:
- Сколько есть комбинаций вида
**?где на позиции первого идёт какой-то другой тип? - Это все комбинации:
**N,**B,**R,**Q,**Kгде**— оставшиеся 2 любых - Это C(6+2-1, 2) = C(7, 2) = 21…
Нет, погодите. На самом деле это:
*?где*это один оставшийся кубик, и?это один из типов после P
Это C(5+2-1, 2) = C(6, 2) = 15…
Хм, давайте подумаем правильно…
Истинное объяснение:
pascal[7][3] = 35 означает: “сколько комбинаций из 6 оставшихся типов с 3 кубиками?”
Мы добавляем это, потому что если вместо P из типа 0 мы выберем ЧТО-ТО из типов 1-5, то получим 35 вариантов.
Потом для второй P:
- pascal[6][2] = 15 — комбинации из типов 1-5 с 2 оставшимися кубиками
И для B:
- pascal[3][1] = 3 — комбинации из типов 3-5 с 1 кубиком (это N, R, Q, K = 4 типа)
Стоп, pascal[3][1] = 3, а не 4…
ПРАВИЛЬНО:
После двух P нам остаётся 1 кубик, и нам нужно выбрать тип.
У нас осталось типов 6 - 2 = 4 (всё кроме P и N).
Число способов выбрать 1 из 4 = C(4, 1) = 4
Но pascal[3][1] = C(3,1) = 3…
Ахаа! Потому что len = 6 - 2 - 1 = 3, то есть это типы после B.
Типы после B это {R, Q, K} = 3 типа!
Итак, по типам: P(0), N(1), B(2), R(3), Q(4), K(5)
После B остаётся 3 типа: R, Q, K
C(3+1-1, 1) = C(3, 1) = 3 ✅
Это ответ на вопрос: “Сколько есть позиций, где вместо B я выбираю одного из типов после B?”
ГЛУБОКИЙ АНАЛИЗ: DECODE()
Теперь обратная операция: из индекса 81 восстановить PPB.
Шаг 1: Определить, сколько кубиков было выброшено
int sum = 0;
while (x >= pascal[PIECES_TYPES_COUNT + sum - 1][sum]) {
x -= pascal[PIECES_TYPES_COUNT + sum - 1][sum];
sum++;
}Входные данные: x = 81
Итерация 1 (sum=0):
- pascal[5][0] = 1
- 81 >= 1? Да
- x = 81 - 1 = 80
- sum = 1
Итерация 2 (sum=1):
- pascal[6][1] = 6
- 80 >= 6? Да
- x = 80 - 6 = 74
- sum = 2
Итерация 3 (sum=2):
- pascal[7][2] = 21
- 74 >= 21? Да
- x = 74 - 21 = 53
- sum = 3
Итерация 4 (sum=3):
- pascal[8][3] = 56
- 53 >= 56? Нет → выход из цикла
Результат: sum = 3, x = 53 ✅
Мы правильно определили, что это комбинация 3 кубиков. И остаток x=53 — это позиция внутри группы 3-кубиков.
Шаг 2: Распределить x=53 по типам фигур
for (size_t i = 0; i < PIECES_TYPES_COUNT; ++i) { // i = 0..5
int len = PIECES_TYPES_COUNT - i - 1; // Типов после i-го
while (sum && x >= pascal[len + sum - 1][sum]) {
ret.count[i]++; // Добавляем кубик типа i
x -= pascal[len + sum - 1][sum];
sum--;
}
}Входные данные: x=53, sum=3, i=0 (Пешка)
Итерация 1.1 (i=0, sum=3, x=53):
- len = 5
- pascal[7][3] = 35
- 53 >= 35? Да
- ret.count[0]++ → ret.count[0] = 1
- x = 53 - 35 = 18
- sum = 2
Итерация 1.2 (i=0, sum=2, x=18):
- len = 5
- pascal[6][2] = 15
- 18 >= 15? Да
- ret.count[0]++ → ret.count[0] = 2 ✅
- x = 18 - 15 = 3
- sum = 1
Итерация 1.3 (i=0, sum=1, x=3):
- len = 5
- pascal[5][1] = 5
- 3 >= 5? Нет → выход из while
Итерация 2 (i=1, sum=1, x=3, Конь):
- len = 4
- pascal[4][1] = 4
- 3 >= 4? Нет → выход из while (не добавляем)
Итерация 3 (i=2, sum=1, x=3, Слон):
- len = 3
- pascal[3][1] = 3
- 3 >= 3? Да
- ret.count[2]++ → ret.count[2] = 1 ✅
- x = 3 - 3 = 0
- sum = 0
Итерация 4..6: sum=0, поэтому while не срабатывает
ФИНАЛЬНЫЙ РЕЗУЛЬТАТ:
ret.count[0] = 2 (P, P)
ret.count[2] = 1 (B)
Остальное = 0
✅ Декодировано: PPB
ВИЗУАЛЬНОЕ ОБЪЯСНЕНИЕ
Все 56 комбинаций с 3 кубиками в порядке:
[0]: --- (пусто)
[1]: P-- (первый тип)
[2]: N--
...
[6]: K-- (последний тип)
[7]: PP- (PP*: все после PP)
[8]: PN-
[9]: PB-
[10]: PR-
[11]: PQ-
[12]: PK-
[13]: NN- (NN*)
[14]: NB-
...
[27]: KK- (KK* = KKK)
[28]: PPP
[29]: PPN
...
[81]: PPB ← нас интересует эта позиция
...
[55]: KKK
лексикографический порядок гарантирует, что:
- Сначала идут комбинации, где первый элемент = самый ранний тип
- Затем второй элемент переступает
- И т.д.
ОБЪЯСНЕНИЕ ЧЕРЕЗ КОМБИНАТОРИКУ: STARS AND BARS
Классическая задача: Распределить n идентичных объектов в k различных ящиков.
Ответ: C(n+k-1, k) = C(n+k-1, n)
Для нас:
- n = 3 (кубика)
- k = 6 (типов)
C(3+6-1, 6) = C(8, 6) = C(8, 2) = 28
Хм, это 28, но мы сказали 56. Погодите…
Правильно: C(3+6-1, 3) = C(8, 3) = 56 ✅
Это означает: “Во сколько способов можем расставить 3 кубика и 5 разделителей?”
Пример расстановки для PPB:
| P P | N | B | R | Q | K
^ это означает: 2 в первый ящик (P), 0 во второй (N), 1 в третий (B), 0,0,0 в остальные
Абстрактно: Комбинаторное число — это количество неупорядоченных выборок.
Кодирование → преобразование неупорядоченной выборки в уникальный индекс лексикографического порядка.
ПРОВЕРКА: ПОЛНЫЙ ЦИКЛ КОДИРОВАНИЯ
Давайте проверим несколько комбинаций:
| Комбинация | count[] | Ожидаемый индекс | Процесс |
|---|---|---|---|
| --- | [0,0,0,0,0,0] | 0 | sum=0, ret=0 |
| P | [1,0,0,0,0,0] | 1 | sum=1, ret=1 + 0 = 1 |
| K | [0,0,0,0,0,1] | 6 | sum=1, skip все типы, ret=1+6-1=6 |
| PP | [2,0,0,0,0,0] | 7 | sum=2, ret=1+6=7, затем 2 П подряд |
| PPP | [3,0,0,0,0,0] | 28 | sum=3, ret=28, затем 3 П подряд |
| KKK | [0,0,0,0,0,3] | 55 | sum=3, ret=28, все 3 к типу K |
СЛОЖНОСТЬ АЛГОРИТМА
| Операция | Сложность | Причина |
|---|---|---|
| encode() | O(DICE_COUNT × PIECES_TYPES_COUNT) = O(18) | Два вложенных цикла, максимум 3 × 6 |
| decode() | O(DICE_COUNT × PIECES_TYPES_COUNT) = O(18) | Два вложенных цикла |
| Доступ к Pascal | O(1) | Предвычислено |
| Результат | O(1) амортизированно | Константные размеры |
ПОЧЕМУ ПАСКАЛЬ А НЕ ЧТО-ТО ДРУГОЕ?
Альтернативы:
- Факториалы: O(n!) — взрыв
- Битовая маска: Нужна O(2⁶) = 64 позиций (неоптимально)
- Хеширование: O(hash time), менее предсказуемо
Паскалева треугольника:
- ✅ Строгие комбинаторные причины
- ✅ Гарантирует уникальность
- ✅ Легко кодировать/декодировать
- ✅ Предсчитывается один раз (compile-time!)
ПРАКТИЧЕСКОЕ ПРИМЕНЕНИЕ
// Создание таблицы всех возможных позиций после хода
std::array<std::vector<board>, 56> moves;
for (int idx = 0; idx < 56; ++idx) {
dice_roll roll = dice_roll::decode(idx); // Быстро: O(1)
moves[idx] = generate_moves_for_roll(roll);
}
// Позже, для конкретного броска:
dice_roll my_roll = {2, 0, 1, 0, 0, 0}; // PPB
int idx = my_roll.encode(); // Быстро: O(1)
std::vector<board>& possible_positions = moves[idx];Прямая аналогия: Как индекс массива в памяти, но для комбинаторных объектов!
ИТОГОВАЯ ТАБЛИЦА: ПЕРВЫЕ 20 КОМБИНАЦИЙ
idx | Комбинация | Интерпретация
-----|------------|------------------
0 | --- | (пусто)
1 | P | 1 пешка
2 | N | 1 конь
3 | B | 1 слон
4 | R | 1 ладья
5 | Q | 1 ферзь
6 | K | 1 король
7 | PP | 2 пешки
8 | PN | пешка + конь
9 | PB | пешка + слон
10 | PR | пешка + ладья
11 | PQ | пешка + ферзь
12 | PK | пешка + король
13 | NN | 2 коня
14 | NB | конь + слон
15 | NR | конь + ладья
16 | NQ | конь + ферзь
17 | NK | конь + король
18 | BB | 2 слона
19 | BR | слон + ладья
20 | BQ | слон + ферзь
Закономерность: Лексикографический порядок по типам, для каждой “группы с первым типом X” перечисляются все “группы <X с первым типом Y>”, потом “группы >=Y”.
Это точно как перечисление неупорядоченных выборок в словаре!