Генерация перестановок. Алгоритм Джонсона‐Троттера - EcsFlash/DataTypes GitHub Wiki

Генерация перестановок

Начнем с перестановок. Для простоты будем считать, что множество переставляемых элементов — это просто множество целых чисел от 1 до n. В общем случае их можно интерпретировать как индексы элементов n-элементного множества {a₁, ..., aₙ}. Как будет выглядеть применение метода умножения размера к задаче о генерации всех n! перестановок множества {1, ..., n}? Задача меньшего на единицу размера состоит в генерации всех (n - 1)! перестановок. Полагая, что задача меньшего размера успешно решена, мы можем получить решение большей задачи путем вставки n в каждую из n возможных позиций среди элементов каждой из перестановок n - 1 элементов. Все получаемые таким образом перестановки будут различны (почему?), а их общее количество будет равно n (n - 1)! = n!. Следовательно, таким образом мы получим все перестановки множества {1, ..., n}.

Мы можем вставлять n в ранее сгенерированных перестановках слева направо или справа налево. Как оказывается, выгодно начинать вставку n в последовательность 12 ... (n - 1) справа налево и изменять направление всякий раз при переходе к новой перестановке множества {1, ..., n - 1}.

Начало 1
Вставка 2 12 21
Вставка 3 123 312 213 132 231 321

Преимущество такого порядка генерации перестановок связано с тем фактом, что этот порядок удовлетворяет так называемому требованию минимальных изменений (minimal-change requirement): каждая перестановка получается из своей непосредственной предшественницы при помощи обмена местами только двух элементов. Требование минимальных изменений выгодно как с точки зрения скорости работы алгоритма, так и для приложений, которое будет использовать сгенерированные перестановки.

Можно получить тот же порядок перестановок n элементов и без явной генерации перестановок для меньших значений n. Это можно сделать, связав с каждым компонентом k перестановки направление. Будем указывать это направление при помощи стрелки над рассматриваемым элементом, например:

←  →  ←  ←
3  2  4  1

Компонент k в такой перестановке с использованием стрелок называется мобильным (mobile), если стрелка указывает на меньшее соседнее число. Например, в перестановке

→  ←  →  ←
3  2  4  1 

числа 3 и 4 мобильны, а 2 и 1 — нет. Воспользовавшись понятием мобильного элемента, мы получаем следующее описание так называемого алгоритма Джонсона–Троттера (Johnson–Trotter algorithm) для генерации перестановок.

АЛГОРИТМ JohnsonTrotter(n)
  // Реализация алгоритма Джонсона–Троттера
  // для генерации перестановок

  // Входные данные: Натуральное число n
  // Выходные данные: Список перестановок множества {1, ..., n}
                                               ←  ←       ←
  Инициализируем первую перестановку значением 1  2  ...  n

  while (есть мобильное число k) do
    Находим наибольшее мобильное число k
    Меняем местами k и соседнее целое число, на которое указывает стрелка у k
    Меняем направление стрелки у всех целых чисел, больших k

Вот пример использования этого алгоритма для n = 3 (наибольшее мобильное число показано полужирным шрифтом):

← ← ←
1 2 3
← ← ←
1 3 2
← ← ←
3 1 2
→ ← ←
3 2 1
← → ←
2 3 1
← ← →
2 1 3

Этот алгоритм — один из наиболее эффективных для генерации перестановок и может быть реализован со временем работы, пропорциональным количеству перестановок, т.е. Θ(n!). Конечно, он очень медленный для всех значений n, кроме самых малых, но это беда не алгоритма, а задачи, которая требует от алгоритма генерации слишком большого количества элементов.

Ну вот код на плюсах, но это полный пиздец:

void generatePermutationsJohnsonTrotter(int n) {
    vector<int> permutation(n);
    vector<bool> direction(n, false); // false - влево, true - вправо

    // Инициализация начальной перестановки (1, 2, ..., n)
    for (int i = 0; i < n; ++i) {
        permutation[i] = i + 1;
    }

    auto printPermutation = [](const vector<int>& perm) {
        for (int num : perm) {
            cout << num << " ";
        }
        cout << endl;
    };

    printPermutation(permutation);

    while (true) {
        // Шаг 1: Найти наибольший подвижный элемент
        int mobile = -1;
        int mobilePos = -1;
        for (int i = 0; i < n; ++i) {
            if (direction[i] == false && i > 0) {
                if (permutation[i] > permutation[i - 1]) {
                    if (permutation[i] > mobile) {
                        mobile = permutation[i];
                        mobilePos = i;
                    }
                }
            }
            if (direction[i] == true && i < n - 1) {
                if (permutation[i] > permutation[i + 1]) {
                    if (permutation[i] > mobile) {
                        mobile = permutation[i];
                        mobilePos = i;
                    }
                }
            }
        }

        if (mobile == -1) {
            break; // Нет подвижных элементов — завершение
        }

        // Шаг 2: Поменять местами с соседом в направлении
        if (direction[mobilePos] == false) {
            swap(permutation[mobilePos], permutation[mobilePos - 1]);
            swap(direction[mobilePos], direction[mobilePos - 1]);
            mobilePos--;
        } else {
            swap(permutation[mobilePos], permutation[mobilePos + 1]);
            swap(direction[mobilePos], direction[mobilePos + 1]);
            mobilePos++;
        }

        // Шаг 3: Изменить направление всех элементов, больших mobile
        for (int i = 0; i < n; ++i) {
            if (permutation[i] > mobile) {
                direction[i] = !direction[i];
            }
        }

        printPermutation(permutation);
    }
}
⚠️ **GitHub.com Fallback** ⚠️