Минимальное количество специальных ходов для сортировки числа

Учитывая список чисел

1 15 2 5 10

мне нужно получить

1 2 5 10 15

Единственная операция, которую я могу сделать, это «переместить число X в позицию Y».

В приведенном выше примере мне нужно только «переместить число 15 в позицию 5».

Я хотел бы минимизировать количество операций, но я не могу найти/вспомнить классический алгоритм для этого, учитывая доступную операцию.

Немного фона:

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

person Loïc Février    schedule 11.02.2015    source источник
comment
Мне кажется, что это XY-вопрос   -  person Rerito    schedule 11.02.2015
comment
Может быть, я что-то упускаю, но в чем отличие этой задачи от стандартной сортировки на основе свопов (например, быстрой сортировки)?   -  person amit    schedule 11.02.2015
comment
переместить число X в позицию Y - сортировка вставками?   -  person IVlad    schedule 11.02.2015
comment
@amit Я думаю, что есть разница между количеством сравнений, необходимых для сортировки коллекции, и фактическим количеством свопов (в его случае это на самом деле не своп, но принцип аналогичен). Для сортировки массива вам потребуется не более N свопов, но вы можете сделать это и меньше, в зависимости от циклов перестановки данных.   -  person Juan Lopes    schedule 11.02.2015
comment
Я вижу, он заинтересован в минимизации только количества свопов, а не общего количества операций. Спасибо @JuanLopes   -  person amit    schedule 11.02.2015
comment
перемещение числа 15 в позицию 5 не завершено - для этого нужно указать пункт назначения или относительное движение или что-то еще, например, переместить число X в положение Y в положение Z или переместить число X в положение Y N позиций влево или что-то в этом роде.   -  person twalberg    schedule 11.02.2015
comment
В мой ответ включено доказательство концепции, пожалуйста, проверьте.   -  person Juan Lopes    schedule 11.02.2015


Ответы (2)


Лемма. Минимальное количество пар (удалить элемент, вставить элемент), которое можно выполнить для сортировки списка L (в порядке возрастания), равно:

Sмин(L) = |L| - |LIC(L)|

Где LIC(L) – это самая длинная возрастающая подпоследовательность.

Таким образом, вы должны:

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

Доказательство: по индукции.

Для списка размера 1 самая длинная возрастающая подпоследовательность имеет длину... 1! Список уже отсортирован, поэтому требуемое количество пар (del,ins) равно

|Л| - |LIC(L)| = 1 - 1 = 0

Теперь пусть Ln — список длины n, 1 ≤ n. Пусть Ln+1 — список, полученный добавлением элемента en+1 слева от < em>Ln.
Этот элемент может влиять или не влиять на Самую длинную возрастающую подпоследовательность. Попробуем посмотреть, как...

Пусть in,1 и in,2 — два первых элемента LIC(L n) (*):

  • Если en+1in,2, то LIC(Ln +1) = LIC(Ln)
  • Если en+1in,1, то LIC(Ln +1) = en+1 || LIC(Ln)
  • В противном случае LIC(Ln+1) = LIC(Ln) - in,1 + en+1. Мы сохраняем LIC с самым высоким первым элементом. Это делается путем удаления in,1 из LIC и замены его на en+1.

В первом случае мы удаляем en+1, таким образом получаем сортировку Ln. По предположению индукции для этого требуется n (удаление, вставка) пар. Затем нам нужно вставить en+1 в соответствующую позицию. Таким образом:

S(Ln+1)мин = 1 + S(Ln) мин S(Ln+1)мин = 1 + n - |LIC(Ln)| S(Ln+1)min = |Ln+1| - |LIC(Ln+1|


Во втором случае мы игнорируем en+1. Начнем с удаления элементов, не входящих в LIC(Ln). Эти элементы должны быть вставлены снова! Есть

S(Ln)min = |Ln| - |LIC(Ln)|

такие элементы.

Теперь нам просто нужно позаботиться и вставить их в правильном порядке (относительно en+1). В итоге требуется:

S(Ln+1)min = |Ln| - |LIC(Ln)| S(Ln+1)min = |Ln| + 1 - (|LIC(Ln)| + 1)

Поскольку у нас есть |LIC(Ln+1)| = |LIC(Ln)| + 1 и |Ln+1| = |Ln| + 1, в итоге имеем:

S(Ln+1)min = |Ln+1| - |LIC(Ln+1)|


Последний случай можно доказать, рассмотрев список L'n, полученный удалением in,1 из < em>Ln+1. В этом случае LIC(L'n) = LIC(Ln+1) и, таким образом:

|LIC(L'n)| = |LIC(Ln)| (1)

Оттуда мы можем отсортировать L'n (что занимает |L'n| - | LIC(L'n| по предположению индукции. Предыдущее равенство (1) приводит к результату.

(*): если LIC(Ln) ‹ 2, то in,2< /em> не существует. Просто не обращайте внимания на сравнения с ним. В этом случае применяются только случай 2 и случай 3... Результат по-прежнему действителен

person Rerito    schedule 11.02.2015
comment
Я думаю, что у него нет фактической операции обмена, это больше похоже на удаление из X и добавление в Y. - person Juan Lopes; 11.02.2015
comment
@JuanLopes О ... Ну, в таком случае мой ответ бесполезен, я думаю: с - person Rerito; 11.02.2015

Одно из возможных решений — найти самую длинную возрастающую подпоследовательность и переместить только те элементы, которые не входят в нее.

Я не могу доказать, что это оптимально, но легко доказать, что это правильно и лучше, чем N свопов.

Вот доказательство концепции в Python 2. Я реализовал его как алгоритм O(n2), но я почти уверен, что его можно сократить до O(n log n).

from operator import itemgetter

def LIS(V):
    T = [1]*(len(V))
    P = [-1]*(len(V))

    for i, v in enumerate(V):
        for j in xrange(i-1, -1, -1):
            if T[j]+1 > T[i] and V[j] <= V[i]:
                T[i] = T[j] + 1
                P[i] = j

    i, _ = max(enumerate(T), key=itemgetter(1))
    while i != -1:
        yield i
        i = P[i]

def complement(L, n):
    for a, b in zip(L, L[1:]+[n]):
        for i in range(a+1, b):
            yield i

def find_moves(V):
    n = len(V)
    L = list(LIS(V))[::-1]
    SV = sorted(range(n), key=lambda i:V[i])
    moves = [(x, SV.index(x)) for x in complement(L, n)]

    while len(moves):
        a, b = moves.pop()
        yield a, b
        moves = [(x-(x>a)+(x>b), y) for x, y in moves]


def make_and_print_moves(V):
    print 'Initial array:', V

    for a, b in find_moves(V):
        x = V.pop(a)
        V.insert(b, x)        
        print 'Move {} to {}. Result: {}'.format(a, b, V)
    print '***'

make_and_print_moves([1, 15, 2, 5, 10])

make_and_print_moves([4, 3, 2, 1])

make_and_print_moves([1, 2, 4, 3])

Выводит что-то вроде:

Initial array: [1, 15, 2, 5, 10]
Move 1 to 4. Result: [1, 2, 5, 10, 15]
***
Initial array: [4, 3, 2, 1]
Move 3 to 0. Result: [1, 4, 3, 2]
Move 3 to 1. Result: [1, 2, 4, 3]
Move 3 to 2. Result: [1, 2, 3, 4]
***
Initial array: [1, 2, 4, 3]
Move 3 to 2. Result: [1, 2, 3, 4]
***
person Juan Lopes    schedule 11.02.2015
comment
Считаю оптимальным, можно доказать по индукции, если не ошибаюсь - person Rerito; 11.02.2015
comment
это поможет только тогда, когда последовательность чисел не является полностью случайной. - person Techfist; 11.02.2015
comment
@Techfist, что ты имеешь в виду? - person Juan Lopes; 11.02.2015
comment
Я имею в виду, что этот метод даст оптимальный ответ только тогда, когда последовательность состоит из возрастающей подпоследовательности, иначе не будет ??, но если его первоначальный вопрос состоит только в том, чтобы отсортировать предоставленное им число, это решение кажется оптимальным. - person Techfist; 11.02.2015
comment
Если в данных нет возрастающей подпоследовательности с длиной > 1 (т. е. массив отсортирован в порядке убывания), то нет возможности отсортировать данные менее чем за N-1 ходов. @Rerito только что доказал это. - person Juan Lopes; 11.02.2015
comment
Код недостаточно протестирован. Попробуйте make_and_print_moves([4,5,6,1,2,3]), и он не сможет правильно отсортировать числа. - person hedgehog90; 04.04.2020