Подсчет смежных свопов, необходимых для преобразования одной перестановки в другую

Нам даны две последовательности строчных букв латинского алфавита. Они имеют одинаковую длину и имеют одинаковое количество заданных типов букв (первое имеет равное число t как второе и так на). Мы должны найти минимальное количество свопов (под "свопом" мы подразумеваем изменение порядок двух соседних букв), необходимых для преобразуйте первую последовательность во вторую. Мы можно смело предположить, что каждая из двух последовательностей МОЖЕТ быть преобразована друг к другу. Мы могли бы сделать это с помощью грубой силы, но последовательности слишком велики для этого.

Ввод:
Длина последовательностей (не менее 2, не более 999999) и затем две последовательности.

Вывод:
Целое число, представляющее количество свопов, необходимых для последовательности, чтобы они стали одинаковыми.

Пример:
{5, aaaaa, aaaaa} должен выводить {0},
{4, abcd, acdb} должен выводить {2}.

Первое, что мне пришло в голову, - пузырь. Мы можем просто пузыриться по последовательности, подсчитывающей каждый своп. Проблема в том, что: a) это O (n ^ 2) худший случай b) Я не убежден, что это даст мне наименьшее число для каждого случая... Даже оптимизированный пузырьковый корабль, похоже, не делает этого трюка. Мы могли бы реализовать вид коктейля, который бы разрешил проблему с черепахами - но даст ли он мне лучшую производительность? Или может быть что-то более простое/быстрое?

Этот вопрос также может быть сформулирован как: Как определить расстояние редактирования между двумя строками, когда разрешена только операция?

Ответ 1

Здесь простое и эффективное решение:

Пусть Q[ s2[i] ] = the positions character s2[i] is on in s2. Пусть P[i] = on what position is the character corresponding to s1[i] in the second string.

Чтобы построить Q и P:

for ( int i = 0; i < s1.size(); ++i )
    Q[ s2[i] ].push_back(i); // basically, Q is a vector [0 .. 25] of lists

temp[0 .. 25] = {0}
for ( int i = 0; i < s1.size(); ++i )
    P[i + 1] = 1 + Q[ s1[i] ][ temp[ s1[i] ]++ ];

Пример:

    1234
s1: abcd
s2: acdb
Q: Q[a = 0] = {0}, Q[b = 1] = {3}, Q[c = 2] = {1}, Q[d = 3] = {2}
P: P[1] = 1, P[2] = 4 (because the b in s1 is on position 4 in s2), P[3] = 2
   P[4] = 3

P имеет 2 инверсии (4 2 и 4 3), так что это ответ.

Это решение O(n log n), потому что построение P и Q может быть выполнено в O(n), а сортировка слияния может подсчитывать инверсии в O(n log n).

Ответ 2

В отношении минимального количества (необязательно смежных) свопов, необходимых для преобразования перестановки в другую, метрикой, которую вы должны использовать, является расстояние Кэйли, которое по существу является величиной перестановки - количеством циклов.

Подсчет количества циклов в перестановке является довольно тривиальной проблемой. Простой пример. Предположим, что перестановка 521634.

Если вы проверите первую позицию, у вас есть 5, в пятом - 3, а в третьем - 1, закрыв первый цикл. 2 находится во 2-м положении, поэтому он делает сам цикл, а 4 и 6 делает последний цикл (4 находится в 6-й позиции и 6 в 4-м). Если вы хотите преобразовать эту перестановку в перестановку идентичности (с минимальным количеством свопов), вам необходимо переупорядочить каждый цикл независимо. Общее число свопов - это длина перестановки (6) минус количество циклов (3).

При любых двух перестановках расстояние между ними эквивалентно расстоянию между композицией первого с инверсией второго и идентификатором (вычисленным как объяснено выше). Поэтому единственное, что вам нужно сделать, это составить первую перестановку и инверсию второй и подсчитать количество циклов в результате. Все эти операции - O (n), поэтому вы можете получить минимальное количество свопов в линейном времени.

Ответ 3

То, что вы ищете, может быть идентично " Kendall tau distance", которое является (нормированной) разницей конкордантных минус несогласных пар. См. Wikipedia, где утверждается, что он эквивалентен расстоянию сортировки пузырьков.

В R функции могут быть доступны не только для вычисления tau, например,

cor( X, method="kendall", use="pairwise" ) ,

но также для проверки значимости разницы, например

cor.test( x1, x2, method="kendall" ) ,

и они даже способны правильно учесть связи.

Подробнее см. здесь.

Ответ 4

Я написал класс Permutation, который, среди прочего, может вернуть несколько транспозиций, необходимых для преобразования данной перестановки в идентификатор. Это делается путем создания орбит (циклов) и подсчета их длины. Терминология взята из Кострикина А.И., "Введение в линейную алгебру I".

Включает:

#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <iterator>

Класс Перенос:

class Permutation {
public:
    struct ei_element {    /* element of the orbit*/
        int e; /* identity index */
        int i; /* actual permutation index */
    };
    typedef std::vector<ei_element> Orbit; /* a cycle */

    Permutation( std::vector<int> const& i_vector);
    /* permute i element, vector is 0 indexed */
    int pi( int i) const { return iv[ i - 1]; }
    int i( int k) const { return pi( k); } /* i_k = pi(k) */
    int q() const { /* TODO: return rank = q such that pi^q = e */ return 0; }
    int n() const { return n_; }
    /* return the sequence 1, 2, ..., n */
    std::vector<int> const& Omega() const { return ev; }
    /* return vector of cycles */
    std::vector<Orbit> const& orbits() const { return orbits_; }
    int l( int k) const { return orbits_[ k].size(); } /* length of k-th cycle */
    int transpositionsCount() const;  /* return sum of all transpositions */
    void make_orbits();

    private:
    struct Increment {
        int current;
        Increment(int start) : current(start) {}
        int operator() () {
            return current++;
        }
    };
    int n_;
    std::vector<int> iv; /* actual permutation */
    std::vector<int> ev; /* identity permutation */
    std::vector<Orbit> orbits_;
};

Определения:

Permutation::Permutation( std::vector<int> const& i_vector) : 
                                                      n_( i_vector.size()), 
                                                      iv( i_vector), ev( n_) {
        if ( n_) { /* fill identity vector 1, 2, ..., n */
            Increment g ( 1);
            std::generate( ev.begin(), ev.end(), g);
        }
}

/* create orbits (cycles) */
void Permutation::make_orbits() {
    std::set<int> to_visit( ev.begin(), ev.end()); // identity elements to visit
    while ( !to_visit.empty()) {
        /* new cycle */
        Orbit orbit;
        int first_to_visit_e = *to_visit.begin();
        to_visit.erase( first_to_visit_e);
        int k = first_to_visit_e; // element in identity vector
        /* first orbit element */
        ei_element element;
        element.e = first_to_visit_e;
        element.i = i( first_to_visit_e);
        orbit.push_back( element);
        /* traverse permutation until cycle is closed */
        while ( pi( k) != first_to_visit_e && !to_visit.empty()) {
            k = pi( k);
            ei_element element;
            element.e = k;
            element.i = pi( k);
            orbit.push_back( element);
            to_visit.erase( k);
        }
        orbits_.push_back( orbit);
    }
}

и

/* return sum of all transpositions */
int Permutation::transpositionsCount() const {
    int count = 0;
    int k = 0;
    while ( k < orbits_.size()) {
        count += l( k++) - 1; /* sum += l_k - 1 */ 
    }
    return count;
}

использование:

/*
 * 
 */
int main(int argc, char** argv) {
                       //1, 2, 3, 4, 5, 6, 7, 8       identity (e)
    int permutation[] = {2, 3, 4, 5, 1, 7, 6, 8}; //  actual (i)
    std::vector<int> vp( permutation, permutation + 8);

    Permutation p( vp);
    p.make_orbits();
    int k = p.orbits().size();
    std::cout << "Number of cycles:" << k << std::endl;

    for ( int i = 0; i < k; ++i) {
        std::vector<Permutation::ei_element> v = p.orbits()[ i];
        for ( int j = 0; j < v.size(); ++j) {
            std::cout << v[ j].e << "," << v[ j].i << " | ";
        }
        std::cout << std::endl;
    }

    std::cout << "Steps needed to create identity permutation: " 
                                                << p.transpositionsCount();
    return 0;
}

выход:

Число циклов: 3

1,2 | 2,3 | 3,4 | 4,5 | 5,1 |

6,7 | 7,6 |

8,8 |

Шаги, необходимые для создания перестановки идентичности: 5

RUN SUCCESSFUL (общее время: 82 мс)


coliru

Ответ 5

" алгоритм Kendall tau distance - это точное решение в этом случае, где должно быть найдено количество свопов соседних элементов.

Пример.

eyssaasse (базовая строка)
seasysaes

Базовая строка предоставляет индексы для каждого элемента: e= 0, y= 1, s= 2, s= 3, a= 4, a= 5, s= 6, s= 7, е= 8;

Некоторые элементы дублируются, поэтому:
1) Создайте словарь, где элементы - это ключи, а значения - это списки индексов:

idx= {' e' = > [0, 8], ' y' = > [1], ' s '= > [2, 3, 6, 7],' a '= > [4, 5]}

2) Создайте карту индексов второй строки, используя индексы элементов в словаре idx:

seasysaes → 204316587 (цикл "seasysaes" и поп следующий индекс из списков для каждого ключа в idx)

3) Создайте список всех парных комбинаций этой карты, 204316587: 20 24 23 21 26 25 28 27 04 03 01 06... 65 68 67 58 57 87;
Прокрутите эти пары, считая те, где первое число больше второго. Этот счетчик является искомым числом смежных свопов между строками.

Python script:

from itertools import combinations, cycle

word = 'eyssaasse' # base string
cmpr = 'seasysaes' # a string to find number of swaps from the base string
swaps = 0

# 1)
chars = {c: [] for c in word}
[chars[c].append(i) for i, c in enumerate(word)]
for k in chars.keys():
    chars[k] = cycle(chars[k])

# 2)
idxs = [next(chars[c]) for c in cmpr]

# 3)
for cmb in combinations(idxs, 2):
    if cmb[0] > cmb[1]:
        swaps += 1

print(swaps)

Количество свопов между "eyssaasse" и "seasysaes" равно 7.
Для "reviver" и "vrerevi" это 8.

Ответ 6

Преобразование перестановки из одного в другое может быть преобразовано в аналогичную проблему (Число свопов в перестановке), инвертируя целевую перестановку в O (n), составив перестановки в O (n), а затем нахождение числа свопов оттуда до подстановки подстановки. Данный:

int P1[] = {0, 1, 2, 3}; // abcd
int P2[] = {0, 2, 3, 1}; // acdb

// we can follow a simple algebraic modification
// (see http://en.wikipedia.org/wiki/Permutation#Product_and_inverse):
// P1 * P = P2                   | premultiply P1^-1 *
// P1^-1 * P1 * P = P1^-1 * P2
// I * P = P1^-1 * P2
// P = P1^-1 * P2
// where P is a permutation that makes P1 into P2.
// also, the number of steps from P to identity equals
// the number of steps from P1 to P2.

int P1_inv[4];
for(int i = 0; i < 4; ++ i)
    P1_inv[P1[i]] = i;
// invert the first permutation O(n)

int P[4];
for(int i = 0; i < 4; ++ i)
    P[i] = P2[P1_inv[i]];
// chain the permutations

int num_steps = NumSteps(P, 4); // will return 2
// now we just need to count the steps

Чтобы подсчитать шаги, можно разработать простой алгоритм, например:

int NumSteps(int *P, int n)
{
    int count = 0;
    for(int i = 0; i < n; ++ i) {
        for(; P[i] != i; ++ count) // could be permuted multiple times
            swap(P[P[i]], P[i]); // look where the number at hand should be
    }
    // count number of permutations

    return count;
}

Это всегда заменяет элемент для места, где он должен находиться в перестановке идентичности, поэтому на каждом шаге он отменяет и подсчитывает один своп. Теперь, при условии, что количество возвращаемых им свопов действительно минимально, время выполнения алгоритма ограничено им и гарантируется завершение (вместо того, чтобы застревать в бесконечном цикле). Он будет выполняться в циклах O(m) или O(m + n), где m - количество свопов (возвращается count), а n - количество элементов в последовательности (4). Обратите внимание, что m < n всегда истинно. Поэтому это должно быть выше решений O(n log n), так как верхняя граница O(n - 1) свопов или O(n + n - 1) итераций цикла здесь, которая практически равна O(n) (постоянный коэффициент 2 опущен в последнем случае).

Алгоритм будет работать только для действительных перестановок, он будет бесконечно циклически чередоваться с последовательностями с повторяющимися значениями и будет делать доступ к массиву вне пределов (и сбоев) для последовательностей со значениями, отличными от [0, n). Полный тестовый пример можно найти здесь (строит с помощью Visual Studio 2008, сам алгоритм должен быть довольно портативным). Он генерирует все возможные перестановки с длиной от 1 до 32 и проверяет на решения, сгенерированные с помощью первого поиска ширины (BFS), похоже, работает для всех перестановок длиной от 1 до 12, тогда он становится довольно медленным, но я предполагаю, что он просто продолжит работу.